Convert LocationPickerActivity to Kotlin

pull/1066/head
Alex Baker 4 years ago
parent bbf71bae38
commit 0f27915f82

@ -1,20 +0,0 @@
package org.tasks.data
import androidx.lifecycle.LiveData
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
@Deprecated("use coroutines")
class LocationDaoBlocking @Inject constructor(private val dao: LocationDao) {
fun insert(place: Place): Long = runBlocking {
dao.insert(place)
}
fun getPlaceUsage(): LiveData<List<PlaceUsage>> {
return dao.getPlaceUsage()
}
fun findPlace(latitude: String, longitude: String): Place? = runBlocking {
dao.findPlace(latitude, longitude)
}
}

@ -1,472 +0,0 @@
package org.tasks.location;
import static com.google.common.collect.Lists.transform;
import static com.todoroo.andlib.utility.AndroidUtilities.hideKeyboard;
import static org.tasks.PermissionUtil.verifyPermissions;
import static org.tasks.Strings.isNullOrEmpty;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.res.Configuration;
import android.location.Location;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnActionExpandListener;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.SearchView.OnQueryTextListener;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.widget.ContentLoadingProgressBar;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.AppBarLayout.Behavior;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.mapbox.android.core.location.LocationEngineCallback;
import com.mapbox.android.core.location.LocationEngineProvider;
import com.mapbox.android.core.location.LocationEngineResult;
import dagger.hilt.android.AndroidEntryPoint;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.tasks.Event;
import org.tasks.R;
import org.tasks.activities.PlaceSettingsActivity;
import org.tasks.billing.Inventory;
import org.tasks.caldav.GeoUtils;
import org.tasks.data.LocationDaoBlocking;
import org.tasks.data.Place;
import org.tasks.data.PlaceUsage;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.InjectingAppCompatActivity;
import org.tasks.location.LocationPickerAdapter.OnLocationPicked;
import org.tasks.location.LocationSearchAdapter.OnPredictionPicked;
import org.tasks.location.MapFragment.MapFragmentCallback;
import org.tasks.preferences.ActivityPermissionRequestor;
import org.tasks.preferences.PermissionChecker;
import org.tasks.preferences.PermissionRequestor;
import org.tasks.themes.ColorProvider;
import org.tasks.themes.Theme;
import org.tasks.themes.ThemeColor;
import org.tasks.ui.Toaster;
import timber.log.Timber;
@AndroidEntryPoint
public class LocationPickerActivity extends InjectingAppCompatActivity
implements OnMenuItemClickListener,
MapFragmentCallback,
OnLocationPicked,
OnQueryTextListener,
OnPredictionPicked,
OnActionExpandListener {
public static final String EXTRA_PLACE = "extra_place";
private static final String EXTRA_MAP_POSITION = "extra_map_position";
private static final String EXTRA_APPBAR_OFFSET = "extra_appbar_offset";
private static final int SEARCH_DEBOUNCE_TIMEOUT = 300;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.app_bar_layout)
AppBarLayout appBarLayout;
@BindView(R.id.collapsing_toolbar_layout)
CollapsingToolbarLayout toolbarLayout;
@BindView(R.id.coordinator)
CoordinatorLayout coordinatorLayout;
@BindView(R.id.search)
View searchView;
@BindView(R.id.loading_indicator)
ContentLoadingProgressBar loadingIndicator;
@BindView(R.id.choose_recent_location)
View chooseRecentLocation;
@BindView(R.id.recent_locations)
RecyclerView recyclerView;
@Inject Theme theme;
@Inject Toaster toaster;
@Inject LocationDaoBlocking locationDao;
@Inject PlaceSearchProvider searchProvider;
@Inject PermissionChecker permissionChecker;
@Inject ActivityPermissionRequestor permissionRequestor;
@Inject DialogBuilder dialogBuilder;
@Inject MapFragment map;
@Inject Geocoder geocoder;
@Inject Inventory inventory;
@Inject ColorProvider colorProvider;
private CompositeDisposable disposables;
@Nullable private MapPosition mapPosition;
private LocationPickerAdapter recentsAdapter;
private LocationSearchAdapter searchAdapter;
private List<PlaceUsage> places = Collections.emptyList();
private int offset;
private MenuItem search;
private final PublishSubject<String> searchSubject = PublishSubject.create();
private PlaceSearchViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
theme.applyTheme(this);
setContentView(R.layout.activity_location_picker);
ButterKnife.bind(this);
viewModel = new ViewModelProvider(this).get(PlaceSearchViewModel.class);
viewModel.setSearchProvider(searchProvider);
Configuration configuration = getResources().getConfiguration();
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.smallestScreenWidthDp < 480) {
searchView.setVisibility(View.GONE);
}
Place currentPlace = getIntent().getParcelableExtra(EXTRA_PLACE);
if (savedInstanceState == null) {
mapPosition =
currentPlace == null
? getIntent().getParcelableExtra(EXTRA_MAP_POSITION)
: currentPlace.getMapPosition();
} else {
mapPosition = savedInstanceState.getParcelable(EXTRA_MAP_POSITION);
offset = savedInstanceState.getInt(EXTRA_APPBAR_OFFSET);
viewModel.restoreState(savedInstanceState);
}
toolbar.setNavigationIcon(R.drawable.ic_outline_arrow_back_24px);
toolbar.setNavigationOnClickListener(v -> collapseToolbar());
toolbar.inflateMenu(R.menu.menu_location_picker);
Menu menu = toolbar.getMenu();
search = menu.findItem(R.id.menu_search);
search.setOnActionExpandListener(this);
((SearchView) search.getActionView()).setOnQueryTextListener(this);
toolbar.setOnMenuItemClickListener(this);
ThemeColor themeColor = theme.getThemeColor();
themeColor.applyToStatusBarIcons(this);
themeColor.applyToNavigationBar(this);
themeColor.setStatusBarColor(toolbarLayout);
themeColor.apply(toolbar);
boolean dark = theme.getThemeBase().isDarkTheme(this);
map.init(getSupportFragmentManager(), this, dark);
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
Behavior behavior = new Behavior();
behavior.setDragCallback(
new AppBarLayout.Behavior.DragCallback() {
@Override
public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
return false;
}
});
params.setBehavior(behavior);
appBarLayout.addOnOffsetChangedListener(
(appBarLayout, offset) -> {
if (offset == 0 && this.offset != 0) {
closeSearch();
hideKeyboard(this);
}
this.offset = offset;
toolbar.setAlpha(Math.abs(offset / (float) appBarLayout.getTotalScrollRange()));
});
coordinatorLayout.addOnLayoutChangeListener(
new OnLayoutChangeListener() {
@Override
public void onLayoutChange(
View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) {
coordinatorLayout.removeOnLayoutChangeListener(this);
locationDao
.getPlaceUsage()
.observe(LocationPickerActivity.this, LocationPickerActivity.this::updatePlaces);
}
});
if (offset != 0) {
appBarLayout.post(() -> expandToolbar(false));
}
findViewById(map.getMarkerId()).setVisibility(View.VISIBLE);
searchAdapter = new LocationSearchAdapter(searchProvider.getAttributionRes(dark), this);
recentsAdapter = new LocationPickerAdapter(this, inventory, colorProvider, this);
recentsAdapter.setHasStableIds(true);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(
search != null && search.isActionViewExpanded() ? searchAdapter : recentsAdapter);
}
@Override
public void onMapReady(MapFragment mapFragment) {
map = mapFragment;
updateMarkers();
if (permissionChecker.canAccessLocation()) {
mapFragment.showMyLocation();
}
if (mapPosition != null) {
map.movePosition(mapPosition, false);
} else if (permissionRequestor.requestFineLocation()) {
moveToCurrentLocation(false);
}
}
@Override
public void onBackPressed() {
if (closeSearch()) {
return;
}
if (offset != 0) {
collapseToolbar();
return;
}
super.onBackPressed();
}
private boolean closeSearch() {
return search != null && search.isActionViewExpanded() && search.collapseActionView();
}
@Override
public void onPlaceSelected(org.tasks.data.Place place) {
returnPlace(place);
}
@OnClick(R.id.current_location)
void onClick() {
if (permissionRequestor.requestFineLocation()) {
moveToCurrentLocation(true);
}
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PermissionRequestor.REQUEST_LOCATION) {
if (verifyPermissions(grantResults)) {
map.showMyLocation();
moveToCurrentLocation(true);
} else {
dialogBuilder
.newDialog(R.string.missing_permissions)
.setMessage(R.string.location_permission_required_location)
.setPositiveButton(android.R.string.ok, null)
.show();
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@OnClick(R.id.select_this_location)
void selectLocation() {
loadingIndicator.setVisibility(View.VISIBLE);
MapPosition mapPosition = map.getMapPosition();
disposables.add(
Single.fromCallable(() -> geocoder.reverseGeocode(mapPosition))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally(() -> loadingIndicator.setVisibility(View.GONE))
.subscribe(this::returnPlace, e -> toaster.longToast(e.getMessage())));
}
@OnClick(R.id.search)
void searchPlace() {
mapPosition = map.getMapPosition();
expandToolbar(true);
search.expandActionView();
}
@SuppressLint("MissingPermission")
private void moveToCurrentLocation(boolean animate) {
LocationEngineProvider.getBestLocationEngine(this)
.getLastLocation(
new LocationEngineCallback<LocationEngineResult>() {
@Override
public void onSuccess(LocationEngineResult result) {
Location location = result.getLastLocation();
if (location != null) {
map.movePosition(
new MapPosition(location.getLatitude(), location.getLongitude()), animate);
}
}
@Override
public void onFailure(@NonNull Exception exception) {
toaster.longToast(exception.getMessage());
}
});
}
private void returnPlace(@Nullable org.tasks.data.Place place) {
if (place == null) {
Timber.e("Place is null");
return;
}
if (place.getId() <= 0) {
org.tasks.data.Place existing =
locationDao.findPlace(
GeoUtils.INSTANCE.toLikeString(place.getLatitude()),
GeoUtils.INSTANCE.toLikeString(place.getLongitude()));
if (existing == null) {
place.setId(locationDao.insert(place));
} else {
place = existing;
}
}
hideKeyboard(this);
setResult(RESULT_OK, new Intent().putExtra(EXTRA_PLACE, (Parcelable) place));
finish();
}
@Override
protected void onResume() {
super.onResume();
viewModel.observe(this, searchAdapter::submitList, this::returnPlace, this::handleError);
disposables =
new CompositeDisposable(
searchSubject
.debounce(SEARCH_DEBOUNCE_TIMEOUT, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(query -> viewModel.query(query, mapPosition)));
}
private void handleError(Event<String> error) {
String message = error.getIfUnhandled();
if (!isNullOrEmpty(message)) {
toaster.longToast(message);
}
}
private void updatePlaces(List<PlaceUsage> places) {
this.places = places;
updateMarkers();
recentsAdapter.submitList(places);
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
int height = coordinatorLayout.getHeight();
if (this.places.isEmpty()) {
params.height = height;
chooseRecentLocation.setVisibility(View.GONE);
collapseToolbar();
} else {
params.height = (height * 75) / 100;
chooseRecentLocation.setVisibility(View.VISIBLE);
}
}
private void updateMarkers() {
if (map != null) {
map.setMarkers(transform(places, PlaceUsage::getPlace));
}
}
private void collapseToolbar() {
appBarLayout.setExpanded(true, true);
}
private void expandToolbar(boolean animate) {
appBarLayout.setExpanded(false, animate);
}
@Override
protected void onPause() {
super.onPause();
disposables.dispose();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(EXTRA_MAP_POSITION, map.getMapPosition());
outState.putInt(EXTRA_APPBAR_OFFSET, offset);
viewModel.saveState(outState);
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.menu_search) {
searchPlace();
return true;
} else {
return false;
}
}
@Override
public void picked(org.tasks.data.Place place) {
returnPlace(place);
}
@Override
public void settings(org.tasks.data.Place place) {
Intent intent = new Intent(this, PlaceSettingsActivity.class);
intent.putExtra(PlaceSettingsActivity.EXTRA_PLACE, (Parcelable) place);
startActivity(intent);
}
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String query) {
searchSubject.onNext(query);
return true;
}
@Override
public void picked(PlaceSearchResult prediction) {
viewModel.fetch(prediction);
}
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
searchAdapter.submitList(Collections.emptyList());
recyclerView.setAdapter(searchAdapter);
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
recyclerView.setAdapter(recentsAdapter);
if (places.isEmpty()) {
collapseToolbar();
}
return true;
}
}

