From 59c68f7b41175e0244550db5797683db568a61f8 Mon Sep 17 00:00:00 2001 From: Andrew Shkrob Date: Tue, 26 Dec 2023 01:23:25 +0100 Subject: [PATCH] [android-auto] Better permissions request Signed-off-by: Andrew Shkrob --- android/app/src/main/AndroidManifest.xml | 2 + .../app/organicmaps/car/CarAppService.java | 15 +- .../app/organicmaps/car/CarAppSession.java | 12 +- .../organicmaps/car/screens/ErrorScreen.java | 3 +- .../RequestPermissionsActivity.java | 72 +++++++ .../RequestPermissionsScreenBuilder.java | 29 +++ .../RequestPermissionsScreenWithApi.java} | 12 +- ...uestPermissionsScreenWithNotification.java | 124 ++++++++++++ .../car/util/UserActionRequired.java | 7 + .../ic_location_permission_request.xml | 22 +++ .../layout/activity_request_permissions.xml | 34 ++++ .../src/main/res/values/donottranslate.xml | 3 +- data/strings/strings.txt | 177 +++++++++++++----- 13 files changed, 447 insertions(+), 65 deletions(-) create mode 100644 android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsActivity.java create mode 100644 android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenBuilder.java rename android/app/src/main/java/app/organicmaps/car/screens/{RequestPermissionsScreen.java => permissions/RequestPermissionsScreenWithApi.java} (86%) create mode 100644 android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithNotification.java create mode 100644 android/app/src/main/java/app/organicmaps/car/util/UserActionRequired.java create mode 100644 android/app/src/main/res/drawable/ic_location_permission_request.xml create mode 100644 android/app/src/main/res/layout/activity_request_permissions.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ddc4abbd4c..67ed66ef78 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -443,6 +443,8 @@ android:label="@string/driving_options_title"/> + mPermissionsRequest; + + @Override + protected void onSafeCreate(@Nullable Bundle savedInstanceState) + { + super.onSafeCreate(savedInstanceState); + setContentView(R.layout.activity_request_permissions); + + findViewById(R.id.btn_grant_permissions).setOnClickListener(unused -> openAppPermissionSettings()); + mPermissionsRequest = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), + (grantedPermissions) -> closeIfPermissionsGranted()); + mPermissionsRequest.launch(LOCATION_PERMISSIONS); + } + + @Override + protected void onResume() + { + super.onResume(); + closeIfPermissionsGranted(); + } + + @Override + protected void onSafeDestroy() + { + super.onSafeDestroy(); + Objects.requireNonNull(mPermissionsRequest).unregister(); + mPermissionsRequest = null; + } + + private void openAppPermissionSettings() + { + final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", getPackageName(), null)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + private void closeIfPermissionsGranted() + { + if (!LocationUtils.checkLocationPermission(this)) + return; + + NotificationManagerCompat.from(this).cancel(RequestPermissionsScreenWithNotification.NOTIFICATION_ID); + finish(); + } +} diff --git a/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenBuilder.java b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenBuilder.java new file mode 100644 index 0000000000..3b90a793b1 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenBuilder.java @@ -0,0 +1,29 @@ +package app.organicmaps.car.screens.permissions; + +import static android.Manifest.permission.POST_NOTIFICATIONS; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.car.app.CarContext; +import androidx.car.app.Screen; +import androidx.core.content.ContextCompat; + +import app.organicmaps.util.log.Logger; + +public class RequestPermissionsScreenBuilder +{ + private static final String TAG = RequestPermissionsScreenBuilder.class.getSimpleName(); + + public static Screen build(@NonNull CarContext carContext, @NonNull Runnable permissionsGrantedCallback) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ContextCompat.checkSelfPermission(carContext, POST_NOTIFICATIONS) != PERMISSION_GRANTED) + { + Logger.w(TAG, "Permission POST_NOTIFICATIONS is not granted, using API-based permissions request"); + return new RequestPermissionsScreenWithApi(carContext, permissionsGrantedCallback); + } + return new RequestPermissionsScreenWithNotification(carContext, permissionsGrantedCallback); + } +} diff --git a/android/app/src/main/java/app/organicmaps/car/screens/RequestPermissionsScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithApi.java similarity index 86% rename from android/app/src/main/java/app/organicmaps/car/screens/RequestPermissionsScreen.java rename to android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithApi.java index 017d9e585b..8cd3a641e2 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/RequestPermissionsScreen.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithApi.java @@ -1,4 +1,4 @@ -package app.organicmaps.car.screens; +package app.organicmaps.car.screens.permissions; import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; @@ -15,21 +15,23 @@ import androidx.core.graphics.drawable.IconCompat; import androidx.lifecycle.LifecycleOwner; import app.organicmaps.R; +import app.organicmaps.car.screens.ErrorScreen; import app.organicmaps.car.screens.base.BaseScreen; import app.organicmaps.car.util.Colors; +import app.organicmaps.car.util.UserActionRequired; import app.organicmaps.util.LocationUtils; import java.util.Arrays; import java.util.List; -public class RequestPermissionsScreen extends BaseScreen +public class RequestPermissionsScreenWithApi extends BaseScreen implements UserActionRequired { private static final List LOCATION_PERMISSIONS = Arrays.asList(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION); @NonNull private final Runnable mPermissionsGrantedCallback; - public RequestPermissionsScreen(@NonNull CarContext carContext, @NonNull Runnable permissionsGrantedCallback) + public RequestPermissionsScreenWithApi(@NonNull CarContext carContext, @NonNull Runnable permissionsGrantedCallback) { super(carContext); mPermissionsGrantedCallback = permissionsGrantedCallback; @@ -39,7 +41,7 @@ public class RequestPermissionsScreen extends BaseScreen @Override public Template onGetTemplate() { - final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(R.string.aa_location_permissions_request)); + final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(R.string.aa_request_permission_activity_text)); final Action grantPermissions = new Action.Builder() .setTitle(getCarContext().getString(R.string.aa_grant_permissions)) .setBackgroundColor(Colors.BUTTON_ACCEPT) @@ -83,4 +85,4 @@ public class RequestPermissionsScreen extends BaseScreen mPermissionsGrantedCallback.run(); finish(); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithNotification.java b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithNotification.java new file mode 100644 index 0000000000..c9412a899c --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/permissions/RequestPermissionsScreenWithNotification.java @@ -0,0 +1,124 @@ +package app.organicmaps.car.screens.permissions; + +import android.Manifest; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresPermission; +import androidx.car.app.CarContext; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.Header; +import androidx.car.app.model.MessageTemplate; +import androidx.car.app.model.Template; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.IconCompat; +import androidx.lifecycle.LifecycleOwner; + +import app.organicmaps.R; +import app.organicmaps.car.CarAppService; +import app.organicmaps.car.screens.base.BaseScreen; +import app.organicmaps.car.util.UserActionRequired; +import app.organicmaps.util.LocationUtils; +import app.organicmaps.util.concurrency.ThreadPool; +import app.organicmaps.util.concurrency.UiThread; + +import java.util.concurrent.ExecutorService; + +public class RequestPermissionsScreenWithNotification extends BaseScreen implements UserActionRequired +{ + public static final int NOTIFICATION_ID = RequestPermissionsScreenWithNotification.class.getSimpleName().hashCode(); + + @NonNull + private final ExecutorService mBackgroundExecutor; + private boolean mIsPermissionCheckEnabled = true; + @NonNull + private final Runnable mPermissionsGrantedCallback; + + public RequestPermissionsScreenWithNotification(@NonNull CarContext carContext, @NonNull Runnable permissionsGrantedCallback) + { + super(carContext); + mBackgroundExecutor = ThreadPool.getWorker(); + mPermissionsGrantedCallback = permissionsGrantedCallback; + } + + @NonNull + @Override + public Template onGetTemplate() + { + final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(R.string.aa_location_permissions_request)); + + final Header.Builder headerBuilder = new Header.Builder(); + headerBuilder.setStartHeaderAction(Action.APP_ICON); + headerBuilder.setTitle(getCarContext().getString(R.string.aa_grant_permissions)); + builder.setHeader(headerBuilder.build()); + + builder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_location_off)).build()); + return builder.build(); + } + + @Override + @RequiresPermission(value = Manifest.permission.POST_NOTIFICATIONS) + public void onStart(@NonNull LifecycleOwner owner) + { + mIsPermissionCheckEnabled = true; + mBackgroundExecutor.execute(this::checkPermissions); + sendPermissionsRequestNotification(); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) + { + mIsPermissionCheckEnabled = false; + } + + @Override + public void onDestroy(@NonNull LifecycleOwner owner) + { + NotificationManagerCompat.from(getCarContext()).cancel(NOTIFICATION_ID); + } + + private void checkPermissions() + { + if (!mIsPermissionCheckEnabled) + return; + + if (LocationUtils.checkLocationPermission(getCarContext())) + { + UiThread.runLater(() -> { + mPermissionsGrantedCallback.run(); + finish(); + }); + } + else + mBackgroundExecutor.execute(this::checkPermissions); + } + + @RequiresPermission(value = Manifest.permission.POST_NOTIFICATIONS) + private void sendPermissionsRequestNotification() + { + final int FLAG_IMMUTABLE = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ? 0 : PendingIntent.FLAG_IMMUTABLE; + final Intent contentIntent = new Intent(getCarContext(), RequestPermissionsActivity.class); + final PendingIntent pendingIntent = PendingIntent.getActivity(getCarContext(), 0, contentIntent, + PendingIntent.FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); + + final NotificationCompat.Builder builder = new NotificationCompat.Builder(getCarContext(), CarAppService.ANDROID_AUTO_NOTIFICATION_CHANNEL_ID); + builder.setCategory(NotificationCompat.CATEGORY_NAVIGATION) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setOngoing(true) + .setShowWhen(false) + .setOnlyAlertOnce(true) + .setSmallIcon(R.drawable.ic_my_location) + .setColor(ContextCompat.getColor(getCarContext(), R.color.notification)) + .setContentTitle(getCarContext().getString(R.string.aa_request_permission_notification)) + .setContentIntent(pendingIntent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + builder.setPriority(NotificationManager.IMPORTANCE_HIGH); + NotificationManagerCompat.from(getCarContext()).notify(NOTIFICATION_ID, builder.build()); + } +} diff --git a/android/app/src/main/java/app/organicmaps/car/util/UserActionRequired.java b/android/app/src/main/java/app/organicmaps/car/util/UserActionRequired.java new file mode 100644 index 0000000000..0c20ecda66 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/util/UserActionRequired.java @@ -0,0 +1,7 @@ +package app.organicmaps.car.util; + +/// Marker interface for screens that require user action to proceed. +/// These screens can't be dropped from AA's screen stack. +public interface UserActionRequired +{ +} diff --git a/android/app/src/main/res/drawable/ic_location_permission_request.xml b/android/app/src/main/res/drawable/ic_location_permission_request.xml new file mode 100644 index 0000000000..43dde8a8ca --- /dev/null +++ b/android/app/src/main/res/drawable/ic_location_permission_request.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_request_permissions.xml b/android/app/src/main/res/layout/activity_request_permissions.xml new file mode 100644 index 0000000000..b0f822496e --- /dev/null +++ b/android/app/src/main/res/layout/activity_request_permissions.xml @@ -0,0 +1,34 @@ + + + + + + + +