Commit c81c441f authored by bttk's avatar bttk Committed by Commit Bot

Android permission requests with one-time permissions in mind

A change of the meaning of shared preferences starting with
"HasRequestedAndroidPermission::"

  Without one-time permissions it was enough to remember *if* the user
  was asked for an Android permission. But now, we need to remember
  user's response. A permission that at first was granted can be later
  denied without any user action.

This change is tested using the newest methods provided by Robolectric
and androidx.test

  Robolectric.buildActivity is a low-level tool for managing Activity
  lifetime and Robolectric.setupActivity is deprecated in favor of
  androidx.test.core.app.ActivityScenario

  For the ActivityScenario to work, the Activity used in test must be
  declared in the AndroidManifest

Change-Id: I2f809e43dd4790a09cdac08cc0e0803a6fed086e
Bug: 1096085
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2285470
Commit-Queue: who/bttk <bttk@chromium.org>
Reviewed-by: default avatarPatrick Noland <pnoland@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Cr-Commit-Position: refs/heads/master@{#790189}
parent adc9d250
...@@ -370,6 +370,7 @@ android_library("ui_java_test_support") { ...@@ -370,6 +370,7 @@ android_library("ui_java_test_support") {
"javatests/src/org/chromium/ui/test/util/UiRestriction.java", "javatests/src/org/chromium/ui/test/util/UiRestriction.java",
"javatests/src/org/chromium/ui/test/util/UiRestrictionSkipCheck.java", "javatests/src/org/chromium/ui/test/util/UiRestrictionSkipCheck.java",
"javatests/src/org/chromium/ui/test/util/modelutil/FakeViewProvider.java", "javatests/src/org/chromium/ui/test/util/modelutil/FakeViewProvider.java",
"junit/src/org/chromium/ui/base/TestActivity.java",
] ]
deps = [ deps = [
":ui_java", ":ui_java",
...@@ -383,19 +384,20 @@ android_library("ui_java_test_support") { ...@@ -383,19 +384,20 @@ android_library("ui_java_test_support") {
} }
android_resources("ui_javatest_resources") { android_resources("ui_javatest_resources") {
custom_package = "org.chromium.test.ui" testonly = true
sources = [ sources = [
"junit/res/layout/inflated_view.xml", "junit/res/layout/inflated_view.xml",
"junit/res/layout/layout_view_builder_test.xml", "junit/res/layout/layout_view_builder_test.xml",
"junit/res/layout/main_view.xml", "junit/res/layout/main_view.xml",
] ]
android_manifest = "junit/AndroidManifest.xml"
} }
junit_binary("ui_junit_tests") { junit_binary("ui_junit_tests") {
package_name = "org.chromium.test.ui"
sources = [ sources = [
"junit/src/org/chromium/ui/AsyncViewProviderTest.java", "junit/src/org/chromium/ui/AsyncViewProviderTest.java",
"junit/src/org/chromium/ui/AsyncViewStubTest.java", "junit/src/org/chromium/ui/AsyncViewStubTest.java",
"junit/src/org/chromium/ui/base/ActivityAndroidPermissionDelegateTest.java",
"junit/src/org/chromium/ui/base/ApplicationViewportInsetSupplierTest.java", "junit/src/org/chromium/ui/base/ApplicationViewportInsetSupplierTest.java",
"junit/src/org/chromium/ui/base/ClipboardTest.java", "junit/src/org/chromium/ui/base/ClipboardTest.java",
"junit/src/org/chromium/ui/base/EventOffsetHandlerTest.java", "junit/src/org/chromium/ui/base/EventOffsetHandlerTest.java",
...@@ -431,11 +433,16 @@ junit_binary("ui_junit_tests") { ...@@ -431,11 +433,16 @@ junit_binary("ui_junit_tests") {
"//base:base_junit_test_support", "//base:base_junit_test_support",
"//base/test:test_support_java", "//base/test:test_support_java",
"//third_party/android_deps:androidx_annotation_annotation_java", "//third_party/android_deps:androidx_annotation_annotation_java",
"//third_party/android_deps:androidx_appcompat_appcompat_java",
"//third_party/android_deps:androidx_appcompat_appcompat_resources_java", "//third_party/android_deps:androidx_appcompat_appcompat_resources_java",
"//third_party/android_deps:androidx_asynclayoutinflater_asynclayoutinflater_java", "//third_party/android_deps:androidx_asynclayoutinflater_asynclayoutinflater_java",
"//third_party/android_deps:androidx_test_core_java",
"//third_party/android_deps:androidx_test_ext_junit_java",
"//third_party/android_deps:androidx_test_runner_java", "//third_party/android_deps:androidx_test_runner_java",
"//third_party/hamcrest:hamcrest_java", "//third_party/hamcrest:hamcrest_java",
"//third_party/mockito:mockito_java",
] ]
android_manifest = "junit/AndroidManifest.xml"
} }
test("ui_android_unittests") { test("ui_android_unittests") {
......
...@@ -10,6 +10,7 @@ package org.chromium.ui.base; ...@@ -10,6 +10,7 @@ package org.chromium.ui.base;
public interface AndroidPermissionDelegate { public interface AndroidPermissionDelegate {
/** /**
* Determine whether access to a particular permission is granted. * Determine whether access to a particular permission is granted.
*
* @param permission The permission whose access is to be checked. * @param permission The permission whose access is to be checked.
* @return Whether access to the permission is granted. * @return Whether access to the permission is granted.
*/ */
...@@ -18,13 +19,14 @@ public interface AndroidPermissionDelegate { ...@@ -18,13 +19,14 @@ public interface AndroidPermissionDelegate {
/** /**
* Determine whether the specified permission can be requested. * Determine whether the specified permission can be requested.
* <p> * <p>
* A permission can be requested in the following states: * A permission can not be requested in the following states:
* 1.) Default un-granted state, permission can be requested * <ul>
* 2.) Permission previously requested but denied by the user, but the user did not select * <li>Permission is denied by policy.
* "Never ask again". * <li>Permission previously denied and the user selected "Never ask again".
* </ul>
* *
* @param permission The permission name. * @param permission The permission name.
* @return Whether the requesting the permission is allowed. * @return Whether the permission can be requested.
*/ */
boolean canRequestPermission(String permission); boolean canRequestPermission(String permission);
......
...@@ -30,7 +30,18 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP ...@@ -30,7 +30,18 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP
private static final int REQUEST_CODE_PREFIX = 1000; private static final int REQUEST_CODE_PREFIX = 1000;
private static final int REQUEST_CODE_RANGE_SIZE = 100; private static final int REQUEST_CODE_RANGE_SIZE = 100;
private static final String PERMISSION_QUERIED_KEY_PREFIX = "HasRequestedAndroidPermission::"; /**
* Shared preference key prefix for remembering Android permissions denied by the user.
* <p>
* <b>NOTE:</b> As of M86 the semantics of shared prefs using this key prefix has changed:
* <ul>
* <li>Previously: {@code true} if the user was ever asked for a permission, otherwise absent.
* <li>M86+: {@code true} if the user most recently has denied permission access,
* otherwise absent.
* </ul>
*/
private static final String PERMISSION_WAS_DENIED_KEY_PREFIX =
"HasRequestedAndroidPermission::";
public AndroidPermissionDelegateWithRequester() { public AndroidPermissionDelegateWithRequester() {
mHandler = new Handler(); mHandler = new Handler();
...@@ -39,9 +50,27 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP ...@@ -39,9 +50,27 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP
@Override @Override
public final boolean hasPermission(String permission) { public final boolean hasPermission(String permission) {
return ApiCompatibilityUtils.checkPermission(ContextUtils.getApplicationContext(), boolean isGranted =
permission, Process.myPid(), Process.myUid()) ApiCompatibilityUtils.checkPermission(ContextUtils.getApplicationContext(),
permission, Process.myPid(), Process.myUid())
== PackageManager.PERMISSION_GRANTED; == PackageManager.PERMISSION_GRANTED;
if (isGranted) {
clearPermissionWasDenied(permission);
}
return isGranted;
}
/**
* Clear the shared pref indicating that {@code permission} was denied by the user.
*/
private void clearPermissionWasDenied(String permission) {
String key = getPermissionWasDeniedKey(permission);
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
if (!prefs.contains(key)) return;
SharedPreferences.Editor editor = prefs.edit();
editor.remove(key);
editor.apply();
} }
/** @see Activity.shouldShowRequestPermissionRationale */ /** @see Activity.shouldShowRequestPermissionRationale */
...@@ -51,22 +80,30 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP ...@@ -51,22 +80,30 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP
public final boolean canRequestPermission(String permission) { public final boolean canRequestPermission(String permission) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false;
if (hasPermission(permission)) {
// There is no need to call clearPermissionWasDenied - hasPermission already cleared
// the shared pref if needed.
return true;
}
if (isPermissionRevokedByPolicy(permission)) { if (isPermissionRevokedByPolicy(permission)) {
return false; return false;
} }
if (shouldShowRequestPermissionRationale(permission)) { if (shouldShowRequestPermissionRationale(permission)) {
// This information from Android suggests we should not assume the user will always deny
// the permission.
clearPermissionWasDenied(permission);
return true; return true;
} }
// Check whether we have ever asked for this permission by checking whether we saved // Check whether we have been denied this permission by checking whether we saved
// a preference associated with it before. // a preference associated with it before.
String permissionQueriedKey = getHasRequestedPermissionKey(permission);
SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
return !prefs.getBoolean(permissionQueriedKey, false); return !prefs.getBoolean(getPermissionWasDeniedKey(permission), false);
} }
/** @see PackageManager.isPermissionRevokedByPolicy */ /** @see PackageManager#isPermissionRevokedByPolicy(String, String) */
protected abstract boolean isPermissionRevokedByPolicyInternal(String permission); protected abstract boolean isPermissionRevokedByPolicyInternal(String permission);
@Override @Override
...@@ -102,7 +139,11 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP ...@@ -102,7 +139,11 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP
int requestCode, String[] permissions, int[] grantResults) { int requestCode, String[] permissions, int[] grantResults) {
SharedPreferences.Editor editor = ContextUtils.getAppSharedPreferences().edit(); SharedPreferences.Editor editor = ContextUtils.getAppSharedPreferences().edit();
for (int i = 0; i < permissions.length; i++) { for (int i = 0; i < permissions.length; i++) {
editor.putBoolean(getHasRequestedPermissionKey(permissions[i]), true); if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
editor.remove(getPermissionWasDeniedKey(permissions[i]));
} else {
editor.putBoolean(getPermissionWasDeniedKey(permissions[i]), true);
}
} }
editor.apply(); editor.apply();
...@@ -133,8 +174,7 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP ...@@ -133,8 +174,7 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP
return true; return true;
} }
private String getHasRequestedPermissionKey(String permission) { private String normalizePermissionName(String permission) {
String permissionQueriedKey = permission;
// Prior to O, permissions were granted at the group level. Post O, each permission is // Prior to O, permissions were granted at the group level. Post O, each permission is
// granted individually. // granted individually.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
...@@ -151,13 +191,21 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP ...@@ -151,13 +191,21 @@ public abstract class AndroidPermissionDelegateWithRequester implements AndroidP
permission, PackageManager.GET_META_DATA); permission, PackageManager.GET_META_DATA);
if (!TextUtils.isEmpty(permissionInfo.group)) { if (!TextUtils.isEmpty(permissionInfo.group)) {
permissionQueriedKey = permissionInfo.group; return permissionInfo.group;
} }
} catch (NameNotFoundException e) { } catch (NameNotFoundException e) {
// Unknown permission. Default back to the permission name instead of the group. // Unknown permission. Default back to the permission name instead of the group.
} }
} }
return PERMISSION_QUERIED_KEY_PREFIX + permissionQueriedKey; return permission;
}
/**
* Returns the name of a shared preferences key used to store whether Chrome was denied
* {@code permission}.
*/
private String getPermissionWasDeniedKey(String permission) {
return PERMISSION_WAS_DENIED_KEY_PREFIX + normalizePermissionName(permission);
} }
} }
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<!-- This manifest is required for tests using
androidx.test.core.app.ActivityScenario to work.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.chromium.test.ui">
<!-- Value used by ActivityAndroidPermissionDelegateTest -->
<uses-permission android:name="android.permission.INTERNET"/>
<application android:theme="@style/Theme.AppCompat.Light">
<activity android:name="org.chromium.ui.base.TestActivity"/>
</application>
</manifest>
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.ui.base;
import androidx.appcompat.app.AppCompatActivity;
/**
* Activity used in {@code ui/base} tests.
* <p>
* This activity is declared in {@code ui/android/junit/AndroidManifest.xml}.
*/
public class TestActivity extends AppCompatActivity {}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment