package org.tm.archive.permissions; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; 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 com.annimon.stream.Stream; import com.annimon.stream.function.Consumer; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.logging.Log; import org.tm.archive.R; import org.tm.archive.util.LRUCache; import org.tm.archive.util.ServiceUtil; import java.lang.ref.WeakReference; import java.security.SecureRandom; import java.util.Arrays; import java.util.List; import java.util.Map; public class Permissions { private static final String TAG = Log.tag(Permissions.class); private static final Map 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 boolean isRuntimePermissionsRequired() { return Build.VERSION.SDK_INT >= 23; } 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> someGrantedListener; private Consumer> someDeniedListener; private Consumer> somePermanentlyDeniedListener; private @DrawableRes int[] rationalDialogHeader; private String rationaleDialogMessage; 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) { this.rationalDialogHeader = headers; this.rationaleDialogMessage = message; 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 onAnyPermanentlyDenied(new SettingsDialogListener(permissionObject.getContext(), message, onDialogDismissed)); } 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> someGrantedListener) { this.someGrantedListener = someGrantedListener; return this; } public PermissionsBuilder onSomeDenied(Consumer> someDeniedListener) { this.someDeniedListener = someDeniedListener; return this; } public PermissionsBuilder onSomePermanentlyDenied(Consumer> 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 && rationalDialogHeader != null) { executePermissionsRequestWithRationale(request); } else { executePermissionsRequest(request); } } private void executePreGrantedPermissionsRequest(PermissionsRequest request) { int[] grantResults = new int[requestedPermissions.length]; for (int i=0;i executePermissionsRequest(request)) .setNegativeButton(R.string.Permissions_not_now, (dialog, which) -> executeNoPermissionsRequest(request)) .setCancelable(rationaleDialogCancelable) .show() .getWindow() .setLayout((int)(permissionObject.getWindowWidth() * .75), ViewGroup.LayoutParams.WRAP_CONTENT); } 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 = Stream.of(permissions).mapToInt(permission -> PackageManager.PERMISSION_DENIED).toArray(); 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 Stream.of(permissions) .filter(permission -> ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) .toList() .toArray(new String[0]); } public static boolean hasAny(@NonNull Context context, String... permissions) { return !isRuntimePermissionsRequired() || Stream.of(permissions).anyMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED); } public static boolean hasAll(@NonNull Context context, String... permissions) { return !isRuntimePermissionsRequired() || Stream.of(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 context; private final Runnable onDialogDismissed; private final String message; SettingsDialogListener(Context context, String message, @Nullable Runnable onDialogDismissed) { this.message = message; this.context = new WeakReference<>(context); this.onDialogDismissed = onDialogDismissed; } @Override public void run() { Context context = this.context.get(); if (context != null) { 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(); } } } }