Bug 1235347 - Add Permissions helper class for checking and requesting runtime permissions. r=nalexander

--HG--
extra : commitid : LVJJOpNAzdc
extra : rebase_source : 6e18b47ca930146c558ff199cfb84ce19938f985
This commit is contained in:
Sebastian Kaspari 2016-01-11 12:05:08 +01:00
Родитель d7d2e5a08c
Коммит 5de57b6ff3
5 изменённых файлов: 461 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,99 @@
/* -*- 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 Activity activity;
private String[] permissions;
private boolean onUIThread;
private Runnable onPermissionsGranted;
private Runnable onPermissionsDenied;
/* package-private */ PermissionBlock(Activity activity, PermissionsHelper helper) {
this.activity = activity;
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;
}
/**
* Execute the specified runnable if the app has been granted all permissions. Calling this method will prompt the
* user if needed.
*/
public void run(@NonNull Runnable onPermissionsGranted) {
this.onPermissionsGranted = onPermissionsGranted;
if (hasPermissions(activity)) {
onPermissionsGranted();
} else {
Permissions.prompt(activity, this);
}
// This reference is no longer needed. Let's clear it now to avoid memory leaks.
activity = 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);
}
}

Просмотреть файл

@ -0,0 +1,130 @@
/* -*- 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.pm.PackageManager;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* 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.
*/
public static PermissionBlock from(@NonNull Activity activity) {
return new PermissionBlock(activity, permissionHelper);
}
/* 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);
}
/* 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) {
HashSet<String> grantedPermissions = collectGrantedPermissions(permissions, grantResults);
while (!prompt.isEmpty()) {
PermissionBlock block = prompt.poll();
if (allPermissionsGranted(block, grantedPermissions)) {
block.onPermissionsGranted();
} else {
block.onPermissionsDenied();
}
}
}
private static synchronized void processQueue(Activity activity) {
while (!waiting.isEmpty()) {
PermissionBlock block = waiting.poll();
if (block.hasPermissions(activity)) {
block.onPermissionsGranted();
} 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) {
HashSet<String> grantedPermissions = new HashSet<>(permissions.length);
for (int i = 0; i < permissions.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
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;
}
}

Просмотреть файл

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

Просмотреть файл

@ -455,6 +455,9 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'overlays/ui/SendTabList.java', 'overlays/ui/SendTabList.java',
'overlays/ui/SendTabTargetSelectedListener.java', 'overlays/ui/SendTabTargetSelectedListener.java',
'overlays/ui/ShareDialog.java', 'overlays/ui/ShareDialog.java',
'permissions/PermissionBlock.java',
'permissions/Permissions.java',
'permissions/PermissionsHelper.java',
'preferences/AlignRightLinkPreference.java', 'preferences/AlignRightLinkPreference.java',
'preferences/AndroidImport.java', 'preferences/AndroidImport.java',
'preferences/AndroidImportPreference.java', 'preferences/AndroidImportPreference.java',

Просмотреть файл

@ -0,0 +1,197 @@
/* -*- 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.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import static org.mockito.Mockito.*;
@RunWith(TestRunner.class)
public class TestPermissions {
@Test
public void testSuccessRunnableIsExecutedIfPermissionsAreGranted() {
Permissions.setPermissionHelper(mockGrantingHelper());
Runnable onPermissionsGranted = mock(Runnable.class);
Runnable onPermissionsDenied = mock(Runnable.class);
Permissions.from(mockActivity())
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.andFallback(onPermissionsDenied)
.run(onPermissionsGranted);
verify(onPermissionsDenied, never()).run();
verify(onPermissionsGranted).run();
}
@Test
public void testFallbackRunnableIsExecutedIfPermissionsAreDenied() {
Permissions.setPermissionHelper(mockDenyingHelper());
Runnable onPermissionsGranted = mock(Runnable.class);
Runnable onPermissionsDenied = mock(Runnable.class);
Activity activity = mockActivity();
Permissions.from(activity)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.andFallback(onPermissionsDenied)
.run(onPermissionsGranted);
Permissions.onRequestPermissionsResult(activity, new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, new int[]{
PackageManager.PERMISSION_DENIED
});
verify(onPermissionsDenied).run();
verify(onPermissionsGranted, never()).run();
}
@Test
public void testPromptingForNotGrantedPermissions() {
Activity activity = mockActivity();
PermissionsHelper helper = mockDenyingHelper();
Permissions.setPermissionHelper(helper);
Permissions.from(activity)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.andFallback(mock(Runnable.class))
.run(mock(Runnable.class));
verify(helper).prompt(anyActivity(), any(String[].class));
Permissions.onRequestPermissionsResult(activity, new String[0], new int[0]);
}
@Test
public void testMultipleRequestsAreQueuedAndDispatchedSequentially() {
Activity activity = mockActivity();
PermissionsHelper helper = mockDenyingHelper();
Permissions.setPermissionHelper(helper);
Runnable onFirstPermissionGranted = mock(Runnable.class);
Runnable onSecondPermissionDenied = mock(Runnable.class);
Permissions.from(activity)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.andFallback(mock(Runnable.class))
.run(onFirstPermissionGranted);
Permissions.from(activity)
.withPermissions(Manifest.permission.CAMERA)
.andFallback(onSecondPermissionDenied)
.run(mock(Runnable.class));
Permissions.onRequestPermissionsResult(activity, new String[] {
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, new int[] {
PackageManager.PERMISSION_GRANTED
});
verify(onFirstPermissionGranted).run();
verify(onSecondPermissionDenied, never()).run(); // Second request is queued but not executed yet
Permissions.onRequestPermissionsResult(activity, new String[]{
Manifest.permission.CAMERA
}, new int[]{
PackageManager.PERMISSION_DENIED
});
verify(onFirstPermissionGranted).run();
verify(onSecondPermissionDenied).run();
verify(helper, times(2)).prompt(anyActivity(), any(String[].class));
}
@Test
public void testSecondRequestWillNotPromptIfPermissionHasBeenGranted() {
Activity activity = mockActivity();
PermissionsHelper helper = mock(PermissionsHelper.class);
Permissions.setPermissionHelper(helper);
when(helper.hasPermissions(anyContext(), anyPermissions()))
.thenReturn(false)
.thenReturn(false)
.thenReturn(true); // Revaluation is successful
Permissions.from(activity)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.andFallback(mock(Runnable.class))
.run(mock(Runnable.class));
Permissions.from(activity)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.andFallback(mock(Runnable.class))
.run(mock(Runnable.class));
Permissions.onRequestPermissionsResult(activity, new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, new int[]{
PackageManager.PERMISSION_GRANTED
});
verify(helper, times(1)).prompt(anyActivity(), any(String[].class));
}
@Test
public void testEmptyPermissionsArrayWillExecuteRunnableAndNotTryToPrompt() {
PermissionsHelper helper = spy(new PermissionsHelper());
Permissions.setPermissionHelper(helper);
Runnable onPermissionGranted = mock(Runnable.class);
Runnable onPermissionDenied = mock(Runnable.class);
Permissions.from(mockActivity())
.withPermissions()
.andFallback(onPermissionDenied)
.run(onPermissionGranted);
verify(onPermissionGranted).run();
verify(onPermissionDenied, never()).run();
verify(helper, never()).prompt(anyActivity(), any(String[].class));
}
private Activity mockActivity() {
return mock(Activity.class);
}
private PermissionsHelper mockGrantingHelper() {
PermissionsHelper helper = mock(PermissionsHelper.class);
doReturn(true).when(helper).hasPermissions(any(Context.class), anyPermissions());
return helper;
}
private PermissionsHelper mockDenyingHelper() {
PermissionsHelper helper = mock(PermissionsHelper.class);
doReturn(false).when(helper).hasPermissions(any(Context.class), anyPermissions());
return helper;
}
private String anyPermissions() {
return Matchers.anyVararg();
}
private Activity anyActivity() {
return any(Activity.class);
}
private Context anyContext() {
return any(Context.class);
}
}