react-native-notify-kit 9.3.0 → 9.5.0
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 +68 -2
- package/android/build.gradle +5 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/androidTest/java/app/notifee/core/DoScheduledWorkOrderingTest.java +234 -0
- package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +204 -0
- package/android/src/androidTest/java/app/notifee/core/database/TimingWorkDataRepository.java +56 -0
- package/android/src/androidTest/java/app/notifee/core/database/WorkDataRepositoryRaceTest.java +221 -0
- package/android/src/androidTest/res/drawable/test_icon.xml +14 -0
- package/android/src/main/baseline-prof.txt +382 -0
- package/android/src/main/java/app/notifee/core/ForegroundService.java +269 -80
- package/android/src/main/java/app/notifee/core/InitProvider.java +48 -5
- package/android/src/main/java/app/notifee/core/Logger.java +5 -0
- package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +160 -79
- package/android/src/main/java/app/notifee/core/NotificationManager.java +370 -288
- package/android/src/main/java/app/notifee/core/WarmupHelper.java +84 -0
- package/android/src/main/java/app/notifee/core/database/WorkDataRepository.java +38 -34
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +17 -1
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +30 -0
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +182 -0
- package/android/src/test/java/app/notifee/core/database/WorkDataRepositoryFutureContractTest.java +279 -0
- package/dist/NotifeeApiModule.d.ts +1 -0
- package/dist/NotifeeApiModule.js +6 -0
- package/dist/NotifeeApiModule.js.map +1 -1
- package/dist/specs/NativeNotifeeModule.d.ts +1 -0
- package/dist/specs/NativeNotifeeModule.js.map +1 -1
- package/dist/types/Module.d.ts +19 -0
- package/dist/types/NotificationAndroid.d.ts +58 -0
- package/dist/types/NotificationAndroid.js +43 -0
- package/dist/types/NotificationAndroid.js.map +1 -1
- package/dist/utils/validate.d.ts +1 -0
- package/dist/utils/validate.js +10 -4
- package/dist/utils/validate.js.map +1 -1
- package/dist/validators/validateAndroidChannel.js +3 -3
- package/dist/validators/validateAndroidChannel.js.map +1 -1
- package/dist/validators/validateAndroidNotification.js +36 -12
- package/dist/validators/validateAndroidNotification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m +5 -1
- package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +5 -5
- package/ios/NotifeeCore/NotifeeCore.m +28 -9
- package/ios/NotifeeCore/NotifeeCoreDelegateHolder.m +5 -1
- package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +7 -2
- package/ios/RNNotifee/NotifeeApiModule.mm +31 -10
- package/package.json +1 -1
- package/src/NotifeeApiModule.ts +7 -0
- package/src/specs/NativeNotifeeModule.ts +1 -0
- package/src/types/Module.ts +20 -0
- package/src/types/NotificationAndroid.ts +62 -0
- package/src/utils/validate.ts +12 -4
- package/src/validators/validateAndroidChannel.ts +3 -3
- package/src/validators/validateAndroidNotification.ts +43 -10
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -222,7 +222,17 @@ To modify push notification content before display (e.g., attach images), create
|
|
|
222
222
|
}
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
-
4.
|
|
225
|
+
4. Implement `serviceExtensionTimeWillExpire` as a safety net. Notification Service Extensions have a ~30-second time budget; if your notification includes a large image attachment and the download is slow, the extension may be terminated before the content handler is called. Deliver a best-effort notification in the expiration handler:
|
|
226
|
+
|
|
227
|
+
```objc
|
|
228
|
+
- (void)serviceExtensionTimeWillExpire {
|
|
229
|
+
// Deliver the notification with whatever content we have so far
|
|
230
|
+
// (e.g., without the image attachment if the download didn't finish).
|
|
231
|
+
self.contentHandler(self.bestAttemptContent);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
5. Run `cd ios && pod install`
|
|
226
236
|
|
|
227
237
|
## Jest Testing
|
|
228
238
|
|
|
@@ -253,9 +263,10 @@ This fork is a complete migration to React Native's **New Architecture**:
|
|
|
253
263
|
- **Toolchain**: Yarn 4, Node 22+, Java 17, compileSdk/targetSdk 35
|
|
254
264
|
- **Single Android module** — the original Notifee shipped a pre-compiled AAR bundled inside the npm tarball under a frozen Maven coordinate; this fork compiles the core from source as part of the React Native bridge module on every consumer build. Eliminates the `FAIL_ON_PROJECT_REPOS` issue on RN 0.74+ and the Gradle cache staleness bug that could serve outdated bytecode after `yarn upgrade`.
|
|
255
265
|
- **Core notification logic (NotifeeCore) is unchanged** — the public API is fully compatible with the original Notifee
|
|
256
|
-
- **
|
|
266
|
+
- **30 upstream bugs fixed** — see [Bugs Fixed from Upstream Notifee](#bugs-fixed-from-upstream-notifee) below
|
|
257
267
|
- **Reliable trigger notifications** — AlarmManager is the default backend instead of WorkManager, with automatic fallback when exact alarm permission is not granted
|
|
258
268
|
- **New API: `setNotificationConfig()`** — opt-out flag to prevent Notifee from intercepting iOS remote notification handlers (see [New APIs](#new-apis) below)
|
|
269
|
+
- **Baseline Profile** — the library AAR ships a Baseline Profile that instructs ART to AOT-compile the foreground service notification hot path at install time, eliminating JIT penalty on first invocation
|
|
259
270
|
|
|
260
271
|
## Bugs Fixed from Upstream Notifee
|
|
261
272
|
|
|
@@ -286,6 +297,13 @@ This fork fixes the following bugs that were never resolved in the original Noti
|
|
|
286
297
|
| Stale Gradle cache could serve outdated AAR bytecode after `yarn upgrade` — same Maven coordinate reused across releases violated Gradle's coordinate-immutability assumption | Android | N/A (architectural) | 9.2.0 |
|
|
287
298
|
| `EventType.DELIVERED` not emitted for `displayNotification()` in foreground (only for trigger notifications) — `notifeeTrigger != nil` guard in `willPresentNotification:` suppressed the event, breaking iOS/Android symmetry | iOS | Pre-existing | 9.3.0 |
|
|
288
299
|
| Tapping a notification without explicit `pressAction` does nothing (app doesn't open) — `NotificationPendingIntent.createIntent()` creates a tap-less PendingIntent when `pressActionModelBundle` is null, especially visible on trigger notifications after app kill | Android | Pre-existing (latent) | 9.3.0 |
|
|
300
|
+
| Foreground service notifications delayed up to 10 seconds on Android 12+ — library never calls `setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE)` | Android | [#272](https://github.com/invertase/notifee/issues/272), [#1242](https://github.com/invertase/notifee/issues/1242) | 9.4.0 |
|
|
301
|
+
| `didReceiveNotificationResponse:` completionHandler delayed by 15 seconds via `dispatch_after`, blocking subsequent notification taps and risking handler leaks if the app is suspended during the wait | iOS | Pre-existing (TODO since 2020) | 9.4.0 |
|
|
302
|
+
| `requestPermission:` silently swallows `NSError` from `requestAuthorizationWithOptions`, making MDM and parental-control authorization failures invisible to JS consumers | iOS | Pre-existing (TODO since day 1) | 9.4.0 |
|
|
303
|
+
| `contentByUpdatingWithProvider:` errors suppressed via `nil` error pointer in `displayNotification:` and `createTriggerNotification:` — communication notifications with malformed SiriKit intents silently fail with nil content | iOS | Pre-existing | 9.4.0 |
|
|
304
|
+
| `getBadgeCount:` completion block never called when running in an app extension, causing JS promises to hang forever in NSE handlers | iOS | Pre-existing | 9.4.0 |
|
|
305
|
+
| Notification Service Extension attachment downloads had no timeout cap (default 60-second `NSURLSession` timeout exceeds iOS's ~30-second NSE budget), causing extension process kill and notification loss on slow networks | iOS | Pre-existing | 9.4.0 |
|
|
306
|
+
| `cancelTriggerNotifications()` / `createTriggerNotification()` promises resolve before Room DB write completes, causing ~3% race on cancel-then-create patterns. Also fixes a previously-undocumented reboot-recovery data-loss bug in `NotifeeAlarmManager.rescheduleNotification` and an ordering bug in `NotificationManager.doScheduledWork` | Android | [#549](https://github.com/invertase/notifee/issues/549) | 9.5.0 |
|
|
289
307
|
|
|
290
308
|
> **Note for apps requiring guaranteed exact alarms (alarm clocks, timers, calendars):**
|
|
291
309
|
> Add `<uses-permission android:name="android.permission.USE_EXACT_ALARM" />` to your app's
|
|
@@ -314,6 +332,8 @@ In addition to bug fixes, the fork makes a few opinionated default changes vs `@
|
|
|
314
332
|
|
|
315
333
|
- **Library no longer hardcodes `foregroundServiceType` in its manifest** (since 9.1.13 — **BREAKING vs upstream**). Apps using `asForegroundService: true` on Android 14+ must declare their own `foregroundServiceType` on `app.notifee.core.ForegroundService` in their app manifest. See [Foreground Service Setup](#foreground-service-setup-android-14) below for migration instructions. Upstream hardcoded `shortService`, which caused a manifest merger failure ([#1108](https://github.com/invertase/notifee/issues/1108)) and a 3-minute timeout ANR crash ([#703](https://github.com/invertase/notifee/issues/703)).
|
|
316
334
|
|
|
335
|
+
- **Foreground service notifications use `FOREGROUND_SERVICE_IMMEDIATE` by default** (since 9.4.0 — **BREAKING vs upstream**). Upstream Notifee never called `setForegroundServiceBehavior()`, causing Android 12+ to defer foreground service notification display by up to 10 seconds unless the notification qualified for a system exemption. The fork now sets `FOREGROUND_SERVICE_IMMEDIATE` by default when `asForegroundService: true`, eliminating the delay. Opt out per-notification with `foregroundServiceBehavior: AndroidForegroundServiceBehavior.DEFERRED`. Additionally, the library now pre-loads critical foreground service classes and Binder proxies on a background thread during app startup (`InitProvider.onCreate`), reducing first-display cold-start latency by ~50–100 ms. Opt out of the warmup via `<meta-data android:name="notifee_init_warmup_enabled" android:value="false" />` in your app's `AndroidManifest.xml`.
|
|
336
|
+
|
|
317
337
|
- **iOS `EventType.DELIVERED` now emitted for all foreground notifications** (since 9.3.0 — **BREAKING vs upstream**). Upstream Notifee had a guard in `willPresentNotification:` that suppressed DELIVERED for notifications created via `displayNotification()` (immediate display), emitting it only for trigger notifications. Android always emitted DELIVERED in both cases. The fork removes the guard so iOS matches Android. If you registered `onForegroundEvent` listeners that did heavy work on DELIVERED assuming the event would only fire for trigger notifications, audit them — you may now receive an event per `displayNotification()` call while in foreground. **Known limitation**: trigger notifications that fire while the app is in background or killed still do not emit DELIVERED on iOS — this is a platform limitation (`willPresentNotification:` is only invoked in foreground, and iOS provides no delegate callback for background-delivered triggers). If you need delivery confirmation for background trigger notifications on iOS, check the notification's presence via `getDisplayedNotifications()` after the app returns to foreground.
|
|
318
338
|
|
|
319
339
|
These changes are documented in the [CHANGELOG](CHANGELOG.md) under the release that introduced them. If you rely on any of the upstream defaults, you can either pin to the specific behavior via the opt-out flags listed above, or open an issue to discuss.
|
|
@@ -418,6 +438,52 @@ With `handleRemoteNotifications: false`:
|
|
|
418
438
|
|
|
419
439
|
Default is `true` (backward compatible — Notifee handles everything, same as original Notifee behavior).
|
|
420
440
|
|
|
441
|
+
## Advanced / Troubleshooting
|
|
442
|
+
|
|
443
|
+
### Manual warmup control
|
|
444
|
+
|
|
445
|
+
The library automatically pre-warms the foreground service notification path during app startup via `InitProvider`. **Most apps do not need to do anything extra.** However, in certain edge cases the automatic warmup may not be sufficient:
|
|
446
|
+
|
|
447
|
+
- **Lazy-loaded library** — if `react-native-notify-kit` is code-split or lazy-loaded, `InitProvider` runs but the TurboModule/JS bridge side isn't initialized yet.
|
|
448
|
+
- **Post-splash-screen warmup** — apps that want to defer warmup to after the splash screen instead of during `Application.onCreate()`.
|
|
449
|
+
- **Low-end devices** — rare cases where the `InitProvider` warmup hasn't finished by the time the user triggers the first notification.
|
|
450
|
+
|
|
451
|
+
For these cases, call `prewarmForegroundService()` at a moment of your choosing:
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
import notifee from 'react-native-notify-kit';
|
|
455
|
+
|
|
456
|
+
// Call after splash screen, during onboarding, or before the user
|
|
457
|
+
// is likely to trigger a foreground service notification.
|
|
458
|
+
await notifee.prewarmForegroundService();
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**Key facts:**
|
|
462
|
+
|
|
463
|
+
- **Idempotent** — safe to call multiple times; class loading after the first call is a no-op from ART's perspective.
|
|
464
|
+
- **iOS no-op** — resolves immediately on iOS (Android-only optimization).
|
|
465
|
+
- **Does NOT start a foreground service** — it only performs class loading and Binder proxy warming. No Google Play policy risk.
|
|
466
|
+
- **Best-effort** — internal failures are logged and swallowed; the promise always resolves.
|
|
467
|
+
|
|
468
|
+
To verify whether calling this method provides a measurable benefit for your app, capture a Perfetto trace with the `notifee:*` trace sections enabled and compare the `notifee:displayNotification` duration with and without the prewarm call.
|
|
469
|
+
|
|
470
|
+
### Regenerating the Baseline Profile
|
|
471
|
+
|
|
472
|
+
The library ships a Baseline Profile (`packages/react-native/android/src/main/baseline-prof.txt`) that instructs ART to AOT-compile the notification hot path at install time. This profile should be regenerated after significant changes to the notification display code path.
|
|
473
|
+
|
|
474
|
+
**Prerequisites:**
|
|
475
|
+
|
|
476
|
+
- A physical device connected via adb (Pixel 9 Pro XL with Android 16+ recommended) or a running emulator with API 33+
|
|
477
|
+
- The smoke app must be buildable (`yarn install` in the repo root)
|
|
478
|
+
|
|
479
|
+
**Command:**
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
bash scripts/generate-baseline-profile.sh
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
The script runs the macrobenchmark test in `apps/smoke/android/baselineprofile/`, captures the profile on the connected device, filters it to library-only rules, and copies it to the library's `src/main/baseline-prof.txt`. Review the generated file for unexpected entries, then commit it.
|
|
486
|
+
|
|
421
487
|
## Documentation
|
|
422
488
|
|
|
423
489
|
The upstream Notifee documentation remains the best reference for the public API and platform guides used by this fork.
|
package/android/build.gradle
CHANGED
|
@@ -139,6 +139,10 @@ dependencies {
|
|
|
139
139
|
|
|
140
140
|
api 'org.greenrobot:eventbus:3.3.1' // https://github.com/greenrobot/EventBus/releases
|
|
141
141
|
|
|
142
|
+
// ProfileInstaller bootstraps baseline profiles on devices that do not receive
|
|
143
|
+
// cloud-compiled profiles from Play Store (sideloads, debug builds).
|
|
144
|
+
api 'androidx.profileinstaller:profileinstaller:1.4.1'
|
|
145
|
+
|
|
142
146
|
// --- Bridge dependencies ---
|
|
143
147
|
implementation 'androidx.lifecycle:lifecycle-process:2.3.1'
|
|
144
148
|
//noinspection GradleDynamicVersion
|
|
@@ -147,6 +151,7 @@ dependencies {
|
|
|
147
151
|
// --- Test dependencies ---
|
|
148
152
|
testImplementation 'junit:junit:4.13.2' // https://github.com/junit-team/junit4/releases
|
|
149
153
|
testImplementation 'org.robolectric:robolectric:4.14.1' // https://github.com/robolectric/robolectric/releases
|
|
154
|
+
testImplementation 'org.mockito:mockito-core:5.19.0' // https://github.com/mockito/mockito/releases
|
|
150
155
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3' // https://developer.android.com/jetpack/androidx/releases/test
|
|
151
156
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' // see above
|
|
152
157
|
androidTestImplementation "androidx.room:room-testing:$room_version"
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
# InitProvider is subclassed by the RN bridge module (NotifeeInitProvider).
|
|
20
20
|
# R8 must not finalize its methods, otherwise the bridge cannot override onCreate().
|
|
21
21
|
-keep class app.notifee.core.InitProvider { *; }
|
|
22
|
+
-keep class app.notifee.core.WarmupHelper { *; }
|
|
22
23
|
-keeppackagenames app.notifee.core.**
|
|
23
24
|
|
|
24
25
|
# --- Annotations ---
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
package app.notifee.core;
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2016-present Invertase Limited & Contributors
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this library except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* ---
|
|
13
|
+
*
|
|
14
|
+
* NOT RUN IN CI.
|
|
15
|
+
*
|
|
16
|
+
* Instrumented regression test for caller #5 (NotificationManager.doScheduledWork
|
|
17
|
+
* delete-then-completer ordering) from pre-fix-549-audit.md. Seeds a single
|
|
18
|
+
* one-time trigger row, invokes doScheduledWork via a real
|
|
19
|
+
* CallbackToFutureAdapter.Completer, and asserts both ordering (delete DAO
|
|
20
|
+
* call returned strictly before completer.set fired) and side-effect
|
|
21
|
+
* (the row is gone from Room after the test).
|
|
22
|
+
*
|
|
23
|
+
* Observation mechanism:
|
|
24
|
+
*
|
|
25
|
+
* - TimingWorkDataRepository (in package app.notifee.core.database) subclasses
|
|
26
|
+
* WorkDataRepository via the @VisibleForTesting package-private constructor
|
|
27
|
+
* and records the nanoTime at which deleteById's future completes.
|
|
28
|
+
* - The production singleton is swapped to the TimingWorkDataRepository
|
|
29
|
+
* instance via reflection before the test runs and restored in @After.
|
|
30
|
+
* - completerSetNanos is captured via a directExecutor listener on the
|
|
31
|
+
* resultFuture returned by CallbackToFutureAdapter.getFuture — this runs
|
|
32
|
+
* synchronously on the thread that called completer.set(...).
|
|
33
|
+
*
|
|
34
|
+
* Run manually:
|
|
35
|
+
* cd apps/smoke/android
|
|
36
|
+
* ./gradlew :react-native-notify-kit:connectedDebugAndroidTest \
|
|
37
|
+
* --tests app.notifee.core.DoScheduledWorkOrderingTest
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import static org.junit.Assert.assertEquals;
|
|
41
|
+
import static org.junit.Assert.assertNotNull;
|
|
42
|
+
import static org.junit.Assert.assertNull;
|
|
43
|
+
import static org.junit.Assert.assertTrue;
|
|
44
|
+
|
|
45
|
+
import android.content.Context;
|
|
46
|
+
import android.os.Bundle;
|
|
47
|
+
import androidx.concurrent.futures.CallbackToFutureAdapter;
|
|
48
|
+
import androidx.core.app.NotificationChannelCompat;
|
|
49
|
+
import androidx.core.app.NotificationManagerCompat;
|
|
50
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
51
|
+
import androidx.test.platform.app.InstrumentationRegistry;
|
|
52
|
+
import androidx.work.Data;
|
|
53
|
+
import androidx.work.ListenableWorker;
|
|
54
|
+
import app.notifee.core.database.TimingWorkDataRepository;
|
|
55
|
+
import app.notifee.core.database.WorkDataEntity;
|
|
56
|
+
import app.notifee.core.database.WorkDataRepository;
|
|
57
|
+
import app.notifee.core.utility.ObjectUtils;
|
|
58
|
+
import com.google.common.util.concurrent.ListenableFuture;
|
|
59
|
+
import com.google.common.util.concurrent.MoreExecutors;
|
|
60
|
+
import java.lang.reflect.Field;
|
|
61
|
+
import java.util.concurrent.CountDownLatch;
|
|
62
|
+
import java.util.concurrent.TimeUnit;
|
|
63
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
64
|
+
import java.util.concurrent.atomic.AtomicReference;
|
|
65
|
+
import org.junit.After;
|
|
66
|
+
import org.junit.Before;
|
|
67
|
+
import org.junit.Test;
|
|
68
|
+
import org.junit.runner.RunWith;
|
|
69
|
+
|
|
70
|
+
@RunWith(AndroidJUnit4.class)
|
|
71
|
+
public class DoScheduledWorkOrderingTest {
|
|
72
|
+
|
|
73
|
+
private static final String TEST_ID = "do-scheduled-work-ordering-test";
|
|
74
|
+
private static final String TEST_CHANNEL_ID = "do-scheduled-work-ordering-test-channel";
|
|
75
|
+
|
|
76
|
+
private Context context;
|
|
77
|
+
private WorkDataRepository realRepo;
|
|
78
|
+
private TimingWorkDataRepository timingRepo;
|
|
79
|
+
private Field singletonField;
|
|
80
|
+
|
|
81
|
+
@Before
|
|
82
|
+
public void setUp() throws Exception {
|
|
83
|
+
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
84
|
+
ContextHolder.setApplicationContext(context.getApplicationContext());
|
|
85
|
+
|
|
86
|
+
// Prime the production singleton and seed the row via the real DB.
|
|
87
|
+
realRepo = WorkDataRepository.getInstance(context);
|
|
88
|
+
realRepo.deleteAll().get(5, TimeUnit.SECONDS);
|
|
89
|
+
|
|
90
|
+
// Create a channel for the displayNotification call inside doScheduledWork.
|
|
91
|
+
// Use NotificationManagerCompat directly — Notifee.getInstance() would
|
|
92
|
+
// require Notifee SDK initialization which is not available in
|
|
93
|
+
// instrumented tests.
|
|
94
|
+
NotificationManagerCompat.from(context)
|
|
95
|
+
.createNotificationChannel(
|
|
96
|
+
new NotificationChannelCompat.Builder(
|
|
97
|
+
TEST_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
|
98
|
+
.setName("DoScheduledWorkOrderingTest")
|
|
99
|
+
.build());
|
|
100
|
+
|
|
101
|
+
// Build the timing-instrumented repo that wraps the SAME DAO as the real
|
|
102
|
+
// singleton. Because NotifeeCoreDatabase.getDatabase(context) is itself a
|
|
103
|
+
// singleton, realRepo and timingRepo read/write the same underlying Room
|
|
104
|
+
// tables — seeding via realRepo is visible to timingRepo and vice versa.
|
|
105
|
+
// The factory lives in the database package because the constructor and
|
|
106
|
+
// the NotifeeCoreDatabase accessors are package-private.
|
|
107
|
+
timingRepo = TimingWorkDataRepository.createForProductionDb(context);
|
|
108
|
+
|
|
109
|
+
// Swap the singleton so NotificationManager.doScheduledWork's delete path
|
|
110
|
+
// (WorkDataRepository.getInstance(ctx).deleteById(...)) goes through the
|
|
111
|
+
// timing instrumentation.
|
|
112
|
+
singletonField = WorkDataRepository.class.getDeclaredField("mInstance");
|
|
113
|
+
singletonField.setAccessible(true);
|
|
114
|
+
singletonField.set(null, timingRepo);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@After
|
|
118
|
+
public void tearDown() throws Exception {
|
|
119
|
+
if (singletonField != null) {
|
|
120
|
+
singletonField.set(null, realRepo);
|
|
121
|
+
}
|
|
122
|
+
if (realRepo != null) {
|
|
123
|
+
realRepo.deleteAll().get(5, TimeUnit.SECONDS);
|
|
124
|
+
}
|
|
125
|
+
// Best-effort: dismiss any notification the test displayed.
|
|
126
|
+
androidx.core.app.NotificationManagerCompat.from(context).cancelAll();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Test
|
|
130
|
+
public void doScheduledWork_deleteCompletesBeforeCompleterSet() throws Exception {
|
|
131
|
+
// Seed the row that doScheduledWork will read, display, and then delete.
|
|
132
|
+
WorkDataEntity seed = buildSeedEntity(TEST_ID);
|
|
133
|
+
timingRepo.insert(seed).get(5, TimeUnit.SECONDS);
|
|
134
|
+
assertEquals(1, timingRepo.getAll().get(5, TimeUnit.SECONDS).size());
|
|
135
|
+
|
|
136
|
+
// Build a real Completer via CallbackToFutureAdapter so we can wait on the
|
|
137
|
+
// returned future and record the moment at which completer.set(...) fires.
|
|
138
|
+
AtomicReference<CallbackToFutureAdapter.Completer<ListenableWorker.Result>> completerRef =
|
|
139
|
+
new AtomicReference<>();
|
|
140
|
+
ListenableFuture<ListenableWorker.Result> resultFuture =
|
|
141
|
+
CallbackToFutureAdapter.getFuture(
|
|
142
|
+
completer -> {
|
|
143
|
+
completerRef.set(completer);
|
|
144
|
+
return "DoScheduledWorkOrderingTest";
|
|
145
|
+
});
|
|
146
|
+
CallbackToFutureAdapter.Completer<ListenableWorker.Result> completer = completerRef.get();
|
|
147
|
+
assertNotNull("Completer must be populated synchronously by getFuture", completer);
|
|
148
|
+
|
|
149
|
+
// The listener records completerSetNanos AND counts down the latch, giving
|
|
150
|
+
// the test a happens-before barrier to await on. resultFuture.get() alone
|
|
151
|
+
// is insufficient — Guava only guarantees that .get() returns after the
|
|
152
|
+
// future is marked done, not after all listeners have fired, so the test
|
|
153
|
+
// could race the listener on a cached-threadpool scheduler.
|
|
154
|
+
AtomicLong completerSetNanos = new AtomicLong();
|
|
155
|
+
CountDownLatch completerListenerRan = new CountDownLatch(1);
|
|
156
|
+
resultFuture.addListener(
|
|
157
|
+
() -> {
|
|
158
|
+
completerSetNanos.compareAndSet(0, System.nanoTime());
|
|
159
|
+
completerListenerRan.countDown();
|
|
160
|
+
},
|
|
161
|
+
MoreExecutors.directExecutor());
|
|
162
|
+
|
|
163
|
+
// Build the Data payload doScheduledWork expects.
|
|
164
|
+
Data data =
|
|
165
|
+
new Data.Builder()
|
|
166
|
+
.putString("id", TEST_ID)
|
|
167
|
+
.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_ONE_TIME)
|
|
168
|
+
.build();
|
|
169
|
+
|
|
170
|
+
// Invoke the production entry point. Futures are chained internally; the
|
|
171
|
+
// resultFuture resolves when completer.set(...) fires.
|
|
172
|
+
NotificationManager.doScheduledWork(data, completer);
|
|
173
|
+
|
|
174
|
+
// Wait for the work to report completion. 15s is generous — the whole chain
|
|
175
|
+
// (Room read → displayNotification → Room delete → completer.set) usually
|
|
176
|
+
// takes <200ms on a Pixel 9 Pro XL warm.
|
|
177
|
+
ListenableWorker.Result result = resultFuture.get(15, TimeUnit.SECONDS);
|
|
178
|
+
assertEquals(ListenableWorker.Result.success(), result);
|
|
179
|
+
|
|
180
|
+
// Explicitly wait for the listener to have fired — see comment at the
|
|
181
|
+
// listener registration above for why resultFuture.get() alone is racy.
|
|
182
|
+
assertTrue(
|
|
183
|
+
"completer.set listener must fire within 5s of resultFuture completing",
|
|
184
|
+
completerListenerRan.await(5, TimeUnit.SECONDS));
|
|
185
|
+
|
|
186
|
+
long deleteNanos = timingRepo.firstDeleteCompletionNanos.get();
|
|
187
|
+
long completerNanos = completerSetNanos.get();
|
|
188
|
+
|
|
189
|
+
assertTrue("delete DAO call must actually have completed", deleteNanos > 0);
|
|
190
|
+
assertTrue("completer.set must actually have fired", completerNanos > 0);
|
|
191
|
+
assertTrue(
|
|
192
|
+
"delete must complete strictly before completer.set — otherwise "
|
|
193
|
+
+ "WorkManager could start a new work instance that reads the stale row "
|
|
194
|
+
+ "(pre-#549 behavior). deleteNanos="
|
|
195
|
+
+ deleteNanos
|
|
196
|
+
+ " completerNanos="
|
|
197
|
+
+ completerNanos
|
|
198
|
+
+ " deltaNanos="
|
|
199
|
+
+ (completerNanos - deleteNanos),
|
|
200
|
+
deleteNanos < completerNanos);
|
|
201
|
+
|
|
202
|
+
// Side-effect assertion: the row must be gone.
|
|
203
|
+
assertNull(
|
|
204
|
+
"row must be deleted from Room after doScheduledWork completes",
|
|
205
|
+
timingRepo.getWorkDataById(TEST_ID).get(5, TimeUnit.SECONDS));
|
|
206
|
+
assertTrue(
|
|
207
|
+
"Room must contain no entries after doScheduledWork completes",
|
|
208
|
+
timingRepo.getAll().get(5, TimeUnit.SECONDS).isEmpty());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private static WorkDataEntity buildSeedEntity(String id) {
|
|
212
|
+
Bundle notificationBundle = new Bundle();
|
|
213
|
+
notificationBundle.putString("id", id);
|
|
214
|
+
notificationBundle.putString("title", "DoScheduledWorkOrderingTest");
|
|
215
|
+
Bundle androidBundle = new Bundle();
|
|
216
|
+
androidBundle.putString("channelId", TEST_CHANNEL_ID);
|
|
217
|
+
// A smallIcon is required by Android's NotificationManager.fixNotification —
|
|
218
|
+
// a notification without one throws IllegalArgumentException. The
|
|
219
|
+
// androidTest res/drawable/test_icon.xml is a transparent placeholder that
|
|
220
|
+
// exists only in the test APK.
|
|
221
|
+
androidBundle.putString("smallIcon", "test_icon");
|
|
222
|
+
notificationBundle.putBundle("android", androidBundle);
|
|
223
|
+
|
|
224
|
+
Bundle triggerBundle = new Bundle();
|
|
225
|
+
triggerBundle.putInt("type", 0); // TIMESTAMP
|
|
226
|
+
triggerBundle.putLong("timestamp", System.currentTimeMillis());
|
|
227
|
+
|
|
228
|
+
return new WorkDataEntity(
|
|
229
|
+
id,
|
|
230
|
+
ObjectUtils.bundleToBytes(notificationBundle),
|
|
231
|
+
ObjectUtils.bundleToBytes(triggerBundle),
|
|
232
|
+
false /* withAlarmManager — WorkManager path */);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
package app.notifee.core;
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2016-present Invertase Limited & Contributors
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this library except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* ---
|
|
13
|
+
*
|
|
14
|
+
* NOT RUN IN CI.
|
|
15
|
+
*
|
|
16
|
+
* Instrumented regression test for caller #8 (reboot recovery anchor
|
|
17
|
+
* persistence) from pre-fix-549-audit.md. Seeds five recurring trigger rows
|
|
18
|
+
* with timestamps in the past, invokes NotifeeAlarmManager.rescheduleNotifications
|
|
19
|
+
* via its public entry point, and asserts that every row has an advanced
|
|
20
|
+
* timestamp once the reschedule completes — proving that the per-entity
|
|
21
|
+
* WorkDataRepository.update(...) futures are awaited before the
|
|
22
|
+
* BroadcastReceiver finish path, which is the #549 fix commit
|
|
23
|
+
* 71fa20e ("fix(android): persist reboot recovery anchor updates before
|
|
24
|
+
* finishing receiver").
|
|
25
|
+
*
|
|
26
|
+
* Run manually:
|
|
27
|
+
* cd apps/smoke/android
|
|
28
|
+
* ./gradlew :react-native-notify-kit:connectedDebugAndroidTest \
|
|
29
|
+
* --tests app.notifee.core.RebootRecoveryTest
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import static org.junit.Assert.assertEquals;
|
|
33
|
+
import static org.junit.Assert.assertTrue;
|
|
34
|
+
import static org.junit.Assert.fail;
|
|
35
|
+
|
|
36
|
+
import android.content.Context;
|
|
37
|
+
import android.os.Bundle;
|
|
38
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
39
|
+
import androidx.test.platform.app.InstrumentationRegistry;
|
|
40
|
+
import app.notifee.core.database.WorkDataEntity;
|
|
41
|
+
import app.notifee.core.database.WorkDataRepository;
|
|
42
|
+
import app.notifee.core.utility.ObjectUtils;
|
|
43
|
+
import java.util.List;
|
|
44
|
+
import java.util.concurrent.TimeUnit;
|
|
45
|
+
import org.junit.After;
|
|
46
|
+
import org.junit.Before;
|
|
47
|
+
import org.junit.Test;
|
|
48
|
+
import org.junit.runner.RunWith;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ⚠️ WARNING — DESTRUCTIVE INSTRUMENTED TEST ⚠️
|
|
52
|
+
*
|
|
53
|
+
* <p>This test calls {@code WorkDataRepository.getInstance(context)}, which returns the production
|
|
54
|
+
* singleton backed by the on-disk {@code notifee_core_database}. The {@code @Before} and
|
|
55
|
+
* {@code @After} hooks call {@code deleteAll()} on that database.
|
|
56
|
+
*
|
|
57
|
+
* <p>If you run this test on a device that has REAL scheduled notifee notifications (your personal
|
|
58
|
+
* phone, a shared QA device, a customer's device), THOSE NOTIFICATIONS WILL BE SILENTLY DELETED.
|
|
59
|
+
* There is no undo.
|
|
60
|
+
*
|
|
61
|
+
* <p>Run this test only on:
|
|
62
|
+
*
|
|
63
|
+
* <ul>
|
|
64
|
+
* <li>Throwaway emulators
|
|
65
|
+
* <li>Dedicated test devices
|
|
66
|
+
* <li>Devices where you have explicitly verified that no production notifee state exists
|
|
67
|
+
* </ul>
|
|
68
|
+
*
|
|
69
|
+
* <p>A structural fix (migrate to an in-memory Room database for tests) is the correct long-term
|
|
70
|
+
* solution but is out of scope for the #549 fix PR.
|
|
71
|
+
*/
|
|
72
|
+
@RunWith(AndroidJUnit4.class)
|
|
73
|
+
public class RebootRecoveryTest {
|
|
74
|
+
|
|
75
|
+
private static final int SEED_COUNT = 5;
|
|
76
|
+
private static final long HOUR_IN_MS = 60L * 60 * 1000;
|
|
77
|
+
|
|
78
|
+
/** 5 hours before "now" — well in the past so setNextTimestamp must advance. */
|
|
79
|
+
private static final long OLD_ANCHOR_OFFSET_MS = 5 * HOUR_IN_MS;
|
|
80
|
+
|
|
81
|
+
private static final long POLL_DEADLINE_MS = 15_000;
|
|
82
|
+
private static final long POLL_INTERVAL_MS = 100;
|
|
83
|
+
|
|
84
|
+
private Context context;
|
|
85
|
+
private WorkDataRepository repo;
|
|
86
|
+
|
|
87
|
+
@Before
|
|
88
|
+
public void setUp() throws Exception {
|
|
89
|
+
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
90
|
+
// Ensure ContextHolder is populated — production code paths inside
|
|
91
|
+
// NotifeeAlarmManager call ContextHolder.getApplicationContext() and
|
|
92
|
+
// WorkDataRepository.getInstance(getApplicationContext()).
|
|
93
|
+
ContextHolder.setApplicationContext(context.getApplicationContext());
|
|
94
|
+
repo = WorkDataRepository.getInstance(context);
|
|
95
|
+
repo.deleteAll().get(5, TimeUnit.SECONDS);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@After
|
|
99
|
+
public void tearDown() throws Exception {
|
|
100
|
+
// Cancel every real alarm the test scheduled so the device is left clean.
|
|
101
|
+
for (int i = 0; i < SEED_COUNT; i++) {
|
|
102
|
+
NotifeeAlarmManager.cancelNotification(testId(i));
|
|
103
|
+
}
|
|
104
|
+
repo.deleteAll().get(5, TimeUnit.SECONDS);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Test
|
|
108
|
+
public void rescheduleNotifications_advancesEveryAnchorBeforeCompleting() throws Exception {
|
|
109
|
+
long now = System.currentTimeMillis();
|
|
110
|
+
long oldAnchor = now - OLD_ANCHOR_OFFSET_MS;
|
|
111
|
+
long minExpectedAnchor = now; // every anchor must move strictly into the future
|
|
112
|
+
|
|
113
|
+
// Seed SEED_COUNT entities, each a recurring hourly alarm with the old anchor.
|
|
114
|
+
for (int i = 0; i < SEED_COUNT; i++) {
|
|
115
|
+
repo.insert(buildEntity(testId(i), oldAnchor)).get(5, TimeUnit.SECONDS);
|
|
116
|
+
}
|
|
117
|
+
assertEquals(SEED_COUNT, repo.getAll().get(5, TimeUnit.SECONDS).size());
|
|
118
|
+
|
|
119
|
+
// Invoke the real reboot-recovery entry point. PendingResult is null because
|
|
120
|
+
// this test is not driven by a BroadcastReceiver goAsync() scope; the fix
|
|
121
|
+
// guards every finish() call with a null-check, so passing null exercises
|
|
122
|
+
// the same future chain without actually broadcasting anything.
|
|
123
|
+
new NotifeeAlarmManager().rescheduleNotifications(null);
|
|
124
|
+
|
|
125
|
+
// Poll for completion. The reschedule fires several async stages
|
|
126
|
+
// (getScheduledNotifications → per-entity update → allAsList → withTimeout),
|
|
127
|
+
// so we cannot directly latch on "done". Instead we observe the side
|
|
128
|
+
// effect: every row's timestamp must have been moved into the future.
|
|
129
|
+
List<WorkDataEntity> rows = awaitAllAnchorsAdvanced(minExpectedAnchor);
|
|
130
|
+
|
|
131
|
+
assertEquals("no rows lost during reboot recovery", SEED_COUNT, rows.size());
|
|
132
|
+
for (WorkDataEntity row : rows) {
|
|
133
|
+
Bundle triggerBundle = ObjectUtils.bytesToBundle(row.getTrigger());
|
|
134
|
+
long newAnchor = ObjectUtils.getLong(triggerBundle.get("timestamp"));
|
|
135
|
+
assertTrue(
|
|
136
|
+
"row "
|
|
137
|
+
+ row.getId()
|
|
138
|
+
+ " must have an advanced anchor: was="
|
|
139
|
+
+ oldAnchor
|
|
140
|
+
+ " now="
|
|
141
|
+
+ newAnchor,
|
|
142
|
+
newAnchor >= oldAnchor + HOUR_IN_MS);
|
|
143
|
+
assertTrue(
|
|
144
|
+
"row " + row.getId() + " anchor must be in the future: " + newAnchor,
|
|
145
|
+
newAnchor >= minExpectedAnchor);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Polls {@link WorkDataRepository#getAll()} until every row has a timestamp at or after {@code
|
|
151
|
+
* minExpectedAnchor}, or fails on timeout.
|
|
152
|
+
*/
|
|
153
|
+
private List<WorkDataEntity> awaitAllAnchorsAdvanced(long minExpectedAnchor) throws Exception {
|
|
154
|
+
long deadline = System.currentTimeMillis() + POLL_DEADLINE_MS;
|
|
155
|
+
while (System.currentTimeMillis() < deadline) {
|
|
156
|
+
List<WorkDataEntity> rows = repo.getAll().get(5, TimeUnit.SECONDS);
|
|
157
|
+
if (rows.size() == SEED_COUNT && allAdvanced(rows, minExpectedAnchor)) {
|
|
158
|
+
return rows;
|
|
159
|
+
}
|
|
160
|
+
Thread.sleep(POLL_INTERVAL_MS);
|
|
161
|
+
}
|
|
162
|
+
fail(
|
|
163
|
+
"reboot recovery did not advance every anchor within "
|
|
164
|
+
+ POLL_DEADLINE_MS
|
|
165
|
+
+ "ms — the fix in commit 71fa20e may be broken");
|
|
166
|
+
return null; // unreachable
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private static boolean allAdvanced(List<WorkDataEntity> rows, long minExpectedAnchor) {
|
|
170
|
+
for (WorkDataEntity row : rows) {
|
|
171
|
+
Bundle trigger = ObjectUtils.bytesToBundle(row.getTrigger());
|
|
172
|
+
long ts = ObjectUtils.getLong(trigger.get("timestamp"));
|
|
173
|
+
if (ts < minExpectedAnchor) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Build a recurring-hourly trigger entity with the given id and initial anchor. */
|
|
181
|
+
private static WorkDataEntity buildEntity(String id, long anchorMs) {
|
|
182
|
+
Bundle notificationBundle = new Bundle();
|
|
183
|
+
notificationBundle.putString("id", id);
|
|
184
|
+
notificationBundle.putString("title", "RebootRecoveryTest " + id);
|
|
185
|
+
|
|
186
|
+
Bundle triggerBundle = new Bundle();
|
|
187
|
+
triggerBundle.putInt("type", 0); // TIMESTAMP
|
|
188
|
+
triggerBundle.putLong("timestamp", anchorMs);
|
|
189
|
+
triggerBundle.putInt("repeatFrequency", 0); // HOURLY
|
|
190
|
+
Bundle alarmManagerBundle = new Bundle();
|
|
191
|
+
alarmManagerBundle.putInt("type", 3); // SET_EXACT_AND_ALLOW_WHILE_IDLE
|
|
192
|
+
triggerBundle.putBundle("alarmManager", alarmManagerBundle);
|
|
193
|
+
|
|
194
|
+
return new WorkDataEntity(
|
|
195
|
+
id,
|
|
196
|
+
ObjectUtils.bundleToBytes(notificationBundle),
|
|
197
|
+
ObjectUtils.bundleToBytes(triggerBundle),
|
|
198
|
+
true /* withAlarmManager */);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private static String testId(int i) {
|
|
202
|
+
return "reboot-recovery-test-" + i;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package app.notifee.core.database;
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2016-present Invertase Limited & Contributors
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this library except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* ---
|
|
13
|
+
*
|
|
14
|
+
* Test-only WorkDataRepository subclass that captures the wall-clock nanoTime
|
|
15
|
+
* at which deleteById(id)'s underlying DAO call completes. Lives in the
|
|
16
|
+
* database package so it can call the @VisibleForTesting package-private
|
|
17
|
+
* WorkDataRepository(WorkDataDao, ListeningExecutorService) constructor.
|
|
18
|
+
* Consumers (e.g. DoScheduledWorkOrderingTest) instantiate via the public
|
|
19
|
+
* constructor below and inject this instance into the production singleton
|
|
20
|
+
* via reflection.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import android.content.Context;
|
|
24
|
+
import com.google.common.util.concurrent.ListenableFuture;
|
|
25
|
+
import com.google.common.util.concurrent.ListeningExecutorService;
|
|
26
|
+
import com.google.common.util.concurrent.MoreExecutors;
|
|
27
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
28
|
+
|
|
29
|
+
public class TimingWorkDataRepository extends WorkDataRepository {
|
|
30
|
+
|
|
31
|
+
/** nanoTime when the FIRST deleteById future completed. 0 if never called. */
|
|
32
|
+
public final AtomicLong firstDeleteCompletionNanos = new AtomicLong();
|
|
33
|
+
|
|
34
|
+
public TimingWorkDataRepository(WorkDataDao dao, ListeningExecutorService executor) {
|
|
35
|
+
super(dao, executor);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Factory that wraps the production {@link NotifeeCoreDatabase} DAO and executor, so that reads
|
|
40
|
+
* and writes share the same underlying Room tables as the production singleton.
|
|
41
|
+
*/
|
|
42
|
+
public static TimingWorkDataRepository createForProductionDb(Context context) {
|
|
43
|
+
return new TimingWorkDataRepository(
|
|
44
|
+
NotifeeCoreDatabase.getDatabase(context).workDao(),
|
|
45
|
+
NotifeeCoreDatabase.databaseWriteListeningExecutor);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
public ListenableFuture<Void> deleteById(String id) {
|
|
50
|
+
ListenableFuture<Void> f = super.deleteById(id);
|
|
51
|
+
f.addListener(
|
|
52
|
+
() -> firstDeleteCompletionNanos.compareAndSet(0, System.nanoTime()),
|
|
53
|
+
MoreExecutors.directExecutor());
|
|
54
|
+
return f;
|
|
55
|
+
}
|
|
56
|
+
}
|