From 70a508fdb8ae41eb8a585517ccf9b3961f7c1eb9 Mon Sep 17 00:00:00 2001 From: KAMRONBEK Date: Thu, 13 Aug 2026 13:08:20 +0500 Subject: [PATCH] feat(android): report full-screen intent availability in getNotificationSettings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android 14 / API 34 turned USE_FULL_SCREEN_INTENT into a user-revocable special app access: the Play Store revokes it on install for apps outside the calling and alarm categories, apps installed before the device upgraded keep it, and the user can toggle it at any time. A denial is silent. The notification still posts, the promise resolves and nothing throws — only the full screen presentation is dropped. Neither Build.VERSION.SDK_INT nor the manifest permission is a usable signal, so apps had no way to explain the behaviour or offer the fix. Expose it as android.fullScreenIntent on getNotificationSettings(), beside the existing android.alarm field and with the same semantics: ENABLED below API 34, and NotificationManager.canUseFullScreenIntent() at or above it. The check fails open when the NotificationManager is unavailable, so a state we cannot prove never has apps nag the user. Closes #64 --- CHANGELOG.md | 4 + .../__tests__/NotifeeApiModule.test.ts | 20 +++++ .../main/java/app/notifee/core/Notifee.java | 9 ++ .../core/utility/FullScreenIntentUtils.java | 71 +++++++++++++++ .../utility/FullScreenIntentUtilsTest.java | 86 +++++++++++++++++++ packages/react-native/jest-mock.js | 1 + packages/react-native/src/NotifeeApiModule.ts | 4 + .../src/types/NotificationAndroid.ts | 15 ++++ 8 files changed, 210 insertions(+) create mode 100644 packages/react-native/android/src/main/java/app/notifee/core/utility/FullScreenIntentUtils.java create mode 100644 packages/react-native/android/src/test/java/app/notifee/core/utility/FullScreenIntentUtilsTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a2e8de..3ee361f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added + +- **Android**: `getNotificationSettings()` now reports `android.fullScreenIntent`, an `AndroidNotificationSetting` describing whether a notification posted with a `fullScreenAction` will actually be shown full screen. On Android 14 / API 34 and above, `USE_FULL_SCREEN_INTENT` is a user-revocable special app access that the Play Store revokes on install for apps outside the calling and alarm categories — and a denial was previously undetectable, because the notification still posts and no error is raised. Below API 34 the value is always `ENABLED`, matching the existing `android.alarm` field. + ## [10.5.0] - 2026-07-24 ### Fixed diff --git a/packages/react-native/__tests__/NotifeeApiModule.test.ts b/packages/react-native/__tests__/NotifeeApiModule.test.ts index 01fd677e..cb25c053 100644 --- a/packages/react-native/__tests__/NotifeeApiModule.test.ts +++ b/packages/react-native/__tests__/NotifeeApiModule.test.ts @@ -298,6 +298,7 @@ describe('Notifee Api Module', () => { authorizationStatus: AuthorizationStatus.AUTHORIZED, android: { alarm: AndroidNotificationSetting.DISABLED, + fullScreenIntent: AndroidNotificationSetting.ENABLED, }, }); const settings = await apiModule.getNotificationSettings(); @@ -305,6 +306,7 @@ describe('Notifee Api Module', () => { authorizationStatus: AuthorizationStatus.AUTHORIZED, android: { alarm: 0, + fullScreenIntent: 1, }, ios: { alert: 1, @@ -322,6 +324,23 @@ describe('Notifee Api Module', () => { web: {}, }); }); + + test('passes a denied fullScreenIntent through from the native module', async () => { + mockNotifeeNativeModule.getNotificationSettings.mockResolvedValue({ + authorizationStatus: AuthorizationStatus.AUTHORIZED, + android: { + alarm: AndroidNotificationSetting.ENABLED, + fullScreenIntent: AndroidNotificationSetting.DISABLED, + }, + }); + + const settings = await apiModule.getNotificationSettings(); + + expect(settings.android).toEqual({ + alarm: AndroidNotificationSetting.ENABLED, + fullScreenIntent: AndroidNotificationSetting.DISABLED, + }); + }); }); describe('on iOS', () => { @@ -352,6 +371,7 @@ describe('Notifee Api Module', () => { authorizationStatus: AuthorizationStatus.NOT_DETERMINED, android: { alarm: AndroidNotificationSetting.ENABLED, + fullScreenIntent: AndroidNotificationSetting.ENABLED, }, ios: { alert: 1, diff --git a/packages/react-native/android/src/main/java/app/notifee/core/Notifee.java b/packages/react-native/android/src/main/java/app/notifee/core/Notifee.java index d70526e7..f59478a9 100644 --- a/packages/react-native/android/src/main/java/app/notifee/core/Notifee.java +++ b/packages/react-native/android/src/main/java/app/notifee/core/Notifee.java @@ -36,6 +36,7 @@ import app.notifee.core.model.ChannelModel; import app.notifee.core.model.NotificationModel; import app.notifee.core.utility.AlarmUtils; +import app.notifee.core.utility.FullScreenIntentUtils; import app.notifee.core.utility.PowerManagerUtils; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -508,6 +509,14 @@ public void getNotificationSettings(MethodCallResult result) { androidSettingsBundle.putInt("alarm", 0); } + boolean canUseFullScreenIntent = FullScreenIntentUtils.canUseFullScreenIntent(); + + if (canUseFullScreenIntent) { + androidSettingsBundle.putInt("fullScreenIntent", 1); + } else { + androidSettingsBundle.putInt("fullScreenIntent", 0); + } + notificationSettingsBundle.putBundle("android", androidSettingsBundle); result.onComplete(null, notificationSettingsBundle); } diff --git a/packages/react-native/android/src/main/java/app/notifee/core/utility/FullScreenIntentUtils.java b/packages/react-native/android/src/main/java/app/notifee/core/utility/FullScreenIntentUtils.java new file mode 100644 index 00000000..63cd557a --- /dev/null +++ b/packages/react-native/android/src/main/java/app/notifee/core/utility/FullScreenIntentUtils.java @@ -0,0 +1,71 @@ +package app.notifee.core.utility; + +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import static app.notifee.core.ContextHolder.getApplicationContext; + +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +public class FullScreenIntentUtils { + + @Nullable + private static NotificationManager getNotificationManager() { + Context context = getApplicationContext(); + if (context == null) { + return null; + } + return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + /** + * Whether a notification posted with a full screen action will actually be shown full screen. + * + *

