Commit 11055f41 by Jeroen Weener Committed by GitHub

Return 'permanently denied' as permission status (#1130)

* Return 'permanently denied' as status when appropriate

* Make `checkPermissionStatus` callable from background

* Update version number

* Make use of `Activity` more explicit

* Format java code

* Refactor activity and context bookkeeping

* Initialize `PermissionManager` in `onAttachedToEngine`

* Follow-up from merge
parent 921652cd
## 11.0.0
* **BREAKING CHANGE:** Fixes a bug where the permission status would return 'denied' regardless of whether the status was 'denied' or 'permanently denied'.
## 10.3.6
* Fixes a bug where requesting multiple permissions would crash the app if at least one of the permissions was a [special permission](https://developer.android.com/guide/topics/permissions/overview#special).
......
package com.baseflow.permissionhandler;
import android.app.Activity;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.Result;
......@@ -19,81 +17,70 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {
private final ServiceManager serviceManager;
MethodCallHandlerImpl(
Context applicationContext,
AppSettingsManager appSettingsManager,
PermissionManager permissionManager,
ServiceManager serviceManager) {
Context applicationContext,
AppSettingsManager appSettingsManager,
PermissionManager permissionManager,
ServiceManager serviceManager) {
this.applicationContext = applicationContext;
this.appSettingsManager = appSettingsManager;
this.permissionManager = permissionManager;
this.serviceManager = serviceManager;
}
@Nullable
private Activity activity;
public void setActivity(@Nullable Activity activity) {
this.activity = activity;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
{
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) {
switch (call.method) {
case "checkServiceStatus": {
@PermissionConstants.PermissionGroup final int permission = Integer.parseInt(call.arguments.toString());
serviceManager.checkServiceStatus(
permission,
applicationContext,
result::success,
(String errorCode, String errorDescription) -> result.error(
errorCode,
errorDescription,
null));
permission,
applicationContext,
result::success,
(String errorCode, String errorDescription) -> result.error(
errorCode,
errorDescription,
null));
break;
}
case "checkPermissionStatus": {
@PermissionConstants.PermissionGroup final int permission = Integer.parseInt(call.arguments.toString());
permissionManager.checkPermissionStatus(
permission,
applicationContext,
result::success);
permission,
result::success);
break;
}
case "requestPermissions":
final List<Integer> permissions = call.arguments();
permissionManager.requestPermissions(
permissions,
activity,
result::success,
(String errorCode, String errorDescription) -> result.error(
errorCode,
errorDescription,
null));
permissions,
result::success,
(String errorCode, String errorDescription) -> result.error(
errorCode,
errorDescription,
null));
break;
case "shouldShowRequestPermissionRationale": {
@PermissionConstants.PermissionGroup final int permission = Integer.parseInt(call.arguments.toString());
permissionManager.shouldShowRequestPermissionRationale(
permission,
activity,
result::success,
(String errorCode, String errorDescription) -> result.error(
errorCode,
errorDescription,
null));
permission,
result::success,
(String errorCode, String errorDescription) -> result.error(
errorCode,
errorDescription,
null));
break;
}
case "openAppSettings":
appSettingsManager.openAppSettings(
applicationContext,
result::success,
(String errorCode, String errorDescription) -> result.error(
errorCode,
errorDescription,
null));
applicationContext,
result::success,
(String errorCode, String errorDescription) -> result.error(
errorCode,
errorDescription,
null));
break;
default:
......
......@@ -13,7 +13,7 @@ import io.flutter.plugin.common.MethodChannel;
/**
* Platform implementation of the permission_handler Flutter plugin.
*
* <p>Instantiate this in an add to app scenario to gracefully handle activity and context changes.
* <p>Instantiate this in an add-to-app scenario to gracefully handle activity and context changes.
* See {@code com.example.permissionhandlerexample.MainActivity} for an example.
*
* <p>Call {@link #registerWith(io.flutter.plugin.common.PluginRegistry.Registrar)} to register an
......@@ -21,7 +21,8 @@ import io.flutter.plugin.common.MethodChannel;
*/
public final class PermissionHandlerPlugin implements FlutterPlugin, ActivityAware {
private final PermissionManager permissionManager;
private PermissionManager permissionManager;
private MethodChannel methodChannel;
@SuppressWarnings("deprecation")
......@@ -32,10 +33,6 @@ public final class PermissionHandlerPlugin implements FlutterPlugin, ActivityAwa
@Nullable
private MethodCallHandlerImpl methodCallHandler;
public PermissionHandlerPlugin() {
this.permissionManager = new PermissionManager();
}
/**
* Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common}
* package.
......@@ -48,6 +45,7 @@ public final class PermissionHandlerPlugin implements FlutterPlugin, ActivityAwa
final PermissionHandlerPlugin plugin = new PermissionHandlerPlugin();
plugin.pluginRegistrar = registrar;
plugin.permissionManager = new PermissionManager(registrar.context());
plugin.registerListeners();
plugin.startListening(registrar.context(), registrar.messenger());
......@@ -61,6 +59,8 @@ public final class PermissionHandlerPlugin implements FlutterPlugin, ActivityAwa
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
this.permissionManager = new PermissionManager(binding.getApplicationContext());
startListening(
binding.getApplicationContext(),
binding.getBinaryMessenger()
......@@ -124,14 +124,14 @@ public final class PermissionHandlerPlugin implements FlutterPlugin, ActivityAwa
private void startListeningToActivity(
Activity activity
) {
if (methodCallHandler != null) {
methodCallHandler.setActivity(activity);
if (permissionManager != null) {
permissionManager.setActivity(activity);
}
}
private void stopListeningToActivity() {
if (methodCallHandler != null) {
methodCallHandler.setActivity(null);
if (permissionManager != null) {
permissionManager.setActivity(null);
}
}
......
......@@ -36,10 +36,13 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
@Nullable
private Activity activity;
@NonNull
private final Context context;
/**
* The number of pending permission requests.
* <p>
* This number is set by {@link this#requestPermissions(List, Activity, RequestPermissionsSuccessCallback, ErrorCallback)}
* This number is set by {@link this#requestPermissions(List, RequestPermissionsSuccessCallback, ErrorCallback)}
* and then reduced when receiving results in {@link this#onActivityResult(int, int, Intent)}
* and {@link this#onRequestPermissionsResult(int, String[], int[])}.
*/
......@@ -51,10 +54,18 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
* {@link this#onActivityResult(int, int, Intent)} and
* {@link this#onRequestPermissionsResult(int, String[], int[])}.
* It is (re)initialized when new permissions are requested through
* {@link this#requestPermissions(List, Activity, RequestPermissionsSuccessCallback, ErrorCallback)}.
* {@link this#requestPermissions(List, RequestPermissionsSuccessCallback, ErrorCallback)}.
*/
private Map<Integer, Integer> requestResults;
public PermissionManager(@NonNull Context context) {
this.context = context;
}
public void setActivity(@Nullable Activity activity) {
this.activity = activity;
}
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode != PermissionConstants.PERMISSION_CODE_IGNORE_BATTERY_OPTIMIZATIONS &&
......@@ -231,14 +242,22 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
void onSuccess(boolean shouldShowRequestPermissionRationale);
}
/**
* Determines the permission status of the provided permission.
* <p>
* To distinguish between a status of 'denied' and a status of 'permanently denied', the plugin
* needs access to an activity. If `this.activity` is null, for example when running the
* application in the background, the resolved status will be 'denied' for both 'denied' and
* 'permanently denied'.
*
* @param permission the permission for which to determine the status.
* @param successCallback the callback to which the resolved status must be supplied.
*/
void checkPermissionStatus(
@PermissionConstants.PermissionGroup int permission,
Context context,
CheckPermissionsSuccessCallback successCallback) {
final @PermissionConstants.PermissionGroup int permission,
final CheckPermissionsSuccessCallback successCallback) {
successCallback.onSuccess(determinePermissionStatus(
permission,
context));
successCallback.onSuccess(determinePermissionStatus(permission));
}
/**
......@@ -265,13 +284,11 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
* requested through this method were handled, and if so, return the result back to Dart.
*
* @param permissions the permissions that are requested.
* @param activity the activity.
* @param successCallback the callback for returning the permission results.
* @param errorCallback the callback to call in case of an error.
*/
void requestPermissions(
List<Integer> permissions,
Activity activity,
RequestPermissionsSuccessCallback successCallback,
ErrorCallback errorCallback) {
if (pendingRequestCount > 0) {
......@@ -291,13 +308,12 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
}
this.successCallback = successCallback;
this.activity = activity;
this.requestResults = new HashMap<>();
this.pendingRequestCount = 0; // sanity check
ArrayList<String> permissionsToRequest = new ArrayList<>();
for (Integer permission : permissions) {
@PermissionConstants.PermissionStatus final int permissionStatus = determinePermissionStatus(permission, activity);
@PermissionConstants.PermissionStatus final int permissionStatus = determinePermissionStatus(permission);
if (permissionStatus == PermissionConstants.PERMISSION_STATUS_GRANTED) {
if (!requestResults.containsKey(permission)) {
requestResults.put(permission, PermissionConstants.PERMISSION_STATUS_GRANTED);
......@@ -377,23 +393,21 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
}
@PermissionConstants.PermissionStatus
private int determinePermissionStatus(
@PermissionConstants.PermissionGroup int permission,
Context context) {
private int determinePermissionStatus(final @PermissionConstants.PermissionGroup int permission) {
if (permission == PermissionConstants.PERMISSION_GROUP_NOTIFICATION) {
return checkNotificationPermissionStatus(context);
return checkNotificationPermissionStatus();
}
if (permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH) {
return checkBluetoothPermissionStatus(context);
return checkBluetoothPermissionStatus();
}
if (permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH_CONNECT
|| permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH_SCAN
|| permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH_ADVERTISE) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return checkBluetoothPermissionStatus(context);
return checkBluetoothPermissionStatus();
}
}
......@@ -498,7 +512,7 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
final int permissionStatus = ContextCompat.checkSelfPermission(context, name);
if (permissionStatus != PackageManager.PERMISSION_GRANTED) {
return PermissionConstants.PERMISSION_STATUS_DENIED;
return PermissionUtils.determineDeniedVariant(activity, name);
}
}
}
......@@ -527,7 +541,6 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
void shouldShowRequestPermissionRationale(
int permission,
Activity activity,
ShouldShowRequestPermissionRationaleSuccessCallback successCallback,
ErrorCallback errorCallback) {
if (activity == null) {
......@@ -557,22 +570,26 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
successCallback.onSuccess(ActivityCompat.shouldShowRequestPermissionRationale(activity, names.get(0)));
}
private int checkNotificationPermissionStatus(Context context) {
@PermissionConstants.PermissionStatus
private int checkNotificationPermissionStatus() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
NotificationManagerCompat manager = NotificationManagerCompat.from(context);
boolean isGranted = manager.areNotificationsEnabled();
final boolean isGranted = manager.areNotificationsEnabled();
if (isGranted) {
return PermissionConstants.PERMISSION_STATUS_GRANTED;
}
return PermissionConstants.PERMISSION_STATUS_DENIED;
}
return context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
? PermissionConstants.PERMISSION_STATUS_GRANTED
: PermissionConstants.PERMISSION_STATUS_DENIED;
final int status = context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS);
if (status == PackageManager.PERMISSION_GRANTED) {
return PermissionConstants.PERMISSION_STATUS_GRANTED;
}
return PermissionConstants.PERMISSION_STATUS_DENIED;
}
private int checkBluetoothPermissionStatus(Context context) {
@PermissionConstants.PermissionStatus
private int checkBluetoothPermissionStatus() {
List<String> names = PermissionUtils.getManifestNames(context, PermissionConstants.PERMISSION_GROUP_BLUETOOTH);
boolean missingInManifest = names == null || names.isEmpty();
if (missingInManifest) {
......
......@@ -10,6 +10,8 @@ import android.os.Build;
import android.os.Environment;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
......@@ -437,39 +439,70 @@ public class PermissionUtils {
*
* @param activity the activity for context
* @param permissionName the name of the permission
* @param grantResult the result of the permission intent
* @param grantResult the result of the permission intent. Either
* {@link PackageManager#PERMISSION_DENIED} or {@link PackageManager#PERMISSION_GRANTED}.
* @return {@link PermissionConstants#PERMISSION_STATUS_GRANTED},
* {@link PermissionConstants#PERMISSION_STATUS_DENIED}, or
* {@link PermissionConstants#PERMISSION_STATUS_NEVER_ASK_AGAIN}.
*/
@PermissionConstants.PermissionStatus
static int toPermissionStatus(final Activity activity, final String permissionName, int grantResult) {
if (grantResult == PackageManager.PERMISSION_DENIED) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return PermissionConstants.PERMISSION_STATUS_DENIED;
}
static int toPermissionStatus(
final @Nullable Activity activity,
final String permissionName,
int grantResult) {
final boolean wasDeniedBefore = PermissionUtils.wasPermissionDeniedBefore(activity, permissionName);
final boolean shouldShowRational = !PermissionUtils.isNeverAskAgainSelected(activity, permissionName);
if (grantResult == PackageManager.PERMISSION_DENIED) {
return determineDeniedVariant(activity, permissionName);
}
//noinspection SimplifiableConditionalExpression
final boolean isDenied = wasDeniedBefore ? !shouldShowRational : shouldShowRational;
return PermissionConstants.PERMISSION_STATUS_GRANTED;
}
if (!wasDeniedBefore && isDenied) {
setPermissionDenied(activity, permissionName);
}
/**
* Determines whether a permission was either 'denied' or 'permanently denied'.
* <p>
* To distinguish between these two variants, the method needs access to an {@link Activity}.
* If the provided activity is null, the result will always be resolved to 'denied'.
*
* @param activity the activity needed to resolve the permission status.
* @param permissionName the name of the permission.
* @return either {@link PermissionConstants#PERMISSION_STATUS_DENIED} or
* {@link PermissionConstants#PERMISSION_STATUS_NEVER_ASK_AGAIN}.
*/
@PermissionConstants.PermissionStatus
static int determineDeniedVariant(
final @Nullable Activity activity,
final String permissionName) {
if (wasDeniedBefore && isDenied) {
return PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN;
}
if (activity == null) {
return PermissionConstants.PERMISSION_STATUS_DENIED;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return PermissionConstants.PERMISSION_STATUS_DENIED;
}
return PermissionConstants.PERMISSION_STATUS_GRANTED;
final boolean wasDeniedBefore = PermissionUtils.wasPermissionDeniedBefore(activity, permissionName);
final boolean shouldShowRational = !PermissionUtils.isNeverAskAgainSelected(activity, permissionName);
//noinspection SimplifiableConditionalExpression
final boolean isDenied = wasDeniedBefore ? !shouldShowRational : shouldShowRational;
if (!wasDeniedBefore && isDenied) {
setPermissionDenied(activity, permissionName);
}
if (wasDeniedBefore && isDenied) {
return PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN;
}
return PermissionConstants.PERMISSION_STATUS_DENIED;
}
static void updatePermissionShouldShowStatus(final Activity activity, @PermissionConstants.PermissionGroup int permission) {
static void updatePermissionShouldShowStatus(
@Nullable final Activity activity,
@PermissionConstants.PermissionGroup int permission) {
if (activity == null) {
return;
}
......@@ -482,10 +515,9 @@ public class PermissionUtils {
}
@RequiresApi(api = Build.VERSION_CODES.M)
static boolean isNeverAskAgainSelected(final Activity activity, final String name) {
if (activity == null) {
return false;
}
static boolean isNeverAskAgainSelected(
@NonNull final Activity activity,
final String name) {
final boolean shouldShowRequestPermissionRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, name);
return !shouldShowRequestPermissionRationale;
......
name: permission_handler_android
description: Permission plugin for Flutter. This plugin provides the Android API to request and check permissions.
homepage: https://github.com/baseflow/flutter-permission-handler
version: 10.3.6
version: 11.0.0
environment:
sdk: ">=2.15.0 <4.0.0"
......
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