Skip to content

Commit

Permalink
checkPermissionStatus for normal permissions (Baseflow#1198)
Browse files Browse the repository at this point in the history
* Implement `checkPermissionStatus`

* Add tests

* Make small touch-ups

* Run `dart format .`
  • Loading branch information
JeroenWeener committed Nov 1, 2023
1 parent 24d756f commit 28a934c
Show file tree
Hide file tree
Showing 17 changed files with 625 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ public ActivityCompatHostApiImpl(
this.instanceManager = instanceManager;
}

@NonNull
@Override
public Boolean shouldShowRequestPermissionRationale(
@NonNull public Boolean shouldShowRequestPermissionRationale(
@NonNull String activityInstanceId,
@NonNull String permission
) {
Expand All @@ -53,4 +52,17 @@ public Boolean shouldShowRequestPermissionRationale(
}
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
}

@Override
@NonNull public Long checkSelfPermission(
@NonNull String activityInstanceId,
@NonNull String permission
) {
final UUID activityInstanceUuid = UUID.fromString(activityInstanceId);
final Activity activity = instanceManager.getInstance(activityInstanceUuid);
if (activity == null) {
throw new ActivityNotFoundException();
}
return (long) ActivityCompat.checkSelfPermission(activity, permission);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public interface ActivityCompatHostApi {
/** Gets whether you should show UI with rationale before requesting a permission. */
@NonNull
Boolean shouldShowRequestPermissionRationale(@NonNull String activityInstanceId, @NonNull String permission);
/** Determine whether you have been granted a particular permission. */
@NonNull
Long checkSelfPermission(@NonNull String activityInstanceId, @NonNull String permission);

/** The codec used by ActivityCompatHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
Expand All @@ -93,6 +96,31 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ActivityCo
Boolean output = api.shouldShowRequestPermissionRationale(activityInstanceIdArg, permissionArg);
wrapped.add(0, output);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.permission_handler_android.ActivityCompatHostApi.checkSelfPermission", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String activityInstanceIdArg = (String) args.get(0);
String permissionArg = (String) args.get(1);
try {
Long output = api.checkSelfPermission(activityInstanceIdArg, permissionArg);
wrapped.add(0, output);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export 'src/android_object_mirrors/activity.dart';
export 'src/android_object_mirrors/activity_compat.dart';
export 'src/android_object_mirrors/manifest.dart';
export 'src/extensions.dart';
export 'src/missing_android_activity_exception.dart';

export 'src/android.dart';
export 'src/permission_handler_android.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'activity.dart';
///
/// See https://developer.android.com/reference/androidx/core/app/ActivityCompat.
class ActivityCompat {
const ActivityCompat._();

static ActivityCompatHostApiImpl _api = ActivityCompatHostApiImpl();

@visibleForTesting
Expand All @@ -22,4 +24,15 @@ class ActivityCompat {
permission,
);
}

/// Gets whether the app has been granted the given permission.
static Future<int> checkSelfPermission(
Activity activity,
String permission,
) {
return _api.checkSelfPermissionFromInstance(
activity,
permission,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// Class for retrieving various kinds of information related to the application
/// packages that are currently installed on the device. You can find this class
/// through Context#getPackageManager.
///
/// See https://developer.android.com/reference/android/content/pm/PackageManager.
class PackageManager {
const PackageManager._();

/// Permission check result: this is returned by checkPermission(String, String) if the permission has not been granted to the given package.
///
/// Constant Value: -1 (0xffffffff)
static const int permissionDenied = -1;

/// Permission check result: this is returned by checkPermission(String, String) if the permission has been granted to the given package.
///
/// Constant Value: 0 (0x00000000)
static const int permissionGranted = 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ class ActivityCompatHostApiImpl extends ActivityCompatHostApi {
permission,
);
}

/// Determine whether you have been granted a particular permission.
Future<int> checkSelfPermissionFromInstance(
Activity activity,
String permission,
) async {
final String activityInstanceId = instanceManager.getIdentifier(activity)!;

return checkSelfPermission(
activityInstanceId,
permission,
);
}
}

/// Flutter API implementation of Activity.
Expand Down
28 changes: 28 additions & 0 deletions permission_handler_android/lib/src/extensions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';

import 'android_object_mirrors/manifest.dart';

/// An extension on [Permission] that provides a [manifestStrings] getter.
extension PermissionToManifestStrings on Permission {
/// Returns the matching Manifest.permission strings for this permission.
///
/// TODO(jweener): translate all permissions that will be universally
/// available.
List<String> get manifestStrings {
// ignore: deprecated_member_use
if (this == Permission.calendarFullAccess || this == Permission.calendar) {
return [
Manifest.permission.readCalendar,
Manifest.permission.writeCalendar,
];
} else if (this == Permission.calendarWriteOnly) {
return [Manifest.permission.writeCalendar];
} else if (this == Permission.camera) {
return [Manifest.permission.camera];
}

throw UnimplementedError(
'There is no matching Manifest.permission string for $this',
);
}
}
31 changes: 31 additions & 0 deletions permission_handler_android/lib/src/permission_handler.pigeon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,37 @@ class ActivityCompatHostApi {
return (replyList[0] as bool?)!;
}
}

/// Determine whether you have been granted a particular permission.
Future<int> checkSelfPermission(
String arg_activityInstanceId, String arg_permission) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.permission_handler_android.ActivityCompatHostApi.checkSelfPermission',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_activityInstanceId, arg_permission])
as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as int?)!;
}
}
}

/// Flutter API for `Activity`.
Expand Down
51 changes: 42 additions & 9 deletions permission_handler_android/lib/src/permission_handler_android.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:permission_handler_android/src/extensions.dart';
import 'package:permission_handler_android/src/utils.dart';
import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';

import 'android_object_mirrors/activity.dart';
Expand Down Expand Up @@ -40,10 +42,33 @@ class PermissionHandlerAndroid extends PermissionHandlerPlatform {
);
}

/// TODO(jweener): implement this method.
/// TODO(jweener): handle special permissions.
@override
Future<PermissionStatus> checkPermissionStatus(Permission permission) {
return Future(() => PermissionStatus.denied);
Future<PermissionStatus> checkPermissionStatus(Permission permission) async {
if (_activity == null) {
throw const MissingAndroidActivityException();
}

final Iterable<PermissionStatus> statuses = await Future.wait(
permission.manifestStrings.map(
(String manifestString) async {
final int grantResult = await ActivityCompat.checkSelfPermission(
_activity!,
manifestString,
);

final PermissionStatus status = await grantResultToPermissionStatus(
_activity!,
manifestString,
grantResult,
);

return status;
},
),
);

return statuses.strictest;
}

/// TODO(jweener): implement this method.
Expand All @@ -54,16 +79,24 @@ class PermissionHandlerAndroid extends PermissionHandlerPlatform {
}

@override
Future<bool> shouldShowRequestPermissionRationale(Permission permission) {
Future<bool> shouldShowRequestPermissionRationale(
Permission permission,
) async {
if (_activity == null) {
throw const MissingAndroidActivityException();
}

return ActivityCompat.shouldShowRequestPermissionRationale(
_activity!,
// TODO(jweener): replace with Android manifest name for permission once
// they have been ported over.
'android.permission.READ_CONTACTS',
final Iterable<bool> shouldShowRationales = await Future.wait(
permission.manifestStrings.map(
(String manifestString) {
return ActivityCompat.shouldShowRequestPermissionRationale(
_activity!,
manifestString,
);
},
),
);

return shouldShowRationales.any((bool shouldShow) => shouldShow);
}
}
110 changes: 110 additions & 0 deletions permission_handler_android/lib/src/utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'android_object_mirrors/activity.dart';
import 'android_object_mirrors/activity_compat.dart';
import 'android_object_mirrors/package_manager.dart';

/// A class that provides methods for setting and getting whether a manifest
/// permission was denied before.
class ManifestPersistentStorage {
const ManifestPersistentStorage._();

/// Writes to shared preferences that the permission was denied before.
static Future<void> setDeniedBefore(String manifestString) async {
final sp = await SharedPreferences.getInstance();
sp.setBool(manifestString, true);
}

/// Reads from shared preferences if the permission was denied before.
static Future<bool> wasDeniedBefore(String manifestString) async {
final sp = await SharedPreferences.getInstance();
return sp.getBool(manifestString) ?? false;
}
}

/// Returns a [PermissionStatus] for a given manifest permission.
///
/// Note: This method has side-effects as it will store whether the permission
/// was denied before in persistent memory.
///
/// When [PackageManager.permissionDenied] is received, we do not know if the
/// permission was denied permanently. The OS does not tell us whether the
/// user dismissed the dialog or pressed 'deny'. Therefore, we need a more
/// sophisticated (read: hacky) approach to determine whether the permission
/// status is [PermissionStatus.denied] or
/// [PermissionStatus.permanentlyDenied].
///
/// The OS behavior has been researched experimentally and is displayed in the
/// following diagrams:
///
/// **State machine diagram:**
///
/// Dismissed
/// ┌┐
/// ┌──┘▼─────┐ Granted ┌───────┐
/// │Not asked├──────────►Granted│
/// └─┬───────┘ └─▲─────┘
/// │ Granted │
/// │Denied ┌───────────┘
/// │ │
/// ┌─▼────────┴┐ ┌────────────────────────────────┐
/// │Denied once├────────►Denied twice(permanently denied)│
/// └──▲┌───────┘ Denied └────────────────────────────────┘
/// └┘
/// Dismissed
///
/// **Scenario table listing output of
/// [ActivityCompat.shouldShowRequestPermissionRationale]:**
///
/// ┌────────────┬────────────────┬─────────┬───────────────────────────────────┬─────────────────────────┐
/// │ Scenario # │ Previous state │ Action │ New state │ 'Show rationale' output │
/// ├────────────┼────────────────┼─────────┼───────────────────────────────────┼─────────────────────────┤
/// │ 1. │ Not asked │ Dismiss │ Not asked │ false │
/// │ 2. │ Not asked │ Deny │ Denied once │ true │
/// │ 3. │ Denied once │ Dismiss │ Denied once │ true │
/// │ 4. │ Denied once │ Deny │ Denied twice (permanently denied) │ false │
/// └────────────┴────────────────┴─────────┴───────────────────────────────────┴─────────────────────────┘
///
/// To distinguish between scenarios, we can use
/// [ActivityCompat.shouldShowRequestPermissionRationale]. If it returns true,
/// we can safely return [PermissionStatus.denied]. To distinguish between
/// scenarios 1 and 4, however, we need an extra mechanism. We opt to store a
/// boolean stating whether permission has been requested before. Using a
/// combination of checking for showing the permission rationale and the
/// boolean, we can distinguish all scenarios and return the appropriate
/// permission status.
///
/// Changing permissions via the app info screen (so outside of the application)
/// changes the permission state to 'Granted' if the permission is allowed, or
/// 'Denied once' if denied. This behavior should not require any additional
/// logic.
Future<PermissionStatus> grantResultToPermissionStatus(
Activity activity,
String manifestString,
int grantResult,
) async {
if (grantResult == PackageManager.permissionGranted) {
return PermissionStatus.granted;
}

final bool wasDeniedBefore =
await ManifestPersistentStorage.wasDeniedBefore(manifestString);
final bool shouldShowRationale =
await ActivityCompat.shouldShowRequestPermissionRationale(
activity,
manifestString,
);

final bool isDeniedNow =
wasDeniedBefore ? !shouldShowRationale : shouldShowRationale;

if (!wasDeniedBefore && isDeniedNow) {
ManifestPersistentStorage.setDeniedBefore(manifestString);
}

if (wasDeniedBefore && isDeniedNow) {
return PermissionStatus.permanentlyDenied;
}
return PermissionStatus.denied;
}
Loading

0 comments on commit 28a934c

Please sign in to comment.