react-native-notify-kit 10.2.0 → 10.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -421,11 +421,11 @@ As bugs are fixed, this table is updated. See [CHANGELOG.md](CHANGELOG.md) for f
421
421
 
422
422
  Some upstream Notifee issues are not bugs in the library itself but platform-level limitations imposed by Android's Doze mode and vendor power management — no library code can make `AlarmManager` deliver an alarm to, or a foreground service survive inside, an app the OEM has explicitly paused. For these, the fork provides **documented mitigations**: user-facing helper APIs, code-level self-healing where possible, and decision guides that steer consumers toward the Android primitive most resilient to the specific vendor policy.
423
423
 
424
- | Upstream issue | Symptom | Platform root cause | Fork mitigation |
425
- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
426
- | [invertase/notifee#410](https://github.com/invertase/notifee/issues/410) | Foreground service paused on screen lock (Samsung OneUI, ~6 seconds after screen off on battery) and killed immediately when the app is backgrounded (Xiaomi MIUI) | Vendor aggressive battery-saver and autostart policies suspend or terminate foreground services of apps not whitelisted in the OEM's protected-apps / autostart settings. Partially Doze-related on non-exempt `foregroundServiceType` values; mostly OEM-specific behavior catalogued at [dontkillmyapp.com](https://dontkillmyapp.com/). | **(1) Decision guide** — the [Timers: foreground service or `SET_ALARM_CLOCK`?](#timers-foreground-service-or-set_alarm_clock) section recommends the `SET_ALARM_CLOCK` trigger over a silent foreground service for rest, cooking, and recovery timer use cases. `setAlarmClock` is the same primitive the stock Clock app uses and is generally respected by vendor aggressive-kill policies. **(2) Foreground service use case matrix** — the [Foreground service use case guide](#foreground-service-use-case-guide) documents which `foregroundServiceType` values are Doze-CPU-exempt, which have type-specific timeouts, and the Google Play policy constraints that rule out misusing `mediaPlayback` for silent timers. **(3) `openPowerManagerSettings()` helper API** — deep-links the user to the correct vendor autostart / protected-apps screen on 16 manufacturers; whitelisting the app prevents both `BOOT_COMPLETED` suppression and background FGS kills. |
427
- | [invertase/notifee#734](https://github.com/invertase/notifee/issues/734) | Scheduled trigger notifications silently lost across a device reboot on OEM devices (Xiaomi MIUI, OnePlus, Huawei EMUI, Oppo ColorOS, Vivo FuntouchOS) | The vendor OS suppresses the `BOOT_COMPLETED` broadcast to apps the user has not manually whitelisted, so the library's `RebootBroadcastReceiver` never runs and persisted `AlarmManager` triggers are never re-armed after reboot. | **(1) `BOOT_COUNT` cold-start self-heal (code)** — on every app init, `InitProvider` compares `Settings.Global.BOOT_COUNT` against the last-known value in `SharedPreferences` and re-arms every persisted trigger on a background thread if a boot delta is detected, even when `BOOT_COMPLETED` was never delivered. Paired with a process-wide `AtomicBoolean` race guard in `NotifeeAlarmManager.rescheduleNotifications` that prevents double-advancement when the reboot receiver and the cold-start path race. **(2) `openPowerManagerSettings()` helper API** — the same vendor-settings deep-link used by #410, pointing the user at the autostart whitelist for defense in depth. |
428
- | [invertase/notifee#927](https://github.com/invertase/notifee/issues/927) | Custom sound passed via `displayNotification({ android: { sound, channelId }, ios: { sound } })` is ignored for **remote push notifications** (FCM/APNs) delivered while the app is in background or killed — the system default sound plays instead. Foreground delivery and **locally-scheduled notifications** (`displayNotification`, `createTriggerNotification`) are unaffected. | When a remote push arrives while the app is killed, the JavaScript layer never runs — the system tray item is drawn by the OS (Android system + Firebase SDK; iOS + APNs) before any Notifee code executes. On Android API 26+, the `NotificationChannel` sound is set once at channel creation and is immutable thereafter — `NotificationCompat.Builder.setSound()` is silently ignored when the builder has a `channelId`. On iOS, the Notification Service Extension only rewrites incoming push content when the payload contains a `notifee_options` key (see `NotifeeCoreExtensionHelper.m:43`); a plain APNs payload is delivered unmodified. | **Documentation only — the platform contract cannot be worked around at the library layer.** Recipes by platform: **(Android)** create the `NotificationChannel` with the desired sound at first-run (the channel sound is immutable; to change it the channel must be deleted and recreated under a new `channelId`), and configure `AndroidNotification.sound` in the FCM payload server-side so the system tray honors it for background pushes. As a heavier alternative, switch the backend to an FCM data-only payload and call `displayNotification()` from a headless task — the JS-side `android.sound` is then honored, but this trades simplicity for the cost of running JS on every push. **(iOS)** either set `aps.sound` directly in the APNs payload, or install the Notification Service Extension (see `docs/react-native/ios/remote-notification-support.mdx`) and ship the sound under `notifee_options.ios.sound` in the push payload. |
424
+ | Upstream issue | Symptom | Platform root cause | Fork mitigation |
425
+ | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
426
+ | [invertase/notifee#410](https://github.com/invertase/notifee/issues/410) | Foreground service paused on screen lock (Samsung OneUI, ~6 seconds after screen off on battery) and killed immediately when the app is backgrounded (Xiaomi MIUI) | Vendor aggressive battery-saver and autostart policies suspend or terminate foreground services of apps not whitelisted in the OEM's protected-apps / autostart settings. Partially Doze-related on non-exempt `foregroundServiceType` values; mostly OEM-specific behavior catalogued at [dontkillmyapp.com](https://dontkillmyapp.com/). | **(1) Decision guide** — the [Timers: foreground service or `SET_ALARM_CLOCK`?](#timers-foreground-service-or-set_alarm_clock) section recommends the `SET_ALARM_CLOCK` trigger over a silent foreground service for rest, cooking, and recovery timer use cases. `setAlarmClock` is the same primitive the stock Clock app uses and is generally respected by vendor aggressive-kill policies. **(2) Foreground service use case matrix** — the [Foreground service use case guide](#foreground-service-use-case-guide) documents which `foregroundServiceType` values are Doze-CPU-exempt, which have type-specific timeouts, and the Google Play policy constraints that rule out misusing `mediaPlayback` for silent timers. **(3) `openPowerManagerSettings()` helper API** — points users toward known vendor autostart / protected-apps screen candidates when available; these links are best-effort because firmware variants may move, block, or remove those settings activities. |
427
+ | [invertase/notifee#734](https://github.com/invertase/notifee/issues/734) | Scheduled trigger notifications silently lost across a device reboot on OEM devices (Xiaomi MIUI, OnePlus, Huawei EMUI, Oppo ColorOS, Vivo FuntouchOS) | The vendor OS suppresses the `BOOT_COMPLETED` broadcast to apps the user has not manually whitelisted, so the library's `RebootBroadcastReceiver` never runs and persisted `AlarmManager` triggers are never re-armed after reboot. | **(1) `BOOT_COUNT` cold-start self-heal (code)** — on every app init, `InitProvider` compares `Settings.Global.BOOT_COUNT` against the last-known value in `SharedPreferences` and re-arms every persisted trigger on a background thread if a boot delta is detected, even when `BOOT_COMPLETED` was never delivered. Paired with a process-wide `AtomicBoolean` race guard in `NotifeeAlarmManager.rescheduleNotifications` that prevents double-advancement when the reboot receiver and the cold-start path race. **(2) `openPowerManagerSettings()` helper API** — the same vendor-settings deep-link used by #410, pointing the user at the autostart whitelist for defense in depth. |
428
+ | [invertase/notifee#927](https://github.com/invertase/notifee/issues/927) | Custom sound passed via `displayNotification({ android: { sound, channelId }, ios: { sound } })` is ignored for **remote push notifications** (FCM/APNs) delivered while the app is in background or killed — the system default sound plays instead. Foreground delivery and **locally-scheduled notifications** (`displayNotification`, `createTriggerNotification`) are unaffected. | When a remote push arrives while the app is killed, the JavaScript layer never runs — the system tray item is drawn by the OS (Android system + Firebase SDK; iOS + APNs) before any Notifee code executes. On Android API 26+, the `NotificationChannel` sound is set once at channel creation and is immutable thereafter — `NotificationCompat.Builder.setSound()` is silently ignored when the builder has a `channelId`. On iOS, the Notification Service Extension only rewrites incoming push content when the payload contains a `notifee_options` key (see `NotifeeCoreExtensionHelper.m:43`); a plain APNs payload is delivered unmodified. | **Documentation only — the platform contract cannot be worked around at the library layer.** Recipes by platform: **(Android)** create the `NotificationChannel` with the desired sound at first-run (the channel sound is immutable; to change it the channel must be deleted and recreated under a new `channelId`), and configure `AndroidNotification.sound` in the FCM payload server-side so the system tray honors it for background pushes. As a heavier alternative, switch the backend to an FCM data-only payload and call `displayNotification()` from a headless task — the JS-side `android.sound` is then honored, but this trades simplicity for the cost of running JS on every push. **(iOS)** either set `aps.sound` directly in the APNs payload, or install the Notification Service Extension (see `docs/react-native/ios/remote-notification-support.mdx`) and ship the sound under `notifee_options.ios.sound` in the push payload. |
429
429
 
430
430
  Both mitigations are intentionally additive to the existing reboot-recovery and foreground-service code paths and do not replace the consumer's responsibility to prompt the user for battery-optimization exemption when the use case warrants it. For a complete vendor-by-vendor reference of autostart, battery-saver, and background-restriction behavior, see [dontkillmyapp.com](https://dontkillmyapp.com/).
431
431
 
@@ -526,7 +526,7 @@ The fork mitigates this with two layers that work together:
526
526
 
527
527
  **1. Automatic cold-start recovery.** On every app init, the library compares `Settings.Global.BOOT_COUNT` against the value recorded on the previous run. If a reboot has occurred since the last run — whether or not `BOOT_COMPLETED` was delivered to your app — the library re-arms every persisted trigger on a background thread. This means that on an OEM device where `BOOT_COMPLETED` was suppressed, simply opening your app (or having it cold-started by any other entry point: push notification, geofence, share intent) recovers all missed and upcoming alarms. Previously, opening the app alone did not recover them. This recovery runs unconditionally — it is not gated by the `notifee_init_warmup_enabled` metadata flag, because it is a correctness fix rather than a startup optimization.
528
528
 
529
- **2. Vendor settings helper APIs.** The existing `getPowerManagerInfo()` and `openPowerManagerSettings()` APIs let your app guide the user directly to the correct vendor settings screen (Xiaomi Autostart, Huawei Protected Apps, Oppo Startup Manager, and 13 more vendors) to whitelist the app. Once whitelisted, `BOOT_COMPLETED` is delivered normally on every reboot and exact alarm timing is preserved without waiting for the next app cold-start. The same whitelist also prevents the OS from killing your foreground service when the app is backgrounded — so this helper is the primary mitigation path for **both** trigger-notification reliability and foreground-service reliability on OEM devices.
529
+ **2. Vendor settings helper APIs.** The existing `getPowerManagerInfo()` and `openPowerManagerSettings()` APIs let your app guide the user toward known vendor settings candidates (Xiaomi Autostart, Huawei Protected Apps, Oppo Startup Manager, and 13 more vendors) to whitelist the app. These candidates are opened best-effort: Android may reject or fail to resolve a vendor-specific settings activity on some firmware variants, and the helper fails safely instead of crashing. Consumer apps do not need to inherit package-visibility `<queries>` declarations for these helpers, and the helper does not use the direct `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` request path. Whitelisting the app in vendor settings can restore normal reboot delivery and reduce background foreground-service kills on affected devices, but this remains subject to OEM firmware and user/device policy — so this helper is a mitigation path for **both** trigger-notification reliability and foreground-service reliability on OEM devices.
530
530
 
531
531
  A typical integration that combines both layers looks like this:
532
532
 
@@ -8,7 +8,6 @@
8
8
  <uses-permission android:name="android.permission.INTERNET" />
9
9
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
10
10
  <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
11
- <uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" android:maxSdkVersion="30" />
12
11
  <!-- For Xiaomi devices to enable heads-up notifications as default (https://github.com/invertase/notifee/issues/296) -->
13
12
  <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" android:minSdkVersion="23" />
14
13
 
@@ -23,10 +23,12 @@ import static app.notifee.core.event.NotificationEvent.TYPE_ACTION_PRESS;
23
23
  import static app.notifee.core.event.NotificationEvent.TYPE_PRESS;
24
24
  import static java.lang.Integer.parseInt;
25
25
 
26
+ import android.Manifest;
26
27
  import android.app.Notification;
27
28
  import android.app.PendingIntent;
28
29
  import android.content.Context;
29
30
  import android.content.Intent;
31
+ import android.content.pm.PackageManager;
30
32
  import android.graphics.Bitmap;
31
33
  import android.net.Uri;
32
34
  import android.os.Build;
@@ -248,9 +250,7 @@ class NotificationManager {
248
250
 
249
251
  builder.setColorized(androidModel.getColorized());
250
252
 
251
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
252
- builder.setChronometerCountDown(androidModel.getChronometerCountDown());
253
- }
253
+ builder.setChronometerCountDown(androidModel.getChronometerCountDown());
254
254
 
255
255
  if (androidModel.getGroup() != null) {
256
256
  builder.setGroup(androidModel.getGroup());
@@ -716,7 +716,18 @@ class NotificationManager {
716
716
  Trace.endSection();
717
717
  }
718
718
  } else {
719
- NotificationManagerCompat.from(getApplicationContext())
719
+ Context context = getApplicationContext();
720
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
721
+ && context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
722
+ != PackageManager.PERMISSION_GRANTED) {
723
+ String message =
724
+ "POST_NOTIFICATIONS permission is not granted. Notification was not"
725
+ + " displayed.";
726
+ Logger.w(TAG, message);
727
+ return Futures.immediateFailedFuture(new SecurityException(message));
728
+ }
729
+
730
+ NotificationManagerCompat.from(context)
720
731
  .notify(androidBundle.getTag(), hashCode, notification);
721
732
  }
722
733
 
@@ -873,10 +884,6 @@ class NotificationManager {
873
884
  () -> {
874
885
  List<Bundle> notifications = new ArrayList<Bundle>();
875
886
 
876
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
877
- return notifications;
878
- }
879
-
880
887
  android.app.NotificationManager notificationManager =
881
888
  (android.app.NotificationManager)
882
889
  getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
@@ -21,6 +21,7 @@ import static app.notifee.core.event.NotificationEvent.TYPE_ACTION_PRESS;
21
21
  import static app.notifee.core.event.NotificationEvent.TYPE_DISMISSED;
22
22
  import static app.notifee.core.event.NotificationEvent.TYPE_PRESS;
23
23
 
24
+ import android.annotation.SuppressLint;
24
25
  import android.app.PendingIntent;
25
26
  import android.app.Service;
26
27
  import android.content.Context;
@@ -171,10 +172,6 @@ public class ReceiverService extends Service {
171
172
  }
172
173
  }
173
174
 
174
- // ACTION_CLOSE_SYSTEM_DIALOGS is deprecated since API 31, but is still needed on API < 31
175
- // to close the notification drawer after an action press. Already guarded with Build.VERSION
176
- // check.
177
- @SuppressWarnings("deprecation")
178
175
  private void onActionPressIntent(Intent intent) {
179
176
  Bundle notification = intent.getBundleExtra("notification");
180
177
  Bundle pressAction = intent.getBundleExtra("pressAction");
@@ -221,16 +218,24 @@ public class ReceiverService extends Service {
221
218
  mainComponent,
222
219
  pressActionBundle.getLaunchActivityFlags());
223
220
 
224
- int targetSdkVersion =
225
- ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion;
221
+ closeSystemDialogsBestEffort();
222
+ }
223
+ }
226
224
 
227
- // Close notification drawer if application SDK is Android 11 and lower
228
- // See
229
- // https://developer.android.com/about/versions/12/behavior-changes-all#close-system-dialogs
230
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
231
- ContextHolder.getApplicationContext()
232
- .sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
233
- }
225
+ @SuppressWarnings("deprecation")
226
+ @SuppressLint("MissingPermission")
227
+ private void closeSystemDialogsBestEffort() {
228
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
229
+ return;
230
+ }
231
+
232
+ // BROADCAST_CLOSE_SYSTEM_DIALOGS is protected/system-only, so the library intentionally does
233
+ // not declare it. Keep this pre-Android 12 legacy broadcast best-effort; Android 12+ skips it.
234
+ try {
235
+ ContextHolder.getApplicationContext()
236
+ .sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
237
+ } catch (SecurityException e) {
238
+ Logger.w(TAG, "Unable to close system dialogs with legacy broadcast", e);
234
239
  }
235
240
  }
236
241
 
@@ -77,8 +77,7 @@ public class NotificationAndroidActionModel {
77
77
  * @return RemoteInput
78
78
  */
79
79
  public @Nullable RemoteInput getRemoteInput(NotificationCompat.Action.Builder actionBuilder) {
80
- if (mNotificationAndroidActionBundle.containsKey("input")
81
- && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT_WATCH) {
80
+ if (mNotificationAndroidActionBundle.containsKey("input")) {
82
81
  Bundle inputBundle =
83
82
  Objects.requireNonNull(mNotificationAndroidActionBundle.getBundle("input"));
84
83
 
@@ -105,9 +105,7 @@ public class NotificationAndroidPressActionModel {
105
105
  baseFlags |= Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET;
106
106
  break;
107
107
  case 12:
108
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
109
- baseFlags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
110
- }
108
+ baseFlags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
111
109
  break;
112
110
  case 13:
113
111
  baseFlags |= Intent.FLAG_ACTIVITY_NO_USER_ACTION;
@@ -125,14 +123,10 @@ public class NotificationAndroidPressActionModel {
125
123
  baseFlags |= Intent.FLAG_ACTIVITY_TASK_ON_HOME;
126
124
  break;
127
125
  case 18:
128
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
129
- baseFlags |= Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
130
- }
126
+ baseFlags |= Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
131
127
  break;
132
128
  case 19:
133
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
134
- baseFlags |= Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
135
- }
129
+ baseFlags |= Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
136
130
  break;
137
131
  case 20:
138
132
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -22,40 +22,12 @@ import static app.notifee.core.ContextHolder.getApplicationContext;
22
22
  import android.app.Activity;
23
23
  import android.content.Context;
24
24
  import android.content.Intent;
25
- import android.content.pm.PackageManager;
26
- import android.content.pm.ResolveInfo;
27
- import android.os.Build;
28
25
  import androidx.annotation.Nullable;
29
26
  import app.notifee.core.Logger;
30
- import java.util.List;
31
27
 
32
28
  public class IntentUtils {
33
29
  private static final String TAG = "IntentUtils";
34
30
 
35
- @SuppressWarnings("deprecation")
36
- public static boolean isAvailableOnDevice(Context ctx, Intent intent) {
37
- try {
38
- if (ctx == null || intent == null) {
39
- return false;
40
- }
41
-
42
- final PackageManager mgr = ctx.getPackageManager();
43
- List<ResolveInfo> list;
44
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
45
- list =
46
- mgr.queryIntentActivities(
47
- intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
48
- } else {
49
- list = mgr.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
50
- }
51
- return !list.isEmpty();
52
- } catch (Exception e) {
53
- Logger.e(TAG, "An error occurred whilst trying to check if intent is available on device", e);
54
-
55
- return false;
56
- }
57
- }
58
-
59
31
  public static String getActivityName(Intent intent) {
60
32
  if (intent == null) {
61
33
  return null;
@@ -18,6 +18,7 @@ package app.notifee.core.utility;
18
18
  */
19
19
 
20
20
  import android.app.Activity;
21
+ import android.content.ActivityNotFoundException;
21
22
  import android.content.ComponentName;
22
23
  import android.content.Context;
23
24
  import android.content.Intent;
@@ -55,24 +56,12 @@ public class PowerManagerUtils {
55
56
  * @param activity
56
57
  */
57
58
  public static void openBatteryOptimizationSettings(Activity activity) {
58
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
59
- return;
60
- }
61
-
62
59
  try {
63
60
  Intent intent = new Intent();
64
61
  intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
65
62
  intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
66
63
 
67
64
  if (activity != null) {
68
- Boolean isAvailableOnDevice =
69
- IntentUtils.isAvailableOnDevice(ContextHolder.getApplicationContext(), intent);
70
-
71
- if (!isAvailableOnDevice) {
72
- Logger.d(TAG, "battery optimization settings is not available on device");
73
- return;
74
- }
75
-
76
65
  IntentUtils.startActivityOnUiThread(activity, intent);
77
66
  }
78
67
  } catch (Exception e) {
@@ -86,9 +75,6 @@ public class PowerManagerUtils {
86
75
  * @param context
87
76
  */
88
77
  public static Boolean isBatteryOptimizationEnabled(Context context) {
89
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
90
- return false;
91
- }
92
78
  PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
93
79
  return !pm.isIgnoringBatteryOptimizations(context.getPackageName());
94
80
  }
@@ -129,7 +115,7 @@ public class PowerManagerUtils {
129
115
  public static PowerManagerInfo getPowerManagerInfo() {
130
116
  String activityName;
131
117
 
132
- Intent intent = findPowerManagerIntent(ContextHolder.getApplicationContext());
118
+ Intent intent = getFirstPowerManagerIntent();
133
119
  activityName = IntentUtils.getActivityName(intent);
134
120
 
135
121
  PowerManagerInfo result =
@@ -143,37 +129,90 @@ public class PowerManagerUtils {
143
129
  * @param activity
144
130
  */
145
131
  public static void openPowerManagerSettings(Activity activity) {
146
- Intent intent = getPowerManagerIntent();
147
- if (intent == null) {
148
- intent = findPowerManagerIntent(ContextHolder.getApplicationContext());
132
+ if (activity == null) {
133
+ Logger.w(TAG, "Activity is null when trying to open the device's power manager");
134
+ return;
149
135
  }
150
136
 
151
- if (intent != null) {
152
- try {
153
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
154
- IntentUtils.startActivityOnUiThread(activity, intent);
155
- } catch (Exception exception) {
156
- Logger.e(
157
- TAG, "Unable to start activity: " + IntentUtils.getActivityName(intent), exception);
158
- }
159
- } else {
137
+ Context context = ContextHolder.getApplicationContext();
138
+ if (context == null) {
139
+ Logger.w(TAG, "Unable to get application context when opening the device's power manager");
140
+ return;
141
+ }
142
+
143
+ List<Intent> possibleIntents = getPowerManagerIntentCandidates();
144
+ if (possibleIntents.isEmpty()) {
160
145
  Logger.w(TAG, "Unable to find an activity to open the device's power manager");
146
+ return;
147
+ }
148
+
149
+ activity.runOnUiThread(
150
+ () -> {
151
+ for (Intent possibleIntent : possibleIntents) {
152
+ if (startPowerManagerIntent(context, possibleIntent)) {
153
+ return;
154
+ }
155
+ }
156
+
157
+ Logger.w(TAG, "Unable to open the device's power manager");
158
+ });
159
+ }
160
+
161
+ private static Intent getFirstPowerManagerIntent() {
162
+ List<Intent> possibleIntents = getManufacturerPowerManagerIntents(getManufacturerName());
163
+ if (possibleIntents.isEmpty()) {
164
+ return null;
165
+ }
166
+
167
+ return possibleIntents.get(0);
168
+ }
169
+
170
+ private static List<Intent> getPowerManagerIntentCandidates() {
171
+ List<Intent> possibleIntents = new ArrayList<>();
172
+ Intent cachedIntent = getPowerManagerIntent();
173
+
174
+ if (cachedIntent != null) {
175
+ possibleIntents.add(cachedIntent);
161
176
  }
177
+
178
+ for (Intent manufacturerIntent : getManufacturerPowerManagerIntents(getManufacturerName())) {
179
+ if (!containsEquivalentIntent(possibleIntents, manufacturerIntent)) {
180
+ possibleIntents.add(manufacturerIntent);
181
+ }
182
+ }
183
+
184
+ return possibleIntents;
162
185
  }
163
186
 
164
- private static Intent findPowerManagerIntent(Context context) {
165
- String manufacturerName = Build.BRAND.toLowerCase(Locale.US);
166
- List<Intent> possibleIntents = getManufacturerPowerManagerIntents(manufacturerName);
187
+ private static String getManufacturerName() {
188
+ return Build.BRAND.toLowerCase(Locale.US);
189
+ }
167
190
 
168
- for (int i = 0; i < possibleIntents.size(); i++) {
169
- Intent possibleIntent = possibleIntents.get(i);
170
- Boolean isAvailableOnDevice = IntentUtils.isAvailableOnDevice(context, possibleIntent);
171
- if (isAvailableOnDevice) {
172
- setPowerManagerIntentCache(possibleIntent);
173
- return possibleIntent;
191
+ private static boolean containsEquivalentIntent(List<Intent> possibleIntents, Intent candidate) {
192
+ for (Intent possibleIntent : possibleIntents) {
193
+ if (possibleIntent.filterEquals(candidate)) {
194
+ return true;
174
195
  }
175
196
  }
176
- return null;
197
+
198
+ return false;
199
+ }
200
+
201
+ private static boolean startPowerManagerIntent(Context context, Intent possibleIntent) {
202
+ Intent intent = new Intent(possibleIntent);
203
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
204
+
205
+ try {
206
+ context.startActivity(intent);
207
+ setPowerManagerIntentCache(intent);
208
+ return true;
209
+ } catch (ActivityNotFoundException | SecurityException e) {
210
+ Logger.w(TAG, "Unable to start activity: " + IntentUtils.getActivityName(intent), e);
211
+ } catch (RuntimeException e) {
212
+ Logger.w(TAG, "Unable to start activity: " + IntentUtils.getActivityName(intent), e);
213
+ }
214
+
215
+ return false;
177
216
  }
178
217
 
179
218
  private static List<Intent> getManufacturerPowerManagerIntents(String manufacturerName) {
@@ -249,9 +288,8 @@ public class PowerManagerUtils {
249
288
  createIntent(
250
289
  "com.oppo.safe", "com.oppo.safe.permission.startup.StartupAppListActivity"),
251
290
  createIntent(
252
- "com.coloros.safecenter",
253
- "com.coloros.safecenter.startupapp.StartupAppListActivity")
254
- .setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS),
291
+ "com.coloros.safecenter",
292
+ "com.coloros.safecenter.startupapp.StartupAppListActivity"),
255
293
  createIntent(
256
294
  "com.coloros.oppoguardelf",
257
295
  "com.coloros.powermanager.fuelgaue.PowerUsageModelActivity"),
@@ -17,6 +17,7 @@ package app.notifee.core.utility;
17
17
  *
18
18
  */
19
19
 
20
+ import android.annotation.SuppressLint;
20
21
  import android.content.Context;
21
22
  import android.content.pm.ApplicationInfo;
22
23
  import android.graphics.Bitmap;
@@ -253,6 +254,7 @@ public class ResourceUtils {
253
254
  }
254
255
 
255
256
  /** Attempts to find a resource id by name and type */
257
+ @SuppressLint("DiscouragedApi")
256
258
  private static int getResourceIdByName(String name, String type) {
257
259
  if (name == null || name.isEmpty()) {
258
260
  return 0;
@@ -271,6 +273,7 @@ public class ResourceUtils {
271
273
  Context context = ContextHolder.getApplicationContext();
272
274
  String packageName = context.getPackageName();
273
275
 
276
+ // Consumer resource names are provided dynamically from JS, so runtime lookup is intentional.
274
277
  int id = context.getResources().getIdentifier(name, type, packageName);
275
278
  getResourceIdCache().put(key, id);
276
279
  return id;
@@ -5,7 +5,6 @@
5
5
  package io.invertase.notifee
6
6
 
7
7
  import android.annotation.SuppressLint
8
- import android.os.Build
9
8
  import android.os.Bundle
10
9
  import android.util.Log
11
10
  import androidx.lifecycle.Lifecycle
@@ -138,7 +137,7 @@ object NotifeeReactUtils {
138
137
  try {
139
138
  val service = context.getSystemService("statusbar")
140
139
  val statusbarManager = Class.forName("android.app.StatusBarManager")
141
- val methodName = if (Build.VERSION.SDK_INT >= 17) "collapsePanels" else "collapse"
140
+ val methodName = "collapsePanels"
142
141
  val collapse: Method = statusbarManager.getMethod(methodName)
143
142
  collapse.isAccessible = true
144
143
  collapse.invoke(service)
@@ -142,8 +142,7 @@ public class TimestampTriggerModelTest {
142
142
  @Test
143
143
  public void setNextTimestamp_dailyEveryTwoDays_advancesByRepeatInterval() {
144
144
  long original = mNow - ONE_MINUTE_MS;
145
- TimestampTriggerModel trigger =
146
- buildRepeatingTrigger(original, REPEAT_FREQUENCY_DAILY, 2);
145
+ TimestampTriggerModel trigger = buildRepeatingTrigger(original, REPEAT_FREQUENCY_DAILY, 2);
147
146
 
148
147
  trigger.setNextTimestamp();
149
148
  long next = trigger.getTimestamp();
@@ -174,8 +173,7 @@ public class TimestampTriggerModelTest {
174
173
  @Test
175
174
  public void setNextTimestamp_weeklyEveryTwoWeeks_advancesByRepeatInterval() {
176
175
  long original = mNow - ONE_MINUTE_MS;
177
- TimestampTriggerModel trigger =
178
- buildRepeatingTrigger(original, REPEAT_FREQUENCY_WEEKLY, 2);
176
+ TimestampTriggerModel trigger = buildRepeatingTrigger(original, REPEAT_FREQUENCY_WEEKLY, 2);
179
177
 
180
178
  trigger.setNextTimestamp();
181
179
  long next = trigger.getTimestamp();
@@ -205,8 +203,7 @@ public class TimestampTriggerModelTest {
205
203
  @Test
206
204
  public void setNextTimestamp_monthlyEveryThreeMonths_advancesByRepeatInterval() {
207
205
  long original = mNow - ONE_MINUTE_MS;
208
- TimestampTriggerModel trigger =
209
- buildRepeatingTrigger(original, REPEAT_FREQUENCY_MONTHLY, 3);
206
+ TimestampTriggerModel trigger = buildRepeatingTrigger(original, REPEAT_FREQUENCY_MONTHLY, 3);
210
207
 
211
208
  trigger.setNextTimestamp();
212
209
  long next = trigger.getTimestamp();
@@ -228,8 +225,7 @@ public class TimestampTriggerModelTest {
228
225
  start.set(2020, Calendar.JANUARY, 31, 12, 45, 0);
229
226
  long original = start.getTimeInMillis();
230
227
 
231
- TimestampTriggerModel trigger =
232
- buildRepeatingTrigger(original, REPEAT_FREQUENCY_MONTHLY, 1);
228
+ TimestampTriggerModel trigger = buildRepeatingTrigger(original, REPEAT_FREQUENCY_MONTHLY, 1);
233
229
 
234
230
  trigger.setNextTimestamp();
235
231
  long next = trigger.getTimestamp();
@@ -2,7 +2,9 @@
2
2
  * Patches the Podfile to add the NSE target with RNNotifeeCore pod.
3
3
  * The NSE target is nested inside the main app target so CocoaPods can
4
4
  * detect the host→extension relationship. Uses `inherit! :search_paths`.
5
- * Idempotent: if the target already exists, returns false.
5
+ * Also installs a post_install hook that keeps React Native Firebase's
6
+ * generated Info.plist input path from recreating a host-extension build cycle.
7
+ * Idempotent: returns false when no Podfile changes are needed.
6
8
  */
7
9
  export declare function patchPodfile(podfilePath: string, targetName: string, dryRun: boolean): boolean;
8
10
  /**