diff --git a/app/src/main/java/org/tasks/data/LocationDaoBlocking.kt b/app/src/main/java/org/tasks/data/LocationDaoBlocking.kt deleted file mode 100644 index 01ad74ffa..000000000 --- a/app/src/main/java/org/tasks/data/LocationDaoBlocking.kt +++ /dev/null @@ -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> { - return dao.getPlaceUsage() - } - - fun findPlace(latitude: String, longitude: String): Place? = runBlocking { - dao.findPlace(latitude, longitude) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/location/LocationPickerActivity.java b/app/src/main/java/org/tasks/location/LocationPickerActivity.java deleted file mode 100644 index 3efb5bb1d..000000000 --- a/app/src/main/java/org/tasks/location/LocationPickerActivity.java +++ /dev/null @@ -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 places = Collections.emptyList(); - private int offset; - private MenuItem search; - private final PublishSubject 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() { - @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 error) { - String message = error.getIfUnhandled(); - if (!isNullOrEmpty(message)) { - toaster.longToast(message); - } - } - - private void updatePlaces(List 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; - } -} diff --git a/app/src/main/java/org/tasks/location/LocationPickerActivity.kt b/app/src/main/java/org/tasks/location/LocationPickerActivity.kt new file mode 100644 index 000000000..e9c110fc9 --- /dev/null +++ b/app/src/main/java/org/tasks/location/LocationPickerActivity.kt @@ -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 = emptyList() + private var offset = 0 + private lateinit var search: MenuItem + private val searchSubject = PublishSubject.create() + 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 -> updatePlaces(places) }) + } + }) + if (offset != 0) { + appBarLayout.post { expandToolbar(false) } + } + findViewById(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, 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 { + 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? -> searchAdapter!!.submitList(list) }, Observer { place: Place? -> returnPlace(place) }, Observer { error: Event -> 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) { + val message = error.ifUnhandled + if (!isNullOrEmpty(message)) { + toaster.longToast(message) + } + } + + private fun updatePlaces(places: List) { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/location/PlaceSearchViewModel.java b/app/src/main/java/org/tasks/location/PlaceSearchViewModel.java deleted file mode 100644 index 2a96fe56e..000000000 --- a/app/src/main/java/org/tasks/location/PlaceSearchViewModel.java +++ /dev/null @@ -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> searchResults = new MutableLiveData<>(); - private final MutableLiveData> error = new MutableLiveData<>(); - private final MutableLiveData selection = new MutableLiveData<>(); - - void setSearchProvider(PlaceSearchProvider searchProvider) { - this.searchProvider = searchProvider; - } - - void observe( - LifecycleOwner owner, - Observer> onResults, - Observer onSelection, - Observer> 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)); - } -} diff --git a/app/src/main/java/org/tasks/location/PlaceSearchViewModel.kt b/app/src/main/java/org/tasks/location/PlaceSearchViewModel.kt new file mode 100644 index 000000000..82b546a99 --- /dev/null +++ b/app/src/main/java/org/tasks/location/PlaceSearchViewModel.kt @@ -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>() + private val error = MutableLiveData>() + private val selection = MutableLiveData() + + fun observe( + owner: LifecycleOwner?, + onResults: Observer>?, + onSelection: Observer?, + onError: Observer>?) { + 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 -> 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) + } +} \ No newline at end of file