Skip to content

Conversation

@seyedmostafahasani
Copy link

@seyedmostafahasani seyedmostafahasani commented Jun 3, 2024

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

  • I have tested this on a device and a simulator
  • I added the documentation in README.md
  • I added a sample use of the API in the example project (example/App.tsx)

zoontek
zoontek previously approved these changes Jun 6, 2024
@zoontek zoontek dismissed their stale review June 6, 2024 20:17

Must be request changes (instead of approval)

Copy link
Owner

@zoontek zoontek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkMultiple / requestMultiple support must be added

@seyedmostafahasani
Copy link
Author

checkMultiple / requestMultiple support must be added

I handled schedule exact alarm with both functions.

@kevynb
Copy link

kevynb commented Jun 20, 2024

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
Copy link
Owner

zoontek commented Jun 20, 2024

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
Copy link

Zhigamovsky commented Jun 20, 2024

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

@seyedmostafahasani
Copy link
Author

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

@ThomasReyskens
Copy link

ThomasReyskens commented Jul 11, 2024

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
Copy link
Author

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

@seyedmostafahasani
Copy link
Author

@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.

@zoontek zoontek mentioned this pull request Sep 3, 2024
@zoontek zoontek closed this in #890 Oct 12, 2024
@ezwaydev
Copy link

Excuse me, how was this merged ? I do not see any of this functionality in d253fb0 (v5.0.0)

@zoontek
Copy link
Owner

zoontek commented Oct 16, 2024

@ezwaydev It has been automatically closed by Github as #890 has been merged and contains closes …, but you are right, it's not merged.

@zoontek zoontek reopened this Oct 16, 2024
# Conflicts:
#	android/src/main/java/com/zoontek/rnpermissions/RNPermissionsModuleImpl.java
@seyedmostafahasani
Copy link
Author

@zoontek
I resolved conflicts.

@seyedmostafahasani
Copy link
Author

@zoontek
Would you please review my PR?

@zoontek
Copy link
Owner

zoontek commented Oct 28, 2024

@seyedmostafahasani It's on my TODO, but please stop pressuring maintainers (for context: you already reached me by email too). I'm currently working a lot on react-native-edge-to-edge and helping debugging new architecture issues, this comes just after.

EDIT: Just ran the example app for a really quick check and without specifically trying to achieve weird stuff (just a check + a request), I've been able to create a never-resolving Promise 😕

Screen.Recording.2024-10-28.at.10.46.58.mp4

@zoontek
Copy link
Owner

zoontek commented Oct 29, 2024

Closed in favor of #905, which is available in 5.1.0

@zoontek zoontek closed this Oct 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants