Support for selective permissions

This commit is contained in:
Moxie Marlinspike
2017-11-24 22:00:30 -08:00
parent 99a26e2bcc
commit 64c8b4b2ef
71 changed files with 1309 additions and 317 deletions

View File

@@ -0,0 +1,330 @@
package org.thoughtcrime.securesms.permissions;
import android.app.Activity;
import android.app.AlertDialog;
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.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.ViewGroup;
import android.view.WindowManager;
import com.annimon.stream.Stream;
import com.annimon.stream.function.Consumer;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.security.SecureRandom;
import java.util.List;
import java.util.Map;
public class Permissions {
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 boolean ifNecesary;
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 withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) {
this.rationalDialogHeader = headers;
this.rationaleDialogMessage = message;
return this;
}
public PermissionsBuilder withPermanentDenialDialog(@NonNull String message) {
return onAnyPermanentlyDenied(new SettingsDialogListener(permissionObject.getContext(), message));
}
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)) {
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<grantResults.length;i++) grantResults[i] = PackageManager.PERMISSION_GRANTED;
request.onResult(requestedPermissions, grantResults, new boolean[requestedPermissions.length]);
}
@SuppressWarnings("ConstantConditions")
private void executePermissionsRequestWithRationale(PermissionsRequest request) {
RationaleDialog.createFor(permissionObject.getContext(), rationaleDialogMessage, rationalDialogHeader)
.setPositiveButton("Continue", (dialog, which) -> executePermissionsRequest(request))
.setNegativeButton("Not now", null)
.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 static void requestPermissions(@NonNull Activity activity, int requestCode, String... permissions) {
ActivityCompat.requestPermissions(activity, filterNotGranted(activity, permissions), requestCode);
}
private static void requestPermissions(@NonNull Fragment fragment, int requestCode, String... permissions) {
fragment.requestPermissions(filterNotGranted(fragment.getContext(), permissions), 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 Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
Stream.of(permissions).anyMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
}
public static boolean hasAll(@NonNull Context context, String... permissions) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
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<permissions.length;i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
shouldShowRationaleDialog[i] = context.shouldShouldPermissionRationale(permissions[i]);
}
}
resultListener.onResult(permissions, grantResults, shouldShowRationaleDialog);
}
private 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 = ServiceUtil.getWindowManager(getContext());
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 Context context;
private final String message;
SettingsDialogListener(Context context, String message) {
this.message = message;
this.context = context.getApplicationContext();
}
@Override
public void run() {
new AlertDialog.Builder(context)
.setTitle("Permission required")
.setMessage(message)
.setPositiveButton("Continue", (dialog, which) -> context.startActivity(getApplicationSettingsIntent(context)))
.setNegativeButton("Cancel", null)
.show();
}
}
}

View File

@@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.permissions;
import android.content.pm.PackageManager;
import android.support.annotation.Nullable;
import com.annimon.stream.function.Consumer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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);
}
}

View File

@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.permissions;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Color;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
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 org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class RationaleDialog {
public static AlertDialog.Builder 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);
for (int i=0;i<drawables.length;i++) {
ImageView imageView = new ImageView(context);
imageView.setImageDrawable(context.getResources().getDrawable(drawables[i]));
imageView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
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(ViewUtil.dpToPx(context, 20), 0, ViewUtil.dpToPx(context, 20), 0);
plus.setLayoutParams(layoutParams);
header.addView(plus);
}
}
text.setText(message);
return new AlertDialog.Builder(context, R.style.RationaleDialog).setView(view);
}
}