зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
d7d2e5a08c
Коммит
5de57b6ff3
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче