From e28d93eb566a22f9ca85712492c71525f99a3b52 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Thu, 4 Apr 2019 12:31:03 -0500 Subject: [PATCH] Location picker changes * Request location permission before opening geofence settings * Enable maps in generic build (Android 5+) * Resolve Play Services errors in settings --- .../java/org/tasks/location/GeofenceApi.java | 6 +- .../org/tasks/location/GoogleMapFragment.java | 5 + .../location/GooglePlacesSearchProvider.java | 5 + app/src/generic/res/values-v21/bools.xml | 4 + app/src/generic/res/values/bools.xml | 5 + app/src/googleplay/res/values/bools.xml | 3 +- .../astrid/core/DefaultsPreferences.java | 2 +- .../main/java/org/tasks/data/Geofence.java | 8 ++ ...ocationDialog.java => GeofenceDialog.java} | 37 +------ .../TaskEditControlSetFragmentManager.java | 12 ++- .../org/tasks/injection/ActivityModule.java | 9 -- .../injection/DialogFragmentComponent.java | 4 +- .../tasks/preferences/BasicPreferences.java | 9 +- .../java/org/tasks/preferences/Device.java | 12 +-- .../java/org/tasks/ui/LocationControlSet.java | 100 +++++++++++++----- .../ic_map_marker_select_red_48dp.xml | 4 +- app/src/main/res/layout/location_details.xml | 1 + app/src/main/res/layout/location_row.xml | 10 +- app/src/main/res/values/bools.xml | 1 - app/src/main/res/values/strings.xml | 1 - 20 files changed, 145 insertions(+), 93 deletions(-) create mode 100644 app/src/generic/res/values-v21/bools.xml create mode 100644 app/src/generic/res/values/bools.xml rename app/src/main/java/org/tasks/dialogs/{LocationDialog.java => GeofenceDialog.java} (78%) diff --git a/app/src/generic/java/org/tasks/location/GeofenceApi.java b/app/src/generic/java/org/tasks/location/GeofenceApi.java index e77a061a1..47d91f35f 100644 --- a/app/src/generic/java/org/tasks/location/GeofenceApi.java +++ b/app/src/generic/java/org/tasks/location/GeofenceApi.java @@ -14,5 +14,9 @@ public class GeofenceApi { public void cancel(Location geofence) {} - public void cancel(List geofences) {} + public void registerAll() {} + + public void cancel(long taskId) {} + + public void register(long taskId) {} } diff --git a/app/src/generic/java/org/tasks/location/GoogleMapFragment.java b/app/src/generic/java/org/tasks/location/GoogleMapFragment.java index 692d6b4ce..343ba1af4 100644 --- a/app/src/generic/java/org/tasks/location/GoogleMapFragment.java +++ b/app/src/generic/java/org/tasks/location/GoogleMapFragment.java @@ -25,4 +25,9 @@ public class GoogleMapFragment implements MapFragment { @Override public void showMyLocation() {} + + @Override + public int getMarkerId() { + return 0; + } } diff --git a/app/src/generic/java/org/tasks/location/GooglePlacesSearchProvider.java b/app/src/generic/java/org/tasks/location/GooglePlacesSearchProvider.java index c89a6e99a..7ff0ca69b 100644 --- a/app/src/generic/java/org/tasks/location/GooglePlacesSearchProvider.java +++ b/app/src/generic/java/org/tasks/location/GooglePlacesSearchProvider.java @@ -16,6 +16,11 @@ public class GooglePlacesSearchProvider implements PlaceSearchProvider { @Override public void saveState(Bundle outState) {} + @Override + public int getAttributionRes(boolean dark) { + return 0; + } + @Override public void search( String query, diff --git a/app/src/generic/res/values-v21/bools.xml b/app/src/generic/res/values-v21/bools.xml new file mode 100644 index 000000000..b7a44fba9 --- /dev/null +++ b/app/src/generic/res/values-v21/bools.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/generic/res/values/bools.xml b/app/src/generic/res/values/bools.xml new file mode 100644 index 000000000..9d507f477 --- /dev/null +++ b/app/src/generic/res/values/bools.xml @@ -0,0 +1,5 @@ + + + false + false + \ No newline at end of file diff --git a/app/src/googleplay/res/values/bools.xml b/app/src/googleplay/res/values/bools.xml index fa6f32e79..a583e9e13 100644 --- a/app/src/googleplay/res/values/bools.xml +++ b/app/src/googleplay/res/values/bools.xml @@ -1,4 +1,5 @@ - true + true + true \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/core/DefaultsPreferences.java b/app/src/main/java/com/todoroo/astrid/core/DefaultsPreferences.java index 5c3b6662f..0d98d9d00 100644 --- a/app/src/main/java/com/todoroo/astrid/core/DefaultsPreferences.java +++ b/app/src/main/java/com/todoroo/astrid/core/DefaultsPreferences.java @@ -99,7 +99,7 @@ public class DefaultsPreferences extends InjectingPreferenceActivity updateRadius(); requires(syncAdapters.isSyncEnabled(), R.string.p_default_remote_list); - requires(device.supportsLocationServices(), R.string.p_default_location_reminder_key); + requires(device.supportsGeofences(), R.string.p_default_location_reminder_key); } private void startCalendarSelectionActivity() { diff --git a/app/src/main/java/org/tasks/data/Geofence.java b/app/src/main/java/org/tasks/data/Geofence.java index c3d8ad6cd..d0f687ff4 100644 --- a/app/src/main/java/org/tasks/data/Geofence.java +++ b/app/src/main/java/org/tasks/data/Geofence.java @@ -45,6 +45,14 @@ public class Geofence implements Serializable, Parcelable { public Geofence() {} + @Ignore + public Geofence(String place, boolean arrival, boolean departure, int radius) { + this.place = place; + this.arrival = arrival; + this.departure = departure; + this.radius = radius; + } + @Ignore public Geofence(Geofence o) { id = o.id; diff --git a/app/src/main/java/org/tasks/dialogs/LocationDialog.java b/app/src/main/java/org/tasks/dialogs/GeofenceDialog.java similarity index 78% rename from app/src/main/java/org/tasks/dialogs/LocationDialog.java rename to app/src/main/java/org/tasks/dialogs/GeofenceDialog.java index 509e3ab87..05849fef7 100644 --- a/app/src/main/java/org/tasks/dialogs/LocationDialog.java +++ b/app/src/main/java/org/tasks/dialogs/GeofenceDialog.java @@ -1,7 +1,6 @@ package org.tasks.dialogs; import static android.app.Activity.RESULT_OK; -import static org.tasks.PermissionUtil.verifyPermissions; import static org.tasks.dialogs.SeekBarDialog.newSeekBarDialog; import android.app.Dialog; @@ -18,7 +17,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import butterknife.BindView; import butterknife.ButterKnife; -import butterknife.OnCheckedChanged; import butterknife.OnClick; import javax.inject.Inject; import org.tasks.R; @@ -28,12 +26,10 @@ import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.ForActivity; import org.tasks.injection.InjectingDialogFragment; import org.tasks.locale.Locale; -import org.tasks.preferences.FragmentPermissionRequestor; import org.tasks.preferences.PermissionChecker; -import org.tasks.preferences.PermissionRequestor; import org.tasks.ui.Toaster; -public class LocationDialog extends InjectingDialogFragment { +public class GeofenceDialog extends InjectingDialogFragment { public static final String EXTRA_GEOFENCE = "extra_geofence"; private static final String EXTRA_ORIGINAL = "extra_original"; @@ -45,7 +41,6 @@ public class LocationDialog extends InjectingDialogFragment { @Inject @ForActivity Context context; @Inject Locale locale; @Inject PermissionChecker permissionChecker; - @Inject FragmentPermissionRequestor permissionRequestor; @Inject Toaster toaster; @BindView(R.id.location_arrival) @@ -57,21 +52,14 @@ public class LocationDialog extends InjectingDialogFragment { @BindView(R.id.location_radius_value) TextView radiusValue; - public static LocationDialog newLocationDialog(Location location) { - LocationDialog dialog = new LocationDialog(); + public static GeofenceDialog newGeofenceDialog(Location location) { + GeofenceDialog dialog = new GeofenceDialog(); Bundle args = new Bundle(); args.putParcelable(EXTRA_ORIGINAL, location); dialog.setArguments(args); return dialog; } - @OnCheckedChanged({R.id.location_arrival, R.id.location_departure}) - void geofenceCheckedChanged(boolean enabled) { - if (enabled) { - permissionRequestor.requestFineLocation(); - } - } - @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { @@ -130,24 +118,7 @@ public class LocationDialog extends InjectingDialogFragment { super.onResume(); if (!permissionChecker.canAccessLocation()) { - arrivalView.setChecked(false); - departureView.setChecked(false); - } - } - - @Override - public void onRequestPermissionsResult( - int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == PermissionRequestor.REQUEST_LOCATION) { - if (!verifyPermissions(grantResults)) { - dialogBuilder - .newMessageDialog(R.string.location_permission_required_geofence) - .setTitle(R.string.missing_permissions) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - } else { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); + dismiss(); } } diff --git a/app/src/main/java/org/tasks/fragments/TaskEditControlSetFragmentManager.java b/app/src/main/java/org/tasks/fragments/TaskEditControlSetFragmentManager.java index 55d6fadc6..68a496479 100644 --- a/app/src/main/java/org/tasks/fragments/TaskEditControlSetFragmentManager.java +++ b/app/src/main/java/org/tasks/fragments/TaskEditControlSetFragmentManager.java @@ -17,8 +17,10 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import javax.inject.Inject; import org.tasks.BuildConfig; import org.tasks.R; +import org.tasks.injection.ForActivity; import org.tasks.preferences.Device; import org.tasks.preferences.Preferences; import org.tasks.sync.SyncAdapters; @@ -80,12 +82,18 @@ public class TaskEditControlSetFragmentManager { private final Context context; private final List displayOrder; private final SyncAdapters syncAdapters; + private final Device device; private int numRows; + @Inject public TaskEditControlSetFragmentManager( - Context context, Preferences preferences, SyncAdapters syncAdapters) { + @ForActivity Context context, + Preferences preferences, + SyncAdapters syncAdapters, + Device device) { this.context = context; this.syncAdapters = syncAdapters; + this.device = device; displayOrder = BeastModePreferences.constructOrderedControlList(preferences, context); displayOrder.add(0, context.getString(EditTitleControlSet.TAG)); displayOrder.add(1, context.getString(CommentBarFragment.TAG)); @@ -154,7 +162,7 @@ public class TaskEditControlSetFragmentManager { case ReminderControlSet.TAG: return new ReminderControlSet(); case LocationControlSet.TAG: - return Device.SupportsLocationServices(context) ? new LocationControlSet() : null; + return device.supportsMaps() ? new LocationControlSet() : null; case FilesControlSet.TAG: return new FilesControlSet(); case TimerControlSet.TAG: diff --git a/app/src/main/java/org/tasks/injection/ActivityModule.java b/app/src/main/java/org/tasks/injection/ActivityModule.java index b334bfe9b..f87f4e90a 100644 --- a/app/src/main/java/org/tasks/injection/ActivityModule.java +++ b/app/src/main/java/org/tasks/injection/ActivityModule.java @@ -6,7 +6,6 @@ import dagger.Module; import dagger.Provides; import org.tasks.R; import org.tasks.billing.Inventory; -import org.tasks.fragments.TaskEditControlSetFragmentManager; import org.tasks.gtasks.PlayServices; import org.tasks.location.GoogleMapFragment; import org.tasks.location.GooglePlacesSearchProvider; @@ -15,7 +14,6 @@ import org.tasks.location.MapboxMapFragment; import org.tasks.location.MapboxSearchProvider; import org.tasks.location.PlaceSearchProvider; import org.tasks.preferences.Preferences; -import org.tasks.sync.SyncAdapters; import org.tasks.themes.ThemeAccent; import org.tasks.themes.ThemeBase; import org.tasks.themes.ThemeCache; @@ -59,13 +57,6 @@ public class ActivityModule { return themeCache.getThemeAccent(preferences.getInt(R.string.p_theme_accent, 1)); } - @Provides - @ActivityScope - public TaskEditControlSetFragmentManager getTaskEditControlSetFragmentManager( - Preferences preferences, SyncAdapters syncAdapters) { - return new TaskEditControlSetFragmentManager(activity, preferences, syncAdapters); - } - @Provides @ActivityScope public PlaceSearchProvider getPlaceSearchProvider( diff --git a/app/src/main/java/org/tasks/injection/DialogFragmentComponent.java b/app/src/main/java/org/tasks/injection/DialogFragmentComponent.java index 052805f30..429cce574 100644 --- a/app/src/main/java/org/tasks/injection/DialogFragmentComponent.java +++ b/app/src/main/java/org/tasks/injection/DialogFragmentComponent.java @@ -5,7 +5,7 @@ import org.tasks.activities.CalendarSelectionDialog; import org.tasks.activities.RemoteListSupportPicker; import org.tasks.dialogs.AddAttachmentDialog; import org.tasks.dialogs.ColorPickerDialog; -import org.tasks.dialogs.LocationDialog; +import org.tasks.dialogs.GeofenceDialog; import org.tasks.dialogs.RecordAudioDialog; import org.tasks.dialogs.SeekBarDialog; import org.tasks.dialogs.SortDialog; @@ -37,7 +37,7 @@ public interface DialogFragmentComponent { void inject(BasicRecurrenceDialog basicRecurrenceDialog); - void inject(LocationDialog locationDialog); + void inject(GeofenceDialog geofenceDialog); void inject(SeekBarDialog seekBarDialog); } diff --git a/app/src/main/java/org/tasks/preferences/BasicPreferences.java b/app/src/main/java/org/tasks/preferences/BasicPreferences.java index 70c1a92f9..233bc9109 100644 --- a/app/src/main/java/org/tasks/preferences/BasicPreferences.java +++ b/app/src/main/java/org/tasks/preferences/BasicPreferences.java @@ -77,6 +77,7 @@ public class BasicPreferences extends InjectingPreferenceActivity @Inject Inventory inventory; @Inject PlayServices playServices; @Inject Toaster toaster; + @Inject Device device; private Bundle result; @@ -238,8 +239,8 @@ public class BasicPreferences extends InjectingPreferenceActivity return; } } else if (which == 1) { - if (!playServices.isPlayServicesAvailable()) { - toaster.longToast(R.string.requires_google_play_services); + if (!playServices.refreshAndCheck()) { + playServices.resolve(this); dialog.dismiss(); return; } @@ -272,8 +273,8 @@ public class BasicPreferences extends InjectingPreferenceActivity return; } } else if (which == 1) { - if (!playServices.isPlayServicesAvailable()) { - toaster.longToast(R.string.requires_google_play_services); + if (!playServices.refreshAndCheck()) { + playServices.resolve(this); dialog.dismiss(); return; } diff --git a/app/src/main/java/org/tasks/preferences/Device.java b/app/src/main/java/org/tasks/preferences/Device.java index d9e1b2d0c..ff4bd5542 100644 --- a/app/src/main/java/org/tasks/preferences/Device.java +++ b/app/src/main/java/org/tasks/preferences/Device.java @@ -29,10 +29,6 @@ public class Device { this.locale = locale; } - public static boolean SupportsLocationServices(Context context) { - return context.getResources().getBoolean(R.bool.location_enabled); - } - public boolean hasCamera() { return context .getPackageManager() @@ -47,8 +43,12 @@ public class Device { return intent.resolveActivity(context.getPackageManager()) != null; } - public boolean supportsLocationServices() { - return SupportsLocationServices(context); + public boolean supportsMaps() { + return context.getResources().getBoolean(R.bool.support_maps); + } + + public boolean supportsGeofences() { + return context.getResources().getBoolean(R.bool.support_geofences); } public boolean voiceInputAvailable() { diff --git a/app/src/main/java/org/tasks/ui/LocationControlSet.java b/app/src/main/java/org/tasks/ui/LocationControlSet.java index 8900e36b2..85ce8db64 100644 --- a/app/src/main/java/org/tasks/ui/LocationControlSet.java +++ b/app/src/main/java/org/tasks/ui/LocationControlSet.java @@ -2,7 +2,8 @@ package org.tasks.ui; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.transform; -import static org.tasks.dialogs.LocationDialog.newLocationDialog; +import static org.tasks.PermissionUtil.verifyPermissions; +import static org.tasks.dialogs.GeofenceDialog.newGeofenceDialog; import static org.tasks.location.LocationPickerActivity.EXTRA_PLACE; import android.app.Activity; @@ -36,17 +37,21 @@ import org.tasks.data.Location; import org.tasks.data.LocationDao; import org.tasks.data.Place; import org.tasks.dialogs.DialogBuilder; -import org.tasks.dialogs.LocationDialog; +import org.tasks.dialogs.GeofenceDialog; import org.tasks.injection.FragmentComponent; import org.tasks.location.GeofenceApi; import org.tasks.location.LocationPickerActivity; +import org.tasks.preferences.Device; +import org.tasks.preferences.FragmentPermissionRequestor; +import org.tasks.preferences.PermissionChecker; +import org.tasks.preferences.PermissionRequestor; import org.tasks.preferences.Preferences; public class LocationControlSet extends TaskEditControlFragment { public static final int TAG = R.string.TEA_ctrl_locations_pref; private static final int REQUEST_LOCATION_REMINDER = 12153; - private static final int REQUEST_LOCATION_DETAILS = 12154; + private static final int REQUEST_GEOFENCE_DETAILS = 12154; private static final String FRAG_TAG_LOCATION_DIALOG = "location_dialog"; private static final String EXTRA_ORIGINAL = "extra_original_location"; private static final String EXTRA_LOCATION = "extra_new_location"; @@ -55,6 +60,9 @@ public class LocationControlSet extends TaskEditControlFragment { @Inject DialogBuilder dialogBuilder; @Inject GeofenceApi geofenceApi; @Inject LocationDao locationDao; + @Inject Device device; + @Inject FragmentPermissionRequestor permissionRequestor; + @Inject PermissionChecker permissionChecker; @BindView(R.id.location_name) TextView locationName; @@ -62,8 +70,8 @@ public class LocationControlSet extends TaskEditControlFragment { @BindView(R.id.location_address) TextView locationAddress; - @BindView(R.id.location_more) - ImageView locationOptions; + @BindView(R.id.geofence_options) + ImageView geofenceOptions; private Location original; private Location location; @@ -78,27 +86,39 @@ public class LocationControlSet extends TaskEditControlFragment { if (!task.isNew()) { original = locationDao.getGeofences(task.getId()); if (original != null) { - setLocation(new Location(original.geofence, original.place)); + location = new Location(original.geofence, original.place); } } } else { original = savedInstanceState.getParcelable(EXTRA_ORIGINAL); - setLocation(savedInstanceState.getParcelable(EXTRA_LOCATION)); + location = savedInstanceState.getParcelable(EXTRA_LOCATION); } return view; } + @Override + public void onResume() { + super.onResume(); + + updateUi(); + } + private void setLocation(@Nullable Location location) { this.location = location; + updateUi(); + } + + private void updateUi() { if (this.location == null) { locationName.setText(""); - locationOptions.setVisibility(View.GONE); + geofenceOptions.setVisibility(View.GONE); locationAddress.setVisibility(View.GONE); } else { - locationOptions.setVisibility(View.VISIBLE); - locationOptions.setImageResource( - this.location.isArrival() || this.location.isDeparture() + geofenceOptions.setVisibility(device.supportsGeofences() ? View.VISIBLE : View.GONE); + geofenceOptions.setImageResource( + permissionChecker.canAccessLocation() + && (this.location.isArrival() || this.location.isDeparture()) ? R.drawable.ic_outline_notifications_24px : R.drawable.ic_outline_notifications_off_24px); String name = this.location.getDisplayName(); @@ -155,10 +175,34 @@ public class LocationControlSet extends TaskEditControlFragment { startActivityForResult(intent, REQUEST_LOCATION_REMINDER); } - @OnClick(R.id.location_more) - void locationOptions(View view) { - LocationDialog dialog = newLocationDialog(location); - dialog.setTargetFragment(this, REQUEST_LOCATION_DETAILS); + @OnClick(R.id.geofence_options) + void geofenceOptions(View view) { + if (permissionRequestor.requestFineLocation()) { + showGeofenceOptions(); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == PermissionRequestor.REQUEST_LOCATION) { + if (verifyPermissions(grantResults)) { + showGeofenceOptions(); + } else { + dialogBuilder + .newMessageDialog(R.string.location_permission_required_geofence) + .setTitle(R.string.missing_permissions) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + private void showGeofenceOptions() { + GeofenceDialog dialog = newGeofenceDialog(location); + dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS); dialog.show(getFragmentManager(), FRAG_TAG_LOCATION_DIALOG); } @@ -237,25 +281,31 @@ public class LocationControlSet extends TaskEditControlFragment { if (requestCode == REQUEST_LOCATION_REMINDER) { if (resultCode == Activity.RESULT_OK) { Place place = data.getParcelableExtra(EXTRA_PLACE); - Geofence geofence = new Geofence(); + Geofence geofence; if (location == null) { - geofence.setRadius(preferences.getInt(R.string.p_default_location_radius, 250)); int defaultReminders = preferences.getIntegerFromString(R.string.p_default_location_reminder_key, 1); - geofence.setArrival(defaultReminders == 1 || defaultReminders == 3); - geofence.setDeparture(defaultReminders == 2 || defaultReminders == 3); + geofence = + new Geofence( + place.getUid(), + defaultReminders == 1 || defaultReminders == 3, + defaultReminders == 2 || defaultReminders == 3, + preferences.getInt(R.string.p_default_location_radius, 250)); } else { Geofence existing = location.geofence; - geofence.setArrival(existing.isArrival()); - geofence.setDeparture(existing.isDeparture()); - geofence.setRadius(existing.getRadius()); + geofence = + new Geofence( + place.getUid(), + existing.isArrival(), + existing.isDeparture(), + existing.getRadius()); } setLocation(new Location(geofence, place)); } - } else if (requestCode == REQUEST_LOCATION_DETAILS) { + } else if (requestCode == REQUEST_GEOFENCE_DETAILS) { if (resultCode == Activity.RESULT_OK) { - location.geofence = data.getParcelableExtra(LocationDialog.EXTRA_GEOFENCE); - setLocation(location); + location.geofence = data.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE); + updateUi(); } } else { super.onActivityResult(requestCode, resultCode, data); diff --git a/app/src/main/res/drawable/ic_map_marker_select_red_48dp.xml b/app/src/main/res/drawable/ic_map_marker_select_red_48dp.xml index 339fce0a4..1cfd2a190 100644 --- a/app/src/main/res/drawable/ic_map_marker_select_red_48dp.xml +++ b/app/src/main/res/drawable/ic_map_marker_select_red_48dp.xml @@ -4,11 +4,11 @@ android:viewportWidth="48" android:viewportHeight="48"> diff --git a/app/src/main/res/layout/location_details.xml b/app/src/main/res/layout/location_details.xml index 8edb17f3a..f1876d46b 100644 --- a/app/src/main/res/layout/location_details.xml +++ b/app/src/main/res/layout/location_details.xml @@ -7,6 +7,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingTop="@dimen/header_gap" android:orientation="vertical"> false - false false \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e32eda428..fb9386031 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -878,7 +878,6 @@ File %1$s contained %2$s.\n\n Map provider Search provider Requires Android %s - Requires Google Play Services Missing permissions Location permissions are needed for location reminders Location permissions are needed to find your current location