zoontek / react-native-permissions

An unified permissions API for React Native on iOS, Android and Windows.
MIT License
4.04k stars 828 forks source link

feat(android): add schedule exact alarm permission #878

Open seyedmostafahasani opened 4 months ago

seyedmostafahasani commented 4 months ago

Summary

I managed the new permission for scheduling exact alarms in Android 13 and higher.

Test Plan

I added this permission to the sample app.

What's required for testing (prerequisites)?

What are the steps to test it (after prerequisites)?

Compatibility

OS Implemented
iOS
Android

Checklist

seyedmostafahasani commented 3 months ago

checkMultiple / requestMultiple support must be added

I handled schedule exact alarm with both functions.

kevynb commented 3 months ago

Hello @zoontek, anything we could do to help this PR be merged?

We also need the feature and I was wondering if we could provide additional support to speed things up.

zoontek commented 3 months ago

Not really, I'm in a middle of packing / selling all my stuff because I'm moving soon.

I will probably have a bit more bandwidth next week.

Zhigamovsky commented 3 months ago

I think the method to open a specific Alarm settings could be added as well.

seyedmostafahasani commented 3 months ago

@zoontek If you have any suggestions, I am available to apply them.

ThomasReyskens commented 2 months ago

I just tried the code. When requesting permission, it opens up a list, displaying all apps. To create a better UX, I would add intent.setData(Uri.fromParts("package", reactContext.getPackageName(), null)); at line 215 of the RNPermissionsModuleImpl.java.

patch:

index 97bc712..cda5f87 100644
--- a/node_modules/react-native-permissions/android/src/main/java/com/zoontek/rnpermissions/RNPermissionsModuleImpl.java
+++ b/node_modules/react-native-permissions/android/src/main/java/com/zoontek/rnpermissions/RNPermissionsModuleImpl.java
@@ -2,6 +2,7 @@ package com.zoontek.rnpermissions;

 import android.Manifest;
 import android.app.Activity;
+import android.app.AlarmManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -12,11 +13,13 @@ import android.provider.Settings;
 import android.util.SparseArray;

 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 import androidx.core.app.NotificationManagerCompat;

 import com.facebook.common.logging.FLog;
 import com.facebook.react.bridge.Arguments;
 import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.LifecycleEventListener;
 import com.facebook.react.bridge.Promise;
 import com.facebook.react.bridge.ReactApplicationContext;
 import com.facebook.react.bridge.ReadableArray;
