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,21 @@ 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; + // Application can request multiple permission, but all + // will be resolved via `onRequestPermissionsResult` callback + // in MainActivity. Therefore we use custom request codes + // to differentiate between different permission requests + public static final int COMM_ANDROID_NOTIFICATIONS_REQUEST_CODE = 11111; + CommAndroidNotifications(ReactApplicationContext reactContext) { super(reactContext); Context context = reactContext.getApplicationContext(); @@ -105,10 +118,7 @@ @ReactMethod public void hasPermission(Promise promise) { - boolean enabled = - NotificationManagerCompat.from(getReactApplicationContext()) - .areNotificationsEnabled(); - promise.resolve(enabled); + promise.resolve(hasPermissionInternal()); } @ReactMethod @@ -122,6 +132,45 @@ }); } + @ReactMethod + public void canRequestNotificationsPermissionFromUser(Promise promise) { + promise.resolve(canRequestNotificationsPermissionInternal()); + } + + @ReactMethod + public void requestNotificationsPermission(Promise promise) { + if (!canRequestNotificationsPermissionInternal()) { + // If this method is erroneously called on older Android we should + // resolve to the value from notifications settings. + promise.resolve(hasPermissionInternal()); + 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(hasPermissionInternal()); + return; + } + + if (!notificationsPermissionRequestPromise.compareAndSet(null, promise)) { + promise.reject( + "Programmer error: permission request already in progress."); + return; + } + + 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 +178,28 @@ "NOTIFICATIONS_IMPORTANCE_HIGH", NotificationManager.IMPORTANCE_HIGH); return constants; } + + public static void resolveNotificationsPermissionRequestPromise( + Activity mainActivity, + boolean isPermissionGranted) { + if (notificationsPermissionRequestPromise.get() == null) { + return; + } + + notificationsPermissionRequestPromise.getAndSet(null).resolve( + isPermissionGranted); + } + + private boolean hasPermissionInternal() { + return NotificationManagerCompat.from(getReactApplicationContext()) + .areNotificationsEnabled(); + } + + private boolean canRequestNotificationsPermissionInternal() { + // 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 android.os.Build.VERSION.SDK_INT >= + android.os.Build.VERSION_CODES.TIRAMISU; + } } 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 = {