New place picker with google and mapbox support

pull/795/head
Alex Baker 7 years ago
parent 1cc5bf2e0e
commit 54fcad1aae

@ -44,10 +44,14 @@ android {
buildTypes {
debug {
resValue 'string', "mapbox_key", project.hasProperty('tasks_mapbox_key') ? tasks_mapbox_key : ''
resValue 'string', "google_key", project.hasProperty('tasks_google_key') ? tasks_google_key : ''
multiDexEnabled true
testCoverageEnabled true
}
release {
resValue 'string', "mapbox_key", tasks_mapbox_key
resValue 'string', "google_key", tasks_google_key
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.pro'
signingConfig signingConfigs.release
@ -175,8 +179,12 @@ dependencies {
googleplayImplementation 'com.crashlytics.sdk.android:crashlytics:2.9.9'
googleplayImplementation "com.google.firebase:firebase-core:16.0.7"
googleplayImplementation "com.google.android.gms:play-services-location:16.0.0"
googleplayImplementation 'com.google.android.gms:play-services-maps:16.1.0'
googleplayImplementation "com.google.android.gms:play-services-auth:16.0.1"
googleplayImplementation "com.google.android.gms:play-services-places:16.0.0"
googleplayImplementation 'com.google.android.libraries.places:places:1.0.0'
googleplayImplementation 'com.google.android.gms:play-services-maps:16.1.0'
googleplayImplementation 'androidx.appcompat:appcompat:1.0.2'
googleplayImplementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-places-v7:0.7.0'
amazonImplementation "com.google.android.gms:play-services-analytics:16.0.7"

@ -24,6 +24,13 @@
<application tools:ignore="GoogleAppIndexingWarning">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_key"/>
<activity
android:name=".location.LocationPicker"/>
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false"/>
@ -36,10 +43,6 @@
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBq82FiK4U8Ke2TV_F8UjDZwgtSy8K9cyQ"/>
<service android:name=".location.GeofenceTransitionsIntentService"/>
<activity android:name=".activities.GoogleTaskListSettingsActivity"/>

@ -17,6 +17,7 @@ import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.internal.disposables.EmptyDisposable;
import io.reactivex.schedulers.Schedulers;
import java.io.IOException;
import javax.inject.Inject;
@ -70,12 +71,27 @@ public class PlayServices {
});
}
public Disposable checkMaps(Activity activity) {
if (preferences.useGooglePlaces() || preferences.useGoogleMaps()) {
return Single.fromCallable(this::refreshAndCheck)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(success -> {
if (!success) {
resolve(activity);
}
});
} else {
return EmptyDisposable.INSTANCE;
}
}
public boolean refreshAndCheck() {
refresh();
return isPlayServicesAvailable();
}
boolean isPlayServicesAvailable() {
public boolean isPlayServicesAvailable() {
return getResult() == ConnectionResult.SUCCESS;
}

@ -0,0 +1,95 @@
package org.tasks.location;
import android.annotation.SuppressLint;
import android.content.Context;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.UiSettings;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapStyleOptions;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import java.util.ArrayList;
import java.util.List;
import org.tasks.R;
import org.tasks.data.Place;
public class GoogleMapFragment implements MapFragment, OnMapReadyCallback, OnMarkerClickListener {
private final Context context;
private final MapFragmentCallback callbacks;
private final boolean dark;
private final List<Marker> markers = new ArrayList<>();
private GoogleMap map;
GoogleMapFragment(
Context context, SupportMapFragment fragment, MapFragmentCallback callbacks, boolean dark) {
this.context = context;
this.callbacks = callbacks;
this.dark = dark;
fragment.getMapAsync(this);
}
@Override
public MapPosition getMapPosition() {
CameraPosition cameraPosition = map.getCameraPosition();
LatLng target = cameraPosition.target;
return new MapPosition(target.latitude, target.longitude, cameraPosition.zoom);
}
@Override
public void movePosition(MapPosition mapPosition, boolean animate) {
CameraUpdate cameraUpdate =
CameraUpdateFactory.newCameraPosition(
CameraPosition.builder()
.target(new LatLng(mapPosition.getLatitude(), mapPosition.getLongitude()))
.zoom(mapPosition.getZoom())
.build());
if (animate) {
map.animateCamera(cameraUpdate);
} else {
map.moveCamera(cameraUpdate);
}
}
@Override
public void setMarkers(List<Place> places) {
for (Marker marker : markers) {
marker.remove();
}
markers.clear();
for (Place place : places) {
Marker marker =
map.addMarker(
new MarkerOptions().position(new LatLng(place.getLatitude(), place.getLongitude())));
marker.setTag(place);
markers.add(marker);
}
}
@SuppressLint("MissingPermission")
@Override
public void onMapReady(GoogleMap googleMap) {
map = googleMap;
map.setMyLocationEnabled(true);
if (dark) {
map.setMapStyle(MapStyleOptions.loadRawResourceStyle(context, R.raw.mapstyle_night));
}
UiSettings uiSettings = map.getUiSettings();
uiSettings.setMyLocationButtonEnabled(false);
uiSettings.setRotateGesturesEnabled(false);
map.setOnMarkerClickListener(this);
callbacks.onMapReady(this);
}
@Override
public boolean onMarkerClick(Marker marker) {
callbacks.onPlaceSelected((Place) marker.getTag());
return true;
}
}

@ -0,0 +1,523 @@
package org.tasks.location;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import static java.util.Arrays.asList;
import static org.tasks.data.Place.newPlace;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.widget.ContentLoadingProgressBar;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.libraries.places.api.Places;
import com.google.android.libraries.places.api.model.Place;
import com.google.android.libraries.places.api.model.Place.Field;
import com.google.android.libraries.places.widget.Autocomplete;
import com.google.android.libraries.places.widget.AutocompleteActivity;
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.AppBarLayout.Behavior;
import com.google.common.base.Strings;
import com.mapbox.api.geocoding.v5.models.CarmenFeature;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.plugins.places.autocomplete.PlaceAutocomplete;
import com.mapbox.mapboxsdk.plugins.places.autocomplete.model.PlaceOptions;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.billing.Inventory;
import org.tasks.data.LocationDao;
import org.tasks.data.PlaceUsage;
import org.tasks.gtasks.PlayServices;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.ForApplication;
import org.tasks.injection.InjectingAppCompatActivity;
import org.tasks.location.LocationPickerAdapter.OnLocationPicked;
import org.tasks.location.MapFragment.MapFragmentCallback;
import org.tasks.preferences.Preferences;
import org.tasks.themes.Theme;
import org.tasks.themes.ThemeColor;
import org.tasks.ui.MenuColorizer;
import org.tasks.ui.Toaster;
import timber.log.Timber;
public class LocationPicker extends InjectingAppCompatActivity
implements OnMenuItemClickListener, MapFragmentCallback, OnLocationPicked {
private static final String EXTRA_MAP_POSITION = "extra_map_position";
private static final String EXTRA_APPBAR_OFFSET = "extra_appbar_offset";
private static final String FRAG_TAG_MAP = "frag_tag_map";
private static final int REQUEST_GOOGLE_AUTOCOMPLETE = 10101;
private static final int REQUEST_MAPBOX_AUTOCOMPLETE = 10102;
private static final Pattern pattern = Pattern.compile("(\\d+):(\\d+):(\\d+\\.\\d+)");
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.app_bar_layout)
AppBarLayout appBarLayout;
@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 recentLocations;
@Inject @ForApplication Context context;
@Inject Theme theme;
@Inject Toaster toaster;
@Inject Inventory inventory;
@Inject PlayServices playServices;
@Inject Preferences preferences;
@Inject LocationDao locationDao;
private MapFragment map;
private FusedLocationProviderClient fusedLocationProviderClient;
private CompositeDisposable disposables;
private MapPosition mapPosition;
private LocationPickerAdapter adapter = new LocationPickerAdapter(this);
private List<PlaceUsage> places = Collections.emptyList();
private int offset;
private static String formatCoordinates(org.tasks.data.Place place) {
return String.format(
"%s %s",
formatCoordinate(place.getLatitude(), true), formatCoordinate(place.getLongitude(), false));
}
private static String formatCoordinate(double coordinates, boolean latitude) {
String output = Location.convert(Math.abs(coordinates), Location.FORMAT_SECONDS);
Matcher matcher = pattern.matcher(output);
if (matcher.matches()) {
return String.format(
"%s°%s'%s\"%s",
matcher.group(1),
matcher.group(2),
matcher.group(3),
latitude ? (coordinates > 0 ? "N" : "S") : (coordinates > 0 ? "E" : "W"));
} else {
return Double.toString(coordinates);
}
}
private boolean canSearch() {
return atLeastLollipop() || inventory.hasPro();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
theme.applyTheme(this);
setContentView(R.layout.activity_location_picker);
ButterKnife.bind(this);
Configuration configuration = getResources().getConfiguration();
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.smallestScreenWidthDp < 480) {
searchView.setVisibility(View.GONE);
}
if (savedInstanceState != null) {
mapPosition = savedInstanceState.getParcelable(EXTRA_MAP_POSITION);
offset = savedInstanceState.getInt(EXTRA_APPBAR_OFFSET);
}
toolbar.setNavigationIcon(R.drawable.ic_outline_arrow_back_24px);
toolbar.setNavigationOnClickListener(v -> collapseToolbar());
if (canSearch()) {
toolbar.inflateMenu(R.menu.menu_location_picker);
toolbar.setOnMenuItemClickListener(this);
} else {
searchView.setVisibility(View.GONE);
}
MenuColorizer.colorToolbar(this, toolbar);
ThemeColor themeColor = theme.getThemeColor();
themeColor.applyToStatusBarIcons(this);
themeColor.applyToNavigationBar(this);
if (preferences.useGoogleMaps()) {
initGoogleMaps();
} else {
initMapboxMaps();
}
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this);
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) -> {
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);
updateAppbarLayout();
}
});
if (offset != 0) {
appBarLayout.post(this::expandToolbar);
}
adapter.setHasStableIds(true);
((DefaultItemAnimator) recentLocations.getItemAnimator()).setSupportsChangeAnimations(false);
recentLocations.setLayoutManager(new LinearLayoutManager(this));
recentLocations.setAdapter(adapter);
}
private void initGoogleMaps() {
FragmentManager supportFragmentManager = getSupportFragmentManager();
SupportMapFragment mapFragment =
(SupportMapFragment) supportFragmentManager.findFragmentByTag(FRAG_TAG_MAP);
if (mapFragment == null) {
mapFragment = new SupportMapFragment();
supportFragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit();
}
new GoogleMapFragment(context, mapFragment, this, theme.getThemeBase().isDarkTheme(this));
}
private void initMapboxMaps() {
Mapbox.getInstance(this, getString(R.string.mapbox_key));
FragmentManager supportFragmentManager = getSupportFragmentManager();
com.mapbox.mapboxsdk.maps.SupportMapFragment mapFragment =
(com.mapbox.mapboxsdk.maps.SupportMapFragment)
supportFragmentManager.findFragmentByTag(FRAG_TAG_MAP);
if (mapFragment == null) {
mapFragment = new com.mapbox.mapboxsdk.maps.SupportMapFragment();
supportFragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit();
}
new MapboxMapFragment(context, mapFragment, this, theme.getThemeBase().isDarkTheme(this));
}
@Override
public void onMapReady(MapFragment mapFragment) {
map = mapFragment;
if (mapPosition != null) {
map.movePosition(mapPosition, false);
} else {
moveToCurrentLocation(false);
}
updateMarkers();
}
@Override
public void onPlaceSelected(org.tasks.data.Place place) {
returnPlace(place);
}
@OnClick(R.id.current_location)
void onClick() {
moveToCurrentLocation(true);
}
@OnClick(R.id.select_this_location)
void selectLocation() {
loadingIndicator.setVisibility(View.VISIBLE);
MapPosition mapPosition = map.getMapPosition();
disposables.add(
Single.fromCallable(
() -> {
Geocoder geocoder = new Geocoder(this);
return geocoder.getFromLocation(
mapPosition.getLatitude(), mapPosition.getLongitude(), 1);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(e -> toaster.longToast(e.getMessage()))
.doFinally(() -> loadingIndicator.setVisibility(View.GONE))
.subscribe(
addresses -> {
org.tasks.data.Place place = newPlace();
if (addresses.isEmpty()) {
place.setLatitude(mapPosition.getLatitude());
place.setLongitude(mapPosition.getLongitude());
} else {
Address address = addresses.get(0);
place.setLatitude(address.getLatitude());
place.setLongitude(address.getLongitude());
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) {
stringBuilder.append(address.getAddressLine(i)).append("\n");
}
place.setPhone(address.getPhone());
place.setAddress(stringBuilder.toString().trim());
String url = address.getUrl();
if (!Strings.isNullOrEmpty(url)) {
place.setUrl(url);
}
}
place.setName(formatCoordinates(place));
returnPlace(place);
}));
}
@OnClick(R.id.search)
void searchPlace() {
if (preferences.useGooglePlaces() && inventory.hasPro()) {
if (!Places.isInitialized()) {
Places.initialize(this, getString(R.string.google_key));
}
startActivityForResult(
new Autocomplete.IntentBuilder(
AutocompleteActivityMode.FULLSCREEN,
asList(
Field.ID,
Field.LAT_LNG,
Field.ADDRESS,
Field.WEBSITE_URI,
Field.NAME,
Field.PHONE_NUMBER))
.build(this),
REQUEST_GOOGLE_AUTOCOMPLETE);
} else {
String token = getString(R.string.mapbox_key);
Mapbox.getInstance(this, token);
MapPosition mapPosition = map.getMapPosition();
startActivityForResult(
new PlaceAutocomplete.IntentBuilder()
.accessToken(token)
.placeOptions(
PlaceOptions.builder()
.backgroundColor(getResources().getColor(R.color.white_100))
.proximity(
Point.fromLngLat(mapPosition.getLongitude(), mapPosition.getLatitude()))
.build())
.build(this),
REQUEST_MAPBOX_AUTOCOMPLETE);
}
}
@SuppressLint("MissingPermission")
private void moveToCurrentLocation(boolean animate) {
fusedLocationProviderClient
.getLastLocation()
.addOnSuccessListener(
location -> {
if (location != null) {
map.movePosition(
new MapPosition(location.getLatitude(), location.getLongitude(), 15f), animate);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_GOOGLE_AUTOCOMPLETE) {
if (resultCode == Activity.RESULT_OK && data != null) {
returnPlace(Autocomplete.getPlaceFromIntent(data));
} else if (resultCode == AutocompleteActivity.RESULT_ERROR && data != null) {
Status status = Autocomplete.getStatusFromIntent(data);
toaster.longToast(status.getStatusMessage());
}
} else if (requestCode == REQUEST_MAPBOX_AUTOCOMPLETE) {
if (resultCode == Activity.RESULT_OK && data != null) {
returnPlace(PlaceAutocomplete.getPlace(data));
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void returnPlace(CarmenFeature place) {
org.tasks.data.Place result = newPlace();
result.setName(place.placeName());
result.setAddress(place.address());
result.setLatitude(place.center().latitude());
result.setLongitude(place.center().longitude());
returnPlace(result);
}
private void returnPlace(Place place) {
LatLng latLng = place.getLatLng();
org.tasks.data.Place result = newPlace();
result.setName(place.getName());
CharSequence address = place.getAddress();
if (address != null) {
result.setAddress(place.getAddress());
}
CharSequence phoneNumber = place.getPhoneNumber();
if (phoneNumber != null) {
result.setPhone(phoneNumber.toString());
}
Uri uri = place.getWebsiteUri();
if (uri != null) {
result.setUrl(uri.toString());
}
result.setLatitude(latLng.latitude);
result.setLongitude(latLng.longitude);
returnPlace(result);
}
private void returnPlace(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(place.getLatitude(), place.getLongitude());
if (existing == null) {
long placeId = locationDao.insert(place);
place.setId(placeId);
} else {
existing.apply(place);
locationDao.update(existing);
place = existing;
}
}
setResult(RESULT_OK, new Intent().putExtra(PlacePicker.EXTRA_PLACE, (Parcelable) place));
finish();
}
@Override
protected void onResume() {
super.onResume();
disposables = new CompositeDisposable(playServices.checkMaps(this));
locationDao.getPlaceUsage().observe(this, this::updatePlaces);
}
private void updatePlaces(List<PlaceUsage> places) {
this.places = places;
updateMarkers();
adapter.submitList(places);
updateAppbarLayout();
if (places.isEmpty()) {
collapseToolbar();
}
}
private void updateMarkers() {
if (map != null) {
map.setMarkers(newArrayList(transform(places, PlaceUsage::getPlace)));
}
}
private void updateAppbarLayout() {
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
if (places.isEmpty()) {
params.height = coordinatorLayout.getHeight();
chooseRecentLocation.setVisibility(View.GONE);
} else {
params.height = (coordinatorLayout.getHeight() * 75) / 100;
chooseRecentLocation.setVisibility(View.VISIBLE);
}
}
private void collapseToolbar() {
appBarLayout.setExpanded(true, true);
}
private void expandToolbar() {
appBarLayout.setExpanded(false, false);
}
@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);
}
@Override
public void inject(ActivityComponent component) {
component.inject(this);
}
@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 delete(org.tasks.data.Place place) {
locationDao.delete(place);
}
}

@ -0,0 +1,93 @@
package org.tasks.location;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.base.Strings;
import org.tasks.R;
import org.tasks.data.Place;
import org.tasks.data.PlaceUsage;
import org.tasks.location.LocationPickerAdapter.PlaceViewHolder;
public class LocationPickerAdapter extends ListAdapter<PlaceUsage, PlaceViewHolder> {
private final OnLocationPicked callback;
LocationPickerAdapter(OnLocationPicked callback) {
super(new DiffCallback());
this.callback = callback;
}
@Override
public long getItemId(int position) {
return getItem(position).place.getId();
}
@NonNull
@Override
public PlaceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new PlaceViewHolder(
LayoutInflater.from(parent.getContext()).inflate(R.layout.row_place, parent, false),
callback);
}
@Override
public void onBindViewHolder(@NonNull PlaceViewHolder holder, int position) {
holder.bind(getItem(position));
}
public interface OnLocationPicked {
void picked(Place place);
void delete(Place place);
}
public static class PlaceViewHolder extends RecyclerView.ViewHolder {
private final TextView name;
private final TextView address;
private final View delete;
private Place place;
PlaceViewHolder(@NonNull View itemView, OnLocationPicked onLocationPicked) {
super(itemView);
itemView.setOnClickListener(v -> onLocationPicked.picked(place));
name = itemView.findViewById(R.id.name);
address = itemView.findViewById(R.id.address);
delete = itemView.findViewById(R.id.delete);
delete.setOnClickListener(v -> onLocationPicked.delete(place));
}
public void bind(PlaceUsage placeUsage) {
this.place = placeUsage.place;
String name = place.getDisplayName();
String address = place.getAddress();
this.name.setText(name);
if (Strings.isNullOrEmpty(address) || address.equals(name)) {
this.address.setVisibility(View.GONE);
} else {
this.address.setText(address);
this.address.setVisibility(View.VISIBLE);
}
delete.setVisibility(placeUsage.count > 0 ? View.GONE : View.VISIBLE);
}
}
public static class DiffCallback extends ItemCallback<PlaceUsage> {
@Override
public boolean areItemsTheSame(@NonNull PlaceUsage oldItem, @NonNull PlaceUsage newItem) {
return oldItem.place.getUid().equals(newItem.place.getUid());
}
@Override
public boolean areContentsTheSame(@NonNull PlaceUsage oldItem, @NonNull PlaceUsage newItem) {
return oldItem.equals(newItem);
}
}
}

@ -0,0 +1,103 @@
package org.tasks.location;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.annotations.MarkerOptions;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdate;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.location.LocationComponent;
import com.mapbox.mapboxsdk.location.modes.CameraMode;
import com.mapbox.mapboxsdk.location.modes.RenderMode;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.MapboxMap.OnMarkerClickListener;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.maps.SupportMapFragment;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.tasks.data.Place;
public class MapboxMapFragment implements MapFragment, OnMapReadyCallback, OnMarkerClickListener {
private final Context context;
private final MapFragmentCallback callbacks;
private final boolean dark;
private MapboxMap map;
private Map<Marker, Place> markers = new HashMap<>();
MapboxMapFragment(
Context context, SupportMapFragment fragment, MapFragmentCallback callbacks, boolean dark) {
this.context = context;
this.callbacks = callbacks;
this.dark = dark;
fragment.getMapAsync(this);
}
@Override
public MapPosition getMapPosition() {
CameraPosition cameraPosition = map.getCameraPosition();
LatLng target = cameraPosition.target;
return new MapPosition(
target.getLatitude(), target.getLongitude(), (float) cameraPosition.zoom);
}
@Override
public void movePosition(MapPosition mapPosition, boolean animate) {
CameraUpdate cameraUpdate =
CameraUpdateFactory.newCameraPosition(
new CameraPosition.Builder()
.target(new LatLng(mapPosition.getLatitude(), mapPosition.getLongitude()))
.zoom(mapPosition.getZoom())
.build());
if (animate) {
map.animateCamera(cameraUpdate);
} else {
map.moveCamera(cameraUpdate);
}
}
@Override
public void setMarkers(List<Place> places) {
for (Marker marker : map.getMarkers()) {
map.removeMarker(marker);
}
markers.clear();
for (Place place : places) {
Marker marker =
map.addMarker(
new MarkerOptions()
.setPosition(new LatLng(place.getLatitude(), place.getLongitude())));
markers.put(marker, place);
}
}
@SuppressLint("MissingPermission")
@Override
public void onMapReady(@NonNull MapboxMap mapboxMap) {
map = mapboxMap;
map.getUiSettings().setRotateGesturesEnabled(false);
map.setStyle(
dark ? Style.DARK : Style.MAPBOX_STREETS,
style -> {
LocationComponent locationComponent = map.getLocationComponent();
locationComponent.activateLocationComponent(context, style);
locationComponent.setLocationComponentEnabled(true);
locationComponent.setCameraMode(CameraMode.NONE);
locationComponent.setRenderMode(RenderMode.NORMAL);
});
map.setOnMarkerClickListener(this);
callbacks.onMapReady(this);
}
@Override
public boolean onMarkerClick(@NonNull Marker marker) {
Place place = markers.get(marker);
callbacks.onPlaceSelected(place);
return false;
}
}

@ -1,15 +1,7 @@
package org.tasks.location;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.widget.Toast;
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
import com.google.android.gms.common.GooglePlayServicesRepairableException;
import com.google.android.gms.location.places.Place;
import com.google.android.gms.maps.model.LatLng;
import com.todoroo.astrid.helper.UUIDHelper;
import org.tasks.R;
import org.tasks.data.Geofence;
import org.tasks.data.Location;
@ -18,26 +10,15 @@ import timber.log.Timber;
public class PlacePicker {
static final String EXTRA_PLACE = "extra_place";
public static Intent getIntent(Activity activity) {
com.google.android.gms.location.places.ui.PlacePicker.IntentBuilder builder =
new com.google.android.gms.location.places.ui.PlacePicker.IntentBuilder();
try {
return builder.build(activity);
} catch (GooglePlayServicesRepairableException e) {
Timber.e(e);
activity.startActivity(e.getIntent());
} catch (GooglePlayServicesNotAvailableException e) {
Timber.e(e);
Toast.makeText(
activity, R.string.common_google_play_services_notification_ticker, Toast.LENGTH_LONG)
.show();
}
return null;
return new Intent(activity, LocationPicker.class);
}
public static Location getPlace(Context context, Intent data, Preferences preferences) {
Place place = com.google.android.gms.location.places.ui.PlacePicker.getPlace(context, data);
LatLng latLng = place.getLatLng();
public static Location getPlace(Intent data, Preferences preferences) {
org.tasks.data.Place result = data.getParcelableExtra(EXTRA_PLACE);
Geofence g = new Geofence();
g.setRadius(preferences.getInt(R.string.p_default_location_radius, 250));
int defaultReminders =
@ -45,25 +26,7 @@ public class PlacePicker {
g.setArrival(defaultReminders == 1 || defaultReminders == 3);
g.setDeparture(defaultReminders == 2 || defaultReminders == 3);
org.tasks.data.Place p = new org.tasks.data.Place();
p.setUid(UUIDHelper.newUUID());
p.setName(place.getName().toString());
CharSequence address = place.getAddress();
if (address != null) {
p.setAddress(place.getAddress().toString());
}
CharSequence phoneNumber = place.getPhoneNumber();
if (phoneNumber != null) {
p.setPhone(phoneNumber.toString());
}
Uri uri = place.getWebsiteUri();
if (uri != null) {
p.setUrl(uri.toString());
}
p.setLatitude(latLng.latitude);
p.setLongitude(latLng.longitude);
Location location = new Location(g, p);
Location location = new Location(g, result);
Timber.i("Picked %s", location);
return location;
}

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#EF5350"
android:pathData="M38.2558,14.5577C38.2558,7.0849 31.944,1.026 24.1527,1.0019V1C24.1454,1 24.1372,1.0009 24.1289,1.0009C24.1217,1.0009 24.1135,1 24.1062,1V1.0019C16.3143,1.026 10.0031,7.0849 10.0031,14.5577C10.0031,14.5577 9.8545,17.9323 11.6149,21.4284C12.9266,24.0362 14.5912,25.7794 16.3003,28.1506C18.9353,31.8058 20.187,33.7253 21.5007,37.2636C22.4339,39.7745 23.3293,42.7376 24.1285,47C24.9286,42.7376 25.8245,39.7745 26.7577,37.2636C28.0728,33.7248 29.3245,31.8054 31.9581,28.1506C33.6672,25.7798 35.3308,24.0362 36.644,21.4284C38.4039,17.9323 38.2558,14.5577 38.2558,14.5577ZM24.1193,19.463C21.2291,19.463 18.8874,17.2184 18.8874,14.4482C18.8874,11.6794 21.2287,9.4358 24.1193,9.4358C27.0104,9.4358 29.3516,11.6794 29.3516,14.4482C29.3516,17.2179 27.0104,19.463 24.1193,19.463Z"
android:strokeWidth="1.0"
android:strokeColor="#ffffff" />
<path
android:fillColor="#952F2D"
android:pathData="M18,14.4166a6,5.75 0,1 0,12 0a6,5.75 0,1 0,-12 0z" />
</vector>

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="?attr/overlay_theme">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:statusBarScrim="?attr/colorPrimaryDark"
app:titleEnabled="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/window_background"
android:fitsSystemWindows="true"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.4">
<FrameLayout
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/select_this_location"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingBottom="48dp"
android:src="@drawable/ic_map_marker_select_red_48dp"
app:layout_constraintBottom_toBottomOf="@id/map"
app:layout_constraintLeft_toLeftOf="@id/map"
app:layout_constraintRight_toRightOf="@id/map"
app:layout_constraintTop_toTopOf="@id/map"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/current_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/task_list_fab_margin"
android:layout_marginBottom="@dimen/keyline_first"
android:layout_marginEnd="@dimen/keyline_first"
android:layout_marginRight="@dimen/keyline_first"
android:layout_gravity="end|bottom"
android:padding="0dp"
android:src="@drawable/ic_outline_gps_fixed_24px"
android:tint="?attr/fab_text"
app:borderWidth="0dp"
app:layout_constraintBottom_toBottomOf="@id/map"
app:layout_constraintRight_toRightOf="@id/map"/>
<!--
This view is a hack, when using a Mapbox fragment for some reason other views
dont collapse under the status bar without this
-->
<View
android:layout_width="match_parent"
android:layout_height="0px"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<RelativeLayout
android:id="@+id/select_this_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/keyline_first"
android:paddingBottom="@dimen/keyline_first"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/choose_recent_location"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/map">
<ImageView
android:id="@+id/place_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/keyline_first"
android:layout_marginLeft="@dimen/keyline_first"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:contentDescription="@string/pick_this_location"
android:src="@drawable/ic_outline_place_24px"
android:tint="@color/icon_tint"/>
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/loading_indicator"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="@dimen/keyline_first"
android:layout_marginRight="@dimen/keyline_first"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:indeterminate="true"
android:visibility="gone"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/keyline_second"
android:layout_marginLeft="@dimen/keyline_second"
android:layout_toEndOf="@id/place_icon"
android:layout_toLeftOf="@id/loading_indicator"
android:layout_toRightOf="@id/place_icon"
android:layout_toStartOf="@id/loading_indicator"
android:gravity="start|center_vertical"
android:text="@string/pick_this_location"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance"/>
</RelativeLayout>
<LinearLayout
android:id="@+id/choose_recent_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:background="@color/window_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/select_this_location">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/keyline_first"
android:layout_marginLeft="@dimen/keyline_first"
android:text="@string/or_choose_a_location"
android:textAppearance="@style/TextAppearance"
android:textColor="@color/text_secondary"
android:textSize="14sp"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="?attr/overlay_theme"
app:layout_collapseMode="pin"
app:popupTheme="@style/popup_overlay"
app:title="@string/choose_a_location"/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="@dimen/keyline_first"
android:layout_marginStart="@dimen/card_view_margin"
android:layout_marginEnd="@dimen/card_view_margin"
app:cardBackgroundColor="@color/window_background"
app:cardElevation="@dimen/elevation_app_bar"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier=".7">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/card_view_margin"
android:layout_marginLeft="@dimen/card_view_margin"
android:layout_gravity="center_vertical"
android:contentDescription="@android:string/search_go"
android:src="@drawable/ic_outline_search_24px"
android:tint="@color/icon_tint"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginLeft="64dp"
android:layout_gravity="center_vertical"
android:text="@android:string/search_go"
android:textAppearance="@style/TextAppearance"
android:textColor="@color/text_primary"/>
</com.google.android.material.card.MaterialCardView>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recent_locations"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/window_background"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/row_place"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/place_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:paddingStart="@dimen/keyline_first"
android:paddingEnd="@dimen/keyline_second"
android:paddingLeft="@dimen/keyline_first"
android:paddingRight="@dimen/keyline_second"
android:src="@drawable/ic_outline_place_24px"
android:tint="@color/icon_tint"/>
<ImageView
android:id="@+id/delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:paddingStart="@dimen/keyline_first"
android:paddingEnd="@dimen/keyline_first"
android:paddingLeft="@dimen/keyline_first"
android:paddingRight="@dimen/keyline_first"
android:src="@drawable/ic_outline_delete_24px"
android:tint="@color/icon_tint"/>
<TextView
android:id="@+id/name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/place_icon"
android:layout_toRightOf="@id/place_icon"
android:layout_toLeftOf="@id/delete"
android:layout_toStartOf="@id/delete"
android:paddingStart="0dp"
android:paddingEnd="@dimen/keyline_first"
android:paddingLeft="0dp"
android:paddingRight="@dimen/keyline_first"
android:ellipsize="end"
android:gravity="start|top"
android:maxLines="2"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"/>
</RelativeLayout>
<TextView
android:id="@+id/address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/keyline_content_inset"
android:paddingEnd="@dimen/keyline_first"
android:paddingLeft="@dimen/keyline_content_inset"
android:paddingRight="@dimen/keyline_first"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/text_secondary"
android:visibility="gone"/>
</LinearLayout>

@ -0,0 +1,193 @@
// https://github.com/googlemaps/android-samples/blob/816307c077060b30a27adc125071d0debd478a98/ApiDemos/java/app/src/main/res/raw/mapstyle_night.json
[
{
"featureType": "all",
"elementType": "geometry",
"stylers": [
{
"color": "#242f3e"
}
]
},
{
"featureType": "all",
"elementType": "labels.text.stroke",
"stylers": [
{
"lightness": -80
}
]
},
{
"featureType": "administrative",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#746855"
}
]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#263c3f"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#6b9a76"
}
]
},
{
"featureType": "road",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#2b3544"
}
]
},
{
"featureType": "road",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9ca5b3"
}
]
},
{
"featureType": "road.arterial",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#38414e"
}
]
},
{
"featureType": "road.arterial",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#212a37"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#746855"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#1f2835"
}
]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#f3d19c"
}
]
},
{
"featureType": "road.local",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#38414e"
}
]
},
{
"featureType": "road.local",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#212a37"
}
]
},
{
"featureType": "transit",
"elementType": "geometry",
"stylers": [
{
"color": "#2f3948"
}
]
},
{
"featureType": "transit.station",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#17263c"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#515c6d"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.stroke",
"stylers": [
{
"lightness": -20
}
]
}
]

