mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 19:29:54 +01:00
Move the rest of the permissions classes.
This commit is contained in:
@@ -5,18 +5,28 @@
|
||||
|
||||
package org.signal.core.ui
|
||||
|
||||
import android.app.Application
|
||||
|
||||
object CoreUiDependencies {
|
||||
|
||||
private lateinit var _application: Application
|
||||
private lateinit var _provider: Provider
|
||||
|
||||
fun init(provider: Provider) {
|
||||
fun init(application: Application, provider: Provider) {
|
||||
if (this::_provider.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
_application = application
|
||||
_provider = provider
|
||||
}
|
||||
|
||||
val application: Application
|
||||
get() = _application
|
||||
|
||||
val packageId: String
|
||||
get() = _provider.providePackageId()
|
||||
|
||||
val isIncognitoKeyboardEnabled: Boolean
|
||||
get() = _provider.provideIsIncognitoKeyboardEnabled()
|
||||
|
||||
@@ -27,6 +37,7 @@ object CoreUiDependencies {
|
||||
get() = _provider.provideForceSplitPane()
|
||||
|
||||
interface Provider {
|
||||
fun providePackageId(): String
|
||||
fun provideIsIncognitoKeyboardEnabled(): Boolean
|
||||
fun provideIsScreenSecurityEnabled(): Boolean
|
||||
fun provideForceSplitPane(): Boolean
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui.permissions
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
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.R
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
|
||||
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() {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.66f
|
||||
|
||||
companion object {
|
||||
private const val ARG_TITLE = "argument.title_res"
|
||||
private const val ARG_SUBTITLE = "argument.subtitle_res"
|
||||
private const val ARG_USE_EXTENDED = "argument.use.extended"
|
||||
|
||||
@JvmStatic
|
||||
fun showPermissionFragment(titleRes: Int, subtitleRes: Int, useExtended: Boolean = false): ComposeBottomSheetDialogFragment {
|
||||
return PermissionDeniedBottomSheet().apply {
|
||||
arguments = bundleOf(
|
||||
ARG_TITLE to titleRes,
|
||||
ARG_SUBTITLE to subtitleRes,
|
||||
ARG_USE_EXTENDED to useExtended
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
PermissionDeniedSheetContent(
|
||||
titleRes = remember { requireArguments().getInt(ARG_TITLE) },
|
||||
subtitleRes = remember { requireArguments().getInt(ARG_SUBTITLE) },
|
||||
useExtended = remember { requireArguments().getBoolean(ARG_USE_EXTENDED) },
|
||||
onSettingsClicked = this::goToSettings
|
||||
)
|
||||
}
|
||||
|
||||
private fun goToSettings() {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", requireContext().packageName, null)
|
||||
}
|
||||
requireContext().startActivity(intent)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun PermissionDeniedSheetContentPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
PermissionDeniedSheetContent(
|
||||
titleRes = R.string.PermissionDeniedBottomSheet__settings,
|
||||
subtitleRes = R.string.PermissionDeniedBottomSheet__1_tap_settings_below,
|
||||
onSettingsClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionDeniedSheetContent(
|
||||
titleRes: Int,
|
||||
subtitleRes: Int,
|
||||
useExtended: Boolean = false,
|
||||
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)
|
||||
)
|
||||
|
||||
if (useExtended) {
|
||||
Text(
|
||||
text = stringResource(R.string.PermissionDeniedBottomSheet__2_tap_permissions),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
val stringId = if (useExtended) R.string.PermissionDeniedBottomSheet__3_allow_permission else R.string.PermissionDeniedBottomSheet__2_allow_permission
|
||||
val stepString = stringResource(id = stringId, PLACEHOLDER)
|
||||
val (stepText, stepInlineContent) = remember(stepString) {
|
||||
val parts = stepString.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 = stepText,
|
||||
inlineContent = stepInlineContent,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
package org.signal.core.ui.permissions;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.provider.Settings;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Display;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
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.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.ui.BottomSheetUtil;
|
||||
import org.signal.core.ui.R;
|
||||
import org.signal.core.util.LRUCache;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class Permissions {
|
||||
|
||||
private static final String TAG = Log.tag(Permissions.class);
|
||||
|
||||
private static final Map<Integer, PermissionsRequest> OUTSTANDING = new LRUCache<>(2);
|
||||
|
||||
public static PermissionsBuilder with(@NonNull Activity activity) {
|
||||
return new PermissionsBuilder(new ActivityPermissionObject(activity));
|
||||
}
|
||||
|
||||
public static PermissionsBuilder with(@NonNull Fragment fragment) {
|
||||
return new PermissionsBuilder(new FragmentPermissionObject(fragment));
|
||||
}
|
||||
|
||||
public static class PermissionsBuilder {
|
||||
|
||||
private final PermissionObject permissionObject;
|
||||
|
||||
private String[] requestedPermissions;
|
||||
|
||||
private Runnable allGrantedListener;
|
||||
|
||||
private Runnable anyDeniedListener;
|
||||
private Runnable anyPermanentlyDeniedListener;
|
||||
private Runnable anyResultListener;
|
||||
|
||||
private Consumer<List<String>> someGrantedListener;
|
||||
private Consumer<List<String>> someDeniedListener;
|
||||
private Consumer<List<String>> somePermanentlyDeniedListener;
|
||||
|
||||
private @DrawableRes int[] rationalDialogHeader;
|
||||
private String rationaleDialogMessage;
|
||||
private String rationaleDialogTitle;
|
||||
private String rationaleDialogDetails;
|
||||
private boolean rationaleDialogCancelable;
|
||||
|
||||
private boolean ifNecesary;
|
||||
|
||||
private boolean condition = true;
|
||||
|
||||
PermissionsBuilder(PermissionObject permissionObject) {
|
||||
this.permissionObject = permissionObject;
|
||||
}
|
||||
|
||||
public PermissionsBuilder request(String... requestedPermissions) {
|
||||
this.requestedPermissions = requestedPermissions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder ifNecessary() {
|
||||
this.ifNecesary = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder ifNecessary(boolean condition) {
|
||||
this.ifNecesary = true;
|
||||
this.condition = condition;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) {
|
||||
return withRationaleDialog(message, true, headers);
|
||||
}
|
||||
|
||||
public PermissionsBuilder withRationaleDialog(@NonNull String message, boolean cancelable, @NonNull @DrawableRes int... headers) {
|
||||
return withRationaleDialog(message, null, null, cancelable, headers);
|
||||
}
|
||||
|
||||
public PermissionsBuilder withRationaleDialog(@NonNull String title, @NonNull String details, @NonNull @DrawableRes int... headers) {
|
||||
return withRationaleDialog(null, title, details, true, headers);
|
||||
}
|
||||
|
||||
public PermissionsBuilder withRationaleDialog(@NonNull String title, @NonNull String details, boolean cancelable, @NonNull @DrawableRes int... headers) {
|
||||
return withRationaleDialog(null, title, details, cancelable, headers);
|
||||
}
|
||||
|
||||
public PermissionsBuilder withRationaleDialog(@Nullable String message, @Nullable String title, @Nullable String details, boolean cancelable, @NonNull @DrawableRes int... headers) {
|
||||
this.rationalDialogHeader = headers;
|
||||
this.rationaleDialogMessage = message;
|
||||
this.rationaleDialogTitle = title;
|
||||
this.rationaleDialogDetails = details;
|
||||
this.rationaleDialogCancelable = cancelable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder withPermanentDenialDialog(@NonNull String message) {
|
||||
return withPermanentDenialDialog(message, null);
|
||||
}
|
||||
|
||||
public PermissionsBuilder withPermanentDenialDialog(@NonNull String message, @Nullable Runnable 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 withPermanentDenialDialog(message, onDialogDismissed, titleRes, detailsRes, false, fragmentManager);
|
||||
}
|
||||
|
||||
public PermissionsBuilder withPermanentDenialDialog(@NonNull String message, @Nullable Runnable onDialogDismissed, int titleRes, int detailsRes, boolean useExtended, @Nullable FragmentManager fragmentManager) {
|
||||
return onAnyPermanentlyDenied(new SettingsDialogListener(permissionObject.getContext(), message, onDialogDismissed, titleRes, detailsRes, useExtended, fragmentManager));
|
||||
}
|
||||
|
||||
public PermissionsBuilder onAllGranted(Runnable allGrantedListener) {
|
||||
this.allGrantedListener = allGrantedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onAnyDenied(Runnable anyDeniedListener) {
|
||||
this.anyDeniedListener = anyDeniedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public PermissionsBuilder onAnyPermanentlyDenied(Runnable anyPermanentlyDeniedListener) {
|
||||
this.anyPermanentlyDeniedListener = anyPermanentlyDeniedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onAnyResult(Runnable anyResultListener) {
|
||||
this.anyResultListener = anyResultListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onSomeGranted(Consumer<List<String>> someGrantedListener) {
|
||||
this.someGrantedListener = someGrantedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onSomeDenied(Consumer<List<String>> someDeniedListener) {
|
||||
this.someDeniedListener = someDeniedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PermissionsBuilder onSomePermanentlyDenied(Consumer<List<String>> somePermanentlyDeniedListener) {
|
||||
this.somePermanentlyDeniedListener = somePermanentlyDeniedListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
PermissionsRequest request = new PermissionsRequest(allGrantedListener, anyDeniedListener, anyPermanentlyDeniedListener, anyResultListener,
|
||||
someGrantedListener, someDeniedListener, somePermanentlyDeniedListener);
|
||||
|
||||
if (ifNecesary && (permissionObject.hasAll(requestedPermissions) || !condition)) {
|
||||
executePreGrantedPermissionsRequest(request);
|
||||
} else if ((rationaleDialogMessage != null || (rationaleDialogTitle != null && rationaleDialogDetails != null))
|
||||
&& rationalDialogHeader != null) {
|
||||
executePermissionsRequestWithRationale(request);
|
||||
} else {
|
||||
executePermissionsRequest(request);
|
||||
}
|
||||
}
|
||||
|
||||
private void executePreGrantedPermissionsRequest(PermissionsRequest request) {
|
||||
int[] grantResults = new int[requestedPermissions.length];
|
||||
for (int i=0;i<grantResults.length;i++) grantResults[i] = PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
request.onResult(requestedPermissions, grantResults, new boolean[requestedPermissions.length]);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void executePermissionsRequestWithRationale(PermissionsRequest request) {
|
||||
MaterialAlertDialogBuilder builder = (rationaleDialogMessage != null)
|
||||
? RationaleDialog.createFor(permissionObject.getContext(), rationaleDialogMessage, rationalDialogHeader)
|
||||
: RationaleDialog.createFor(permissionObject.getContext(), rationaleDialogTitle, rationaleDialogDetails, rationalDialogHeader);
|
||||
builder.setPositiveButton(R.string.Permissions_continue, (dialog, which) -> executePermissionsRequest(request))
|
||||
.setNegativeButton(R.string.Permissions_not_now, (dialog, which) -> executeNoPermissionsRequest(request))
|
||||
.setBackgroundInsetTop(0)
|
||||
.setBackgroundInsetBottom(0)
|
||||
.setCancelable(rationaleDialogCancelable);
|
||||
if (rationaleDialogMessage != null) {
|
||||
builder.show().getWindow().setLayout((int)(permissionObject.getWindowWidth() * .75), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
} else {
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void executePermissionsRequest(PermissionsRequest request) {
|
||||
int requestCode = new SecureRandom().nextInt(65434) + 100;
|
||||
|
||||
synchronized (OUTSTANDING) {
|
||||
OUTSTANDING.put(requestCode, request);
|
||||
}
|
||||
|
||||
for (String permission : requestedPermissions) {
|
||||
request.addMapping(permission, permissionObject.shouldShouldPermissionRationale(permission));
|
||||
}
|
||||
|
||||
permissionObject.requestPermissions(requestCode, requestedPermissions);
|
||||
}
|
||||
|
||||
private void executeNoPermissionsRequest(PermissionsRequest request) {
|
||||
for (String permission : requestedPermissions) {
|
||||
request.addMapping(permission, true);
|
||||
}
|
||||
|
||||
String[] permissions = filterNotGranted(permissionObject.getContext(), requestedPermissions);
|
||||
int[] grantResults = new int[permissions.length];
|
||||
Arrays.fill(grantResults, PackageManager.PERMISSION_DENIED);
|
||||
boolean[] showDialog = new boolean[permissions.length];
|
||||
Arrays.fill(showDialog, true);
|
||||
|
||||
request.onResult(permissions, grantResults, showDialog);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void requestPermissions(@NonNull Activity activity, int requestCode, String... permissions) {
|
||||
String[] neededPermissions = filterNotGranted(activity, permissions);
|
||||
|
||||
if (neededPermissions.length == 0) {
|
||||
Log.i(TAG, "No permissions needed!");
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityCompat.requestPermissions(activity, neededPermissions, requestCode);
|
||||
}
|
||||
|
||||
private static void requestPermissions(@NonNull Fragment fragment, int requestCode, String... permissions) {
|
||||
String[] neededPermissions = filterNotGranted(fragment.requireContext(), permissions);
|
||||
|
||||
if (neededPermissions.length == 0) {
|
||||
Log.i(TAG, "No permissions needed!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
fragment.requestPermissions(neededPermissions, requestCode);
|
||||
}
|
||||
|
||||
private static String[] filterNotGranted(@NonNull Context context, String... permissions) {
|
||||
return Arrays.stream(permissions)
|
||||
.filter(permission -> ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
public static boolean hasAny(@NonNull Context context, String... permissions) {
|
||||
return Arrays.stream(permissions).anyMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
}
|
||||
|
||||
public static boolean hasAll(@NonNull Context context, String... permissions) {
|
||||
return Arrays.stream(permissions).allMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
}
|
||||
|
||||
public static void onRequestPermissionsResult(Fragment fragment, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
onRequestPermissionsResult(new FragmentPermissionObject(fragment), requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public static void onRequestPermissionsResult(Activity activity, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
onRequestPermissionsResult(new ActivityPermissionObject(activity), requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private static void onRequestPermissionsResult(@NonNull PermissionObject context, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
PermissionsRequest resultListener;
|
||||
|
||||
synchronized (OUTSTANDING) {
|
||||
resultListener = OUTSTANDING.remove(requestCode);
|
||||
}
|
||||
|
||||
if (resultListener == null) return;
|
||||
|
||||
boolean[] shouldShowRationaleDialog = new boolean[permissions.length];
|
||||
|
||||
for (int i=0;i<permissions.length;i++) {
|
||||
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
|
||||
shouldShowRationaleDialog[i] = context.shouldShouldPermissionRationale(permissions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
resultListener.onResult(permissions, grantResults, shouldShowRationaleDialog);
|
||||
}
|
||||
|
||||
public static Intent getApplicationSettingsIntent(@NonNull Context context) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", context.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
private abstract static class PermissionObject {
|
||||
|
||||
abstract Context getContext();
|
||||
abstract boolean shouldShouldPermissionRationale(String permission);
|
||||
abstract boolean hasAll(String... permissions);
|
||||
abstract void requestPermissions(int requestCode, String... permissions);
|
||||
|
||||
int getWindowWidth() {
|
||||
WindowManager windowManager = ContextCompat.getSystemService(getContext(), WindowManager.class);
|
||||
Display display = windowManager.getDefaultDisplay();
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
display.getMetrics(metrics);
|
||||
|
||||
return metrics.widthPixels;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ActivityPermissionObject extends PermissionObject {
|
||||
|
||||
private Activity activity;
|
||||
|
||||
ActivityPermissionObject(@NonNull Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShouldPermissionRationale(String permission) {
|
||||
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAll(String... permissions) {
|
||||
return Permissions.hasAll(activity, permissions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestPermissions(int requestCode, String... permissions) {
|
||||
Permissions.requestPermissions(activity, requestCode, permissions);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FragmentPermissionObject extends PermissionObject {
|
||||
|
||||
private Fragment fragment;
|
||||
|
||||
FragmentPermissionObject(@NonNull Fragment fragment) {
|
||||
this.fragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return fragment.getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShouldPermissionRationale(String permission) {
|
||||
return fragment.shouldShowRequestPermissionRationale(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAll(String... permissions) {
|
||||
return Permissions.hasAll(fragment.getContext(), permissions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestPermissions(int requestCode, String... permissions) {
|
||||
Permissions.requestPermissions(fragment, requestCode, permissions);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SettingsDialogListener implements Runnable {
|
||||
|
||||
private final WeakReference<Context> context;
|
||||
private final WeakReference<FragmentManager> fragmentManager;
|
||||
private final Runnable onDialogDismissed;
|
||||
private final String message;
|
||||
private final int titleRes;
|
||||
private final int detailsRes;
|
||||
private final boolean useBottomSheet;
|
||||
private final boolean useExtended;
|
||||
|
||||
SettingsDialogListener(Context context, String message, @Nullable Runnable onDialogDismissed, int titleRes, int detailsRes, boolean useExtended, @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.useExtended = useExtended;
|
||||
this.useBottomSheet = fragmentManager != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Context context = this.context.get();
|
||||
FragmentManager fragmentManager = this.fragmentManager.get();
|
||||
|
||||
if (context != null) {
|
||||
if (useBottomSheet && fragmentManager != null) {
|
||||
PermissionDeniedBottomSheet.showPermissionFragment(titleRes, detailsRes, useExtended).show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
} else if (!useBottomSheet){
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.Permissions_permission_required)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.Permissions_continue, (dialog, which) -> context.startActivity(getApplicationSettingsIntent(context)))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setOnDismissListener(d -> {
|
||||
if (onDialogDismissed != null) {
|
||||
onDialogDismissed.run();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.signal.core.ui.permissions;
|
||||
|
||||
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
class PermissionsRequest {
|
||||
|
||||
private final Map<String, Boolean> PRE_REQUEST_MAPPING = new HashMap<>();
|
||||
|
||||
private final @Nullable Runnable allGrantedListener;
|
||||
|
||||
private final @Nullable Runnable anyDeniedListener;
|
||||
private final @Nullable Runnable anyPermanentlyDeniedListener;
|
||||
private final @Nullable Runnable anyResultListener;
|
||||
|
||||
private final @Nullable Consumer<List<String>> someGrantedListener;
|
||||
private final @Nullable Consumer<List<String>> someDeniedListener;
|
||||
private final @Nullable Consumer<List<String>> somePermanentlyDeniedListener;
|
||||
|
||||
PermissionsRequest(@Nullable Runnable allGrantedListener,
|
||||
@Nullable Runnable anyDeniedListener,
|
||||
@Nullable Runnable anyPermanentlyDeniedListener,
|
||||
@Nullable Runnable anyResultListener,
|
||||
@Nullable Consumer<List<String>> someGrantedListener,
|
||||
@Nullable Consumer<List<String>> someDeniedListener,
|
||||
@Nullable Consumer<List<String>> somePermanentlyDeniedListener)
|
||||
{
|
||||
this.allGrantedListener = allGrantedListener;
|
||||
|
||||
this.anyDeniedListener = anyDeniedListener;
|
||||
this.anyPermanentlyDeniedListener = anyPermanentlyDeniedListener;
|
||||
this.anyResultListener = anyResultListener;
|
||||
|
||||
this.someGrantedListener = someGrantedListener;
|
||||
this.someDeniedListener = someDeniedListener;
|
||||
this.somePermanentlyDeniedListener = somePermanentlyDeniedListener;
|
||||
}
|
||||
|
||||
void onResult(String[] permissions, int[] grantResults, boolean[] shouldShowRationaleDialog) {
|
||||
List<String> granted = new ArrayList<>(permissions.length);
|
||||
List<String> denied = new ArrayList<>(permissions.length);
|
||||
List<String> permanentlyDenied = new ArrayList<>(permissions.length);
|
||||
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
granted.add(permissions[i]);
|
||||
} else {
|
||||
boolean preRequestShouldShowRationaleDialog = PRE_REQUEST_MAPPING.get(permissions[i]);
|
||||
|
||||
if ((somePermanentlyDeniedListener != null || anyPermanentlyDeniedListener != null) &&
|
||||
!preRequestShouldShowRationaleDialog && !shouldShowRationaleDialog[i])
|
||||
{
|
||||
permanentlyDenied.add(permissions[i]);
|
||||
} else {
|
||||
denied.add(permissions[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allGrantedListener != null && granted.size() > 0 && (denied.size() == 0 && permanentlyDenied.size() == 0)) {
|
||||
allGrantedListener.run();
|
||||
} else if (someGrantedListener != null && granted.size() > 0) {
|
||||
someGrantedListener.accept(granted);
|
||||
}
|
||||
|
||||
if (denied.size() > 0) {
|
||||
if (anyDeniedListener != null) anyDeniedListener.run();
|
||||
if (someDeniedListener != null) someDeniedListener.accept(denied);
|
||||
}
|
||||
|
||||
if (permanentlyDenied.size() > 0) {
|
||||
if (anyPermanentlyDeniedListener != null) anyPermanentlyDeniedListener.run();
|
||||
if (somePermanentlyDeniedListener != null) somePermanentlyDeniedListener.accept(permanentlyDenied);
|
||||
}
|
||||
|
||||
if (anyResultListener != null) {
|
||||
anyResultListener.run();
|
||||
}
|
||||
}
|
||||
|
||||
void addMapping(String permission, boolean shouldShowRationaleDialog) {
|
||||
PRE_REQUEST_MAPPING.put(permission, shouldShowRationaleDialog);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.signal.core.ui.permissions;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.ui.R;
|
||||
import org.signal.core.ui.util.ThemeUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class RationaleDialog {
|
||||
|
||||
public static MaterialAlertDialogBuilder createFor(@NonNull Context context, @NonNull String title, @NonNull String details, @DrawableRes int... drawables) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.permission_allow_dialog, null);
|
||||
ViewGroup header = view.findViewById(R.id.permission_header_container);
|
||||
TextView titleText = view.findViewById(R.id.permission_title);
|
||||
TextView detailsText = view.findViewById(R.id.permission_details);
|
||||
int iconSize = (int) DimensionUnit.DP.toPixels(32);
|
||||
|
||||
for (int i = 0; i < drawables.length; i++) {
|
||||
Drawable drawable = Objects.requireNonNull(ContextCompat.getDrawable(context, drawables[i]));
|
||||
DrawableCompat.setTint(drawable, ContextCompat.getColor(context, R.color.signal_colorOnPrimaryContainer));
|
||||
|
||||
ImageView imageView = new ImageView(context);
|
||||
imageView.setImageDrawable(drawable);
|
||||
imageView.setLayoutParams(new LayoutParams(iconSize, iconSize));
|
||||
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
|
||||
header.addView(imageView);
|
||||
|
||||
if (i != drawables.length - 1) {
|
||||
TextView plus = new TextView(context);
|
||||
plus.setText("+");
|
||||
plus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 40);
|
||||
plus.setTextColor(Color.WHITE);
|
||||
|
||||
LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.setMargins((int) DimensionUnit.DP.toPixels(20f), 0, (int) DimensionUnit.DP.toPixels(20f), 0);
|
||||
|
||||
plus.setLayoutParams(layoutParams);
|
||||
header.addView(plus);
|
||||
}
|
||||
}
|
||||
|
||||
titleText.setText(title);
|
||||
detailsText.setText(details);
|
||||
|
||||
return new MaterialAlertDialogBuilder(context).setView(view);
|
||||
}
|
||||
|
||||
public static MaterialAlertDialogBuilder createFor(@NonNull Context context, @NonNull String message, @DrawableRes int... drawables) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null);
|
||||
ViewGroup header = view.findViewById(R.id.header_container);
|
||||
TextView text = view.findViewById(R.id.message);
|
||||
int iconSize = (int) DimensionUnit.DP.toPixels(32);
|
||||
|
||||
for (int i=0;i<drawables.length;i++) {
|
||||
Drawable drawable = Objects.requireNonNull(ContextCompat.getDrawable(context, drawables[i]));
|
||||
DrawableCompat.setTint(drawable, Color.WHITE);
|
||||
ImageView imageView = new ImageView(context);
|
||||
imageView.setImageDrawable(drawable);
|
||||
|
||||
imageView.setLayoutParams(new LayoutParams(iconSize, iconSize));
|
||||
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
|
||||
header.addView(imageView);
|
||||
|
||||
if (i != drawables.length - 1) {
|
||||
TextView plus = new TextView(context);
|
||||
plus.setText("+");
|
||||
plus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 40);
|
||||
plus.setTextColor(Color.WHITE);
|
||||
|
||||
LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.setMargins((int) DimensionUnit.DP.toPixels(20f), 0, (int) DimensionUnit.DP.toPixels(20f), 0);
|
||||
|
||||
plus.setLayoutParams(layoutParams);
|
||||
header.addView(plus);
|
||||
}
|
||||
}
|
||||
|
||||
text.setText(message);
|
||||
text.setMovementMethod(new ScrollingMovementMethod());
|
||||
|
||||
int dialogTheme = ThemeUtil.getThemedResourceId(context, R.attr.permissionsRationaleDialogTheme);
|
||||
return new MaterialAlertDialogBuilder(context, dialogTheme)
|
||||
.setView(view);
|
||||
}
|
||||
|
||||
}
|
||||
187
core/ui/src/main/java/org/signal/core/ui/util/StorageUtil.java
Normal file
187
core/ui/src/main/java/org/signal/core/ui/util/StorageUtil.java
Normal file
@@ -0,0 +1,187 @@
|
||||
package org.signal.core.ui.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.os.storage.StorageVolume;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.ui.CoreUiDependencies;
|
||||
import org.signal.core.ui.R;
|
||||
import org.signal.core.util.NoExternalStorageException;
|
||||
import org.signal.core.util.permissions.PermissionCompat;
|
||||
import org.signal.core.ui.permissions.Permissions;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class StorageUtil {
|
||||
|
||||
private static final String PRODUCTION_PACKAGE_ID = "org.thoughtcrime.securesms";
|
||||
|
||||
public static File getOrCreateBackupDirectory() throws NoExternalStorageException {
|
||||
File storage = Environment.getExternalStorageDirectory();
|
||||
|
||||
if (!storage.canWrite()) {
|
||||
throw new NoExternalStorageException();
|
||||
}
|
||||
|
||||
File backups = getBackupDirectory();
|
||||
|
||||
if (!backups.exists()) {
|
||||
if (!backups.mkdirs()) {
|
||||
throw new NoExternalStorageException("Unable to create backup directory...");
|
||||
}
|
||||
}
|
||||
|
||||
return backups;
|
||||
}
|
||||
|
||||
public static File getOrCreateBackupV2Directory() throws NoExternalStorageException {
|
||||
File storage = Environment.getExternalStorageDirectory();
|
||||
|
||||
if (!storage.canWrite()) {
|
||||
throw new NoExternalStorageException();
|
||||
}
|
||||
|
||||
File backups = getBackupV2Directory();
|
||||
|
||||
if (!backups.exists()) {
|
||||
if (!backups.mkdirs()) {
|
||||
throw new NoExternalStorageException("Unable to create backup directory...");
|
||||
}
|
||||
}
|
||||
|
||||
return backups;
|
||||
}
|
||||
|
||||
public static File getBackupDirectory() throws NoExternalStorageException {
|
||||
File storage = Environment.getExternalStorageDirectory();
|
||||
File signal = new File(storage, "Signal");
|
||||
File backups = new File(signal, "Backups");
|
||||
String packageId = CoreUiDependencies.INSTANCE.getPackageId();
|
||||
|
||||
//noinspection ConstantConditions
|
||||
if (packageId.startsWith(PRODUCTION_PACKAGE_ID + ".")) {
|
||||
backups = new File(backups, packageId.substring(PRODUCTION_PACKAGE_ID.length() + 1));
|
||||
}
|
||||
|
||||
return backups;
|
||||
}
|
||||
|
||||
public static File getBackupV2Directory() throws NoExternalStorageException {
|
||||
File storage = Environment.getExternalStorageDirectory();
|
||||
File backups = new File(storage, "Signal");
|
||||
String packageId = CoreUiDependencies.INSTANCE.getPackageId();
|
||||
|
||||
//noinspection ConstantConditions
|
||||
if (packageId.startsWith(PRODUCTION_PACKAGE_ID + ".")) {
|
||||
backups = new File(storage, packageId.substring(PRODUCTION_PACKAGE_ID.length() + 1));
|
||||
}
|
||||
|
||||
return backups;
|
||||
}
|
||||
|
||||
@RequiresApi(24)
|
||||
public static @NonNull String getDisplayPath(@NonNull Context context, @NonNull Uri uri) {
|
||||
String lastPathSegment = Objects.requireNonNull(uri.getLastPathSegment());
|
||||
String backupVolume = lastPathSegment.replaceFirst(":.*", "");
|
||||
String backupName = lastPathSegment.replaceFirst(".*:", "");
|
||||
|
||||
StorageManager storageManager = ContextCompat.getSystemService(context, StorageManager.class);
|
||||
List<StorageVolume> storageVolumes = storageManager.getStorageVolumes();
|
||||
StorageVolume storageVolume = null;
|
||||
|
||||
for (StorageVolume volume : storageVolumes) {
|
||||
if (Objects.equals(volume.getUuid(), backupVolume)) {
|
||||
storageVolume = volume;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (storageVolume == null) {
|
||||
return backupName;
|
||||
} else {
|
||||
return context.getString(R.string.StorageUtil__s_s, storageVolume.getDescription(context), backupName);
|
||||
}
|
||||
}
|
||||
|
||||
private static File getSignalStorageDir() throws NoExternalStorageException {
|
||||
final File storage = Environment.getExternalStorageDirectory();
|
||||
|
||||
if (!storage.canWrite()) {
|
||||
throw new NoExternalStorageException();
|
||||
}
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
public static boolean canWriteInSignalStorageDir() {
|
||||
File storage;
|
||||
|
||||
try {
|
||||
storage = getSignalStorageDir();
|
||||
} catch (NoExternalStorageException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return storage.canWrite();
|
||||
}
|
||||
|
||||
public static boolean canWriteToMediaStore() {
|
||||
return Build.VERSION.SDK_INT > 28 ||
|
||||
Permissions.hasAll(CoreUiDependencies.INSTANCE.getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
}
|
||||
|
||||
public static boolean canReadAnyFromMediaStore() {
|
||||
return Permissions.hasAny(CoreUiDependencies.INSTANCE.getApplication(), PermissionCompat.forImagesAndVideos());
|
||||
}
|
||||
|
||||
public static boolean canOnlyReadSelectedMediaStore() {
|
||||
return Build.VERSION.SDK_INT >= 34 &&
|
||||
Permissions.hasAll(CoreUiDependencies.INSTANCE.getApplication(), Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) &&
|
||||
!Permissions.hasAny(CoreUiDependencies.INSTANCE.getApplication(), Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO);
|
||||
}
|
||||
|
||||
public static @NonNull Uri getVideoUri() {
|
||||
return MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
public static @NonNull Uri getAudioUri() {
|
||||
return MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
public static @NonNull Uri getImageUri() {
|
||||
return MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
public static @NonNull Uri getDownloadUri() {
|
||||
if (Build.VERSION.SDK_INT < 29) {
|
||||
return getLegacyUri(Environment.DIRECTORY_DOWNLOADS);
|
||||
} else {
|
||||
return MediaStore.Downloads.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull Uri getLegacyUri(@NonNull String directory) {
|
||||
return Uri.fromFile(Environment.getExternalStoragePublicDirectory(directory));
|
||||
}
|
||||
|
||||
public static @Nullable String getCleanFileName(@Nullable String fileName) {
|
||||
if (fileName == null) return null;
|
||||
|
||||
fileName = fileName.replace('\u202D', '\uFFFD');
|
||||
fileName = fileName.replace('\u202E', '\uFFFD');
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
38
core/ui/src/main/res/layout/permission_allow_dialog.xml
Normal file
38
core/ui/src/main/res/layout/permission_allow_dialog.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingHorizontal="20dp"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/permission_header_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="10dp">
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/permission_title"
|
||||
style="@style/Signal.Text.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
tools:text="@string/Permissions_permission_required" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/permission_details"
|
||||
style="@style/Signal.Text.BodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
tools:text="@string/Permissions_permission_required" />
|
||||
|
||||
</LinearLayout>
|
||||
31
core/ui/src/main/res/layout/permissions_rationale_dialog.xml
Normal file
31
core/ui/src/main/res/layout/permissions_rationale_dialog.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/header_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/signal_colorPrimary"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="48dp">
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingTop="16dp"
|
||||
android:scrollbars="vertical"
|
||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
tools:text="Signal needs access to your contacts and media in order to connect with friends, exchange messages, and make secure calls." />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -3,4 +3,5 @@
|
||||
<attr name="theme_type" format="string"/>
|
||||
<attr name="fullScreenDialogStyle" format="reference"/>
|
||||
<attr name="fixedRoundedCornerBottomSheetStyle" format="reference"/>
|
||||
<attr name="permissionsRationaleDialogTheme" format="reference"/>
|
||||
</resources>
|
||||
|
||||
@@ -5,4 +5,20 @@
|
||||
<string name="Permissions_continue">Continue</string>
|
||||
<!-- Button label to dismiss or defer a permission request -->
|
||||
<string name="Permissions_not_now">Not now</string>
|
||||
<!-- 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>
|
||||
<!-- Sheet describing step 2 on how to give permissions by tapping permissions in their settings -->
|
||||
<string name="PermissionDeniedBottomSheet__2_tap_permissions">2. Tap "Permissions"</string>
|
||||
<!-- Sheet describing step 3 on how to give permissions for photos and videos in settings where %s will be replaced with an image of a checked button -->
|
||||
<string name="PermissionDeniedBottomSheet__3_allow_permission">3. %1$s Allow the "Photos and videos" permission</string>
|
||||
<!-- Label for button at the bottom of the sheet which opens the system permission settings -->
|
||||
<string name="PermissionDeniedBottomSheet__settings">Settings</string>
|
||||
<!-- Title for dialog shown when a required permission has not been granted -->
|
||||
<string name="Permissions_permission_required">Permission required</string>
|
||||
|
||||
<!-- StorageUtil -->
|
||||
<!-- Format string for displaying a storage path as volume/filename -->
|
||||
<string name="StorageUtil__s_s">%1$s/%2$s</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user