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 +6 -6
- package/android/src/main/AndroidManifest.xml +0 -1
- package/android/src/main/java/app/notifee/core/NotificationManager.java +15 -8
- package/android/src/main/java/app/notifee/core/ReceiverService.java +18 -13
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidActionModel.java +1 -2
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidPressActionModel.java +3 -9
- package/android/src/main/java/app/notifee/core/utility/IntentUtils.java +0 -28
- package/android/src/main/java/app/notifee/core/utility/PowerManagerUtils.java +79 -41
- package/android/src/main/java/app/notifee/core/utility/ResourceUtils.java +3 -0
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeReactUtils.kt +1 -2
- package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +4 -8
- package/cli/dist/lib/patchPodfile.d.ts +3 -1
- package/cli/dist/lib/patchPodfile.js +125 -33
- package/cli/dist/lib/patchPodfile.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/types/Module.d.ts +20 -6
- package/dist/types/PowerManagerInfo.d.ts +9 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore.m +101 -114
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/types/Module.ts +20 -6
- package/src/types/PowerManagerInfo.ts +9 -4
- package/src/version.ts +1 -1
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** —
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
221
|
+
closeSystemDialogsBestEffort();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
226
224
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
package/android/src/main/java/app/notifee/core/model/NotificationAndroidPressActionModel.java
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
187
|
+
private static String getManufacturerName() {
|
|
188
|
+
return Build.BRAND.toLowerCase(Locale.US);
|
|
189
|
+
}
|
|
167
190
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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 =
|
|
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
|
-
*
|
|
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
|
/**
|