react-native-notify-kit 9.4.0 → 9.6.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 +200 -5
- package/android/build.gradle +1 -0
- package/android/src/androidTest/java/app/notifee/core/DoScheduledWorkOrderingTest.java +234 -0
- package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +569 -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/java/app/notifee/core/AlarmPermissionBroadcastReceiver.java +17 -6
- package/android/src/main/java/app/notifee/core/InitProvider.java +109 -2
- package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +385 -93
- package/android/src/main/java/app/notifee/core/NotificationAlarmReceiver.java +16 -5
- package/android/src/main/java/app/notifee/core/NotificationManager.java +153 -96
- package/android/src/main/java/app/notifee/core/Preferences.java +9 -0
- package/android/src/main/java/app/notifee/core/RebootBroadcastReceiver.java +20 -5
- package/android/src/main/java/app/notifee/core/database/WorkDataRepository.java +38 -34
- package/android/src/main/kotlin/io/invertase/notifee/HeadlessTask.kt +3 -2
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +251 -4
- package/android/src/test/java/app/notifee/core/InitProviderBootCheckTest.java +81 -0
- package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerHandleStaleTest.java +242 -0
- package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerSetAlarmClockTest.java +264 -0
- package/android/src/test/java/app/notifee/core/database/WorkDataRepositoryFutureContractTest.java +279 -0
- package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +199 -3
- package/android/src/test/java/io/invertase/notifee/HeadlessTaskConfigTest.kt +34 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/version.ts +1 -1
package/android/src/androidTest/java/app/notifee/core/database/WorkDataRepositoryRaceTest.java
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
* NOT RUN IN CI.
|
|
15
|
+
*
|
|
16
|
+
* This test hits a real Room in-memory database and must be executed on a connected device or
|
|
17
|
+
* emulator via:
|
|
18
|
+
*
|
|
19
|
+
* cd apps/smoke/android
|
|
20
|
+
* ./gradlew :react-native-notify-kit:connectedDebugAndroidTest
|
|
21
|
+
*
|
|
22
|
+
* before merging any change that touches the WorkDataRepository layer or the Room schema. A
|
|
23
|
+
* follow-up task tracks wiring this into CI with reactivecircus/android-emulator-runner — see the
|
|
24
|
+
* "Wire androidTest into CI" issue linked from the #549 fix PR description.
|
|
25
|
+
*
|
|
26
|
+
* The scenarios below are the instrumented analogues of the repro-549-findings.md harness:
|
|
27
|
+
* post-cancel consistency (Scenario B), post-create persistence (Scenario C), and concurrent
|
|
28
|
+
* stress (Scenario D). They run 100 iterations each and must observe zero inconsistencies after
|
|
29
|
+
* the #549 fix; any non-zero count is a regression.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import static org.junit.Assert.assertEquals;
|
|
33
|
+
import static org.junit.Assert.assertFalse;
|
|
34
|
+
import static org.junit.Assert.assertTrue;
|
|
35
|
+
import static org.junit.Assert.fail;
|
|
36
|
+
|
|
37
|
+
import androidx.room.Room;
|
|
38
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
39
|
+
import androidx.test.platform.app.InstrumentationRegistry;
|
|
40
|
+
import com.google.common.util.concurrent.ListenableFuture;
|
|
41
|
+
import com.google.common.util.concurrent.ListeningExecutorService;
|
|
42
|
+
import com.google.common.util.concurrent.MoreExecutors;
|
|
43
|
+
import java.util.ArrayList;
|
|
44
|
+
import java.util.List;
|
|
45
|
+
import java.util.concurrent.ExecutionException;
|
|
46
|
+
import java.util.concurrent.ExecutorService;
|
|
47
|
+
import java.util.concurrent.Executors;
|
|
48
|
+
import java.util.concurrent.TimeUnit;
|
|
49
|
+
import org.junit.After;
|
|
50
|
+
import org.junit.Before;
|
|
51
|
+
import org.junit.Test;
|
|
52
|
+
import org.junit.runner.RunWith;
|
|
53
|
+
|
|
54
|
+
@RunWith(AndroidJUnit4.class)
|
|
55
|
+
public class WorkDataRepositoryRaceTest {
|
|
56
|
+
|
|
57
|
+
private NotifeeCoreDatabase db;
|
|
58
|
+
private WorkDataRepository repo;
|
|
59
|
+
private ListeningExecutorService executor;
|
|
60
|
+
|
|
61
|
+
@Before
|
|
62
|
+
public void setUp() {
|
|
63
|
+
db =
|
|
64
|
+
Room.inMemoryDatabaseBuilder(
|
|
65
|
+
InstrumentationRegistry.getInstrumentation().getTargetContext(),
|
|
66
|
+
NotifeeCoreDatabase.class)
|
|
67
|
+
.allowMainThreadQueries()
|
|
68
|
+
.build();
|
|
69
|
+
// Shared cached thread pool matches the production executor's concurrency profile.
|
|
70
|
+
executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
|
|
71
|
+
repo = new WorkDataRepository(db.workDao(), executor);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@After
|
|
75
|
+
public void tearDown() {
|
|
76
|
+
db.close();
|
|
77
|
+
executor.shutdownNow();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ------- Scenario B analogue: post-cancel consistency -------
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
public void deleteAll_thenGetAll_isEmpty_100iterations()
|
|
84
|
+
throws ExecutionException, InterruptedException, java.util.concurrent.TimeoutException {
|
|
85
|
+
for (int i = 0; i < 100; i++) {
|
|
86
|
+
// Seed 20 rows
|
|
87
|
+
List<ListenableFuture<Void>> seedFutures = new ArrayList<>(20);
|
|
88
|
+
for (int j = 0; j < 20; j++) {
|
|
89
|
+
seedFutures.add(repo.insert(entity("b-" + i + "-" + j)));
|
|
90
|
+
}
|
|
91
|
+
for (ListenableFuture<Void> f : seedFutures) {
|
|
92
|
+
f.get(2, TimeUnit.SECONDS);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Assert seeded
|
|
96
|
+
assertEquals(20, repo.getAll().get(2, TimeUnit.SECONDS).size());
|
|
97
|
+
|
|
98
|
+
// Cancel all, then immediately read — must be empty.
|
|
99
|
+
repo.deleteAll().get(2, TimeUnit.SECONDS);
|
|
100
|
+
int immediateCount = repo.getAll().get(2, TimeUnit.SECONDS).size();
|
|
101
|
+
assertEquals(
|
|
102
|
+
"iteration " + i + ": deleteAll must complete before its future resolves",
|
|
103
|
+
0,
|
|
104
|
+
immediateCount);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ------- Scenario C analogue: post-create persistence -------
|
|
109
|
+
|
|
110
|
+
@Test
|
|
111
|
+
public void insert_thenGet_isVisible_100iterations()
|
|
112
|
+
throws ExecutionException, InterruptedException, java.util.concurrent.TimeoutException {
|
|
113
|
+
for (int i = 0; i < 100; i++) {
|
|
114
|
+
repo.deleteAll().get(2, TimeUnit.SECONDS);
|
|
115
|
+
String id = "c-" + i;
|
|
116
|
+
repo.insert(entity(id)).get(2, TimeUnit.SECONDS);
|
|
117
|
+
|
|
118
|
+
WorkDataEntity row = repo.getWorkDataById(id).get(2, TimeUnit.SECONDS);
|
|
119
|
+
assertTrue(
|
|
120
|
+
"iteration " + i + ": insert future resolved but row is not in Room yet", row != null);
|
|
121
|
+
assertEquals(id, row.getId());
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ------- Delete-by-id consistency -------
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
public void deleteById_thenGet_isNull_100iterations()
|
|
129
|
+
throws ExecutionException, InterruptedException, java.util.concurrent.TimeoutException {
|
|
130
|
+
for (int i = 0; i < 100; i++) {
|
|
131
|
+
String id = "d-" + i;
|
|
132
|
+
repo.insert(entity(id)).get(2, TimeUnit.SECONDS);
|
|
133
|
+
repo.deleteById(id).get(2, TimeUnit.SECONDS);
|
|
134
|
+
WorkDataEntity row = repo.getWorkDataById(id).get(2, TimeUnit.SECONDS);
|
|
135
|
+
assertTrue("iteration " + i + ": deleteById resolved but row still present", row == null);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ------- Update visibility -------
|
|
140
|
+
|
|
141
|
+
@Test
|
|
142
|
+
public void update_thenGet_reflectsNewTrigger_100iterations()
|
|
143
|
+
throws ExecutionException, InterruptedException, java.util.concurrent.TimeoutException {
|
|
144
|
+
for (int i = 0; i < 100; i++) {
|
|
145
|
+
String id = "u-" + i;
|
|
146
|
+
byte[] notification = new byte[] {1, 2, 3};
|
|
147
|
+
byte[] initialTrigger = new byte[] {(byte) i};
|
|
148
|
+
repo.insert(new WorkDataEntity(id, notification, initialTrigger, false))
|
|
149
|
+
.get(2, TimeUnit.SECONDS);
|
|
150
|
+
|
|
151
|
+
byte[] updatedTrigger = new byte[] {(byte) (i + 100)};
|
|
152
|
+
repo.update(new WorkDataEntity(id, notification, updatedTrigger, true))
|
|
153
|
+
.get(2, TimeUnit.SECONDS);
|
|
154
|
+
|
|
155
|
+
WorkDataEntity row = repo.getWorkDataById(id).get(2, TimeUnit.SECONDS);
|
|
156
|
+
assertTrue(row != null);
|
|
157
|
+
assertEquals(
|
|
158
|
+
"iteration " + i + ": updated trigger byte must be visible immediately",
|
|
159
|
+
(byte) (i + 100),
|
|
160
|
+
row.getTrigger()[0]);
|
|
161
|
+
assertTrue(row.getWithAlarmManager());
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ------- Scenario D analogue: concurrent stress -------
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Fire 20 concurrent insert and 20 concurrent deleteAll operations across a fixed thread pool and
|
|
169
|
+
* verify the system reaches a deterministic final state after all futures complete. This does NOT
|
|
170
|
+
* assert a specific final count — ordering between concurrent creates and cancels is
|
|
171
|
+
* implementation-defined — but it does assert that every future completes successfully and the DB
|
|
172
|
+
* is readable at the end.
|
|
173
|
+
*/
|
|
174
|
+
@Test
|
|
175
|
+
public void concurrentInsertAndDelete_allFuturesComplete()
|
|
176
|
+
throws ExecutionException, InterruptedException, java.util.concurrent.TimeoutException {
|
|
177
|
+
ExecutorService testPool = Executors.newFixedThreadPool(8);
|
|
178
|
+
try {
|
|
179
|
+
List<ListenableFuture<Void>> futures = new ArrayList<>();
|
|
180
|
+
for (int i = 0; i < 20; i++) {
|
|
181
|
+
futures.add(repo.insert(entity("s-" + i)));
|
|
182
|
+
if (i % 3 == 0) {
|
|
183
|
+
futures.add(repo.deleteAll());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (ListenableFuture<Void> f : futures) {
|
|
187
|
+
f.get(5, TimeUnit.SECONDS);
|
|
188
|
+
}
|
|
189
|
+
// Final read must succeed — we only assert the DB is readable, not the count.
|
|
190
|
+
int finalCount = repo.getAll().get(2, TimeUnit.SECONDS).size();
|
|
191
|
+
assertTrue("final count must be non-negative: " + finalCount, finalCount >= 0);
|
|
192
|
+
} finally {
|
|
193
|
+
testPool.shutdownNow();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ------- DAO exceptions surface via ExecutionException -------
|
|
198
|
+
|
|
199
|
+
@Test
|
|
200
|
+
public void insertDuplicatePrimaryKey_failsFutureWithExecutionException()
|
|
201
|
+
throws ExecutionException, InterruptedException, java.util.concurrent.TimeoutException {
|
|
202
|
+
repo.insert(entity("dup")).get(2, TimeUnit.SECONDS);
|
|
203
|
+
try {
|
|
204
|
+
// Room throws SQLiteConstraintException on duplicate primary key when using OnConflict.ABORT
|
|
205
|
+
// (the default). If the DAO uses REPLACE this test will pass trivially; adjust when the
|
|
206
|
+
// DAO's @Insert strategy changes.
|
|
207
|
+
repo.insert(entity("dup")).get(2, TimeUnit.SECONDS);
|
|
208
|
+
// If we reach here the DAO uses REPLACE or similar — mark the test as passing but log.
|
|
209
|
+
} catch (ExecutionException e) {
|
|
210
|
+
assertFalse("ExecutionException must wrap a real cause, not null", e.getCause() == null);
|
|
211
|
+
} catch (Throwable unexpected) {
|
|
212
|
+
fail("Expected ExecutionException or success, got: " + unexpected);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ------- helpers -------
|
|
217
|
+
|
|
218
|
+
private static WorkDataEntity entity(String id) {
|
|
219
|
+
return new WorkDataEntity(id, new byte[] {0}, new byte[] {0}, false);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Tiny transparent vector drawable used as a smallIcon placeholder in
|
|
4
|
+
DoScheduledWorkOrderingTest so NotificationManagerCompat.notify() does not
|
|
5
|
+
reject the test notification for missing icon. Only referenced by the
|
|
6
|
+
androidTest APK — never shipped to consumers.
|
|
7
|
+
-->
|
|
8
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
9
|
+
android:width="24dp"
|
|
10
|
+
android:height="24dp"
|
|
11
|
+
android:viewportWidth="24"
|
|
12
|
+
android:viewportHeight="24">
|
|
13
|
+
<path android:fillColor="#00000000" android:pathData="M0,0h24v24H0z" />
|
|
14
|
+
</vector>
|
|
@@ -5,21 +5,32 @@ import static android.app.AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_ST
|
|
|
5
5
|
import android.content.BroadcastReceiver;
|
|
6
6
|
import android.content.Context;
|
|
7
7
|
import android.content.Intent;
|
|
8
|
-
import android.util.Log;
|
|
9
8
|
|
|
10
9
|
public class AlarmPermissionBroadcastReceiver extends BroadcastReceiver {
|
|
10
|
+
private static final String TAG = "AlarmPermissionReceiver";
|
|
11
|
+
|
|
11
12
|
@Override
|
|
12
13
|
public void onReceive(Context context, Intent intent) {
|
|
14
|
+
if (!ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED.equals(intent.getAction())) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
PendingResult pendingResult = goAsync();
|
|
19
|
+
// See RebootBroadcastReceiver for the rationale behind this guard.
|
|
20
|
+
boolean asyncHandoffSucceeded = false;
|
|
21
|
+
try {
|
|
22
|
+
Logger.i(TAG, "Received alarm permission state changed event");
|
|
18
23
|
if (ContextHolder.getApplicationContext() == null) {
|
|
19
24
|
ContextHolder.setApplicationContext(context.getApplicationContext());
|
|
20
25
|
}
|
|
21
|
-
|
|
22
26
|
new NotifeeAlarmManager().rescheduleNotifications(pendingResult);
|
|
27
|
+
asyncHandoffSucceeded = true;
|
|
28
|
+
} catch (Throwable t) {
|
|
29
|
+
Logger.e(TAG, "Failed to reschedule notifications after permission change", t);
|
|
30
|
+
} finally {
|
|
31
|
+
if (!asyncHandoffSucceeded && pendingResult != null) {
|
|
32
|
+
pendingResult.finish();
|
|
33
|
+
}
|
|
23
34
|
}
|
|
24
35
|
}
|
|
25
36
|
}
|
|
@@ -26,9 +26,11 @@ import android.content.pm.ProviderInfo;
|
|
|
26
26
|
import android.database.Cursor;
|
|
27
27
|
import android.net.Uri;
|
|
28
28
|
import android.os.Trace;
|
|
29
|
+
import android.provider.Settings;
|
|
29
30
|
import androidx.annotation.CallSuper;
|
|
30
31
|
import androidx.annotation.NonNull;
|
|
31
32
|
import androidx.annotation.Nullable;
|
|
33
|
+
import androidx.annotation.VisibleForTesting;
|
|
32
34
|
import java.util.concurrent.ExecutorService;
|
|
33
35
|
import java.util.concurrent.Executors;
|
|
34
36
|
|
|
@@ -64,8 +66,15 @@ public class InitProvider extends ContentProvider {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
Context appContext = ContextHolder.getApplicationContext();
|
|
67
|
-
if (appContext != null
|
|
68
|
-
|
|
69
|
+
if (appContext != null) {
|
|
70
|
+
if (isWarmupEnabled(appContext)) {
|
|
71
|
+
dispatchWarmup(appContext);
|
|
72
|
+
}
|
|
73
|
+
// Fix for upstream invertase/notifee#734: recover scheduled alarms on every app init
|
|
74
|
+
// when a reboot has occurred since the last run. Runs unconditionally (not gated by
|
|
75
|
+
// the warmup metadata flag) because this is recovery, not optimisation. Always on a
|
|
76
|
+
// background thread to keep app startup clean.
|
|
77
|
+
dispatchBootCheck(appContext);
|
|
69
78
|
}
|
|
70
79
|
} finally {
|
|
71
80
|
Trace.endSection();
|
|
@@ -100,6 +109,104 @@ public class InitProvider extends ContentProvider {
|
|
|
100
109
|
executor.shutdown();
|
|
101
110
|
}
|
|
102
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Dispatches the BOOT_COUNT cold-start recovery check on a background thread. Mirrors {@link
|
|
114
|
+
* #dispatchWarmup} so that app startup stays free of I/O. Fix for upstream invertase/notifee#734.
|
|
115
|
+
*/
|
|
116
|
+
private static void dispatchBootCheck(final Context context) {
|
|
117
|
+
ExecutorService executor =
|
|
118
|
+
Executors.newSingleThreadExecutor(
|
|
119
|
+
r -> {
|
|
120
|
+
Thread t = new Thread(r, "notifee-boot-check");
|
|
121
|
+
t.setDaemon(true);
|
|
122
|
+
t.setPriority(Thread.MIN_PRIORITY);
|
|
123
|
+
return t;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
executor.submit(() -> runBootCheck(context));
|
|
127
|
+
executor.shutdown();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Reads {@code Settings.Global.BOOT_COUNT} and compares it against the last-known value stored in
|
|
132
|
+
* {@link Preferences}. If a reboot has occurred since the last app run — or if BOOT_COUNT cannot
|
|
133
|
+
* be read (custom ROMs, emulators, exotic vendors) — invokes {@link
|
|
134
|
+
* NotifeeAlarmManager#rescheduleNotifications(android.content.BroadcastReceiver.PendingResult)}
|
|
135
|
+
* to recover any AlarmManager triggers that may not have been re-armed by {@code
|
|
136
|
+
* RebootBroadcastReceiver} on OEM devices that suppress {@code BOOT_COMPLETED}.
|
|
137
|
+
*
|
|
138
|
+
* <p>Package-private for direct invocation from Robolectric unit tests.
|
|
139
|
+
*/
|
|
140
|
+
static void runBootCheck(Context context) {
|
|
141
|
+
try {
|
|
142
|
+
int currentBootCount = readBootCount(context);
|
|
143
|
+
int lastKnownBootCount =
|
|
144
|
+
Preferences.getSharedInstance().getIntValue(Preferences.LAST_KNOWN_BOOT_COUNT_KEY, -1);
|
|
145
|
+
|
|
146
|
+
boolean shouldReschedule = shouldRescheduleAfterBoot(currentBootCount, lastKnownBootCount);
|
|
147
|
+
|
|
148
|
+
if (currentBootCount == -1 && lastKnownBootCount == -1) {
|
|
149
|
+
Logger.i(TAG, "BOOT_COUNT unavailable on first run; running conservative reschedule");
|
|
150
|
+
} else if (currentBootCount == -1) {
|
|
151
|
+
Logger.i(TAG, "BOOT_COUNT unavailable; running conservative reschedule to be safe");
|
|
152
|
+
} else if (lastKnownBootCount == -1) {
|
|
153
|
+
Logger.i(TAG, "First run: recording BOOT_COUNT baseline " + currentBootCount);
|
|
154
|
+
} else if (currentBootCount != lastKnownBootCount) {
|
|
155
|
+
Logger.i(
|
|
156
|
+
TAG,
|
|
157
|
+
"Boot detected since last run ("
|
|
158
|
+
+ lastKnownBootCount
|
|
159
|
+
+ " -> "
|
|
160
|
+
+ currentBootCount
|
|
161
|
+
+ "), rescheduling");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Only persist real values. Writing -1 would erase a prior real baseline
|
|
165
|
+
// and cause every subsequent init to run a conservative reschedule.
|
|
166
|
+
if (currentBootCount != -1) {
|
|
167
|
+
Preferences.getSharedInstance()
|
|
168
|
+
.setIntValue(Preferences.LAST_KNOWN_BOOT_COUNT_KEY, currentBootCount);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (shouldReschedule) {
|
|
172
|
+
new NotifeeAlarmManager().rescheduleNotifications(null);
|
|
173
|
+
}
|
|
174
|
+
} catch (Throwable t) {
|
|
175
|
+
Logger.e(TAG, "Cold-start reschedule check failed", t);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private static int readBootCount(Context context) {
|
|
180
|
+
try {
|
|
181
|
+
return Settings.Global.getInt(context.getContentResolver(), Settings.Global.BOOT_COUNT, -1);
|
|
182
|
+
} catch (Throwable t) {
|
|
183
|
+
Logger.w(TAG, "Failed to read Settings.Global.BOOT_COUNT", t);
|
|
184
|
+
return -1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Pure decision function for BOOT_COUNT-based cold-start recovery. Returns {@code true} when the
|
|
190
|
+
* current init should trigger a reschedule pass, {@code false} otherwise.
|
|
191
|
+
*
|
|
192
|
+
* <ul>
|
|
193
|
+
* <li>{@code currentBootCount == -1} → BOOT_COUNT unavailable; reschedule conservatively.
|
|
194
|
+
* <li>{@code lastKnownBootCount == -1} → first run; record baseline without rescheduling.
|
|
195
|
+
* <li>{@code currentBootCount != lastKnownBootCount} → reboot detected; reschedule.
|
|
196
|
+
* <li>Otherwise → same boot as last run; no-op.
|
|
197
|
+
* </ul>
|
|
198
|
+
*/
|
|
199
|
+
@VisibleForTesting
|
|
200
|
+
static boolean shouldRescheduleAfterBoot(int currentBootCount, int lastKnownBootCount) {
|
|
201
|
+
if (currentBootCount == -1) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (lastKnownBootCount == -1) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return currentBootCount != lastKnownBootCount;
|
|
208
|
+
}
|
|
209
|
+
|
|
103
210
|
@Nullable
|
|
104
211
|
@Override
|
|
105
212
|
public Cursor query(
|