diff --git a/native/android/app/src/main/java/app/comm/android/MainActivity.java b/native/android/app/src/main/java/app/comm/android/MainActivity.java --- a/native/android/app/src/main/java/app/comm/android/MainActivity.java +++ b/native/android/app/src/main/java/app/comm/android/MainActivity.java @@ -1,13 +1,18 @@ package app.comm.android; +import android.Manifest; import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Bundle; +import androidx.core.app.ActivityCompat; +import app.comm.android.notifications.CommAndroidNotifications; import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; import com.facebook.react.ReactRootView; import expo.modules.ReactActivityDelegateWrapper; -public class MainActivity extends ReactActivity { +public class MainActivity extends ReactActivity + implements ActivityCompat.OnRequestPermissionsResultCallback { /** * Returns the name of the main component registered from JavaScript. @@ -66,4 +71,24 @@ public void invokeDefaultOnBackPressed() { moveTaskToBack(true); } + + @Override + public void onRequestPermissionsResult( + int requestCode, + String[] permissions, + int[] grantResults) { + + for (int permissionIndex = 0; permissionIndex < grantResults.length; + permissionIndex++) { + String permissionName = permissions[permissionIndex]; + if (requestCode == + CommAndroidNotifications + .COMM_ANDROID_NOTIFICATIONS_REQUEST_CODE && + permissionName.equals(Manifest.permission.POST_NOTIFICATIONS)) { + CommAndroidNotifications.resolveNotificationsPermissionRequestPromise( + this, + grantResults[permissionIndex] == PackageManager.PERMISSION_GRANTED); + } + } + } } diff --git a/native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java b/native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java --- a/native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java +++ b/native/android/app/src/main/java/app/comm/android/notifications/CommAndroidNotifications.java @@ -1,10 +1,13 @@ package app.comm.android.notifications; +import android.Manifest; +import android.app.Activity; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Bundle; import android.service.notification.StatusBarNotification; +import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationManagerCompat; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.Promise; @@ -17,11 +20,17 @@ import com.google.firebase.messaging.RemoteMessage; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import me.leolin.shortcutbadger.ShortcutBadger; public class CommAndroidNotifications extends ReactContextBaseJavaModule { + private static AtomicReference + notificationsPermissionRequestPromise = new AtomicReference(null); + private NotificationManager notificationManager; + public static final int COMM_ANDROID_NOTIFICATIONS_REQUEST_CODE = 11111; + CommAndroidNotifications(ReactApplicationContext reactContext) { super(reactContext); Context context = reactContext.getApplicationContext(); @@ -105,10 +114,7 @@ @ReactMethod public void hasPermission(Promise promise) { - boolean enabled = - NotificationManagerCompat.from(getReactApplicationContext()) - .areNotificationsEnabled(); - promise.resolve(enabled); + promise.resolve(hasPermission()); } @ReactMethod @@ -122,6 +128,46 @@ }); } + @ReactMethod + public void canRequestNotificationsPermissionFromUser(Promise promise) { + promise.resolve(canRequestNotificationsPermissionFromUser()); + } + + @ReactMethod + public void requestNotificationsPermission(Promise promise) { + if (!canRequestNotificationsPermissionFromUser()) { + // If this method is erroneously called on older Android we should + // resolve to the value from notifications settings. + promise.resolve(hasPermission()); + return; + } + + if (ActivityCompat.shouldShowRequestPermissionRationale( + getCurrentActivity(), Manifest.permission.POST_NOTIFICATIONS)) { + // Since Android 13 user has to deny notifications permission + // twice to remove possibility to show the prompt again. Details: + // https://developer.android.com/training/permissions/requesting#handle-denial + // On iOS user has to deny notifications permission once to disable the + // prompt. We use the method above to mimic iOS behavior on Android. + // Details: + // https://developer.android.com/topic/performance/vitals/permissions#explain + promise.resolve(hasPermission()); + return; + } + + if (notificationsPermissionRequestPromise.get() != null) { + promise.reject( + "Programmer error: permission request already in progress."); + return; + } + + notificationsPermissionRequestPromise.set(promise); + ActivityCompat.requestPermissions( + getCurrentActivity(), + new String[] {Manifest.permission.POST_NOTIFICATIONS}, + CommAndroidNotifications.COMM_ANDROID_NOTIFICATIONS_REQUEST_CODE); + } + @Override public Map getConstants() { final Map constants = new HashMap<>(); @@ -129,4 +175,31 @@ "NOTIFICATIONS_IMPORTANCE_HIGH", NotificationManager.IMPORTANCE_HIGH); return constants; } + + public static void resolveNotificationsPermissionRequestPromise( + Activity mainActivity, + boolean isPermissionGranted) { + if (notificationsPermissionRequestPromise.get() == null) { + return; + } + + notificationsPermissionRequestPromise.get().resolve(isPermissionGranted); + notificationsPermissionRequestPromise.set(null); + } + + private boolean hasPermission() { + return NotificationManagerCompat.from(getReactApplicationContext()) + .areNotificationsEnabled(); + } + + private boolean canRequestNotificationsPermissionFromUser() { + if (android.os.Build.VERSION.SDK_INT >= + android.os.Build.VERSION_CODES.TIRAMISU) { + // Application has to explicitly request notifications permission from + // user since Android 13. Older versions grant them by default. Details: + // https://developer.android.com/develop/ui/views/notifications/notification-permission + return true; + } + return false; + } } diff --git a/native/push/android.js b/native/push/android.js --- a/native/push/android.js +++ b/native/push/android.js @@ -18,6 +18,8 @@ +removeAllDeliveredNotifications: () => void, +hasPermission: () => Promise, +getToken: () => Promise, + +requestNotificationsPermission: () => Promise, + +canRequestNotificationsPermissionFromUser: () => Promise, +NOTIFICATIONS_IMPORTANCE_HIGH: string, }; export type AndroidForegroundMessage = {