Update location permission UI.

This commit is contained in:
mtang-signal 2024-04-26 09:52:25 -04:00 committed by Greyson Parrelli
parent ffc1463cda
commit 18e6c57e75
6 changed files with 263 additions and 12 deletions

View file

@ -16,6 +16,7 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.location.SignalPlace 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)) { if (Permissions.hasAny(fragment.requireContext(), Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)) {
selectLocationLauncher.launch(chatColors) selectLocationLauncher.launch(chatColors)
} else { } else {
Permissions.with(fragment) val dialog = MaterialAlertDialogBuilder(fragment.requireContext())
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) .setView(R.layout.permission_allow_location_dialog)
.ifNecessary() .setPositiveButton(R.string.Permissions_continue) { _, _ ->
.withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location)) Permissions.with(fragment)
.onSomeGranted { selectLocationLauncher.launch(chatColors) } .request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
.execute() .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()
} }
} }

View file

@ -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))
}
}
}

View file

@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.annimon.stream.function.Consumer; 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.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.LRUCache; import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ServiceUtil;
@ -113,7 +115,11 @@ public class Permissions {
} }
public PermissionsBuilder withPermanentDenialDialog(@NonNull String message, @Nullable Runnable onDialogDismissed) { 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) { public PermissionsBuilder onAllGranted(Runnable allGrantedListener) {
@ -368,22 +374,34 @@ public class Permissions {
private static class SettingsDialogListener implements Runnable { private static class SettingsDialogListener implements Runnable {
private final WeakReference<Context> context; private final WeakReference<Context> context;
private final Runnable onDialogDismissed; private final WeakReference<FragmentManager> fragmentManager;
private final String message; 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.message = message;
this.context = new WeakReference<>(context); this.context = new WeakReference<>(context);
this.onDialogDismissed = onDialogDismissed; this.onDialogDismissed = onDialogDismissed;
this.fragmentManager = new WeakReference<>(fragmentManager);
this.titleRes = titleRes;
this.detailsRes = detailsRes;
this.useBottomSheet = fragmentManager != null;
} }
@Override @Override
public void run() { public void run() {
Context context = this.context.get(); Context context = this.context.get();
FragmentManager fragmentManager = this.fragmentManager.get();
if (context != null) { 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) .setTitle(R.string.Permissions_permission_required)
.setMessage(message) .setMessage(message)
.setCancelable(false) .setCancelable(false)
@ -395,6 +413,7 @@ public class Permissions {
} }
}) })
.show(); .show();
}
} }
} }
} }

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="25" android:viewportWidth="24" android:width="23.04dp">
<path android:fillColor="#00000000" android:pathData="M12,12.801m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0" android:strokeColor="#2C58C3" android:strokeWidth="2"/>
<path android:fillColor="#2C58C3" android:pathData="M12,12.801m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" android:strokeColor="#2C58C3" android:strokeWidth="2"/>
</vector>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp"
tools:viewBindingIgnore="true">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:importantForAccessibility="no"
app:tint="@color/signal_colorOnPrimaryContainer"
app:srcCompat="@drawable/symbol_location_white_24" />
<TextView
android:id="@+id/allow_location_title"
style="@style/Signal.Text.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/AttachmentManager_signal_allow_access_location" />
<TextView
android:id="@+id/allow_location_body"
style="@style/Signal.Text.BodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/AttachmentManager_signal_allow_signal_access_location" />
</LinearLayout>

View file

@ -89,6 +89,16 @@
<string name="AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio">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\".</string> <string name="AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio">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\".</string>
<string name="AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information">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\".</string> <string name="AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information">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\".</string>
<string name="AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location">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\".</string> <string name="AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location">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\".</string>
<!-- Dialog title asking users for location permission -->
<string name="AttachmentManager_signal_allow_access_location">Allow access to your location</string>
<!-- Dialog description that will explain the steps needed to give location permission -->
<string name="AttachmentManager_signal_to_send_location">To send your location:</string>
<!-- Alert dialog description asking for location permission -->
<string name="AttachmentManager_signal_allow_signal_access_location">Allow Signal access to send your location.</string>
<!-- Toast text explaining Signal's need for location access -->
<string name="AttachmentManager_signal_needs_location_access">Signal needs location access to send your location.</string>
<!-- Alert dialog title to show the recipient has not activated payments --> <!-- Alert dialog title to show the recipient has not activated payments -->
<string name="AttachmentManager__not_activated_payments">%1$s hasn\'t activated Payments </string> <string name="AttachmentManager__not_activated_payments">%1$s hasn\'t activated Payments </string>
<!-- Alert dialog description to send the recipient a request to activate payments --> <!-- Alert dialog description to send the recipient a request to activate payments -->
@ -3437,6 +3447,14 @@
<!-- Storage permission row description --> <!-- Storage permission row description -->
<string name="GrantPermissionsFragment__send_photos_videos_and_files">Send photos, videos and files from your device.</string> <string name="GrantPermissionsFragment__send_photos_videos_and_files">Send photos, videos and files from your device.</string>
<!-- PermissionDeniedBottomSheet -->
<!-- Sheet describing step 1 on how to give permissions by opening settings -->
<string name="PermissionDeniedBottomSheet__1_tap_settings_below">1. Tap “Settings” below</string>
<!-- Sheet describing step 2 on how to give permissions by checking the permissions button in settings where %s will be replaced with an image of a checked button -->
<string name="PermissionDeniedBottomSheet__2_allow_permission">2. %s Allow the permission</string>
<!-- Label for button at the bottom of the sheet which opens the system permission settings -->
<string name="PermissionDeniedBottomSheet__settings">Settings</string>
<!-- PaymentsSecuritySetupFragment --> <!-- PaymentsSecuritySetupFragment -->
<!-- Toolbar title --> <!-- Toolbar title -->
<string name="PaymentsSecuritySetupFragment__security_setup">Security setup</string> <string name="PaymentsSecuritySetupFragment__security_setup">Security setup</string>