Commit 921652cd by Jeroen Weener Committed by GitHub

Handle special permissions correctly when requesting multiple permissions (#1139)

* Handle special permissions for multirequest

* Update PI docs (#1133)

* Add background location section to faq (#1134)

* Open settings screen (#1138)

* Update Android version mentions (#1135)

* Update Android version references (#1136)

* Refactor launching of special permissions

* Handle special permissions for multirequest

* Refactor launching of special permissions
parent 9da1209b
## 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).
## 10.3.5
* Fixes a bug where `Permission.ScheduleExactAlarm` was not opening the settings
......
......@@ -15,9 +15,9 @@ import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.AlarmManagerCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
......@@ -36,6 +36,23 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
@Nullable
private Activity activity;
/**
* The number of pending permission requests.
* <p>
* This number is set by {@link this#requestPermissions(List, Activity, RequestPermissionsSuccessCallback, ErrorCallback)}
* and then reduced when receiving results in {@link this#onActivityResult(int, int, Intent)}
* and {@link this#onRequestPermissionsResult(int, String[], int[])}.
*/
private int pendingRequestCount;
/**
* The results of resolved permission requests.
* <p>
* This map holds the results to resolved permission requests received through
* {@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)}.
*/
private Map<Integer, Integer> requestResults;
@Override
......@@ -108,16 +125,24 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
return false;
}
HashMap<Integer, Integer> results = new HashMap<>();
results.put(permission, status);
successCallback.onSuccess(results);
requestResults.put(permission, status);
pendingRequestCount--;
// Post result if all requests have been handled.
if (pendingRequestCount == 0) {
this.successCallback.onSuccess(requestResults);
}
return true;
}
@Override
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
public boolean onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode != PermissionConstants.PERMISSION_CODE) {
ongoing = false;
pendingRequestCount = 0;
return false;
}
......@@ -182,8 +207,12 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
PermissionUtils.updatePermissionShouldShowStatus(this.activity, permission);
}
pendingRequestCount -= grantResults.length;
// Post result if all requests have been handled.
if (pendingRequestCount == 0) {
this.successCallback.onSuccess(requestResults);
ongoing = false;
}
return true;
}
......@@ -202,8 +231,6 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
void onSuccess(boolean shouldShowRequestPermissionRationale);
}
private boolean ongoing = false;
void checkPermissionStatus(
@PermissionConstants.PermissionGroup int permission,
Context context,
......@@ -214,12 +241,40 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
context));
}
/**
* Requests the user for the provided permissions.
* <p>
* This method will throw an error if it is called before all permission requests that were
* requested in a previous call have been resolved.
* <p>
* Android distinguishes between
* <a href="https://developer.android.com/guide/topics/permissions/overview#runtime">runtime permissions</a>
* and
* <a href="https://developer.android.com/guide/topics/permissions/overview#special">special permissions</a>.
* Runtime permissions give an app additional access to restricted data or let the app perform
* restricted actions that more substantially affect the system and other apps. These
* permissions present the user with a dialog where they can choose to grant or deny the
* permission. Special permissions guard access to system resources that are particularly
* sensitive or not directly related to user privacy. These permissions are requested by sending
* an {@link Intent} to the OS. The OS will open a special settings page where the user can
* grant the permission.
* Runtime permission request results will be reported through
* {@link this#onRequestPermissionsResult(int, String[], int[])}, while special permissions
* request results will be reported through {@link this#onActivityResult(int, int, Intent)}.
* When these methods receive request results, they check whether all permissions that were
* 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 (ongoing) {
if (pendingRequestCount > 0) {
errorCallback.onError(
"PermissionHandler.PermissionManager",
"A request for permissions is already running, please wait for it to finish before doing another request (note that you can request multiple permissions at the same time).");
......@@ -238,6 +293,7 @@ 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) {
......@@ -274,48 +330,49 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
continue;
}
// Request special permissions.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && permission == PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS) {
executeIntent(
launchSpecialPermission(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
PermissionConstants.PERMISSION_CODE_IGNORE_BATTERY_OPTIMIZATIONS);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && permission == PermissionConstants.PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE) {
executeIntent(
launchSpecialPermission(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
PermissionConstants.PERMISSION_CODE_MANAGE_EXTERNAL_STORAGE);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && permission == PermissionConstants.PERMISSION_GROUP_SYSTEM_ALERT_WINDOW) {
executeIntent(
launchSpecialPermission(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
PermissionConstants.PERMISSION_CODE_SYSTEM_ALERT_WINDOW);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && permission == PermissionConstants.PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES) {
executeIntent(
launchSpecialPermission(
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
PermissionConstants.PERMISSION_CODE_REQUEST_INSTALL_PACKAGES);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && permission == PermissionConstants.PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY) {
executeSimpleIntent(
launchSpecialPermission(
Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS,
PermissionConstants.PERMISSION_CODE_ACCESS_NOTIFICATION_POLICY);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && permission == PermissionConstants.PERMISSION_GROUP_SCHEDULE_EXACT_ALARM) {
executeIntent(
launchSpecialPermission(
Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
PermissionConstants.PERMISSION_CODE_SCHEDULE_EXACT_ALARM);
} else {
permissionsToRequest.addAll(names);
pendingRequestCount += names.size();
}
}
final String[] requestPermissions = permissionsToRequest.toArray(new String[0]);
// Request runtime permissions.
if (permissionsToRequest.size() > 0) {
ongoing = true;
final String[] requestPermissions = permissionsToRequest.toArray(new String[0]);
ActivityCompat.requestPermissions(
activity,
requestPermissions,
PermissionConstants.PERMISSION_CODE);
} else {
ongoing = false;
if (requestResults.size() > 0) {
successCallback.onSuccess(requestResults);
}
// Post results immediately if no requests are pending.
if (pendingRequestCount == 0) {
this.successCallback.onSuccess(requestResults);
}
}
......@@ -334,7 +391,7 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
if (permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH_CONNECT
|| permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH_SCAN
|| permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH_ADVERTISE){
|| permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH_ADVERTISE) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return checkBluetoothPermissionStatus(context);
}
......@@ -448,16 +505,24 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener,
return PermissionConstants.PERMISSION_STATUS_GRANTED;
}
private void executeIntent(String action, int requestCode) {
/**
* Launches a request for a <a href="https://developer.android.com/training/permissions/requesting-special">special permission</a>.
* <p>
* There is a special case for {@link Settings#ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS}. See
* <a href="https://github.com/Baseflow/flutter-permission-handler/pull/587#discussion_r649295489">this comment</a>
* for more details.
*
* @param permissionAction the action for launching the settings page for a particular permission.
* @param requestCode a request code to verify incoming results.
*/
private void launchSpecialPermission(String permissionAction, int requestCode) {
Intent intent = new Intent(permissionAction);
if (!permissionAction.equals(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS)) {
String packageName = activity.getPackageName();
Intent intent = new Intent();
intent.setAction(action);
intent.setData(Uri.parse("package:" + packageName));
activity.startActivityForResult(intent, requestCode);
}
private void executeSimpleIntent(String action, int requestCode) {
activity.startActivityForResult(new Intent(action), requestCode);
activity.startActivityForResult(intent, requestCode);
pendingRequestCount++;
}
void 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.5
version: 10.3.6
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