@ -0,0 +1,411 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.ContentLoadingProgressBar
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import butterknife.OnClick
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback
import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.mapbox.android.core.location.LocationEngineCallback
import com.mapbox.android.core.location.LocationEngineProvider
import com.mapbox.android.core.location.LocationEngineResult
import com.todoroo.andlib.utility.AndroidUtilities
import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.launch
import org.tasks.Event
import org.tasks.PermissionUtil.verifyPermissions
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.activities.PlaceSettingsActivity
import org.tasks.billing.Inventory
import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.data.LocationDao
import org.tasks.data.Place
import org.tasks.data.PlaceUsage
import org.tasks.dialogs.DialogBuilder
import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.location.LocationPickerAdapter.OnLocationPicked
import org.tasks.location.LocationSearchAdapter.OnPredictionPicked
import org.tasks.location.MapFragment.MapFragmentCallback
import org.tasks.preferences.ActivityPermissionRequestor
import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.PermissionRequestor
import org.tasks.themes.ColorProvider
import org.tasks.themes.Theme
import org.tasks.ui.Toaster
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.abs
@AndroidEntryPoint
class LocationPickerActivity : InjectingAppCompatActivity(), Toolbar.OnMenuItemClickListener, MapFragmentCallback, OnLocationPicked, SearchView.OnQueryTextListener, OnPredictionPicked, MenuItem.OnActionExpandListener {
@BindView(R.id.toolbar)
lateinit var toolbar: Toolbar
@BindView(R.id.app_bar_layout)
lateinit var appBarLayout: AppBarLayout
@BindView(R.id.collapsing_toolbar_layout)
lateinit var toolbarLayout: CollapsingToolbarLayout
@BindView(R.id.coordinator)
lateinit var coordinatorLayout: CoordinatorLayout
@BindView(R.id.search)
lateinit var searchView: View
@BindView(R.id.loading_indicator)
lateinit var loadingIndicator: ContentLoadingProgressBar
@BindView(R.id.choose_recent_location)
lateinit var chooseRecentLocation: View
@BindView(R.id.recent_locations)
lateinit var recyclerView: RecyclerView
@Inject lateinit var theme: Theme
@Inject lateinit var toaster: Toaster
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var searchProvider: PlaceSearchProvider
@Inject lateinit var permissionChecker: PermissionChecker
@Inject lateinit var permissionRequestor: ActivityPermissionRequestor
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var map: MapFragment
@Inject lateinit var geocoder: Geocoder
@Inject lateinit var inventory: Inventory
@Inject lateinit var colorProvider: ColorProvider
private var disposables: CompositeDisposable? = null
private var mapPosition: MapPosition? = null
private var recentsAdapter: LocationPickerAdapter? = null
private var searchAdapter: LocationSearchAdapter? = null
private var places: List<PlaceUsage> = emptyList()
private var offset = 0
private lateinit var search: MenuItem
private val searchSubject = PublishSubject.create<String>()
private val viewModel: PlaceSearchViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
theme.applyTheme(this)
setContentView(R.layout.activity_location_picker)
ButterKnife.bind(this)
val configuration = resources.configuration
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.smallestScreenWidthDp < 480) {
searchView.visibility = View.GONE
}
val currentPlace: Place? = intent.getParcelableExtra(EXTRA_PLACE)
if (savedInstanceState == null) {
mapPosition = currentPlace?.mapPosition ?: intent.getParcelableExtra(EXTRA_MAP_POSITION)
} else {
mapPosition = savedInstanceState.getParcelable(EXTRA_MAP_POSITION)
offset = savedInstanceState.getInt(EXTRA_APPBAR_OFFSET)
viewModel.restoreState(savedInstanceState)
}
toolbar.setNavigationIcon(R.drawable.ic_outline_arrow_back_24px)
toolbar.setNavigationOnClickListener { collapseToolbar() }
toolbar.inflateMenu(R.menu.menu_location_picker)
val menu = toolbar.menu
search = menu.findItem(R.id.menu_search)
search.setOnActionExpandListener(this)
(search.actionView as SearchView).setOnQueryTextListener(this)
toolbar.setOnMenuItemClickListener(this)
val themeColor = theme.themeColor
themeColor.applyToStatusBarIcons(this)
themeColor.applyToNavigationBar(this)
themeColor.setStatusBarColor(toolbarLayout)
themeColor.apply(toolbar)
val dark = theme.themeBase.isDarkTheme(this)
map.init(supportFragmentManager, this, dark)
val params = appBarLayout.layoutParams as CoordinatorLayout.LayoutParams
val behavior = AppBarLayout.Behavior()
behavior.setDragCallback(
object : DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout): Boolean {
return false
}
})
params.behavior = behavior
appBarLayout.addOnOffsetChangedListener(
OnOffsetChangedListener { appBarLayout: AppBarLayout, offset: Int ->
if (offset == 0 && this.offset != 0) {
closeSearch()
AndroidUtilities.hideKeyboard(this)
}
this.offset = offset
toolbar.alpha = abs(offset / appBarLayout.totalScrollRange.toFloat())
})
coordinatorLayout.addOnLayoutChangeListener(
object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View, l: Int, t: Int, r: Int, b: Int, ol: Int, ot: Int, or: Int, ob: Int) {
coordinatorLayout.removeOnLayoutChangeListener(this)
locationDao
.getPlaceUsage()
.observe(this@LocationPickerActivity, Observer { places: List<PlaceUsage> -> updatePlaces(places) })
}
})
if (offset != 0) {
appBarLayout.post { expandToolbar(false) }
}
findViewById<View>(map.markerId).visibility = View.VISIBLE
searchAdapter = LocationSearchAdapter(searchProvider.getAttributionRes(dark), this)
recentsAdapter = LocationPickerAdapter(this, inventory, colorProvider, this)
recentsAdapter!!.setHasStableIds(true)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = if (search.isActionViewExpanded) searchAdapter else recentsAdapter
}
override fun onMapReady(mapFragment: MapFragment) {
map = mapFragment
updateMarkers()
if (permissionChecker.canAccessLocation()) {
mapFragment.showMyLocation()
}
if (mapPosition != null) {
map.movePosition(mapPosition, false)
} else if (permissionRequestor.requestFineLocation()) {
moveToCurrentLocation(false)
}
}
override fun onBackPressed() {
if (closeSearch()) {
return
}
if (offset != 0) {
collapseToolbar()
return
}
super.onBackPressed()
}
private fun closeSearch(): Boolean {
return search.isActionViewExpanded && search.collapseActionView()
}
override fun onPlaceSelected(place: Place) {
returnPlace(place)
}
@OnClick(R.id.current_location)
fun onClick() {
if (permissionRequestor.requestFineLocation()) {
moveToCurrentLocation(true)
}
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (requestCode == PermissionRequestor.REQUEST_LOCATION) {
if (verifyPermissions(grantResults)) {
map.showMyLocation()
moveToCurrentLocation(true)
} else {
dialogBuilder
.newDialog(R.string.missing_permissions)
.setMessage(R.string.location_permission_required_location)
.setPositiveButton(android.R.string.ok, null)
.show()
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
@OnClick(R.id.select_this_location)
fun selectLocation() {
loadingIndicator.visibility = View.VISIBLE
val mapPosition = map.mapPosition
disposables!!.add(
Single.fromCallable { geocoder.reverseGeocode(mapPosition) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { loadingIndicator.visibility = View.GONE }
.subscribe({ place: Place? -> returnPlace(place) }) { e: Throwable -> toaster.longToast(e.message) })
}
@OnClick(R.id.search)
fun searchPlace() {
mapPosition = map.mapPosition
expandToolbar(true)
search.expandActionView()
}
@SuppressLint("MissingPermission")
private fun moveToCurrentLocation(animate: Boolean) {
LocationEngineProvider.getBestLocationEngine(this)
.getLastLocation(
object : LocationEngineCallback<LocationEngineResult> {
override fun onSuccess(result: LocationEngineResult) {
val location = result.lastLocation
if (location != null) {
map.movePosition(
MapPosition(location.latitude, location.longitude), animate)
}
}
override fun onFailure(exception: Exception) {
toaster.longToast(exception.message)
}
})
}
private fun returnPlace(place: Place?) {
if (place == null) {
Timber.e("Place is null")
return
}
AndroidUtilities.hideKeyboard(this)
lifecycleScope.launch {
var place = place
if (place.id <= 0) {
val existing = locationDao.findPlace(
place.latitude.toLikeString(),
place.longitude.toLikeString())
if (existing == null) {
place.id = locationDao.insert(place)
} else {
place = existing
}
}
setResult(Activity.RESULT_OK, Intent().putExtra(EXTRA_PLACE, place as Parcelable?))
finish()
}
}
override fun onResume() {
super.onResume()
viewModel.observe(this, Observer { list: List<PlaceSearchResult?>? -> searchAdapter!!.submitList(list) }, Observer { place: Place? -> returnPlace(place) }, Observer { error: Event<String> -> handleError(error) })
disposables = CompositeDisposable(
searchSubject
.debounce(SEARCH_DEBOUNCE_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { query: String? -> viewModel.query(query, mapPosition) })
}
private fun handleError(error: Event<String>) {
val message = error.ifUnhandled
if (!isNullOrEmpty(message)) {
toaster.longToast(message)
}
}
private fun updatePlaces(places: List<PlaceUsage>) {
this.places = places
updateMarkers()
recentsAdapter!!.submitList(places)
val params = appBarLayout.layoutParams as CoordinatorLayout.LayoutParams
val height = coordinatorLayout.height
if (this.places.isEmpty()) {
params.height = height
chooseRecentLocation.visibility = View.GONE
collapseToolbar()
} else {
params.height = height * 75 / 100
chooseRecentLocation.visibility = View.VISIBLE
}
}
private fun updateMarkers() {
map.setMarkers(places.map(PlaceUsage::place))
}
private fun collapseToolbar() {
appBarLayout.setExpanded(true, true)
}
private fun expandToolbar(animate: Boolean) {
appBarLayout.setExpanded(false, animate)
}
override fun onPause() {
super.onPause()
disposables!!.dispose()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(EXTRA_MAP_POSITION, map.mapPosition)
outState.putInt(EXTRA_APPBAR_OFFSET, offset)
viewModel.saveState(outState)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return if (item.itemId == R.id.menu_search) {
searchPlace()
true
} else {
false
}
}
override fun picked(place: Place) {
returnPlace(place)
}
override fun settings(place: Place) {
val intent = Intent(this, PlaceSettingsActivity::class.java)
intent.putExtra(PlaceSettingsActivity.EXTRA_PLACE, place as Parcelable)
startActivity(intent)
}
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(query: String): Boolean {
searchSubject.onNext(query)
return true
}
override fun picked(prediction: PlaceSearchResult) {
viewModel.fetch(prediction)
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchAdapter!!.submitList(emptyList())
recyclerView.adapter = searchAdapter
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
recyclerView.adapter = recentsAdapter
if (places.isEmpty()) {
collapseToolbar()
}
return true
}
companion object {
const val EXTRA_PLACE = "extra_place"
private const val EXTRA_MAP_POSITION = "extra_map_position"
private const val EXTRA_APPBAR_OFFSET = "extra_appbar_offset"
private const val SEARCH_DEBOUNCE_TIMEOUT = 300
}
}

@ -1,64 +0,0 @@
package org.tasks.location;
import static com.todoroo.andlib.utility.AndroidUtilities.assertMainThread;
import static org.tasks.Strings.isNullOrEmpty;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
import java.util.Collections;
import java.util.List;
import org.tasks.Event;
import org.tasks.data.Place;
@SuppressWarnings({"WeakerAccess", "RedundantSuppression"})
public class PlaceSearchViewModel extends ViewModel {
private PlaceSearchProvider searchProvider;
private final MutableLiveData<List<PlaceSearchResult>> searchResults = new MutableLiveData<>();
private final MutableLiveData<Event<String>> error = new MutableLiveData<>();
private final MutableLiveData<Place> selection = new MutableLiveData<>();
void setSearchProvider(PlaceSearchProvider searchProvider) {
this.searchProvider = searchProvider;
}
void observe(
LifecycleOwner owner,
Observer<List<PlaceSearchResult>> onResults,
Observer<Place> onSelection,
Observer<Event<String>> onError) {
searchResults.observe(owner, onResults);
selection.observe(owner, onSelection);
error.observe(owner, onError);
}
void saveState(Bundle outState) {
searchProvider.saveState(outState);
}
void restoreState(Bundle savedInstanceState) {
searchProvider.restoreState(savedInstanceState);
}
public void query(String query, @Nullable MapPosition bias) {
assertMainThread();
if (isNullOrEmpty(query)) {
searchResults.postValue(Collections.emptyList());
} else {
searchProvider.search(query, bias, searchResults::setValue, this::setError);
}
}
public void fetch(PlaceSearchResult result) {
searchProvider.fetch(result, selection::setValue, this::setError);
}
private void setError(String message) {
error.setValue(new Event<>(message));
}
}

@ -0,0 +1,55 @@
package org.tasks.location
import android.os.Bundle
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import com.todoroo.andlib.utility.AndroidUtilities
import org.tasks.Event
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.Place
class PlaceSearchViewModel @ViewModelInject constructor(
private val searchProvider: PlaceSearchProvider
): ViewModel() {
private val searchResults = MutableLiveData<List<PlaceSearchResult>>()
private val error = MutableLiveData<Event<String>>()
private val selection = MutableLiveData<Place>()
fun observe(
owner: LifecycleOwner?,
onResults: Observer<List<PlaceSearchResult>>?,
onSelection: Observer<Place>?,
onError: Observer<Event<String>>?) {
searchResults.observe(owner!!, onResults!!)
selection.observe(owner, onSelection!!)
error.observe(owner, onError!!)
}
fun saveState(outState: Bundle?) {
searchProvider.saveState(outState)
}
fun restoreState(savedInstanceState: Bundle?) {
searchProvider.restoreState(savedInstanceState)
}
fun query(query: String?, bias: MapPosition?) {
AndroidUtilities.assertMainThread()
if (isNullOrEmpty(query)) {
searchResults.postValue(emptyList())
} else {
searchProvider.search(query, bias, { value: List<PlaceSearchResult> -> searchResults.setValue(value) }) { message: String -> setError(message) }
}
}
fun fetch(result: PlaceSearchResult?) {
searchProvider.fetch(result, { value: Place -> selection.setValue(value) }) { message: String -> setError(message) }
}
private fun setError(message: String) {
error.value = Event(message)
}
}
Loading…
Cancel
Save