On Android 14 / API 34 and above, {@code USE_FULL_SCREEN_INTENT} is a user-revocable special + * app access rather than a normal install-time permission: the Play Store revokes it on install + * for apps outside the calling and alarm categories, and the user can toggle it at any time. When + * it is denied the notification still posts normally and nothing throws — only the full screen + * presentation is dropped — so this is the sole reliable way to detect it. + * + *

Below API 34 the manifest permission is granted at install and always honoured. + * + * @return true when a full screen intent will be honoured, false when the user or the system has + * denied it. + */ + public static boolean canUseFullScreenIntent() { + return canUseFullScreenIntent(getNotificationManager()); + } + + @VisibleForTesting + static boolean canUseFullScreenIntent(@Nullable NotificationManager notificationManager) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return true; + } + + // Fail open: an absent NotificationManager tells us nothing, and reporting a denial we + // cannot prove would have apps nag the user about a setting that may well be granted. + if (notificationManager == null) { + return true; + } + + return notificationManager.canUseFullScreenIntent(); + } +} diff --git a/packages/react-native/android/src/test/java/app/notifee/core/utility/FullScreenIntentUtilsTest.java b/packages/react-native/android/src/test/java/app/notifee/core/utility/FullScreenIntentUtilsTest.java new file mode 100644 index 00000000..90d32932 --- /dev/null +++ b/packages/react-native/android/src/test/java/app/notifee/core/utility/FullScreenIntentUtilsTest.java @@ -0,0 +1,86 @@ +package app.notifee.core.utility; + +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.NotificationManager; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit coverage for {@link FullScreenIntentUtils#canUseFullScreenIntent(NotificationManager)}, + * which backs the {@code android.fullScreenIntent} field of {@code getNotificationSettings()}. + * + *

