mirror of https://github.com/tasks/tasks
Convert LocationPickerActivity to Kotlin
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…
Reference in New Issue