permissions = call.arguments();
+ permissionManager.requestPermissions(
+ permissions,
+ activity,
+ 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));
+
+ break;
+ }
+ case "openAppSettings":
+ appSettingsManager.openAppSettings(
+ applicationContext,
+ result::success,
+ (String errorCode, String errorDescription) -> result.error(
+ errorCode,
+ errorDescription,
+ null));
+
+ break;
+ default:
+ result.notImplemented();
+ break;
+ }
+ }
+}
diff --git a/android/src/main/java/com/baseflow/permissionhandler/PermissionConstants.java b/android/src/main/java/com/baseflow/permissionhandler/PermissionConstants.java
new file mode 100644
index 0000000..af5dbdd
--- /dev/null
+++ b/android/src/main/java/com/baseflow/permissionhandler/PermissionConstants.java
@@ -0,0 +1,118 @@
+package com.baseflow.permissionhandler;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+final class PermissionConstants {
+ static final String LOG_TAG = "permissions_handler";
+ static final int PERMISSION_CODE = 24;
+ static final int PERMISSION_CODE_IGNORE_BATTERY_OPTIMIZATIONS = 209;
+ static final int PERMISSION_CODE_MANAGE_EXTERNAL_STORAGE = 210;
+ static final int PERMISSION_CODE_SYSTEM_ALERT_WINDOW = 211;
+ static final int PERMISSION_CODE_REQUEST_INSTALL_PACKAGES = 212;
+ static final int PERMISSION_CODE_ACCESS_NOTIFICATION_POLICY = 213;
+
+ //PERMISSION_GROUP
+ static final int PERMISSION_GROUP_CALENDAR = 0;
+ static final int PERMISSION_GROUP_CAMERA = 1;
+ static final int PERMISSION_GROUP_CONTACTS = 2;
+ static final int PERMISSION_GROUP_LOCATION = 3;
+ static final int PERMISSION_GROUP_LOCATION_ALWAYS = 4;
+ static final int PERMISSION_GROUP_LOCATION_WHEN_IN_USE = 5;
+ static final int PERMISSION_GROUP_MEDIA_LIBRARY = 6;
+ static final int PERMISSION_GROUP_MICROPHONE = 7;
+ static final int PERMISSION_GROUP_PHONE = 8;
+ static final int PERMISSION_GROUP_PHOTOS = 9;
+ static final int PERMISSION_GROUP_PHOTOS_ADD_ONLY = 10;
+ static final int PERMISSION_GROUP_REMINDERS = 11;
+ static final int PERMISSION_GROUP_SENSORS = 12;
+ static final int PERMISSION_GROUP_SMS = 13;
+ static final int PERMISSION_GROUP_SPEECH = 14;
+ static final int PERMISSION_GROUP_STORAGE = 15;
+ static final int PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS = 16;
+ static final int PERMISSION_GROUP_NOTIFICATION = 17;
+ static final int PERMISSION_GROUP_ACCESS_MEDIA_LOCATION = 18;
+ static final int PERMISSION_GROUP_ACTIVITY_RECOGNITION = 19;
+ static final int PERMISSION_GROUP_UNKNOWN = 20;
+ static final int PERMISSION_GROUP_BLUETOOTH = 21;
+ static final int PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE = 22;
+ static final int PERMISSION_GROUP_SYSTEM_ALERT_WINDOW = 23;
+ static final int PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES = 24;
+ static final int PERMISSION_GROUP_APP_TRACK_TRANSPARENCY = 25;
+ static final int PERMISSION_GROUP_CRITICAL_ALERTS = 26;
+ static final int PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY = 27;
+ static final int PERMISSION_GROUP_BLUETOOTH_SCAN = 28;
+ static final int PERMISSION_GROUP_BLUETOOTH_ADVERTISE = 29;
+ static final int PERMISSION_GROUP_BLUETOOTH_CONNECT = 30;
+ static final int PERMISSION_GROUP_CALL_LOG = 31;
+
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ PERMISSION_GROUP_CALENDAR,
+ PERMISSION_GROUP_CAMERA,
+ PERMISSION_GROUP_CONTACTS,
+ PERMISSION_GROUP_LOCATION,
+ PERMISSION_GROUP_LOCATION_ALWAYS,
+ PERMISSION_GROUP_LOCATION_WHEN_IN_USE,
+ PERMISSION_GROUP_MEDIA_LIBRARY,
+ PERMISSION_GROUP_MICROPHONE,
+ PERMISSION_GROUP_PHONE,
+ PERMISSION_GROUP_PHOTOS,
+ PERMISSION_GROUP_REMINDERS,
+ PERMISSION_GROUP_SENSORS,
+ PERMISSION_GROUP_SMS,
+ PERMISSION_GROUP_SPEECH,
+ PERMISSION_GROUP_STORAGE,
+ PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS,
+ PERMISSION_GROUP_NOTIFICATION,
+ PERMISSION_GROUP_ACCESS_MEDIA_LOCATION,
+ PERMISSION_GROUP_ACTIVITY_RECOGNITION,
+ PERMISSION_GROUP_UNKNOWN,
+ PERMISSION_GROUP_BLUETOOTH,
+ PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE,
+ PERMISSION_GROUP_SYSTEM_ALERT_WINDOW,
+ PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES,
+ PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY,
+ PERMISSION_GROUP_BLUETOOTH_SCAN,
+ PERMISSION_GROUP_BLUETOOTH_ADVERTISE,
+ PERMISSION_GROUP_BLUETOOTH_CONNECT,
+ PERMISSION_GROUP_CALL_LOG
+ })
+ @interface PermissionGroup {
+ }
+
+ //PERMISSION_STATUS
+ static final int PERMISSION_STATUS_DENIED = 0;
+ static final int PERMISSION_STATUS_GRANTED = 1;
+ static final int PERMISSION_STATUS_RESTRICTED = 2;
+ static final int PERMISSION_STATUS_LIMITED = 3;
+ static final int PERMISSION_STATUS_NEVER_ASK_AGAIN = 4;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ PERMISSION_STATUS_DENIED,
+ PERMISSION_STATUS_GRANTED,
+ PERMISSION_STATUS_RESTRICTED,
+ PERMISSION_STATUS_LIMITED,
+ PERMISSION_STATUS_NEVER_ASK_AGAIN
+ })
+ @interface PermissionStatus {
+ }
+
+ //SERVICE_STATUS
+ static final int SERVICE_STATUS_DISABLED = 0;
+ static final int SERVICE_STATUS_ENABLED = 1;
+ static final int SERVICE_STATUS_NOT_APPLICABLE = 2;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SERVICE_STATUS_DISABLED,
+ SERVICE_STATUS_ENABLED,
+ SERVICE_STATUS_NOT_APPLICABLE
+ })
+ @interface ServiceStatus {
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPlugin.java b/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPlugin.java
new file mode 100644
index 0000000..c9ff345
--- /dev/null
+++ b/android/src/main/java/com/baseflow/permissionhandler/PermissionHandlerPlugin.java
@@ -0,0 +1,154 @@
+package com.baseflow.permissionhandler;
+
+import android.app.Activity;
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MethodChannel;
+
+/**
+ * Platform implementation of the permission_handler Flutter plugin.
+ *
+ * Instantiate this in an add to app scenario to gracefully handle activity and context changes.
+ * See {@code com.example.permissionhandlerexample.MainActivity} for an example.
+ *
+ *
Call {@link #registerWith(io.flutter.plugin.common.PluginRegistry.Registrar)} to register an
+ * implementation of this that uses the stable {@code io.flutter.plugin.common} package.
+ */
+public final class PermissionHandlerPlugin implements FlutterPlugin, ActivityAware {
+
+ private final PermissionManager permissionManager;
+ private MethodChannel methodChannel;
+
+ @SuppressWarnings("deprecation")
+ @Nullable private io.flutter.plugin.common.PluginRegistry.Registrar pluginRegistrar;
+
+ @Nullable private ActivityPluginBinding pluginBinding;
+
+ @Nullable
+ private MethodCallHandlerImpl methodCallHandler;
+
+ public PermissionHandlerPlugin() {
+ this.permissionManager = new PermissionManager();
+ }
+
+ /**
+ * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common}
+ * package.
+ *
+ *
Calling this automatically initializes the plugin. However plugins initialized this way
+ * won't react to changes in activity or context, unlike {@link PermissionHandlerPlugin}.
+ */
+ @SuppressWarnings("deprecation")
+ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
+ final PermissionHandlerPlugin plugin = new PermissionHandlerPlugin();
+
+ plugin.pluginRegistrar = registrar;
+ plugin.registerListeners();
+
+ plugin.startListening(registrar.context(), registrar.messenger());
+
+ if (registrar.activeContext() instanceof Activity) {
+ plugin.startListeningToActivity(
+ registrar.activity()
+ );
+ }
+ }
+
+ @Override
+ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
+ startListening(
+ binding.getApplicationContext(),
+ binding.getBinaryMessenger()
+ );
+ }
+
+ @Override
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+ stopListening();
+ }
+
+ @Override
+ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
+ startListeningToActivity(
+ binding.getActivity()
+ );
+
+ this.pluginBinding = binding;
+ registerListeners();
+ }
+
+ @Override
+ public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
+ onAttachedToActivity(binding);
+ }
+
+ @Override
+ public void onDetachedFromActivity() {
+ stopListeningToActivity();
+
+ deregisterListeners();
+ }
+
+ @Override
+ public void onDetachedFromActivityForConfigChanges() {
+ onDetachedFromActivity();
+ }
+
+
+ private void startListening(Context applicationContext, BinaryMessenger messenger) {
+ methodChannel = new MethodChannel(
+ messenger,
+ "flutter.baseflow.com/permissions/methods");
+
+ methodCallHandler = new MethodCallHandlerImpl(
+ applicationContext,
+ new AppSettingsManager(),
+ this.permissionManager,
+ new ServiceManager()
+ );
+
+ methodChannel.setMethodCallHandler(methodCallHandler);
+ }
+
+ private void stopListening() {
+ methodChannel.setMethodCallHandler(null);
+ methodChannel = null;
+ methodCallHandler = null;
+ }
+
+ private void startListeningToActivity(
+ Activity activity
+ ) {
+ if (methodCallHandler != null) {
+ methodCallHandler.setActivity(activity);
+ }
+ }
+
+ private void stopListeningToActivity() {
+ if (methodCallHandler != null) {
+ methodCallHandler.setActivity(null);
+ }
+ }
+
+ private void registerListeners() {
+ if (this.pluginRegistrar != null) {
+ this.pluginRegistrar.addActivityResultListener(this.permissionManager);
+ this.pluginRegistrar.addRequestPermissionsResultListener(this.permissionManager);
+ } else if (pluginBinding != null) {
+ this.pluginBinding.addActivityResultListener(this.permissionManager);
+ this.pluginBinding.addRequestPermissionsResultListener(this.permissionManager);
+ }
+ }
+
+ private void deregisterListeners() {
+ if (this.pluginBinding != null) {
+ this.pluginBinding.removeActivityResultListener(this.permissionManager);
+ this.pluginBinding.removeRequestPermissionsResultListener(this.permissionManager);
+ }
+ }
+}
diff --git a/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java b/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java
new file mode 100644
index 0000000..295a649
--- /dev/null
+++ b/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java
@@ -0,0 +1,497 @@
+package com.baseflow.permissionhandler;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.Application;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.PowerManager;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.content.ContextCompat;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.flutter.plugin.common.PluginRegistry;
+
+final class PermissionManager implements PluginRegistry.ActivityResultListener, PluginRegistry.RequestPermissionsResultListener {
+
+ @Nullable
+ private RequestPermissionsSuccessCallback successCallback;
+
+ @Nullable
+ private Activity activity;
+
+ private Map requestResults;
+
+ @Override
+ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode != PermissionConstants.PERMISSION_CODE_IGNORE_BATTERY_OPTIMIZATIONS &&
+ requestCode != PermissionConstants.PERMISSION_CODE_MANAGE_EXTERNAL_STORAGE &&
+ requestCode != PermissionConstants.PERMISSION_CODE_SYSTEM_ALERT_WINDOW &&
+ requestCode != PermissionConstants.PERMISSION_CODE_REQUEST_INSTALL_PACKAGES &&
+ requestCode != PermissionConstants.PERMISSION_CODE_ACCESS_NOTIFICATION_POLICY) {
+ return false;
+ }
+
+ int status = resultCode == Activity.RESULT_OK
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+
+ int permission;
+
+ if (requestCode == PermissionConstants.PERMISSION_CODE_IGNORE_BATTERY_OPTIMIZATIONS) {
+ permission = PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS;
+ } else if (requestCode == PermissionConstants.PERMISSION_CODE_MANAGE_EXTERNAL_STORAGE) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ status = Environment.isExternalStorageManager()
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ } else {
+ return false;
+ }
+ permission = PermissionConstants.PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE;
+ } else if (requestCode == PermissionConstants.PERMISSION_CODE_SYSTEM_ALERT_WINDOW) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ status = Settings.canDrawOverlays(activity)
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ permission = PermissionConstants.PERMISSION_GROUP_SYSTEM_ALERT_WINDOW;
+ } else {
+ return false;
+ }
+ } else if (requestCode == PermissionConstants.PERMISSION_CODE_REQUEST_INSTALL_PACKAGES) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ status = activity.getPackageManager().canRequestPackageInstalls()
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ permission = PermissionConstants.PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES;
+ } else {
+ return false;
+ }
+ } else if (requestCode == PermissionConstants.PERMISSION_CODE_ACCESS_NOTIFICATION_POLICY) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ NotificationManager notificationManager = (NotificationManager) activity.getSystemService(Application.NOTIFICATION_SERVICE);
+ status = notificationManager.isNotificationPolicyAccessGranted()
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ permission = PermissionConstants.PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+
+ HashMap results = new HashMap<>();
+ results.put(permission, status);
+ successCallback.onSuccess(results);
+ return true;
+ }
+
+ @Override
+ public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode != PermissionConstants.PERMISSION_CODE) {
+ ongoing = false;
+ return false;
+ }
+
+ if (requestResults == null) {
+ return false;
+ }
+
+ for (int i = 0; i < permissions.length; i++) {
+ final String permissionName = permissions[i];
+
+ @PermissionConstants.PermissionGroup final int permission =
+ PermissionUtils.parseManifestName(permissionName);
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_UNKNOWN)
+ continue;
+
+ final int result = grantResults[i];
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_MICROPHONE) {
+ if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_MICROPHONE)) {
+ requestResults.put(
+ PermissionConstants.PERMISSION_GROUP_MICROPHONE,
+ PermissionUtils.toPermissionStatus(this.activity, permissionName, result));
+ }
+ if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_SPEECH)) {
+ requestResults.put(
+ PermissionConstants.PERMISSION_GROUP_SPEECH,
+ PermissionUtils.toPermissionStatus(this.activity, permissionName, result));
+ }
+ if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_CALL_LOG)) {
+ requestResults.put(
+ PermissionConstants.PERMISSION_GROUP_CALL_LOG,
+ PermissionUtils.toPermissionStatus(this.activity, permissionName, result));
+ }
+ } else if (permission == PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS) {
+ @PermissionConstants.PermissionStatus int permissionStatus =
+ PermissionUtils.toPermissionStatus(this.activity, permissionName, result);
+
+ if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS)) {
+ requestResults.put(PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS, permissionStatus);
+ }
+ } else if (permission == PermissionConstants.PERMISSION_GROUP_LOCATION) {
+ @PermissionConstants.PermissionStatus int permissionStatus =
+ PermissionUtils.toPermissionStatus(this.activity, permissionName, result);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS)) {
+ requestResults.put(
+ PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS,
+ permissionStatus);
+ }
+ }
+
+ if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE)) {
+ requestResults.put(
+ PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE,
+ permissionStatus);
+ }
+
+ requestResults.put(permission, permissionStatus);
+ } else if (!requestResults.containsKey(permission)) {
+ requestResults.put(
+ permission,
+ PermissionUtils.toPermissionStatus(this.activity, permissionName, result));
+ }
+
+ PermissionUtils.updatePermissionShouldShowStatus(this.activity, permission);
+ }
+
+ this.successCallback.onSuccess(requestResults);
+ ongoing = false;
+ return true;
+ }
+
+ @FunctionalInterface
+ interface RequestPermissionsSuccessCallback {
+ void onSuccess(Map results);
+ }
+
+ @FunctionalInterface
+ interface CheckPermissionsSuccessCallback {
+ void onSuccess(@PermissionConstants.PermissionStatus int permissionStatus);
+ }
+
+ @FunctionalInterface
+ interface ShouldShowRequestPermissionRationaleSuccessCallback {
+ void onSuccess(boolean shouldShowRequestPermissionRationale);
+ }
+
+ private boolean ongoing = false;
+
+ void checkPermissionStatus(
+ @PermissionConstants.PermissionGroup int permission,
+ Context context,
+ CheckPermissionsSuccessCallback successCallback) {
+
+ successCallback.onSuccess(determinePermissionStatus(
+ permission,
+ context));
+ }
+
+ void requestPermissions(
+ List permissions,
+ Activity activity,
+ RequestPermissionsSuccessCallback successCallback,
+ ErrorCallback errorCallback) {
+ if (ongoing) {
+ 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).");
+ return;
+ }
+
+ if (activity == null) {
+ Log.d(PermissionConstants.LOG_TAG, "Unable to detect current Activity.");
+
+ errorCallback.onError(
+ "PermissionHandler.PermissionManager",
+ "Unable to detect current Android Activity.");
+ return;
+ }
+
+ this.successCallback = successCallback;
+ this.activity = activity;
+ this.requestResults = new HashMap<>();
+
+ ArrayList permissionsToRequest = new ArrayList<>();
+ for (Integer permission : permissions) {
+ @PermissionConstants.PermissionStatus final int permissionStatus = determinePermissionStatus(permission, activity);
+ if (permissionStatus == PermissionConstants.PERMISSION_STATUS_GRANTED) {
+ if (!requestResults.containsKey(permission)) {
+ requestResults.put(permission, PermissionConstants.PERMISSION_STATUS_GRANTED);
+ }
+ continue;
+ }
+
+ final List names = PermissionUtils.getManifestNames(activity, permission);
+
+ // check to see if we can find manifest names
+ // if we can't add as unknown and continue
+ if (names == null || names.isEmpty()) {
+ if (!requestResults.containsKey(permission)) {
+ // On Android below M, the android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS flag in AndroidManifest.xml
+ // may be ignored and not visible to the App as it's a new permission setting as a whole.
+ if (permission == PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ requestResults.put(permission, PermissionConstants.PERMISSION_STATUS_RESTRICTED);
+ } else {
+ requestResults.put(permission, PermissionConstants.PERMISSION_STATUS_DENIED);
+ }
+ // On Android below R, the android.permission.MANAGE_EXTERNAL_STORAGE flag in AndroidManifest.xml
+ // may be ignored and not visible to the App as it's a new permission setting as a whole.
+ if (permission == PermissionConstants.PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ requestResults.put(permission, PermissionConstants.PERMISSION_STATUS_RESTRICTED);
+ } else {
+ requestResults.put(permission, PermissionConstants.PERMISSION_STATUS_DENIED);
+ }
+ }
+
+ continue;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && permission == PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS) {
+ executeIntent(
+ 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(
+ 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(
+ 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(
+ 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(
+ Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS,
+ PermissionConstants.PERMISSION_CODE_ACCESS_NOTIFICATION_POLICY);
+ } else {
+ permissionsToRequest.addAll(names);
+ }
+ }
+
+ final String[] requestPermissions = permissionsToRequest.toArray(new String[0]);
+ if (permissionsToRequest.size() > 0) {
+ ongoing = true;
+
+ ActivityCompat.requestPermissions(
+ activity,
+ requestPermissions,
+ PermissionConstants.PERMISSION_CODE);
+ } else {
+ ongoing = false;
+ if (requestResults.size() > 0) {
+ successCallback.onSuccess(requestResults);
+ }
+ }
+ }
+
+ @PermissionConstants.PermissionStatus
+ private int determinePermissionStatus(
+ @PermissionConstants.PermissionGroup int permission,
+ Context context) {
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_NOTIFICATION) {
+ return checkNotificationPermissionStatus(context);
+ }
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH) {
+ return checkBluetoothPermissionStatus(context);
+ }
+
+ 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);
+ }
+ }
+
+ final List names = PermissionUtils.getManifestNames(context, permission);
+
+ if (names == null) {
+ Log.d(PermissionConstants.LOG_TAG, "No android specific permissions needed for: " + permission);
+
+ return PermissionConstants.PERMISSION_STATUS_GRANTED;
+ }
+
+ //if no permissions were found then there is an issue and permission is not set in Android manifest
+ if (names.size() == 0) {
+ Log.d(PermissionConstants.LOG_TAG, "No permissions found in manifest for: " + names + permission);
+
+ // On Android below M, the android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS flag in AndroidManifest.xml
+ // may be ignored and not visible to the App as it's a new permission setting as a whole.
+ if (permission == PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return PermissionConstants.PERMISSION_STATUS_RESTRICTED;
+ }
+ }
+
+ // On Android below R, the android.permission.MANAGE_EXTERNAL_STORAGE flag in AndroidManifest.xml
+ // may be ignored and not visible to the App as it's a new permission setting as a whole.
+ if (permission == PermissionConstants.PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ return PermissionConstants.PERMISSION_STATUS_RESTRICTED;
+ }
+ }
+
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ }
+
+ final boolean targetsMOrHigher = context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.M;
+
+ for (String name : names) {
+ // Only handle them if the client app actually targets a API level greater than M.
+ if (targetsMOrHigher) {
+ if (permission == PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS) {
+ String packageName = context.getPackageName();
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ // PowerManager.isIgnoringBatteryOptimizations has been included in Android M first.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (pm != null && pm.isIgnoringBatteryOptimizations(packageName)) {
+ return PermissionConstants.PERMISSION_STATUS_GRANTED;
+ } else {
+ return PermissionConstants.PERMISSION_STATUS_DENIED;
+ }
+ } else {
+ return PermissionConstants.PERMISSION_STATUS_RESTRICTED;
+ }
+ }
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ return PermissionConstants.PERMISSION_STATUS_RESTRICTED;
+ }
+
+ return Environment.isExternalStorageManager()
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ }
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_SYSTEM_ALERT_WINDOW) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return Settings.canDrawOverlays(context)
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ }
+ }
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ return context.getPackageManager().canRequestPackageInstalls()
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ }
+ }
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Application.NOTIFICATION_SERVICE);
+ return notificationManager.isNotificationPolicyAccessGranted()
+ ? PermissionConstants.PERMISSION_STATUS_GRANTED
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ }
+ }
+
+ final int permissionStatus = ContextCompat.checkSelfPermission(context, name);
+ if (permissionStatus != PackageManager.PERMISSION_GRANTED) {
+ return PermissionConstants.PERMISSION_STATUS_DENIED;
+ }
+ }
+ }
+ return PermissionConstants.PERMISSION_STATUS_GRANTED;
+ }
+
+ private void executeIntent(String action, int requestCode) {
+ 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);
+ }
+
+ void shouldShowRequestPermissionRationale(
+ int permission,
+ Activity activity,
+ ShouldShowRequestPermissionRationaleSuccessCallback successCallback,
+ ErrorCallback errorCallback) {
+ if (activity == null) {
+ Log.d(PermissionConstants.LOG_TAG, "Unable to detect current Activity.");
+
+ errorCallback.onError(
+ "PermissionHandler.PermissionManager",
+ "Unable to detect current Android Activity.");
+ return;
+ }
+
+ List names = PermissionUtils.getManifestNames(activity, permission);
+
+ // if isn't an android specific group then go ahead and return false;
+ if (names == null) {
+ Log.d(PermissionConstants.LOG_TAG, "No android specific permissions needed for: " + permission);
+ successCallback.onSuccess(false);
+ return;
+ }
+
+ if (names.isEmpty()) {
+ Log.d(PermissionConstants.LOG_TAG, "No permissions found in manifest for: " + permission + " no need to show request rationale");
+ successCallback.onSuccess(false);
+ return;
+ }
+
+ successCallback.onSuccess(ActivityCompat.shouldShowRequestPermissionRationale(activity, names.get(0)));
+ }
+
+ private int checkNotificationPermissionStatus(Context context) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ NotificationManagerCompat manager = NotificationManagerCompat.from(context);
+ 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;
+ }
+
+ private int checkBluetoothPermissionStatus(Context context) {
+ List names = PermissionUtils.getManifestNames(context, PermissionConstants.PERMISSION_GROUP_BLUETOOTH);
+ boolean missingInManifest = names == null || names.isEmpty();
+ if (missingInManifest) {
+ Log.d(PermissionConstants.LOG_TAG, "Bluetooth permission missing in manifest");
+ return PermissionConstants.PERMISSION_STATUS_DENIED;
+ }
+ return PermissionConstants.PERMISSION_STATUS_GRANTED;
+ }
+}
diff --git a/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java b/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java
new file mode 100644
index 0000000..61cbee6
--- /dev/null
+++ b/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java
@@ -0,0 +1,407 @@
+package com.baseflow.permissionhandler;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.app.ActivityCompat;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class PermissionUtils {
+
+ @PermissionConstants.PermissionGroup
+ static int parseManifestName(String permission) {
+ switch (permission) {
+ case Manifest.permission.READ_CALENDAR:
+ case Manifest.permission.WRITE_CALENDAR:
+ return PermissionConstants.PERMISSION_GROUP_CALENDAR;
+ case Manifest.permission.CAMERA:
+ return PermissionConstants.PERMISSION_GROUP_CAMERA;
+ case Manifest.permission.READ_CONTACTS:
+ case Manifest.permission.WRITE_CONTACTS:
+ case Manifest.permission.GET_ACCOUNTS:
+ return PermissionConstants.PERMISSION_GROUP_CONTACTS;
+ case Manifest.permission.ACCESS_BACKGROUND_LOCATION:
+ return PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS;
+ case Manifest.permission.ACCESS_COARSE_LOCATION:
+ case Manifest.permission.ACCESS_FINE_LOCATION:
+ return PermissionConstants.PERMISSION_GROUP_LOCATION;
+ case Manifest.permission.RECORD_AUDIO:
+ return PermissionConstants.PERMISSION_GROUP_MICROPHONE;
+ case Manifest.permission.READ_PHONE_STATE:
+ case Manifest.permission.READ_PHONE_NUMBERS:
+ case Manifest.permission.CALL_PHONE:
+ case Manifest.permission.ADD_VOICEMAIL:
+ case Manifest.permission.USE_SIP:
+ case Manifest.permission.BIND_CALL_REDIRECTION_SERVICE:
+ return PermissionConstants.PERMISSION_GROUP_PHONE;
+ case Manifest.permission.BODY_SENSORS:
+ return PermissionConstants.PERMISSION_GROUP_SENSORS;
+ case Manifest.permission.SEND_SMS:
+ case Manifest.permission.RECEIVE_SMS:
+ case Manifest.permission.READ_SMS:
+ case Manifest.permission.RECEIVE_WAP_PUSH:
+ case Manifest.permission.RECEIVE_MMS:
+ return PermissionConstants.PERMISSION_GROUP_SMS;
+ case Manifest.permission.READ_EXTERNAL_STORAGE:
+ case Manifest.permission.WRITE_EXTERNAL_STORAGE:
+ return PermissionConstants.PERMISSION_GROUP_STORAGE;
+ case Manifest.permission.ACCESS_MEDIA_LOCATION:
+ return PermissionConstants.PERMISSION_GROUP_ACCESS_MEDIA_LOCATION;
+ case Manifest.permission.ACTIVITY_RECOGNITION:
+ return PermissionConstants.PERMISSION_GROUP_ACTIVITY_RECOGNITION;
+ case Manifest.permission.MANAGE_EXTERNAL_STORAGE:
+ return PermissionConstants.PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE;
+ case Manifest.permission.SYSTEM_ALERT_WINDOW:
+ return PermissionConstants.PERMISSION_GROUP_SYSTEM_ALERT_WINDOW;
+ case Manifest.permission.REQUEST_INSTALL_PACKAGES:
+ return PermissionConstants.PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES;
+ case Manifest.permission.ACCESS_NOTIFICATION_POLICY:
+ return PermissionConstants.PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY;
+ case Manifest.permission.BLUETOOTH_SCAN:
+ return PermissionConstants.PERMISSION_GROUP_BLUETOOTH_SCAN;
+ case Manifest.permission.BLUETOOTH_ADVERTISE:
+ return PermissionConstants.PERMISSION_GROUP_BLUETOOTH_ADVERTISE;
+ case Manifest.permission.BLUETOOTH_CONNECT:
+ return PermissionConstants.PERMISSION_GROUP_BLUETOOTH_CONNECT;
+ case Manifest.permission.POST_NOTIFICATIONS:
+ return PermissionConstants.PERMISSION_GROUP_NOTIFICATION;
+ case Manifest.permission.READ_CALL_LOG:
+ case Manifest.permission.WRITE_CALL_LOG:
+ return PermissionConstants.PERMISSION_GROUP_CALL_LOG;
+ default:
+ return PermissionConstants.PERMISSION_GROUP_UNKNOWN;
+ }
+ }
+
+ static List getManifestNames(Context context, @PermissionConstants.PermissionGroup int permission) {
+ final ArrayList permissionNames = new ArrayList<>();
+
+ switch (permission) {
+ case PermissionConstants.PERMISSION_GROUP_CALENDAR:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_CALENDAR))
+ permissionNames.add(Manifest.permission.READ_CALENDAR);
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.WRITE_CALENDAR))
+ permissionNames.add(Manifest.permission.WRITE_CALENDAR);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_CAMERA:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.CAMERA))
+ permissionNames.add(Manifest.permission.CAMERA);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_CONTACTS:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_CONTACTS))
+ permissionNames.add(Manifest.permission.READ_CONTACTS);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.WRITE_CONTACTS))
+ permissionNames.add(Manifest.permission.WRITE_CONTACTS);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.GET_ACCOUNTS))
+ permissionNames.add(Manifest.permission.GET_ACCOUNTS);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS:
+ // Note that the LOCATION_ALWAYS will deliberately fallthrough to the LOCATION
+ // case on pre Android Q devices. The ACCESS_BACKGROUND_LOCATION permission was only
+ // introduced in Android Q, before it should be treated as the ACCESS_COARSE_LOCATION or
+ // ACCESS_FINE_LOCATION.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_BACKGROUND_LOCATION))
+ permissionNames.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
+ break;
+ }
+ case PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE:
+ case PermissionConstants.PERMISSION_GROUP_LOCATION:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_COARSE_LOCATION))
+ permissionNames.add(Manifest.permission.ACCESS_COARSE_LOCATION);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_FINE_LOCATION))
+ permissionNames.add(Manifest.permission.ACCESS_FINE_LOCATION);
+ break;
+
+
+ case PermissionConstants.PERMISSION_GROUP_SPEECH:
+ case PermissionConstants.PERMISSION_GROUP_MICROPHONE:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.RECORD_AUDIO))
+ permissionNames.add(Manifest.permission.RECORD_AUDIO);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_PHONE:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_PHONE_STATE))
+ permissionNames.add(Manifest.permission.READ_PHONE_STATE);
+
+ if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.Q && hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_PHONE_NUMBERS)) {
+ permissionNames.add(Manifest.permission.READ_PHONE_NUMBERS);
+ }
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.CALL_PHONE))
+ permissionNames.add(Manifest.permission.CALL_PHONE);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ADD_VOICEMAIL))
+ permissionNames.add(Manifest.permission.ADD_VOICEMAIL);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.USE_SIP))
+ permissionNames.add(Manifest.permission.USE_SIP);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && hasPermissionInManifest(context, permissionNames, Manifest.permission.BIND_CALL_REDIRECTION_SERVICE))
+ permissionNames.add(Manifest.permission.BIND_CALL_REDIRECTION_SERVICE);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasPermissionInManifest(context, permissionNames, Manifest.permission.ANSWER_PHONE_CALLS))
+ permissionNames.add(Manifest.permission.ANSWER_PHONE_CALLS);
+
+ break;
+ case PermissionConstants.PERMISSION_GROUP_CALL_LOG:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_CALL_LOG))
+ permissionNames.add(Manifest.permission.READ_CALL_LOG);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.WRITE_CALL_LOG))
+ permissionNames.add(Manifest.permission.WRITE_CALL_LOG);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_SENSORS:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.BODY_SENSORS)) {
+ permissionNames.add(Manifest.permission.BODY_SENSORS);
+ }
+ }
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_SMS:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.SEND_SMS))
+ permissionNames.add(Manifest.permission.SEND_SMS);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.RECEIVE_SMS))
+ permissionNames.add(Manifest.permission.RECEIVE_SMS);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_SMS))
+ permissionNames.add(Manifest.permission.READ_SMS);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.RECEIVE_WAP_PUSH))
+ permissionNames.add(Manifest.permission.RECEIVE_WAP_PUSH);
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.RECEIVE_MMS))
+ permissionNames.add(Manifest.permission.RECEIVE_MMS);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_STORAGE:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_EXTERNAL_STORAGE))
+ permissionNames.add(Manifest.permission.READ_EXTERNAL_STORAGE);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && Environment.isExternalStorageLegacy())) {
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.WRITE_EXTERNAL_STORAGE))
+ permissionNames.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ break;
+ }
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && hasPermissionInManifest(context, permissionNames, Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS))
+ permissionNames.add(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_ACCESS_MEDIA_LOCATION:
+ // The ACCESS_MEDIA_LOCATION permission is introduced in Android Q, meaning we should
+ // not handle permissions on pre Android Q devices.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
+ return null;
+
+ if(hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_MEDIA_LOCATION))
+ permissionNames.add(Manifest.permission.ACCESS_MEDIA_LOCATION);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_ACTIVITY_RECOGNITION:
+ // The ACTIVITY_RECOGNITION permission is introduced in Android Q, meaning we should
+ // not handle permissions on pre Android Q devices.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
+ return null;
+
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ACTIVITY_RECOGNITION))
+ permissionNames.add(Manifest.permission.ACTIVITY_RECOGNITION);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_BLUETOOTH:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.BLUETOOTH))
+ permissionNames.add(Manifest.permission.BLUETOOTH);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE:
+ // The MANAGE_EXTERNAL_STORAGE permission is introduced in Android R, meaning we should
+ // not handle permissions on pre Android R devices.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hasPermissionInManifest(context, permissionNames, Manifest.permission.MANAGE_EXTERNAL_STORAGE ))
+ permissionNames.add(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_SYSTEM_ALERT_WINDOW:
+ if (hasPermissionInManifest(context, permissionNames, Manifest.permission.SYSTEM_ALERT_WINDOW ))
+ permissionNames.add(Manifest.permission.SYSTEM_ALERT_WINDOW);
+ break;
+
+ case PermissionConstants.PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES:
+ // The REQUEST_INSTALL_PACKAGES permission is introduced in Android M, meaning we should
+ // not handle permissions on pre Android M devices.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && hasPermissionInManifest(context, permissionNames, Manifest.permission.REQUEST_INSTALL_PACKAGES ))
+ permissionNames.add(Manifest.permission.REQUEST_INSTALL_PACKAGES);
+ break;
+ case PermissionConstants.PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY:
+ // The REQUEST_NOTIFICATION_POLICY permission is introduced in Android M, meaning we should
+ // not handle permissions on pre Android M devices.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_NOTIFICATION_POLICY ))
+ permissionNames.add(Manifest.permission.ACCESS_NOTIFICATION_POLICY);
+ break;
+ case PermissionConstants.PERMISSION_GROUP_BLUETOOTH_SCAN: {
+ // The BLUETOOTH_SCAN permission is introduced in Android S, meaning we should
+ // not handle permissions on pre Android S devices.
+ String result = determineBluetoothPermission(context, Manifest.permission.BLUETOOTH_SCAN);
+
+ if (result != null) {
+ permissionNames.add(result);
+ }
+
+ break;
+ }
+ case PermissionConstants.PERMISSION_GROUP_BLUETOOTH_ADVERTISE: {
+ // The BLUETOOTH_ADVERTISE permission is introduced in Android S, meaning we should
+ // not handle permissions on pre Android S devices.
+ String result = determineBluetoothPermission(context, Manifest.permission.BLUETOOTH_ADVERTISE);
+
+ if (result != null) {
+ permissionNames.add(result);
+ }
+
+ break;
+ }
+ case PermissionConstants.PERMISSION_GROUP_BLUETOOTH_CONNECT: {
+ // The BLUETOOTH_CONNECT permission is introduced in Android S, meaning we should
+ // not handle permissions on pre Android S devices.
+ String result = determineBluetoothPermission(context, Manifest.permission.BLUETOOTH_CONNECT);
+
+ if (result != null) {
+ permissionNames.add(result);
+ }
+
+ break;
+ }
+ case PermissionConstants.PERMISSION_GROUP_NOTIFICATION:
+ // The POST_NOTIFICATIONS permission is introduced in Android 13, meaning we should
+ // not handle permissions on pre Android 13 devices.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && hasPermissionInManifest(context, permissionNames, Manifest.permission.POST_NOTIFICATIONS ))
+ permissionNames.add(Manifest.permission.POST_NOTIFICATIONS);
+ break;
+ case PermissionConstants.PERMISSION_GROUP_MEDIA_LIBRARY:
+ case PermissionConstants.PERMISSION_GROUP_PHOTOS:
+ case PermissionConstants.PERMISSION_GROUP_REMINDERS:
+ case PermissionConstants.PERMISSION_GROUP_UNKNOWN:
+ return null;
+ }
+
+ return permissionNames;
+ }
+
+ private static boolean hasPermissionInManifest(Context context, ArrayList confirmedPermissions, String permission) {
+ try {
+ if (confirmedPermissions != null) {
+ for (String r : confirmedPermissions) {
+ if (r.equals(permission)) {
+ return true;
+ }
+ }
+ }
+
+ if (context == null) {
+ Log.d(PermissionConstants.LOG_TAG, "Unable to detect current Activity or App Context.");
+ return false;
+ }
+
+ PackageInfo info = getPackageInfo(context);
+
+ if (info == null) {
+ Log.d(PermissionConstants.LOG_TAG, "Unable to get Package info, will not be able to determine permissions to request.");
+ return false;
+ }
+
+ confirmedPermissions = new ArrayList<>(Arrays.asList(info.requestedPermissions));
+ for (String r : confirmedPermissions) {
+ if (r.equals(permission)) {
+ return true;
+ }
+ }
+ } catch (Exception ex) {
+ Log.d(PermissionConstants.LOG_TAG, "Unable to check manifest for permission: ", ex);
+ }
+ return false;
+ }
+
+ @PermissionConstants.PermissionStatus
+ static int toPermissionStatus(final Activity activity, final String permissionName, int grantResult) {
+ if (grantResult == PackageManager.PERMISSION_DENIED) {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && PermissionUtils.isNeverAskAgainSelected(activity, permissionName)
+ ? PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN
+ : PermissionConstants.PERMISSION_STATUS_DENIED;
+ }
+
+ return PermissionConstants.PERMISSION_STATUS_GRANTED;
+ }
+
+ static void updatePermissionShouldShowStatus(final Activity activity, @PermissionConstants.PermissionGroup int permission) {
+ if (activity == null) {
+ return;
+ }
+
+ List names = getManifestNames(activity, permission);
+
+ if (names == null || names.isEmpty()) {
+ return;
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.M)
+ static boolean isNeverAskAgainSelected(final Activity activity, final String name) {
+ if (activity == null) {
+ return false;
+ }
+
+ final boolean shouldShowRequestPermissionRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, name);
+ return !shouldShowRequestPermissionRationale;
+ }
+
+ private static String determineBluetoothPermission(Context context, String permission) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && hasPermissionInManifest(context, null, permission )) {
+ return permission;
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ if(hasPermissionInManifest(context, null, Manifest.permission.ACCESS_FINE_LOCATION)){
+ return Manifest.permission.ACCESS_FINE_LOCATION;
+ } else if (hasPermissionInManifest(context, null, Manifest.permission.ACCESS_COARSE_LOCATION)){
+ return Manifest.permission.ACCESS_COARSE_LOCATION;
+ }
+
+ return null;
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && hasPermissionInManifest(context, null, Manifest.permission.ACCESS_FINE_LOCATION)) {
+ return Manifest.permission.ACCESS_FINE_LOCATION;
+ }
+
+ return null;
+ }
+
+ // Suppress deprecation warnings since its purpose is to support to be backwards compatible with
+ // pre TIRAMISU versions of Android
+ @SuppressWarnings("deprecation")
+ private static PackageInfo getPackageInfo(Context context) throws PackageManager.NameNotFoundException {
+ final PackageManager pm = context.getPackageManager();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ return pm.getPackageInfo(context.getPackageName(), PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
+ } else {
+ return pm.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
+ }
+ }
+}
diff --git a/android/src/main/java/com/baseflow/permissionhandler/ServiceManager.java b/android/src/main/java/com/baseflow/permissionhandler/ServiceManager.java
new file mode 100644
index 0000000..53b41c3
--- /dev/null
+++ b/android/src/main/java/com/baseflow/permissionhandler/ServiceManager.java
@@ -0,0 +1,177 @@
+package com.baseflow.permissionhandler;
+
+import static android.content.Context.BLUETOOTH_SERVICE;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.location.LocationManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+
+final class ServiceManager {
+ @FunctionalInterface
+ interface SuccessCallback {
+ void onSuccess(@PermissionConstants.ServiceStatus int serviceStatus);
+ }
+
+ void checkServiceStatus(
+ int permission,
+ Context context,
+ SuccessCallback successCallback,
+ ErrorCallback errorCallback) {
+ if(context == null) {
+ Log.d(PermissionConstants.LOG_TAG, "Context cannot be null.");
+ errorCallback.onError("PermissionHandler.ServiceManager", "Android context cannot be null.");
+ return;
+ }
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_LOCATION ||
+ permission == PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS ||
+ permission == PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE) {
+ final int serviceStatus = isLocationServiceEnabled(context)
+ ? PermissionConstants.SERVICE_STATUS_ENABLED
+ : PermissionConstants.SERVICE_STATUS_DISABLED;
+
+ successCallback.onSuccess(serviceStatus);
+ return;
+ }
+ if(permission == PermissionConstants.PERMISSION_GROUP_BLUETOOTH){
+ final int serviceStatus = isBluetoothServiceEnabled(context)
+ ? PermissionConstants.SERVICE_STATUS_ENABLED
+ : PermissionConstants.SERVICE_STATUS_DISABLED;
+
+ successCallback.onSuccess(serviceStatus);
+ }
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_PHONE) {
+ PackageManager pm = context.getPackageManager();
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+ successCallback.onSuccess(PermissionConstants.SERVICE_STATUS_NOT_APPLICABLE);
+ return;
+ }
+
+ TelephonyManager telephonyManager = (TelephonyManager) context
+ .getSystemService(Context.TELEPHONY_SERVICE);
+
+ if (telephonyManager == null || telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE) {
+ successCallback.onSuccess(PermissionConstants.SERVICE_STATUS_NOT_APPLICABLE);
+ return;
+ }
+
+ List callAppsList = getCallAppsList(pm);
+
+ if (callAppsList.isEmpty()) {
+ successCallback.onSuccess(PermissionConstants.SERVICE_STATUS_NOT_APPLICABLE);
+ return;
+ }
+
+ if (telephonyManager.getSimState() != TelephonyManager.SIM_STATE_READY) {
+ successCallback.onSuccess(PermissionConstants.SERVICE_STATUS_DISABLED);
+ return;
+ }
+
+ successCallback.onSuccess(PermissionConstants.SERVICE_STATUS_ENABLED);
+ return;
+ }
+
+ if (permission == PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS) {
+ final int serviceStatus = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ ? PermissionConstants.SERVICE_STATUS_ENABLED
+ : PermissionConstants.SERVICE_STATUS_NOT_APPLICABLE;
+ successCallback.onSuccess(serviceStatus);
+ return;
+ }
+
+ successCallback.onSuccess(PermissionConstants.SERVICE_STATUS_NOT_APPLICABLE);
+ }
+
+ private boolean isLocationServiceEnabled(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ final LocationManager locationManager = context.getSystemService(LocationManager.class);
+ if (locationManager == null) {
+ return false;
+ }
+
+ return locationManager.isLocationEnabled();
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ return isLocationServiceEnabledKitKat(context);
+ } else {
+ return isLocationServiceEnablePreKitKat(context);
+ }
+ }
+
+ // Suppress deprecation warnings since its purpose is to support to be backwards compatible with
+ // pre Pie versions of Android.
+ @SuppressWarnings("deprecation")
+ private static boolean isLocationServiceEnabledKitKat(Context context)
+ {
+ if (Build.VERSION.SDK_INT < VERSION_CODES.KITKAT) {
+ return false;
+ }
+
+ final int locationMode;
+
+ try {
+ locationMode = Settings.Secure.getInt(
+ context.getContentResolver(),
+ Settings.Secure.LOCATION_MODE);
+ } catch (Settings.SettingNotFoundException e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ return locationMode != Settings.Secure.LOCATION_MODE_OFF;
+ }
+
+ // Suppress deprecation warnings since its purpose is to support to be backwards compatible with
+ // pre KitKat versions of Android.
+ @SuppressWarnings("deprecation")
+ private static boolean isLocationServiceEnablePreKitKat(Context context)
+ {
+ if (VERSION.SDK_INT >= VERSION_CODES.KITKAT)
+ return false;
+
+ final String locationProviders = Settings.Secure.getString(
+ context.getContentResolver(),
+ Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
+ return !TextUtils.isEmpty(locationProviders);
+ }
+ // Suppress deprecation warnings since its purpose is to support to be backwards compatible with
+ // pre S versions of Android
+ @SuppressWarnings("deprecation")
+ private boolean isBluetoothServiceEnabled(Context context) {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ return BluetoothAdapter.getDefaultAdapter().isEnabled();
+ }
+
+ BluetoothManager manager = (BluetoothManager) context.getSystemService(BLUETOOTH_SERVICE);
+ final BluetoothAdapter adapter = manager.getAdapter();
+ return adapter.isEnabled();
+ }
+
+ // Suppress deprecation warnings since its purpose is to support to be backwards compatible with
+ // pre TIRAMISU versions of Android
+ @SuppressWarnings("deprecation")
+ private List getCallAppsList(PackageManager pm) {
+ Intent callIntent = new Intent(Intent.ACTION_CALL);
+ callIntent.setData(Uri.parse("tel:123123"));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ return pm.queryIntentActivities(callIntent, PackageManager.ResolveInfoFlags.of(0));
+ } else {
+ return pm.queryIntentActivities(callIntent, 0);
+ }
+ }
+}
diff --git a/example/.metadata b/example/.metadata
new file mode 100644
index 0000000..828ed53
--- /dev/null
+++ b/example/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 06b979c4d5e1b499745422269f01a00341257058
+ channel: master
+
+project_type: app
diff --git a/example/README.md b/example/README.md
new file mode 100644
index 0000000..f1bcd34
--- /dev/null
+++ b/example/README.md
@@ -0,0 +1,16 @@
+# permission_handler_example
+
+Demonstrates how to use the permission_handler plugin.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook)
+
+For help getting started with Flutter, view our
+[online documentation](https://flutter.io/docs), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/example/android/.settings/org.eclipse.buildship.core.prefs b/example/android/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..9851512
--- /dev/null
+++ b/example/android/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,13 @@
+arguments=
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
+connection.project.dir=
+eclipse.preferences.version=1
+gradle.user.home=
+java.home=/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=true
+show.console.view=true
+show.executions.view=true
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
new file mode 100644
index 0000000..fd8b8db
--- /dev/null
+++ b/example/android/app/build.gradle
@@ -0,0 +1,55 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 33
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.baseflow.permissionhandler.example"
+ minSdkVersion 16
+ targetSdkVersion 33
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..d692acb
--- /dev/null
+++ b/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..519117b
--- /dev/null
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
new file mode 100644
index 0000000..e793a00
--- /dev/null
+++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..449a9f9
--- /dev/null
+++ b/example/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..699d4b0
--- /dev/null
+++ b/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/android/build.gradle b/example/android/build.gradle
new file mode 100644
index 0000000..622ddc5
--- /dev/null
+++ b/example/android/build.gradle
@@ -0,0 +1,27 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.0'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/example/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..bc6a58a
--- /dev/null
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
new file mode 100644
index 0000000..5a2f14f
--- /dev/null
+++ b/example/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+ pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+ def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+ include ":$name"
+ project(":$name").projectDir = pluginDirectory
+}
diff --git a/example/lib/main.dart b/example/lib/main.dart
new file mode 100644
index 0000000..e99e698
--- /dev/null
+++ b/example/lib/main.dart
@@ -0,0 +1,141 @@
+import 'package:baseflow_plugin_template/baseflow_plugin_template.dart';
+import 'package:flutter/material.dart';
+import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
+
+void main() {
+ runApp(BaseflowPluginExample(
+ pluginName: 'Permission Handler',
+ githubURL: 'https://github.com/Baseflow/flutter-permission-handler',
+ pubDevURL: 'https://pub.dev/packages/permission_handler',
+ pages: [PermissionHandlerWidget.createPage()]));
+}
+
+///Defines the main theme color
+final MaterialColor themeMaterialColor =
+ BaseflowPluginExample.createMaterialColor(
+ const Color.fromRGBO(48, 49, 60, 1));
+
+/// A Flutter application demonstrating the functionality of this plugin
+class PermissionHandlerWidget extends StatefulWidget {
+ /// Create a page containing the functionality of this plugin
+ static ExamplePage createPage() {
+ return ExamplePage(
+ Icons.location_on, (context) => PermissionHandlerWidget());
+ }
+
+ @override
+ _PermissionHandlerWidgetState createState() =>
+ _PermissionHandlerWidgetState();
+}
+
+class _PermissionHandlerWidgetState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: ListView(
+ children: Permission.values
+ .where((permission) {
+ return permission != Permission.unknown &&
+ permission != Permission.mediaLibrary &&
+ permission != Permission.photos &&
+ permission != Permission.photosAddOnly &&
+ permission != Permission.reminders &&
+ permission != Permission.appTrackingTransparency &&
+ permission != Permission.criticalAlerts;
+ })
+ .map((permission) => PermissionWidget(permission))
+ .toList()),
+ );
+ }
+}
+
+/// Permission widget containing information about the passed [Permission]
+class PermissionWidget extends StatefulWidget {
+ /// Constructs a [PermissionWidget] for the supplied [Permission]
+ const PermissionWidget(this._permission);
+
+ final Permission _permission;
+
+ @override
+ _PermissionState createState() => _PermissionState(_permission);
+}
+
+class _PermissionState extends State {
+ _PermissionState(this._permission);
+
+ final Permission _permission;
+ final PermissionHandlerPlatform _permissionHandler =
+ PermissionHandlerPlatform.instance;
+ PermissionStatus _permissionStatus = PermissionStatus.denied;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _listenForPermissionStatus();
+ }
+
+ void _listenForPermissionStatus() async {
+ final status = await _permissionHandler.checkPermissionStatus(_permission);
+ setState(() => _permissionStatus = status);
+ }
+
+ Color getPermissionColor() {
+ switch (_permissionStatus) {
+ case PermissionStatus.denied:
+ return Colors.red;
+ case PermissionStatus.granted:
+ return Colors.green;
+ case PermissionStatus.limited:
+ return Colors.orange;
+ default:
+ return Colors.grey;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ListTile(
+ title: Text(
+ _permission.toString(),
+ style: Theme.of(context).textTheme.bodyText1,
+ ),
+ subtitle: Text(
+ _permissionStatus.toString(),
+ style: TextStyle(color: getPermissionColor()),
+ ),
+ trailing: (_permission is PermissionWithService)
+ ? IconButton(
+ icon: const Icon(
+ Icons.info,
+ color: Colors.white,
+ ),
+ onPressed: () {
+ checkServiceStatus(
+ context, _permission as PermissionWithService);
+ })
+ : null,
+ onTap: () {
+ requestPermission(_permission);
+ },
+ );
+ }
+
+ void checkServiceStatus(
+ BuildContext context, PermissionWithService permission) async {
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+ content: Text(
+ (await _permissionHandler.checkServiceStatus(permission)).toString()),
+ ));
+ }
+
+ Future requestPermission(Permission permission) async {
+ final status = await _permissionHandler.requestPermissions([permission]);
+
+ setState(() {
+ print(status);
+ _permissionStatus = status[permission] ?? PermissionStatus.denied;
+ print(_permissionStatus);
+ });
+ }
+}
diff --git a/example/pubspec.lock b/example/pubspec.lock
new file mode 100644
index 0000000..9c42656
--- /dev/null
+++ b/example/pubspec.lock
@@ -0,0 +1,245 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ async:
+ dependency: transitive
+ description:
+ name: async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.8.2"
+ baseflow_plugin_template:
+ dependency: "direct main"
+ description:
+ path: "."
+ ref: "v2.1.0"
+ resolved-ref: "0695ef43c098d75efffe5fd6212390b853f05bf7"
+ url: "https://github.com/Baseflow/baseflow_plugin_template.git"
+ source: git
+ version: "2.1.0"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ charcode:
+ dependency: transitive
+ description:
+ name: charcode
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.1"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.16.0"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.0"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_web_plugins:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.4"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.11"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.4"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.1"
+ permission_handler_android:
+ dependency: "direct dev"
+ description:
+ path: ".."
+ relative: true
+ source: path
+ version: "10.0.0"
+ permission_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: permission_handler_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.7.0"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.2"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.2"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.10.0"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.4.9"
+ url_launcher:
+ dependency: "direct dev"
+ description:
+ name: url_launcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.1.5"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.0.17"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.0.17"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.12"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.2"
+sdks:
+ dart: ">=2.17.0-0 <3.0.0"
+ flutter: ">=2.10.0"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
new file mode 100644
index 0000000..e7000f3
--- /dev/null
+++ b/example/pubspec.yaml
@@ -0,0 +1,37 @@
+name: permission_handler_android_example
+description: Demonstrates how to use the permission_handler_android plugin.
+
+environment:
+ sdk: ">=2.15.0 <3.0.0"
+
+dependencies:
+ baseflow_plugin_template:
+ git:
+ url: https://github.com/Baseflow/baseflow_plugin_template.git
+ ref: v2.1.0
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ permission_handler_android:
+ # When depending on this package from a real application you should use:
+ # permission_handler_android: ^x.y.z
+ # See https://dart.dev/tools/pub/dependencies#version-constraints
+ # The example app is bundled with the plugin so we use a path dependency on
+ # the parent directory to use the current plugin's version.
+ path: ../
+
+ url_launcher: ^6.0.12
+
+flutter:
+ uses-material-design: true
+
+ assets:
+ - res/images/baseflow_logo_def_light-02.png
+ - res/images/poweredByBaseflowLogoLight@3x.png
+ - packages/baseflow_plugin_template/logo.png
+ - packages/baseflow_plugin_template/poweredByBaseflow.png
+
diff --git a/example/res/images/baseflow_logo_def_light-02.png b/example/res/images/baseflow_logo_def_light-02.png
new file mode 100644
index 0000000..85f071f
Binary files /dev/null and b/example/res/images/baseflow_logo_def_light-02.png differ
diff --git a/example/res/images/poweredByBaseflowLogoLight.png b/example/res/images/poweredByBaseflowLogoLight.png
new file mode 100644
index 0000000..6b9f1aa
Binary files /dev/null and b/example/res/images/poweredByBaseflowLogoLight.png differ
diff --git a/example/res/images/poweredByBaseflowLogoLight@2x.png b/example/res/images/poweredByBaseflowLogoLight@2x.png
new file mode 100644
index 0000000..66901d9
Binary files /dev/null and b/example/res/images/poweredByBaseflowLogoLight@2x.png differ
diff --git a/example/res/images/poweredByBaseflowLogoLight@3x.png b/example/res/images/poweredByBaseflowLogoLight@3x.png
new file mode 100644
index 0000000..36fbdff
Binary files /dev/null and b/example/res/images/poweredByBaseflowLogoLight@3x.png differ
diff --git a/pubspec.lock b/pubspec.lock
new file mode 100644
index 0000000..bab0fd3
--- /dev/null
+++ b/pubspec.lock
@@ -0,0 +1,79 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.16.0"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.4"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.4"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.0"
+ permission_handler_platform_interface:
+ dependency: "direct main"
+ description:
+ name: permission_handler_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.7.0"
+ plugin_platform_interface:
+ dependency: "direct dev"
+ description:
+ name: plugin_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.2"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.2"
+sdks:
+ dart: ">=2.15.0 <3.0.0"
+ flutter: ">=2.8.0"
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..e8efa98
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,25 @@
+name: permission_handler_android
+description: Permission plugin for Flutter. This plugin provides the Android API to request and check permissions.
+version: 10.0.0
+homepage: https://github.com/baseflow/flutter-permission-handler
+
+environment:
+ sdk: ">=2.15.0 <3.0.0"
+ flutter: ">=2.8.0"
+
+flutter:
+ plugin:
+ implements: permission_handler
+ platforms:
+ android:
+ package: com.baseflow.permissionhandler
+ pluginClass: PermissionHandlerPlugin
+
+dependencies:
+ flutter:
+ sdk: flutter
+ permission_handler_platform_interface: ^3.7.0
+
+dev_dependencies:
+ flutter_lints: ^1.0.4
+ plugin_platform_interface: ^2.0.0