@ -9,8 +9,6 @@ package com.todoroo.andlib.utility;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Looper;
import android.text.InputType;
import android.util.DisplayMetrics;
@ -160,10 +158,6 @@ public class AndroidUtilities {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
public static boolean atLeastOreoMR1() {
return VERSION.SDK_INT >= VERSION_CODES.O_MR1;
}
public static boolean atLeastNougat() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}

@ -6,6 +6,8 @@
package com.todoroo.astrid.backup;
import static org.tasks.data.Place.newPlace;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.res.Resources;
@ -247,8 +249,7 @@ public class TasksXmlImporter {
alarm.setTime(xml.readLong("value"));
alarmDao.insert(alarm);
} else if ("geofence".equals(key)) {
Place place = new Place();
place.setUid(UUIDHelper.newUUID());
Place place = newPlace();
place.setName(xml.readString("value"));
place.setLatitude(xml.readDouble("value2"));
place.setLongitude(xml.readDouble("value3"));

@ -1,6 +1,7 @@
package org.tasks.backup;
import static org.tasks.backup.TasksJsonExporter.UTF_8;
import static org.tasks.data.Place.newPlace;
import android.app.Activity;
import android.app.ProgressDialog;
@ -193,8 +194,7 @@ public class TasksJsonImporter {
googleTaskDao.insert(googleTask);
}
for (LegacyLocation location : backup.locations) {
Place place = new Place();
place.setUid(UUIDHelper.newUUID());
Place place = newPlace();
place.setLongitude(location.longitude);
place.setLatitude(location.latitude);
place.setName(location.name);

@ -3,6 +3,7 @@ package org.tasks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.room.Embedded;
import androidx.room.Ignore;
import java.io.Serializable;
public class Location implements Serializable, Parcelable {
@ -25,11 +26,13 @@ public class Location implements Serializable, Parcelable {
public Location() {}
@Ignore
public Location(Geofence geofence, Place place) {
this.geofence = geofence;
this.place = place;
}
@Ignore
protected Location(Parcel in) {
geofence = in.readParcelable(Geofence.class.getClassLoader());
place = in.readParcelable(Place.class.getClassLoader());

@ -1,10 +1,12 @@
package org.tasks.data;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;
import io.reactivex.Single;
import java.util.List;
@ -15,7 +17,8 @@ public interface LocationDao {
"SELECT * FROM geofences INNER JOIN places ON geofences.place = places.uid WHERE geofence_id = :id LIMIT 1")
Location getGeofence(Long id);
@Query("SELECT * FROM geofences INNER JOIN places ON geofences.place = places.uid WHERE task = :taskId ORDER BY name ASC")
@Query(
"SELECT * FROM geofences INNER JOIN places ON geofences.place = places.uid WHERE task = :taskId ORDER BY name ASC")
List<Location> getGeofences(long taskId);
@Query(
@ -32,12 +35,18 @@ public interface LocationDao {
@Delete
void delete(Geofence location);
@Delete
void delete(Place place);
@Insert
void insert(Geofence location);
@Insert(onConflict = OnConflictStrategy.IGNORE)
long insert(Place place);
@Update
void update(Place place);
@Query("SELECT * FROM places WHERE uid = :uid LIMIT 1")
Place getByUid(String uid);
@ -46,4 +55,11 @@ public interface LocationDao {
@Query("SELECT * FROM places")
List<Place> getPlaces();
@Query(
"SELECT places.*, IFNULL(COUNT(geofence_id),0) AS count FROM places LEFT OUTER JOIN geofences ON geofences.place = places.uid GROUP BY uid ORDER BY COUNT(geofence_id) DESC")
LiveData<List<PlaceUsage>> getPlaceUsage();
@Query("SELECT * FROM places WHERE latitude = :latitude AND longitude = :longitude LIMIT 1")
Place findPlace(double latitude, double longitude);
}

@ -8,6 +8,7 @@ import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.google.common.base.Strings;
import com.todoroo.astrid.helper.UUIDHelper;
import java.io.Serializable;
import java.util.regex.Pattern;
@ -55,6 +56,12 @@ public class Place implements Serializable, Parcelable {
@ColumnInfo(name = "longitude")
private double longitude;
public static Place newPlace() {
Place place = new Place();
place.setUid(UUIDHelper.newUUID());
return place;
}
public Place() {}
@Ignore
@ -145,6 +152,18 @@ public class Place implements Serializable, Parcelable {
this.url = url;
}
public void apply(Place place) {
if (Strings.isNullOrEmpty(address)) {
address = place.address;
}
if (Strings.isNullOrEmpty(phone)) {
phone = place.phone;
}
if (Strings.isNullOrEmpty(url)) {
url = place.url;
}
}
public String getDisplayName() {
if (Strings.isNullOrEmpty(address)) {
return name;

@ -0,0 +1,41 @@
package org.tasks.data;
import androidx.room.Embedded;
public class PlaceUsage {
@Embedded public Place place;
public int count;
public Place getPlace() {
return place;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PlaceUsage that = (PlaceUsage) o;
if (count != that.count) {
return false;
}
return place != null ? place.equals(that.place) : that.place == null;
}
@Override
public int hashCode() {
int result = place != null ? place.hashCode() : 0;
result = 31 * result + count;
return result;
}
@Override
public String toString() {
return "PlaceUsage{" + "place=" + place + ", count=" + count + '}';
}
}

@ -11,6 +11,7 @@ import com.todoroo.astrid.gcal.CalendarReminderActivity;
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity;
import com.todoroo.astrid.reminders.ReminderPreferences;
import dagger.Subcomponent;
import org.tasks.location.LocationPicker;
import org.tasks.activities.CalendarSelectionActivity;
import org.tasks.activities.CameraActivity;
import org.tasks.activities.ColorPickerActivity;
@ -140,4 +141,6 @@ public interface ActivityComponent {
void inject(TaskEditActivity taskEditActivity);
void inject(WidgetClickActivity widgetActivity);
void inject(LocationPicker locationPicker);
}

@ -171,6 +171,10 @@ public class Locale {
return NumberFormat.getNumberInstance(appLocale).format(number);
}
public String formatNumber(double number) {
return NumberFormat.getNumberInstance(appLocale).format(number);
}
public Integer parseInteger(String number) {
try {
return NumberFormat.getNumberInstance(appLocale).parse(number).intValue();

@ -28,7 +28,7 @@ public class GeofenceService {
}
public void setupGeofences() {
geofenceApi.register(getActiveGeofences());
geofenceApi.register(locationDao.getActiveGeofences());
}
public void setupGeofences(long taskId) {
@ -81,8 +81,8 @@ public class GeofenceService {
// everything that remains shall be written
for (Location location : locations) {
Place place = location.place;
locationDao.insert(place);
Geofence geofence = location.geofence;
geofence.setTask(taskId);
geofence.setPlace(place.getUid());
locationDao.insert(geofence);
dirty = true;
@ -91,10 +91,6 @@ public class GeofenceService {
return dirty;
}
private List<Location> getActiveGeofences() {
return locationDao.getActiveGeofences();
}
private List<Location> getGeofencesForTask(long taskId) {
return locationDao.getActiveGeofences(taskId);
}

@ -0,0 +1,19 @@
package org.tasks.location;
import java.util.List;
import org.tasks.data.Place;
public interface MapFragment {
MapPosition getMapPosition();
void movePosition(MapPosition mapPosition, boolean animate);
void setMarkers(List<Place> places);
interface MapFragmentCallback {
void onMapReady(MapFragment mapFragment);
void onPlaceSelected(Place place);
}
}

@ -0,0 +1,59 @@
package org.tasks.location;
import android.os.Parcel;
import android.os.Parcelable;
public class MapPosition implements Parcelable {
public static final Creator<MapPosition> CREATOR =
new Creator<MapPosition>() {
@Override
public MapPosition createFromParcel(Parcel in) {
return new MapPosition(in);
}
@Override
public MapPosition[] newArray(int size) {
return new MapPosition[size];
}
};
private final double latitude;
private final double longitude;
private final float zoom;
public MapPosition(double latitude, double longitude, float zoom) {
this.latitude = latitude;
this.longitude = longitude;
this.zoom = zoom;
}
protected MapPosition(Parcel in) {
latitude = in.readDouble();
longitude = in.readDouble();
zoom = in.readFloat();
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
public float getZoom() {
return zoom;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeDouble(latitude);
dest.writeDouble(longitude);
dest.writeFloat(zoom);
}
}

@ -2,6 +2,8 @@ package org.tasks.preferences;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybeanMR1;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import static com.todoroo.andlib.utility.AndroidUtilities.preLollipop;
import static java.util.Arrays.asList;
import static org.tasks.dialogs.ExportTasksDialog.newExportTasksDialog;
import static org.tasks.dialogs.ImportTasksDialog.newImportTasksDialog;
import static org.tasks.files.FileHelper.newFilePickerIntent;
@ -17,9 +19,11 @@ import android.net.Uri;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceScreen;
import com.google.common.base.Strings;
import com.todoroo.astrid.core.OldTaskPreferences;
import com.todoroo.astrid.reminders.ReminderPreferences;
import java.util.List;
import javax.inject.Inject;
import org.tasks.BuildConfig;
import org.tasks.R;
@ -43,6 +47,8 @@ import org.tasks.themes.ThemeAccent;
import org.tasks.themes.ThemeBase;
import org.tasks.themes.ThemeCache;
import org.tasks.themes.ThemeColor;
import org.tasks.ui.SingleCheckedArrayAdapter;
import org.tasks.ui.Toaster;
public class BasicPreferences extends InjectingPreferenceActivity
implements LocalePickerDialog.LocaleSelectionHandler {
@ -70,6 +76,7 @@ public class BasicPreferences extends InjectingPreferenceActivity
@Inject BillingClient billingClient;
@Inject Inventory inventory;
@Inject PlayServices playServices;
@Inject Toaster toaster;
private Bundle result;
@ -209,11 +216,91 @@ public class BasicPreferences extends InjectingPreferenceActivity
});
}
findPreference(R.string.refresh_purchases).setOnPreferenceClickListener(
List<String> choices =
asList(
getString(R.string.map_provider_mapbox),
getString(R.string.map_provider_google));
SingleCheckedArrayAdapter singleCheckedArrayAdapter =
new SingleCheckedArrayAdapter(this, choices, themeAccent);
Preference mapProviderPreference = findPreference(R.string.p_map_provider);
mapProviderPreference.setOnPreferenceClickListener(
preference -> {
billingClient.queryPurchases();
dialogBuilder
.newDialog()
.setSingleChoiceItems(
singleCheckedArrayAdapter,
getMapProvider(),
(dialog, which) -> {
if (which == 0) {
if (preLollipop()) {
toaster.longToast(R.string.requires_android_version, 5.0);
dialog.dismiss();
return;
}
} else if (which == 1) {
if (!playServices.isPlayServicesAvailable()) {
toaster.longToast(R.string.requires_google_play_services);
dialog.dismiss();
return;
}
}
preferences.setInt(R.string.p_map_provider, which);
mapProviderPreference.setSummary(choices.get(which));
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.showThemedListView();
return false;
});
int mapProvider = getMapProvider();
mapProviderPreference.setSummary(
mapProvider == -1 ? getString(R.string.none) : choices.get(mapProvider));
Preference placeProviderPreference = findPreference(R.string.p_place_provider);
placeProviderPreference.setOnPreferenceClickListener(
preference -> {
dialogBuilder
.newDialog()
.setSingleChoiceItems(
singleCheckedArrayAdapter,
getPlaceProvider(),
(dialog, which) -> {
if (which == 0) {
if (preLollipop()) {
toaster.longToast(R.string.requires_android_version, 5.0);
dialog.dismiss();
return;
}
} else if (which == 1) {
if (!playServices.isPlayServicesAvailable()) {
toaster.longToast(R.string.requires_google_play_services);
dialog.dismiss();
return;
}
if (!inventory.hasPro()) {
toaster.longToast(R.string.requires_pro_subscription);
dialog.dismiss();
return;
}
}
preferences.setInt(R.string.p_place_provider, which);
placeProviderPreference.setSummary(choices.get(which));
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.showThemedListView();
return false;
});
int placeProvider = getPlaceProvider();
placeProviderPreference.setSummary(
placeProvider == -1 ? getString(R.string.none) : choices.get(placeProvider));
findPreference(R.string.refresh_purchases)
.setOnPreferenceClickListener(
preference -> {
billingClient.queryPurchases();
return false;
});
requires(
R.string.settings_localization,
@ -233,6 +320,28 @@ public class BasicPreferences extends InjectingPreferenceActivity
R.string.upgrade_to_pro,
R.string.refresh_purchases);
requires(R.string.privacy, false, R.string.p_collect_statistics);
((PreferenceScreen) findPreference(getString(R.string.preference_screen)))
.removePreference(findPreference(getString(R.string.TEA_control_location)));
}
}
private int getPlaceProvider() {
if (playServices.isPlayServicesAvailable()) {
if (preLollipop()) {
return inventory.hasPro() ? 1 : -1;
} else {
return inventory.hasPro() ? preferences.getInt(R.string.p_place_provider, 0) : 0;
}
} else {
return atLeastLollipop() ? 0 : -1;
}
}
private int getMapProvider() {
if (playServices.isPlayServicesAvailable()) {
return preLollipop() ? 1 : preferences.getInt(R.string.p_map_provider, 0);
} else {
return preLollipop() ? -1 : 0;
}
}

@ -4,6 +4,7 @@ import static android.content.SharedPreferences.Editor;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Sets.newHashSet;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat;
import static com.todoroo.andlib.utility.AndroidUtilities.preLollipop;
import static java.util.Collections.emptySet;
import android.content.ContentResolver;
@ -479,4 +480,12 @@ public class Preferences {
public void setSyncOngoing(boolean value) {
setBoolean(R.string.p_sync_ongoing, value);
}
public boolean useGoogleMaps() {
return preLollipop() || getInt(R.string.p_map_provider, 0) == 1;
}
public boolean useGooglePlaces() {
return preLollipop() || getInt(R.string.p_place_provider, 0) == 1;
}
}

@ -141,11 +141,7 @@ public class ThemeColor implements ColorPickerDialog.Pickable {
public void applyToSystemBars(Activity activity) {
setStatusBarColor(activity);
if (atLeastMarshmallow()) {
View decorView = activity.getWindow().getDecorView();
int systemUiVisibility = applyLightStatusBarFlag(decorView.getSystemUiVisibility());
decorView.setSystemUiVisibility(systemUiVisibility);
}
applyToStatusBarIcons(activity);
applyToNavigationBar(activity);
}
@ -166,6 +162,14 @@ public class ThemeColor implements ColorPickerDialog.Pickable {
}
}
public void applyToStatusBarIcons(Activity activity) {
if (atLeastMarshmallow()) {
View decorView = activity.getWindow().getDecorView();
int systemUiVisibility = applyLightStatusBarFlag(decorView.getSystemUiVisibility());
decorView.setSystemUiVisibility(systemUiVisibility);
}
}
public void applyToNavigationBar(Activity activity) {
if (atLeastLollipop()) {
activity.getWindow().setNavigationBarColor(getPrimaryColor());

@ -6,7 +6,6 @@ import static org.tasks.PermissionUtil.verifyPermissions;
import static org.tasks.dialogs.LocationDialog.newLocationDialog;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@ -36,7 +35,6 @@ import org.tasks.R;
import org.tasks.data.Location;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.dialogs.LocationDialog;
import org.tasks.injection.ForActivity;
import org.tasks.injection.FragmentComponent;
import org.tasks.location.GeofenceService;
import org.tasks.location.PlacePicker;
@ -59,7 +57,6 @@ public class LocationControlSet extends TaskEditControlFragment {
@Inject GeofenceService geofenceService;
@Inject FragmentPermissionRequestor permissionRequestor;
@Inject Preferences preferences;
@Inject @ForActivity Context context;
@Inject DialogBuilder dialogBuilder;
@BindView(R.id.alert_container)
@ -149,8 +146,8 @@ public class LocationControlSet extends TaskEditControlFragment {
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_LOCATION_REMINDER) {
if (resultCode == Activity.RESULT_OK) {
Location place = PlacePicker.getPlace(context, data, preferences);
locations.add(place);
locations.clear();
locations.add(PlacePicker.getPlace(data, preferences));
setup(locations);
}
} else if (requestCode == REQUEST_LOCATION_DETAILS) {

@ -20,15 +20,19 @@ public class Toaster {
this.locale = locale;
}
public void longToast(@StringRes int resId) {
longToast(context.getString(resId));
public void longToast(@StringRes int resId, int number) {
longToast(context.getString(resId, locale.formatNumber(number)));
}
public void longToast(@StringRes int resId, int number) {
public void longToast(@StringRes int resId, double number) {
longToast(context.getString(resId, locale.formatNumber(number)));
}
private void longToast(String text) {
public void longToast(@StringRes int resId) {
longToast(context.getString(resId));
}
public void longToast(String text) {
Toast.makeText(context, text, LENGTH_LONG).show();
}

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_search"
android:icon="@drawable/ic_outline_search_24px"
android:title="@string/TLA_menu_search"
app:showAsAction="always"/>
</menu>

@ -16,6 +16,7 @@
<item format="float" name="alpha_disabled" type="dimen">0.38</item>
<dimen name="elevation_toolbar">8dp</dimen>
<dimen name="elevation_app_bar">4dp</dimen>
<dimen name="elevation_task_list">2dp</dimen>
<dimen name="elevation_refresh_indicator">3dp</dimen>
<dimen name="elevation_padding">0dp</dimen>

@ -305,4 +305,10 @@
<string name="p_show_full_description">show_full_description</string>
<string name="p_linkify_task_list">linkify_task_list</string>
<string name="p_show_list_indicators">show_list_indicators</string>
<string name="map_provider_mapbox">Mapbox</string>
<string name="map_provider_google">Google</string>
<string name="p_map_provider">map_provider</string>
<string name="p_place_provider">place_provider</string>
<string name="preference_screen">preference_screen</string>
</resources>

@ -873,4 +873,11 @@ File %1$s contained %2$s.\n\n
<string name="location_arrived">Arrived at %s</string>
<string name="location_departed">Departed %s</string>
<string name="building_notifications">Generating notifications</string>
<string name="choose_a_location">Choose a recent location</string>
<string name="pick_this_location">Select this location</string>
<string name="or_choose_a_location">Or choose a recent location</string>
<string name="map_provider">Map provider</string>
<string name="map_search_provider">Search provider</string>
<string name="requires_android_version">Requires Android %s</string>
<string name="requires_google_play_services">Requires Google Play Services</string>
</resources>

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:key="@string/preference_screen">
<PreferenceCategory android:title="@string/theme">
@ -65,6 +66,22 @@
</PreferenceCategory>
<PreferenceCategory
android:key="@string/TEA_control_location"
android:title="@string/TEA_control_location">
<Preference
android:defaultValue="-1"
android:key="@string/p_map_provider"
android:title="@string/map_provider" />
<Preference
android:defaultValue="-1"
android:key="@string/p_place_provider"
android:title="@string/map_search_provider" />
</PreferenceCategory>
<PreferenceCategory
android:key="@string/backup_BPr_header"
android:title="@string/backup_BPr_header">

Loading…
Cancel
Save