Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions packages/react-native/__tests__/NotifeeApiModule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,13 +298,15 @@ describe('Notifee Api Module', () => {
authorizationStatus: AuthorizationStatus.AUTHORIZED,
android: {
alarm: AndroidNotificationSetting.DISABLED,
fullScreenIntent: AndroidNotificationSetting.ENABLED,
},
});
const settings = await apiModule.getNotificationSettings();
expect(settings).toEqual({
authorizationStatus: AuthorizationStatus.AUTHORIZED,
android: {
alarm: 0,
fullScreenIntent: 1,
},
ios: {
alert: 1,
Expand All @@ -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', () => {
Expand Down Expand Up @@ -352,6 +371,7 @@ describe('Notifee Api Module', () => {
authorizationStatus: AuthorizationStatus.NOT_DETERMINED,
android: {
alarm: AndroidNotificationSetting.ENABLED,
fullScreenIntent: AndroidNotificationSetting.ENABLED,
},
ios: {
alert: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -508,6 +509,14 @@ public void getNotificationSettings(MethodCallResult<Bundle> 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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();
}
}
Original file line number Diff line number Diff line change
@@ -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()}.
*
* <p>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));
}
}
1 change: 1 addition & 0 deletions packages/react-native/jest-mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const testNotificationSettings = {
authorizationStatus: Notification.AuthorizationStatus.AUTHORIZED,
android: {
alarm: NotificationAndroid.AndroidNotificationSetting.ENABLED,
fullScreenIntent: NotificationAndroid.AndroidNotificationSetting.ENABLED,
},
ios: {
alert: true,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native/src/NotifeeApiModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ export default class NotifeeApiModule extends NotifeeNativeModule implements Mod
ios,
android: {
alarm: AndroidNotificationSetting.ENABLED,
fullScreenIntent: AndroidNotificationSetting.ENABLED,
},
web: {},
};
Expand All @@ -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,
Expand Down Expand Up @@ -725,6 +727,7 @@ export default class NotifeeApiModule extends NotifeeNativeModule implements Mod
ios,
android: {
alarm: AndroidNotificationSetting.ENABLED,
fullScreenIntent: AndroidNotificationSetting.ENABLED,
},
web: {},
};
Expand All @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions packages/react-native/src/types/NotificationAndroid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down