From 18e6c57e75e9346a8449141cf4c6bbc41765bdad Mon Sep 17 00:00:00 2001 From: mtang-signal Date: Fri, 26 Apr 2024 09:52:25 -0400 Subject: [PATCH] Update location permission UI. --- .../v2/ConversationActivityResultContracts.kt | 24 ++- .../PermissionDeniedBottomSheet.kt | 160 ++++++++++++++++++ .../securesms/permissions/Permissions.java | 31 +++- .../res/drawable/ic_radio_button_checked.xml | 7 + .../permission_allow_location_dialog.xml | 35 ++++ app/src/main/res/values/strings.xml | 18 ++ 6 files changed, 263 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/permissions/PermissionDeniedBottomSheet.kt create mode 100644 app/src/main/res/drawable/ic_radio_button_checked.xml create mode 100644 app/src/main/res/layout/permission_allow_location_dialog.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt index e38cadc7b8..8c95352892 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt @@ -16,6 +16,7 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContract import androidx.core.content.IntentCompat import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.location.SignalPlace @@ -106,12 +107,23 @@ class ConversationActivityResultContracts(private val fragment: Fragment, privat if (Permissions.hasAny(fragment.requireContext(), Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)) { selectLocationLauncher.launch(chatColors) } else { - Permissions.with(fragment) - .request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) - .ifNecessary() - .withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location)) - .onSomeGranted { selectLocationLauncher.launch(chatColors) } - .execute() + val dialog = MaterialAlertDialogBuilder(fragment.requireContext()) + .setView(R.layout.permission_allow_location_dialog) + .setPositiveButton(R.string.Permissions_continue) { _, _ -> + Permissions.with(fragment) + .request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) + .ifNecessary() + .withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location), null, R.string.AttachmentManager_signal_allow_access_location, R.string.AttachmentManager_signal_to_send_location, fragment.parentFragmentManager) + .onAnyDenied { Toast.makeText(fragment.requireContext(), R.string.AttachmentManager_signal_needs_location_access, Toast.LENGTH_LONG).show() } + .onSomeGranted { selectLocationLauncher.launch(chatColors) } + .execute() + } + .setNegativeButton(R.string.Permissions_not_now) { d, _ -> + Toast.makeText(fragment.requireContext(), R.string.AttachmentManager_signal_needs_location_access, Toast.LENGTH_LONG).show() + d.dismiss() + } + .create() + dialog.show() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/PermissionDeniedBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/permissions/PermissionDeniedBottomSheet.kt new file mode 100644 index 0000000000..1850351b10 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/PermissionDeniedBottomSheet.kt @@ -0,0 +1,160 @@ +package org.thoughtcrime.securesms.permissions + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.os.bundleOf +import org.signal.core.ui.BottomSheets +import org.signal.core.ui.Buttons +import org.signal.core.ui.Previews +import org.signal.core.ui.SignalPreview +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment + +private const val PLACEHOLDER = "__RADIO_BUTTON_PLACEHOLDER__" + +/** + * Bottom sheet shown when a permission has been previously denied + * + * Displays rationale for the need of a permission and how to grant it + */ +class PermissionDeniedBottomSheet private constructor() : ComposeBottomSheetDialogFragment() { + + companion object { + private const val ARG_TITLE = "argument.title_res" + private const val ARG_SUBTITLE = "argument.subtitle_res" + + @JvmStatic + fun showPermissionFragment(titleRes: Int, subtitleRes: Int): ComposeBottomSheetDialogFragment { + return PermissionDeniedBottomSheet().apply { + arguments = bundleOf( + ARG_TITLE to titleRes, + ARG_SUBTITLE to subtitleRes + ) + } + } + } + + @Composable + override fun SheetContent() { + PermissionDeniedSheetContent( + titleRes = remember { requireArguments().getInt(ARG_TITLE) }, + subtitleRes = remember { requireArguments().getInt(ARG_SUBTITLE) }, + onSettingsClicked = this::goToSettings + ) + } + + private fun goToSettings() { + requireContext().startActivity(Permissions.getApplicationSettingsIntent(requireContext())) + dismissAllowingStateLoss() + } +} + +@SignalPreview +@Composable +private fun PermissionDeniedSheetContentPreview() { + Previews.BottomSheetPreview { + PermissionDeniedSheetContent( + titleRes = R.string.AttachmentManager_signal_allow_access_location, + subtitleRes = R.string.AttachmentManager_signal_to_send_location, + onSettingsClicked = {} + ) + } +} + +@Composable +private fun PermissionDeniedSheetContent( + titleRes: Int, + subtitleRes: Int, + onSettingsClicked: () -> Unit +) { + Column( + modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 12.dp, bottom = 32.dp) + ) { + BottomSheets.Handle( + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Text( + text = stringResource(titleRes), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 12.dp, top = 20.dp) + ) + + Text( + text = stringResource(subtitleRes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 32.dp) + ) + + Text( + text = stringResource(R.string.PermissionDeniedBottomSheet__1_tap_settings_below), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 24.dp) + ) + + val step2String = stringResource(id = R.string.PermissionDeniedBottomSheet__2_allow_permission, PLACEHOLDER) + val (step2Text, step2InlineContent) = remember(step2String) { + val parts = step2String.split(PLACEHOLDER) + val annotatedString = buildAnnotatedString { + append(parts[0]) + appendInlineContent("radio") + append(parts[1]) + } + + val inlineContentMap = mapOf( + "radio" to InlineTextContent(Placeholder(22.sp, 22.sp, PlaceholderVerticalAlign.Center)) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_radio_button_checked), + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } + ) + + annotatedString to inlineContentMap + } + + Text( + text = step2Text, + inlineContent = step2InlineContent, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 32.dp) + ) + + Buttons.LargeTonal( + onClick = onSettingsClicked, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .fillMaxWidth(1f) + ) { + Text(text = stringResource(id = R.string.PermissionDeniedBottomSheet__settings)) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java index 14c6d052d4..a8910f546a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import com.annimon.stream.Stream; import com.annimon.stream.function.Consumer; @@ -26,6 +27,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.BottomSheetUtil; import org.thoughtcrime.securesms.util.LRUCache; import org.thoughtcrime.securesms.util.ServiceUtil; @@ -113,7 +115,11 @@ public class Permissions { } public PermissionsBuilder withPermanentDenialDialog(@NonNull String message, @Nullable Runnable onDialogDismissed) { - return onAnyPermanentlyDenied(new SettingsDialogListener(permissionObject.getContext(), message, onDialogDismissed)); + return withPermanentDenialDialog(message, onDialogDismissed, 0, 0, null); + } + + public PermissionsBuilder withPermanentDenialDialog(@NonNull String message, @Nullable Runnable onDialogDismissed, int titleRes, int detailsRes, @Nullable FragmentManager fragmentManager) { + return onAnyPermanentlyDenied(new SettingsDialogListener(permissionObject.getContext(), message, onDialogDismissed, titleRes, detailsRes, fragmentManager)); } public PermissionsBuilder onAllGranted(Runnable allGrantedListener) { @@ -368,22 +374,34 @@ public class Permissions { private static class SettingsDialogListener implements Runnable { - private final WeakReference context; - private final Runnable onDialogDismissed; - private final String message; + private final WeakReference context; + private final WeakReference fragmentManager; + private final Runnable onDialogDismissed; + private final String message; + private final int titleRes; + private final int detailsRes; + private final boolean useBottomSheet; - SettingsDialogListener(Context context, String message, @Nullable Runnable onDialogDismissed) { + SettingsDialogListener(Context context, String message, @Nullable Runnable onDialogDismissed, int titleRes, int detailsRes, @Nullable FragmentManager fragmentManager) { this.message = message; this.context = new WeakReference<>(context); this.onDialogDismissed = onDialogDismissed; + this.fragmentManager = new WeakReference<>(fragmentManager); + this.titleRes = titleRes; + this.detailsRes = detailsRes; + this.useBottomSheet = fragmentManager != null; } @Override public void run() { Context context = this.context.get(); + FragmentManager fragmentManager = this.fragmentManager.get(); if (context != null) { - new MaterialAlertDialogBuilder(context) + if (useBottomSheet && fragmentManager != null) { + PermissionDeniedBottomSheet.showPermissionFragment(titleRes, detailsRes).show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } else if (!useBottomSheet){ + new MaterialAlertDialogBuilder(context) .setTitle(R.string.Permissions_permission_required) .setMessage(message) .setCancelable(false) @@ -395,6 +413,7 @@ public class Permissions { } }) .show(); + } } } } diff --git a/app/src/main/res/drawable/ic_radio_button_checked.xml b/app/src/main/res/drawable/ic_radio_button_checked.xml new file mode 100644 index 0000000000..7465d286ca --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_button_checked.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/layout/permission_allow_location_dialog.xml b/app/src/main/res/layout/permission_allow_location_dialog.xml new file mode 100644 index 0000000000..40398b2b4d --- /dev/null +++ b/app/src/main/res/layout/permission_allow_location_dialog.xml @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2fdedd61f2..669e328a84 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -89,6 +89,16 @@ Signal requires the Storage permission in order to attach photos, videos, or audio, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Storage\". Signal requires Contacts permission in order to attach contact information, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Contacts\". Signal requires Location permission in order to attach a location, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Location\". + + + Allow access to your location + + To send your location: + + Allow Signal access to send your location. + + Signal needs location access to send your location. + %1$s hasn\'t activated Payments @@ -3437,6 +3447,14 @@ Send photos, videos and files from your device. + + + 1. Tap “Settings” below + + 2. %s Allow the permission + + Settings + Security setup