Android 14 / API 34 turned {@code USE_FULL_SCREEN_INTENT} into a user-revocable special app + * access. The interesting behavior is therefore the API level boundary, so each case pins the SDK + * with {@link Config} and injects the {@link NotificationManager} rather than relying on a shadow — + * Robolectric does not implement {@code canUseFullScreenIntent()}. + */ +@RunWith(RobolectricTestRunner.class) +public class FullScreenIntentUtilsTest { + + @Test + @Config(sdk = 33) + public void returnsTrueBelowApi34WithoutConsultingNotificationManager() { + // Deliberately unstubbed: an unstubbed boolean mock answers false, so a true result proves + // the version guard short-circuited. The method is not referenced by name here because it + // does not exist on the API 33 android-all jar Robolectric loads for this case. + NotificationManager notificationManager = mock(NotificationManager.class); + + assertTrue(FullScreenIntentUtils.canUseFullScreenIntent(notificationManager)); + } + + @Test + @Config(sdk = 34) + public void returnsTrueOnApi34WhenGranted() { + NotificationManager notificationManager = mock(NotificationManager.class); + when(notificationManager.canUseFullScreenIntent()).thenReturn(true); + + assertTrue(FullScreenIntentUtils.canUseFullScreenIntent(notificationManager)); + } + + @Test + @Config(sdk = 34) + public void returnsFalseOnApi34WhenDenied() { + NotificationManager notificationManager = mock(NotificationManager.class); + when(notificationManager.canUseFullScreenIntent()).thenReturn(false); + + assertFalse(FullScreenIntentUtils.canUseFullScreenIntent(notificationManager)); + } + + @Test + @Config(sdk = 35) + public void keepsConsultingNotificationManagerAboveApi34() { + // Guards against the version check regressing into an equality test against 34. + NotificationManager notificationManager = mock(NotificationManager.class); + when(notificationManager.canUseFullScreenIntent()).thenReturn(false); + + assertFalse(FullScreenIntentUtils.canUseFullScreenIntent(notificationManager)); + } + + @Test + @Config(sdk = 34) + public void failsOpenWhenNotificationManagerIsUnavailable() { + assertTrue(FullScreenIntentUtils.canUseFullScreenIntent(null)); + } +} diff --git a/packages/react-native/jest-mock.js b/packages/react-native/jest-mock.js index ecf12482..91246e51 100644 --- a/packages/react-native/jest-mock.js +++ b/packages/react-native/jest-mock.js @@ -83,6 +83,7 @@ export const testNotificationSettings = { authorizationStatus: Notification.AuthorizationStatus.AUTHORIZED, android: { alarm: NotificationAndroid.AndroidNotificationSetting.ENABLED, + fullScreenIntent: NotificationAndroid.AndroidNotificationSetting.ENABLED, }, ios: { alert: true, diff --git a/packages/react-native/src/NotifeeApiModule.ts b/packages/react-native/src/NotifeeApiModule.ts index 99b78799..c0932e42 100644 --- a/packages/react-native/src/NotifeeApiModule.ts +++ b/packages/react-native/src/NotifeeApiModule.ts @@ -602,6 +602,7 @@ export default class NotifeeApiModule extends NotifeeNativeModule implements Mod ios, android: { alarm: AndroidNotificationSetting.ENABLED, + fullScreenIntent: AndroidNotificationSetting.ENABLED, }, web: {}, }; @@ -614,6 +615,7 @@ export default class NotifeeApiModule extends NotifeeNativeModule implements Mod authorizationStatus: AuthorizationStatus.NOT_DETERMINED, android: { alarm: AndroidNotificationSetting.ENABLED, + fullScreenIntent: AndroidNotificationSetting.ENABLED, }, ios: { alert: 1, @@ -725,6 +727,7 @@ export default class NotifeeApiModule extends NotifeeNativeModule implements Mod ios, android: { alarm: AndroidNotificationSetting.ENABLED, + fullScreenIntent: AndroidNotificationSetting.ENABLED, }, web: {}, }; @@ -737,6 +740,7 @@ export default class NotifeeApiModule extends NotifeeNativeModule implements Mod authorizationStatus: AuthorizationStatus.NOT_DETERMINED, android: { alarm: AndroidNotificationSetting.ENABLED, + fullScreenIntent: AndroidNotificationSetting.ENABLED, }, ios: { alert: 1, diff --git a/packages/react-native/src/types/NotificationAndroid.ts b/packages/react-native/src/types/NotificationAndroid.ts index ced6c8f8..f35bb377 100644 --- a/packages/react-native/src/types/NotificationAndroid.ts +++ b/packages/react-native/src/types/NotificationAndroid.ts @@ -501,6 +501,21 @@ export interface AndroidNotificationSettings { * View the [Trigger](/react-native/android/triggers#android-12-limitations) documentation for more information. */ alarm: AndroidNotificationSetting; + + /** + * Enum describing if a notification with a `fullScreenAction` will actually be shown full screen. + * + * For Android < 14 / API < 34, this will default to enabled. + * + * On Android 14 / API 34 and above, `USE_FULL_SCREEN_INTENT` is a user-revocable special app + * access rather than a normal install-time permission. It is revoked on install for apps outside + * the calling and alarm categories, and the user can toggle it at any time. While it is disabled + * the notification still posts and no error is raised — only the full screen presentation is + * dropped, so this setting is the only way to detect it. + * + * Send the user to `Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT` to have it granted. + */ + fullScreenIntent: AndroidNotificationSetting; } /**