diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions')
3 files changed, 375 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java new file mode 100644 index 000000000..a4d72f258 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java @@ -0,0 +1,133 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.permissions; + +import org.mozilla.gecko.util.ThreadUtils; + +import android.app.Activity; +import android.content.Context; +import android.support.annotation.NonNull; + +/** + * Helper class to run code blocks depending on whether a user has granted or denied certain runtime permissions. + */ +public class PermissionBlock { + private final PermissionsHelper helper; + + private Context context; + private String[] permissions; + private boolean onUIThread; + private Runnable onPermissionsGranted; + private Runnable onPermissionsDenied; + private boolean doNotPrompt; + + /* package-private */ PermissionBlock(Context context, PermissionsHelper helper) { + this.context = context; + this.helper = helper; + } + + /** + * Determine whether the app has been granted the specified permissions. + */ + public PermissionBlock withPermissions(@NonNull String... permissions) { + this.permissions = permissions; + return this; + } + + /** + * Execute all callbacks on the UI thread. + */ + public PermissionBlock onUIThread() { + this.onUIThread = true; + return this; + } + + /** + * Do not prompt the user to accept the permission if it has not been granted yet. + */ + public PermissionBlock doNotPrompt() { + doNotPrompt = true; + return this; + } + + /** + * If the condition is true then do not prompt the user to accept the permission if it has not + * been granted yet. + */ + public PermissionBlock doNotPromptIf(boolean condition) { + if (condition) { + doNotPrompt(); + } + + return this; + } + + /** + * Execute this permission block. Calling this method will prompt the user if needed. + */ + public void run() { + run(null); + } + + /** + * Execute the specified runnable if the app has been granted all permissions. Calling this method will prompt the + * user if needed. + */ + public void run(Runnable onPermissionsGranted) { + if (!doNotPrompt && !(context instanceof Activity)) { + throw new IllegalStateException("You need to either specify doNotPrompt() or pass in an Activity context"); + } + + this.onPermissionsGranted = onPermissionsGranted; + + if (hasPermissions(context)) { + onPermissionsGranted(); + } else if (doNotPrompt) { + onPermissionsDenied(); + } else { + Permissions.prompt((Activity) context, this); + } + + // This reference is no longer needed. Let's clear it now to avoid memory leaks. + context = null; + } + + /** + * Execute this fallback if at least one permission has not been granted. + */ + public PermissionBlock andFallback(@NonNull Runnable onPermissionsDenied) { + this.onPermissionsDenied = onPermissionsDenied; + return this; + } + + /* package-private */ void onPermissionsGranted() { + executeRunnable(onPermissionsGranted); + } + + /* package-private */ void onPermissionsDenied() { + executeRunnable(onPermissionsDenied); + } + + private void executeRunnable(Runnable runnable) { + if (runnable == null) { + return; + } + + if (onUIThread) { + ThreadUtils.postToUiThread(runnable); + } else { + runnable.run(); + } + } + + /* package-private */ String[] getPermissions() { + return permissions; + } + + /* packacge-private */ boolean hasPermissions(Context context) { + return helper.hasPermissions(context, permissions); + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java new file mode 100644 index 000000000..c1b38f61c --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java @@ -0,0 +1,210 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.permissions; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.support.annotation.NonNull; + +import org.mozilla.gecko.util.ThreadUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +/** + * Convenience class for checking and prompting for runtime permissions. + * + * Example: + * + * Permissions.from(activity) + * .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + * .onUiThread() + * .andFallback(onPermissionDenied()) + * .run(onPermissionGranted()) + * + * This example will run the runnable returned by onPermissionGranted() if the WRITE_EXTERNAL_STORAGE permission is + * already granted. Otherwise it will prompt the user and run the runnable returned by onPermissionGranted() or + * onPermissionDenied() depending on whether the user accepted or not. If onUiThread() is specified then all callbacks + * will be run on the UI thread. + */ +public class Permissions { + private static final Queue<PermissionBlock> waiting = new LinkedList<>(); + private static final Queue<PermissionBlock> prompt = new LinkedList<>(); + + private static PermissionsHelper permissionHelper = new PermissionsHelper(); + + /** + * Entry point for checking (and optionally prompting for) runtime permissions. + * + * Note: The provided context needs to be an Activity context in order to prompt. Use doNotPrompt() + * for all other contexts. + */ + public static PermissionBlock from(@NonNull Context context) { + return new PermissionBlock(context, permissionHelper); + } + + /** + * This method will block until the specified permissions have been granted or denied by the user. + * If needed the user will be prompted. + * + * @return true if all of the permissions have been granted. False if any of the permissions have been denied. + */ + public static boolean waitFor(@NonNull Activity activity, String... permissions) { + ThreadUtils.assertNotOnUiThread(); // We do not want to block the UI thread. + + // This task will block until all of the permissions have been granted + final FutureTask<Boolean> blockingTask = new FutureTask<>(new Callable<Boolean>() { + @Override + public Boolean call() throws Exception { + return true; + } + }); + + // This runnable will cancel the task if any of the permissions have been denied + Runnable cancelBlockingTask = new Runnable() { + @Override + public void run() { + blockingTask.cancel(true); + } + }; + + Permissions.from(activity) + .withPermissions(permissions) + .andFallback(cancelBlockingTask) + .run(blockingTask); + + try { + return blockingTask.get(); + } catch (InterruptedException | ExecutionException | CancellationException e) { + return false; + } + } + + /** + * Determine whether you have been granted particular permissions. + */ + public static boolean has(Context context, String... permissions) { + return permissionHelper.hasPermissions(context, permissions); + } + + /* package-private */ static void setPermissionHelper(PermissionsHelper permissionHelper) { + Permissions.permissionHelper = permissionHelper; + } + + /** + * Callback for Activity.onRequestPermissionsResult(). All activities that prompt for permissions using this class + * should implement onRequestPermissionsResult() and call this method. + */ + public static synchronized void onRequestPermissionsResult(@NonNull Activity activity, @NonNull String[] permissions, @NonNull int[] grantResults) { + processGrantResults(permissions, grantResults); + + processQueue(activity, permissions, grantResults); + } + + /* package-private */ static synchronized void prompt(Activity activity, PermissionBlock block) { + if (prompt.isEmpty()) { + prompt.add(block); + showPrompt(activity); + } else { + waiting.add(block); + } + } + + private static synchronized void processGrantResults(@NonNull String[] permissions, @NonNull int[] grantResults) { + final HashSet<String> grantedPermissions = collectGrantedPermissions(permissions, grantResults); + + while (!prompt.isEmpty()) { + final PermissionBlock block = prompt.poll(); + + if (allPermissionsGranted(block, grantedPermissions)) { + block.onPermissionsGranted(); + } else { + block.onPermissionsDenied(); + } + } + } + + private static synchronized void processQueue(Activity activity, String[] permissions, int[] grantResults) { + final HashSet<String> deniedPermissions = collectDeniedPermissions(permissions, grantResults); + + while (!waiting.isEmpty()) { + final PermissionBlock block = waiting.poll(); + + if (block.hasPermissions(activity)) { + block.onPermissionsGranted(); + } else { + if (atLeastOnePermissionDenied(block, deniedPermissions)) { + // We just prompted the user and one of the permissions of this block has been denied: + // There's no reason to instantly prompt again; Just reject without prompting. + block.onPermissionsDenied(); + } else { + prompt.add(block); + } + } + } + + if (!prompt.isEmpty()) { + showPrompt(activity); + } + } + + private static synchronized void showPrompt(Activity activity) { + HashSet<String> permissions = new HashSet<>(); + + for (PermissionBlock block : prompt) { + Collections.addAll(permissions, block.getPermissions()); + } + + permissionHelper.prompt(activity, permissions.toArray(new String[permissions.size()])); + } + + private static HashSet<String> collectGrantedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) { + return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_GRANTED); + } + + private static HashSet<String> collectDeniedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) { + return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_DENIED); + } + + private static HashSet<String> filterPermissionsByResult(@NonNull String[] permissions, @NonNull int[] grantResults, int result) { + HashSet<String> grantedPermissions = new HashSet<>(permissions.length); + for (int i = 0; i < permissions.length; i++) { + if (grantResults[i] == result) { + grantedPermissions.add(permissions[i]); + } + } + return grantedPermissions; + } + + private static boolean allPermissionsGranted(PermissionBlock block, HashSet<String> grantedPermissions) { + for (String permission : block.getPermissions()) { + if (!grantedPermissions.contains(permission)) { + return false; + } + } + + return true; + } + + private static boolean atLeastOnePermissionDenied(PermissionBlock block, HashSet<String> deniedPermissions) { + for (String permission : block.getPermissions()) { + if (deniedPermissions.contains(permission)) { + return true; + } + } + + return false; + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java new file mode 100644 index 000000000..945a81f43 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java @@ -0,0 +1,32 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.permissions; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; + +/* package-private */ class PermissionsHelper { + private static final int PERMISSIONS_REQUEST_CODE = 212; + + public boolean hasPermissions(Context context, String... permissions) { + for (String permission : permissions) { + final int permissionCheck = ContextCompat.checkSelfPermission(context, permission); + + if (permissionCheck != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + + return true; + } + + public void prompt(Activity activity, String[] permissions) { + ActivityCompat.requestPermissions(activity, permissions, PERMISSIONS_REQUEST_CODE); + } +} |