@@ -27,6 +30,7 @@ import com.facebook.react.modules.core.PermissionListener;

 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;

 public class RNPermissionsModuleImpl {
@@ -47,6 +51,9 @@ public class RNPermissionsModuleImpl {
   }

   private static boolean isPermissionUnavailable(@NonNull final String permission) {
+    if (Manifest.permission.SCHEDULE_EXACT_ALARM.equals(permission)) {
+      return false;
+    }
     String fieldName = permission
       .replace("android.permission.", "")
       .replace("com.android.voicemail.permission.", "");
@@ -113,6 +120,17 @@ public class RNPermissionsModuleImpl {
       return;
     }

+    if (Manifest.permission.SCHEDULE_EXACT_ALARM.equals(permission)) {
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+        if (context.getSystemService(AlarmManager.class).canScheduleExactAlarms()) {
+          promise.resolve(GRANTED);
+        } else {
+          promise.resolve(DENIED);
+        }
+        return;
+      }
+    }
+
     if (context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
       promise.resolve(GRANTED);
     } else {
@@ -147,6 +165,14 @@ public class RNPermissionsModuleImpl {
             == PackageManager.PERMISSION_GRANTED
             ? GRANTED
             : BLOCKED);
+      } else if (Manifest.permission.SCHEDULE_EXACT_ALARM.equals(permission)) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+          if (context.getSystemService(AlarmManager.class).canScheduleExactAlarms()) {
+            output.putString(permission, GRANTED);
+          } else {
+            output.putString(permission, DENIED);
+          }
+        }
       } else if (context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
         output.putString(permission, GRANTED);
       } else {
@@ -179,6 +205,42 @@ public class RNPermissionsModuleImpl {
       return;
     }

+    if (Manifest.permission.SCHEDULE_EXACT_ALARM.equals(permission)) {
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+        if (context.getSystemService(AlarmManager.class).canScheduleExactAlarms()) {
+            promise.resolve(GRANTED);
+        } else {
+          try {
+            Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
+            intent.setData(Uri.fromParts("package", reactContext.getPackageName(), null));
+            reactContext.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+            // Register a lifecycle listener to check permission status when app resumes
+            reactContext.addLifecycleEventListener(new LifecycleEventListener() {
+              @Override
+              public void onHostResume() {
+                // Check the permission status when the app resumes
+                if (context.getSystemService(AlarmManager.class).canScheduleExactAlarms()) {
+                  promise.resolve(GRANTED);
+                } else {
+                  promise.resolve(DENIED);
+                }
+                reactContext.removeLifecycleEventListener(this);
+              }
+
+              @Override
+              public void onHostPause() {}
+
+              @Override
+              public void onHostDestroy() {}
+            });
+          } catch (Exception e) {
+            promise.reject(ERROR_INVALID_ACTIVITY, e);
+          }
+        }
+        return;
+      }
+    }
+
     if (context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
       promise.resolve(GRANTED);
       return;
@@ -222,6 +284,7 @@ public class RNPermissionsModuleImpl {
     promise.resolve(getLegacyNotificationsResponse(reactContext, BLOCKED));
   }

+  @RequiresApi(api = Build.VERSION_CODES.S)
   public static void requestMultiple(
     final ReactApplicationContext reactContext,
     final PermissionListener listener,
@@ -230,8 +293,8 @@ public class RNPermissionsModuleImpl {
     final Promise promise
   ) {
     final WritableMap output = new WritableNativeMap();
-    final ArrayList<String> permissionsToCheck = new ArrayList<String>();
-    int checkedPermissionsCount = 0;
+    final ArrayList<String> permissionsToCheck = new ArrayList<>();
+    final HashSet<String> pendingPermissions = new HashSet<>();
     Context context = reactContext.getBaseContext();

     for (int i = 0; i < permissions.size(); i++) {
@@ -239,7 +302,6 @@ public class RNPermissionsModuleImpl {

       if (isPermissionUnavailable(permission)) {
         output.putString(permission, UNAVAILABLE);
-        checkedPermissionsCount++;
       } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
         output.putString(
           permission,
@@ -247,17 +309,24 @@ public class RNPermissionsModuleImpl {
             == PackageManager.PERMISSION_GRANTED
             ? GRANTED
             : BLOCKED);
-
-        checkedPermissionsCount++;
+      } else if (Manifest.permission.SCHEDULE_EXACT_ALARM.equals(permission)) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+          if (context.getSystemService(AlarmManager.class).canScheduleExactAlarms()) {
+            output.putString(permission, GRANTED);
+          } else {
+            permissionsToCheck.add(permission);
+            pendingPermissions.add(permission);
+          }
+        }
       } else if (context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
         output.putString(permission, GRANTED);
-        checkedPermissionsCount++;
       } else {
         permissionsToCheck.add(permission);
+        pendingPermissions.add(permission);
       }
     }

-    if (permissions.size() == checkedPermissionsCount) {
+    if (pendingPermissions.isEmpty()) {
       promise.resolve(output);
       return;
     }
@@ -276,18 +345,49 @@ public class RNPermissionsModuleImpl {
             for (int j = 0; j < permissionsToCheck.size(); j++) {
               String permission = permissionsToCheck.get(j);

-              if (results.length > 0 && results[j] == PackageManager.PERMISSION_GRANTED) {
-                output.putString(permission, GRANTED);
+              if (Manifest.permission.SCHEDULE_EXACT_ALARM.equals(permission)) {
+                reactContext.addLifecycleEventListener(new LifecycleEventListener() {
+                  @Override
+                  public void onHostResume() {
+                    if (context.getSystemService(AlarmManager.class).canScheduleExactAlarms()) {
+                      output.putString(permission, GRANTED);
+                    } else {
+                      output.putString(permission, DENIED);
+                    }
+                    reactContext.removeLifecycleEventListener(this);
+                    pendingPermissions.remove(permission);
+
+                    if (pendingPermissions.isEmpty()) {
+                      promise.resolve(output);
+                    }
+                  }
+
+                  @Override
+                  public void onHostPause() {}
+
+                  @Override
+                  public void onHostDestroy() {}
+                });
+
+                Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
+                reactContext.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
               } else {
-                if (activity.shouldShowRequestPermissionRationale(permission)) {
-                  output.putString(permission, DENIED);
+                if (results.length > 0 && results[j] == PackageManager.PERMISSION_GRANTED) {
+                  output.putString(permission, GRANTED);
                 } else {
-                  output.putString(permission, BLOCKED);
+                  if (activity.shouldShowRequestPermissionRationale(permission)) {
+                    output.putString(permission, DENIED);
+                  } else {
+                    output.putString(permission, BLOCKED);
+                  }
                 }
+                pendingPermissions.remove(permission);
               }
             }

-            promise.resolve(output);
+            if (pendingPermissions.isEmpty()) {
+              promise.resolve(output);
+            }
           }
         });

diff --git a/node_modules/react-native-permissions/src/permissions.android.ts b/node_modules/react-native-permissions/src/permissions.android.ts
index ee15437..b029453 100644
--- a/node_modules/react-native-permissions/src/permissions.android.ts
+++ b/node_modules/react-native-permissions/src/permissions.android.ts
@@ -43,6 +43,7 @@ const ANDROID = Object.freeze({
   WRITE_CALL_LOG: 'android.permission.WRITE_CALL_LOG',
   WRITE_CONTACTS: 'android.permission.WRITE_CONTACTS',
   WRITE_EXTERNAL_STORAGE: 'android.permission.WRITE_EXTERNAL_STORAGE',
+  SCHEDULE_EXACT_ALARM: 'android.permission.SCHEDULE_EXACT_ALARM',
 } as const);

 export type AndroidPermissionMap = typeof ANDROID;

(and ofc also in the requestMultiple, but out of scope in my implementation)

seyedmostafahasani commented 2 months ago

@ThomasReyskens it is a good idea. I will add it to the code.

seyedmostafahasani commented 2 months ago

@zoontek if you have enough time, please check out my PR. I think a lot of people would like to use this feature. I am available to apply any suggestions you may have.