@otakit/capacitor-updater 2.1.0 → 2.1.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 +164 -128
- package/android/src/main/java/com/otakit/updater/BundleStore.java +37 -8
- package/android/src/main/java/com/otakit/updater/DeviceEventClient.java +2 -3
- package/android/src/main/java/com/otakit/updater/ManifestClient.java +20 -7
- package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +726 -0
- package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +551 -493
- package/dist/esm/definitions.d.ts +41 -66
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +5 -47
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +4 -3
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +5 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +10 -49
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +10 -49
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +28 -6
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +7 -3
- package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +635 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +1 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +498 -489
- package/package.json +1 -1
|
@@ -5,7 +5,6 @@ import android.content.pm.PackageManager;
|
|
|
5
5
|
import android.os.Build;
|
|
6
6
|
import android.os.Handler;
|
|
7
7
|
import android.os.Looper;
|
|
8
|
-
import com.getcapacitor.JSArray;
|
|
9
8
|
import com.getcapacitor.JSObject;
|
|
10
9
|
import com.getcapacitor.Plugin;
|
|
11
10
|
import com.getcapacitor.PluginCall;
|
|
@@ -19,23 +18,89 @@ import java.net.HttpURLConnection;
|
|
|
19
18
|
import java.net.URL;
|
|
20
19
|
import java.nio.charset.StandardCharsets;
|
|
21
20
|
import java.security.MessageDigest;
|
|
21
|
+
import java.util.concurrent.CountDownLatch;
|
|
22
22
|
import java.util.concurrent.ExecutorService;
|
|
23
23
|
import java.util.concurrent.Executors;
|
|
24
|
-
import java.util.concurrent.atomic.
|
|
24
|
+
import java.util.concurrent.atomic.AtomicReference;
|
|
25
25
|
|
|
26
26
|
@CapacitorPlugin(name = "OtaKit")
|
|
27
27
|
public class UpdaterPlugin extends Plugin {
|
|
28
28
|
|
|
29
|
+
private enum Policy {
|
|
30
|
+
OFF("off"),
|
|
31
|
+
SHADOW("shadow"),
|
|
32
|
+
APPLY_STAGED("apply-staged"),
|
|
33
|
+
IMMEDIATE("immediate");
|
|
34
|
+
|
|
35
|
+
final String value;
|
|
36
|
+
|
|
37
|
+
Policy(String value) {
|
|
38
|
+
this.value = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private static final class CheckResolution {
|
|
43
|
+
|
|
44
|
+
final String kind;
|
|
45
|
+
final ManifestClient.LatestManifest latest;
|
|
46
|
+
final BundleInfo bundle;
|
|
47
|
+
|
|
48
|
+
private CheckResolution(String kind, ManifestClient.LatestManifest latest, BundleInfo bundle) {
|
|
49
|
+
this.kind = kind;
|
|
50
|
+
this.latest = latest;
|
|
51
|
+
this.bundle = bundle;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static CheckResolution noUpdate() {
|
|
55
|
+
return new CheckResolution("no_update", null, null);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static CheckResolution alreadyStaged(ManifestClient.LatestManifest latest, BundleInfo bundle) {
|
|
59
|
+
return new CheckResolution("already_staged", latest, bundle);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static CheckResolution updateAvailable(ManifestClient.LatestManifest latest) {
|
|
63
|
+
return new CheckResolution("update_available", latest, null);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static final class DownloadResolution {
|
|
68
|
+
|
|
69
|
+
final String kind;
|
|
70
|
+
final BundleInfo bundle;
|
|
71
|
+
|
|
72
|
+
private DownloadResolution(String kind, BundleInfo bundle) {
|
|
73
|
+
this.kind = kind;
|
|
74
|
+
this.bundle = bundle;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static DownloadResolution noUpdate() {
|
|
78
|
+
return new DownloadResolution("no_update", null);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static DownloadResolution staged(BundleInfo bundle) {
|
|
82
|
+
return new DownloadResolution("staged", bundle);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@FunctionalInterface
|
|
87
|
+
private interface ThrowingRunnable {
|
|
88
|
+
void run() throws Exception;
|
|
89
|
+
}
|
|
90
|
+
|
|
29
91
|
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
30
92
|
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
31
93
|
private final ZipUtils zipUtils = new ZipUtils();
|
|
32
94
|
|
|
33
95
|
private BundleStore store;
|
|
96
|
+
private UpdaterCoordinator coordinator;
|
|
34
97
|
private Runnable trialTimeoutRunnable;
|
|
35
98
|
|
|
36
99
|
private int appReadyTimeoutMs = 10_000;
|
|
37
100
|
private boolean allowInsecureUrls = false;
|
|
38
|
-
private
|
|
101
|
+
private Policy launchPolicy = Policy.APPLY_STAGED;
|
|
102
|
+
private Policy resumePolicy = Policy.SHADOW;
|
|
103
|
+
private Policy runtimePolicy = Policy.IMMEDIATE;
|
|
39
104
|
private String ingestUrl;
|
|
40
105
|
private String cdnUrl;
|
|
41
106
|
private String appId;
|
|
@@ -43,17 +108,14 @@ public class UpdaterPlugin extends Plugin {
|
|
|
43
108
|
private String runtimeVersion;
|
|
44
109
|
private java.util.List<ManifestVerifier.KeyEntry> manifestKeys = new java.util.ArrayList<>();
|
|
45
110
|
private long checkIntervalMs = 600_000;
|
|
46
|
-
private
|
|
111
|
+
private boolean coldStartInProgress = false;
|
|
47
112
|
private static final String DEFAULT_INGEST_URL = "https://ingest.otakit.app/v1";
|
|
48
113
|
private static final String DEFAULT_CDN_URL = "https://cdn.otakit.app";
|
|
49
114
|
private static final String INGEST_PATH_SUFFIX = "/v1";
|
|
50
|
-
private static final String UPDATE_MODE_MANUAL = "manual";
|
|
51
|
-
private static final String UPDATE_MODE_NEXT_LAUNCH = "next-launch";
|
|
52
|
-
private static final String UPDATE_MODE_NEXT_RESUME = "next-resume";
|
|
53
|
-
private static final String UPDATE_MODE_IMMEDIATE = "immediate";
|
|
54
115
|
private static final String KEY_LAST_CHECK_TIMESTAMP = "last_check_timestamp";
|
|
55
|
-
private static final String
|
|
56
|
-
private static final String
|
|
116
|
+
private static final String DEFAULT_RUNTIME_KEY = "__default__";
|
|
117
|
+
private static final String BUILTIN_ASSET_PATH = "public";
|
|
118
|
+
private UpdaterCoordinator.StartupPreparation pendingStartupPreparation;
|
|
57
119
|
|
|
58
120
|
@Override
|
|
59
121
|
public void load() {
|
|
@@ -78,17 +140,16 @@ public class UpdaterPlugin extends Plugin {
|
|
|
78
140
|
getConfig().getString("ingestUrl"),
|
|
79
141
|
System.getenv("OTAKIT_INGEST_URL")
|
|
80
142
|
);
|
|
81
|
-
this.cdnUrl = resolveCdnUrl(
|
|
82
|
-
getConfig().getString("cdnUrl"),
|
|
83
|
-
System.getenv("OTAKIT_CDN_URL")
|
|
84
|
-
);
|
|
143
|
+
this.cdnUrl = resolveCdnUrl(getConfig().getString("cdnUrl"), System.getenv("OTAKIT_CDN_URL"));
|
|
85
144
|
this.appId = getConfig().getString("appId");
|
|
86
145
|
this.channel = trimToNull(getConfig().getString("channel"));
|
|
87
146
|
this.runtimeVersion = trimToNull(getConfig().getString("runtimeVersion"));
|
|
88
147
|
this.store = new BundleStore(getContext(), builtinVersion, nativeBuild, this.runtimeVersion);
|
|
148
|
+
this.coordinator = new UpdaterCoordinator(this.store);
|
|
89
149
|
this.allowInsecureUrls = getConfig().getBoolean("allowInsecureUrls", false);
|
|
90
|
-
|
|
91
|
-
this.
|
|
150
|
+
this.launchPolicy = resolvePolicy(getConfig().getString("launchPolicy"), Policy.APPLY_STAGED);
|
|
151
|
+
this.resumePolicy = resolvePolicy(getConfig().getString("resumePolicy"), Policy.SHADOW);
|
|
152
|
+
this.runtimePolicy = resolvePolicy(getConfig().getString("runtimePolicy"), Policy.IMMEDIATE);
|
|
92
153
|
|
|
93
154
|
try {
|
|
94
155
|
org.json.JSONArray rawKeys = getConfig().getConfigJSON().optJSONArray("manifestKeys");
|
|
@@ -127,302 +188,337 @@ public class UpdaterPlugin extends Plugin {
|
|
|
127
188
|
}
|
|
128
189
|
|
|
129
190
|
this.appReadyTimeoutMs = Math.max(1000, getConfig().getInt("appReadyTimeout", 10_000));
|
|
130
|
-
this.checkIntervalMs =
|
|
191
|
+
this.checkIntervalMs = getConfig().getInt("checkInterval", 600_000);
|
|
131
192
|
|
|
132
193
|
pruneIncompatibleBundles();
|
|
133
194
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
195
|
+
UpdaterCoordinator.StartupPreparation startup = coordinator.normalizeStartupState(
|
|
196
|
+
this::isBundleUsable
|
|
197
|
+
);
|
|
198
|
+
coordinator.cleanupBundles(startup.cleanupBundleIds);
|
|
199
|
+
pendingStartupPreparation = startup;
|
|
200
|
+
}
|
|
139
201
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
202
|
+
@Override
|
|
203
|
+
protected void handleOnStart() {
|
|
204
|
+
super.handleOnStart();
|
|
205
|
+
consumePendingStartupPreparation();
|
|
206
|
+
}
|
|
143
207
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
208
|
+
@Override
|
|
209
|
+
protected void handleOnResume() {
|
|
210
|
+
super.handleOnResume();
|
|
211
|
+
if (coldStartInProgress) {
|
|
212
|
+
coldStartInProgress = false;
|
|
213
|
+
return;
|
|
148
214
|
}
|
|
215
|
+
handleResume();
|
|
216
|
+
}
|
|
149
217
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
218
|
+
private boolean shouldSkipCheckInterval() {
|
|
219
|
+
if (checkIntervalMs <= 0) return false;
|
|
220
|
+
long lastCheck = store.getPrefs().getLong(KEY_LAST_CHECK_TIMESTAMP, 0);
|
|
221
|
+
if (lastCheck <= 0) return false;
|
|
222
|
+
long elapsed = System.currentTimeMillis() - lastCheck;
|
|
223
|
+
return elapsed < checkIntervalMs;
|
|
224
|
+
}
|
|
154
225
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
226
|
+
private void recordCheckTimestamp() {
|
|
227
|
+
store.getPrefs().edit().putLong(KEY_LAST_CHECK_TIMESTAMP, System.currentTimeMillis()).apply();
|
|
158
228
|
}
|
|
159
229
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
230
|
+
private void dispatchColdStart() {
|
|
231
|
+
if (isRuntimeUnresolved()) {
|
|
232
|
+
handleRuntime();
|
|
233
|
+
} else {
|
|
234
|
+
handleLaunch();
|
|
165
235
|
}
|
|
166
236
|
}
|
|
167
237
|
|
|
168
|
-
private void
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
238
|
+
private void consumePendingStartupPreparation() {
|
|
239
|
+
UpdaterCoordinator.StartupPreparation startup = pendingStartupPreparation;
|
|
240
|
+
if (startup == null) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
pendingStartupPreparation = null;
|
|
172
244
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
reloadWebView();
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
245
|
+
if (startup.activationPath != null && !startup.activationPath.isEmpty()) {
|
|
246
|
+
try {
|
|
247
|
+
applyServerBasePathSynchronously(startup.activationPath);
|
|
248
|
+
} catch (Exception e) {
|
|
249
|
+
android.util.Log.w("OtaKit", "startup activation failed", e);
|
|
181
250
|
}
|
|
251
|
+
}
|
|
182
252
|
|
|
183
|
-
|
|
253
|
+
if (startup.eventPayload != null) {
|
|
254
|
+
sendDeviceEvent(startup.eventPayload);
|
|
184
255
|
}
|
|
185
256
|
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
) {
|
|
191
|
-
return;
|
|
257
|
+
if (startup.trialBundleId != null) {
|
|
258
|
+
scheduleTrialTimeout(startup.trialBundleId);
|
|
259
|
+
} else {
|
|
260
|
+
cancelTrialTimeout();
|
|
192
261
|
}
|
|
193
262
|
|
|
194
|
-
|
|
195
|
-
|
|
263
|
+
coldStartInProgress = true;
|
|
264
|
+
dispatchColdStart();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private void handleRuntime() {
|
|
268
|
+
switch (runtimePolicy) {
|
|
269
|
+
case OFF:
|
|
270
|
+
resolveCurrentRuntimeKey();
|
|
271
|
+
return;
|
|
272
|
+
case APPLY_STAGED:
|
|
273
|
+
boolean hasStagedBundle =
|
|
274
|
+
coordinator.snapshotState(
|
|
275
|
+
bundle -> isCompatibleRuntime(bundle) && isBundleUsable(bundle)
|
|
276
|
+
).staged !=
|
|
277
|
+
null;
|
|
278
|
+
if (hasStagedBundle) {
|
|
279
|
+
resolveCurrentRuntimeKey();
|
|
280
|
+
try {
|
|
281
|
+
if (!applyStaged(false)) {
|
|
282
|
+
android.util.Log.w(
|
|
283
|
+
"OtaKit",
|
|
284
|
+
"Failed to apply a valid staged bundle during runtime handling"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} catch (Exception e) {
|
|
288
|
+
android.util.Log.w("OtaKit", "runtime apply-staged failed", e);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
executeAutomaticUpdate("runtime apply-staged fallback", () -> {
|
|
293
|
+
downloadLatest(false, null);
|
|
294
|
+
resolveCurrentRuntimeKey();
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
case SHADOW:
|
|
298
|
+
executeAutomaticUpdate("runtime shadow", () -> {
|
|
299
|
+
downloadLatest(false, null);
|
|
300
|
+
resolveCurrentRuntimeKey();
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
case IMMEDIATE:
|
|
304
|
+
executeAutomaticUpdate("runtime immediate", () -> {
|
|
305
|
+
DownloadResolution result = downloadLatest(false, null);
|
|
306
|
+
if ("no_update".equals(result.kind)) {
|
|
307
|
+
resolveCurrentRuntimeKey();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
resolveCurrentRuntimeKey();
|
|
311
|
+
requireApplyStaged(true);
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
196
316
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
317
|
+
private void handleLaunch() {
|
|
318
|
+
switch (launchPolicy) {
|
|
319
|
+
case OFF:
|
|
320
|
+
return;
|
|
321
|
+
case APPLY_STAGED:
|
|
200
322
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
activateStagedBundleForReload();
|
|
204
|
-
reloadWebView();
|
|
323
|
+
if (applyStaged(false)) {
|
|
324
|
+
return;
|
|
205
325
|
}
|
|
206
326
|
} catch (Exception e) {
|
|
207
|
-
android.util.Log.w("OtaKit", "
|
|
208
|
-
|
|
209
|
-
checkInProgress.set(false);
|
|
327
|
+
android.util.Log.w("OtaKit", "launch apply-staged failed", e);
|
|
328
|
+
return;
|
|
210
329
|
}
|
|
211
|
-
|
|
212
|
-
|
|
330
|
+
executeAutomaticUpdate("launch apply-staged fallback", () -> downloadLatest(false, null));
|
|
331
|
+
return;
|
|
332
|
+
case SHADOW:
|
|
333
|
+
executeAutomaticUpdate("launch shadow", () -> downloadLatest(false, null));
|
|
334
|
+
return;
|
|
335
|
+
case IMMEDIATE:
|
|
336
|
+
executeAutomaticUpdate("launch immediate", () -> {
|
|
337
|
+
DownloadResolution result = downloadLatest(false, null);
|
|
338
|
+
if ("staged".equals(result.kind)) {
|
|
339
|
+
requireApplyStaged(true);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private void handleResume() {
|
|
347
|
+
switch (resumePolicy) {
|
|
348
|
+
case OFF:
|
|
349
|
+
return;
|
|
350
|
+
case APPLY_STAGED:
|
|
351
|
+
executeAutomaticUpdate("resume apply-staged", () -> {
|
|
352
|
+
if (applyStaged(true)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
downloadLatest(true, null);
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
case SHADOW:
|
|
359
|
+
executeAutomaticUpdate("resume shadow", () -> downloadLatest(true, null));
|
|
360
|
+
return;
|
|
361
|
+
case IMMEDIATE:
|
|
362
|
+
executeAutomaticUpdate("resume immediate", () -> {
|
|
363
|
+
DownloadResolution result = downloadLatest(false, null);
|
|
364
|
+
if ("staged".equals(result.kind)) {
|
|
365
|
+
requireApplyStaged(true);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
return;
|
|
213
369
|
}
|
|
370
|
+
}
|
|
214
371
|
|
|
215
|
-
|
|
372
|
+
private void executeAutomaticUpdate(String label, ThrowingRunnable operation) {
|
|
373
|
+
if (!coordinator.tryBeginOperation()) {
|
|
374
|
+
android.util.Log.d("OtaKit", "Skipping " + label + ": update already in progress");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
216
377
|
executor.execute(() -> {
|
|
217
378
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// Check failed — timestamp not recorded, will retry on next trigger
|
|
379
|
+
operation.run();
|
|
380
|
+
} catch (Exception e) {
|
|
381
|
+
android.util.Log.w("OtaKit", label + " failed", e);
|
|
222
382
|
} finally {
|
|
223
|
-
|
|
383
|
+
coordinator.endOperation();
|
|
224
384
|
}
|
|
225
385
|
});
|
|
226
386
|
}
|
|
227
387
|
|
|
228
|
-
private
|
|
229
|
-
|
|
230
|
-
if (lastCheck <= 0) return false;
|
|
231
|
-
long elapsed = System.currentTimeMillis() - lastCheck;
|
|
232
|
-
return elapsed < checkIntervalMs;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private void recordCheckTimestamp() {
|
|
236
|
-
store.getPrefs().edit()
|
|
237
|
-
.putLong(KEY_LAST_CHECK_TIMESTAMP, System.currentTimeMillis())
|
|
238
|
-
.apply();
|
|
388
|
+
private String currentRuntimeKey() {
|
|
389
|
+
return runtimeVersion != null ? runtimeVersion : DEFAULT_RUNTIME_KEY;
|
|
239
390
|
}
|
|
240
391
|
|
|
241
|
-
private boolean
|
|
242
|
-
return
|
|
392
|
+
private boolean isRuntimeUnresolved() {
|
|
393
|
+
return coordinator.isRuntimeUnresolved(currentRuntimeKey());
|
|
243
394
|
}
|
|
244
395
|
|
|
245
|
-
private
|
|
246
|
-
|
|
396
|
+
private void resolveCurrentRuntimeKey() {
|
|
397
|
+
coordinator.resolveRuntimeKey(currentRuntimeKey());
|
|
247
398
|
}
|
|
248
399
|
|
|
249
|
-
private
|
|
250
|
-
|
|
251
|
-
|
|
400
|
+
private Policy resolvePolicy(String configured, Policy defaultPolicy) {
|
|
401
|
+
String raw = configured == null ? "" : configured.trim().toLowerCase();
|
|
402
|
+
if (raw.isEmpty()) {
|
|
403
|
+
return defaultPolicy;
|
|
252
404
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (raw.isEmpty() || UPDATE_MODE_NEXT_LAUNCH.equals(raw)) {
|
|
256
|
-
return UPDATE_MODE_NEXT_LAUNCH;
|
|
405
|
+
if (Policy.OFF.value.equals(raw)) {
|
|
406
|
+
return Policy.OFF;
|
|
257
407
|
}
|
|
258
|
-
if (
|
|
259
|
-
return
|
|
408
|
+
if (Policy.SHADOW.value.equals(raw)) {
|
|
409
|
+
return Policy.SHADOW;
|
|
260
410
|
}
|
|
261
|
-
if (
|
|
262
|
-
return
|
|
411
|
+
if (Policy.APPLY_STAGED.value.equals(raw)) {
|
|
412
|
+
return Policy.APPLY_STAGED;
|
|
263
413
|
}
|
|
264
|
-
if (
|
|
265
|
-
return
|
|
414
|
+
if (Policy.IMMEDIATE.value.equals(raw)) {
|
|
415
|
+
return Policy.IMMEDIATE;
|
|
266
416
|
}
|
|
267
|
-
|
|
268
417
|
android.util.Log.w(
|
|
269
418
|
"OtaKit",
|
|
270
|
-
"Unknown
|
|
419
|
+
"Unknown policy '" + raw + "', defaulting to '" + defaultPolicy.value + "'"
|
|
271
420
|
);
|
|
272
|
-
return
|
|
421
|
+
return defaultPolicy;
|
|
273
422
|
}
|
|
274
423
|
|
|
275
424
|
@PluginMethod
|
|
276
425
|
public void getState(PluginCall call) {
|
|
426
|
+
UpdaterCoordinator.StateSnapshot snapshot = coordinator.snapshotState(this::isBundleUsable);
|
|
277
427
|
JSObject result = new JSObject();
|
|
278
|
-
result.put("current",
|
|
279
|
-
result.put("fallback",
|
|
280
|
-
result.put("builtinVersion",
|
|
281
|
-
|
|
282
|
-
String stagedId = store.getStagedBundleId();
|
|
283
|
-
if (stagedId != null) {
|
|
284
|
-
BundleInfo staged = store.getBundle(stagedId);
|
|
285
|
-
if (staged == null) {
|
|
286
|
-
store.setStagedBundleId(null);
|
|
287
|
-
}
|
|
288
|
-
result.put("staged", staged != null ? staged.toJSObject() : null);
|
|
289
|
-
} else {
|
|
290
|
-
result.put("staged", null);
|
|
291
|
-
}
|
|
292
|
-
|
|
428
|
+
result.put("current", snapshot.current.toJSObject());
|
|
429
|
+
result.put("fallback", snapshot.fallback.toJSObject());
|
|
430
|
+
result.put("builtinVersion", snapshot.builtinVersion);
|
|
431
|
+
result.put("staged", snapshot.staged != null ? snapshot.staged.toJSObject() : null);
|
|
293
432
|
call.resolve(result);
|
|
294
433
|
}
|
|
295
434
|
|
|
296
435
|
@PluginMethod
|
|
297
436
|
public void check(PluginCall call) {
|
|
298
|
-
if (!
|
|
299
|
-
|
|
300
|
-
if (stagedId != null) {
|
|
301
|
-
BundleInfo staged = store.getBundle(stagedId);
|
|
302
|
-
if (staged != null) {
|
|
303
|
-
JSObject result = new JSObject();
|
|
304
|
-
result.put("version", staged.version);
|
|
305
|
-
result.put("url", "");
|
|
306
|
-
result.put("sha256", staged.sha256 != null ? staged.sha256 : "");
|
|
307
|
-
result.put("size", 0);
|
|
308
|
-
result.put("downloaded", true);
|
|
309
|
-
if (staged.runtimeVersion != null) {
|
|
310
|
-
result.put("runtimeVersion", staged.runtimeVersion);
|
|
311
|
-
}
|
|
312
|
-
if (staged.releaseId != null) {
|
|
313
|
-
result.put("releaseId", staged.releaseId);
|
|
314
|
-
}
|
|
315
|
-
call.resolve(result);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
call.resolve((JSObject) null);
|
|
437
|
+
if (!coordinator.tryBeginOperation()) {
|
|
438
|
+
call.reject("Another update operation is already in progress");
|
|
320
439
|
return;
|
|
321
440
|
}
|
|
322
|
-
String targetChannel = resolveTargetChannel(null);
|
|
323
441
|
executor.execute(() -> {
|
|
324
442
|
try {
|
|
325
|
-
|
|
326
|
-
if (latest == null) {
|
|
327
|
-
call.resolve((JSObject) null);
|
|
328
|
-
} else if (isCurrentBundleLatest(latest, targetChannel)) {
|
|
329
|
-
call.resolve((JSObject) null);
|
|
330
|
-
} else {
|
|
331
|
-
BundleInfo staged = findMatchingStagedBundle(latest, targetChannel);
|
|
332
|
-
call.resolve(manifestToJSObject(latest, staged != null));
|
|
333
|
-
}
|
|
443
|
+
call.resolve(checkResolutionToJSObject(checkLatest(false, null)));
|
|
334
444
|
} catch (Exception e) {
|
|
335
445
|
call.reject("check failed: " + e.getMessage());
|
|
336
446
|
} finally {
|
|
337
|
-
|
|
447
|
+
coordinator.endOperation();
|
|
338
448
|
}
|
|
339
449
|
});
|
|
340
450
|
}
|
|
341
451
|
|
|
342
452
|
@PluginMethod
|
|
343
453
|
public void download(PluginCall call) {
|
|
344
|
-
if (!
|
|
345
|
-
|
|
346
|
-
if (stagedId != null) {
|
|
347
|
-
BundleInfo staged = store.getBundle(stagedId);
|
|
348
|
-
if (staged != null) {
|
|
349
|
-
call.resolve(staged.toJSObject());
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
call.resolve((JSObject) null);
|
|
454
|
+
if (!coordinator.tryBeginOperation()) {
|
|
455
|
+
call.reject("Another update operation is already in progress");
|
|
354
456
|
return;
|
|
355
457
|
}
|
|
356
458
|
executor.execute(() -> {
|
|
357
459
|
try {
|
|
358
|
-
|
|
359
|
-
if (bundle == null) {
|
|
360
|
-
call.resolve((JSObject) null);
|
|
361
|
-
} else {
|
|
362
|
-
call.resolve(bundle.toJSObject());
|
|
363
|
-
}
|
|
460
|
+
call.resolve(downloadResolutionToJSObject(downloadLatest(false, null)));
|
|
364
461
|
} catch (Exception e) {
|
|
365
462
|
call.reject("download failed: " + e.getMessage());
|
|
366
463
|
} finally {
|
|
367
|
-
|
|
464
|
+
coordinator.endOperation();
|
|
368
465
|
}
|
|
369
466
|
});
|
|
370
467
|
}
|
|
371
468
|
|
|
372
469
|
@PluginMethod
|
|
373
470
|
public void apply(PluginCall call) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
call.reject("No staged update to apply");
|
|
471
|
+
if (!coordinator.tryBeginOperation()) {
|
|
472
|
+
call.reject("Another update operation is already in progress");
|
|
377
473
|
return;
|
|
378
474
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
475
|
+
executor.execute(() -> {
|
|
476
|
+
try {
|
|
477
|
+
requireApplyStaged(true);
|
|
478
|
+
} catch (Exception e) {
|
|
479
|
+
call.reject("apply failed: " + e.getMessage());
|
|
480
|
+
} finally {
|
|
481
|
+
coordinator.endOperation();
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
@PluginMethod
|
|
487
|
+
public void update(PluginCall call) {
|
|
488
|
+
if (!coordinator.tryBeginOperation()) {
|
|
489
|
+
call.reject("Another update operation is already in progress");
|
|
382
490
|
return;
|
|
383
491
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
492
|
+
executor.execute(() -> {
|
|
493
|
+
try {
|
|
494
|
+
DownloadResolution result = downloadLatest(false, null);
|
|
495
|
+
if ("staged".equals(result.kind)) {
|
|
496
|
+
requireApplyStaged(true);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
call.resolve();
|
|
500
|
+
} catch (Exception e) {
|
|
501
|
+
call.reject("update failed: " + e.getMessage());
|
|
502
|
+
} finally {
|
|
503
|
+
coordinator.endOperation();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
388
506
|
}
|
|
389
507
|
|
|
390
508
|
@PluginMethod
|
|
391
509
|
public void notifyAppReady(PluginCall call) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
BundleInfo oldFallback = store.getFallbackBundle();
|
|
398
|
-
|
|
399
|
-
store.markStatus(current.id, BundleStatus.SUCCESS);
|
|
400
|
-
store.setFallbackBundleId(current.id);
|
|
401
|
-
BundleInfo updated = store.getBundle(current.id);
|
|
402
|
-
notifyListeners("appReady", updated != null ? updated.toJSObject() : current.toJSObject());
|
|
403
|
-
|
|
404
|
-
sendDeviceEvent(
|
|
405
|
-
"applied",
|
|
406
|
-
current.version,
|
|
407
|
-
current.runtimeVersion,
|
|
408
|
-
current.channel,
|
|
409
|
-
current.releaseId,
|
|
410
|
-
null
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
if (!oldFallback.isBuiltin() && !oldFallback.id.equals(current.id)) {
|
|
414
|
-
try {
|
|
415
|
-
store.deleteBundle(oldFallback.id);
|
|
416
|
-
} catch (Exception ignored) {}
|
|
417
|
-
}
|
|
510
|
+
cancelTrialTimeout();
|
|
511
|
+
UpdaterCoordinator.NotifyReadyPreparation preparation = coordinator.prepareNotifyAppReady();
|
|
512
|
+
coordinator.cleanupBundles(preparation.cleanupBundleIds);
|
|
513
|
+
if (preparation.eventPayload != null) {
|
|
514
|
+
sendDeviceEvent(preparation.eventPayload);
|
|
418
515
|
}
|
|
419
|
-
|
|
420
516
|
call.resolve();
|
|
421
517
|
}
|
|
422
518
|
|
|
423
519
|
@PluginMethod
|
|
424
520
|
public void getLastFailure(PluginCall call) {
|
|
425
|
-
BundleInfo failed =
|
|
521
|
+
BundleInfo failed = coordinator.lastFailure();
|
|
426
522
|
if (failed == null) {
|
|
427
523
|
call.resolve((JSObject) null);
|
|
428
524
|
return;
|
|
@@ -445,65 +541,109 @@ public class UpdaterPlugin extends Plugin {
|
|
|
445
541
|
);
|
|
446
542
|
}
|
|
447
543
|
|
|
448
|
-
private
|
|
544
|
+
private CheckResolution checkLatest(boolean respectInterval, String channel) throws Exception {
|
|
449
545
|
String targetChannel = resolveTargetChannel(channel);
|
|
546
|
+
if (respectInterval && shouldSkipCheckInterval()) {
|
|
547
|
+
android.util.Log.d("OtaKit", "Skipping resume check: checkInterval has not elapsed");
|
|
548
|
+
return CheckResolution.noUpdate();
|
|
549
|
+
}
|
|
550
|
+
|
|
450
551
|
ManifestClient.LatestManifest latest = fetchLatest(targetChannel);
|
|
451
552
|
if (latest == null) {
|
|
452
|
-
if (
|
|
453
|
-
|
|
553
|
+
if (respectInterval) {
|
|
554
|
+
recordCheckTimestamp();
|
|
454
555
|
}
|
|
455
|
-
return
|
|
556
|
+
return CheckResolution.noUpdate();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
CheckResolution resolution = classifyLatestManifest(latest, targetChannel);
|
|
560
|
+
|
|
561
|
+
if (respectInterval) {
|
|
562
|
+
recordCheckTimestamp();
|
|
456
563
|
}
|
|
564
|
+
return resolution;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private DownloadResolution downloadLatest(boolean respectInterval, String channel)
|
|
568
|
+
throws Exception {
|
|
569
|
+
String targetChannel = resolveTargetChannel(channel);
|
|
570
|
+
CheckResolution result = checkLatest(respectInterval, channel);
|
|
571
|
+
switch (result.kind) {
|
|
572
|
+
case "no_update":
|
|
573
|
+
return DownloadResolution.noUpdate();
|
|
574
|
+
case "already_staged":
|
|
575
|
+
return DownloadResolution.staged(result.bundle);
|
|
576
|
+
case "update_available":
|
|
577
|
+
try {
|
|
578
|
+
return DownloadResolution.staged(downloadLatestManifest(result.latest, targetChannel));
|
|
579
|
+
} catch (Exception e) {
|
|
580
|
+
if (!isExpiredURLError(e)) {
|
|
581
|
+
throw e;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
ManifestClient.LatestManifest refreshed = fetchLatest(targetChannel);
|
|
585
|
+
if (refreshed == null) {
|
|
586
|
+
return DownloadResolution.noUpdate();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
CheckResolution refreshedResolution = classifyLatestManifest(refreshed, targetChannel);
|
|
590
|
+
switch (refreshedResolution.kind) {
|
|
591
|
+
case "no_update":
|
|
592
|
+
return DownloadResolution.noUpdate();
|
|
593
|
+
case "already_staged":
|
|
594
|
+
return DownloadResolution.staged(refreshedResolution.bundle);
|
|
595
|
+
case "update_available":
|
|
596
|
+
return DownloadResolution.staged(
|
|
597
|
+
downloadLatestManifest(refreshedResolution.latest, targetChannel)
|
|
598
|
+
);
|
|
599
|
+
default:
|
|
600
|
+
throw new IllegalStateException(
|
|
601
|
+
"Unknown refreshed check result: " + refreshedResolution.kind
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
default:
|
|
606
|
+
throw new IllegalStateException("Unknown check result: " + result.kind);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
457
609
|
|
|
610
|
+
private CheckResolution classifyLatestManifest(
|
|
611
|
+
ManifestClient.LatestManifest latest,
|
|
612
|
+
String targetChannel
|
|
613
|
+
) throws Exception {
|
|
458
614
|
if (!isCompatibleRuntime(latest.runtimeVersion)) {
|
|
459
615
|
throw new IllegalStateException(
|
|
460
616
|
"Manifest runtimeVersion does not match the installed app runtime"
|
|
461
617
|
);
|
|
462
618
|
}
|
|
463
619
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return null;
|
|
620
|
+
UpdaterCoordinator.LatestManifestClassification classification =
|
|
621
|
+
coordinator.classifyLatestManifest(latest, targetChannel, this::isBundleUsable);
|
|
622
|
+
if ("no_update".equals(classification.kind)) {
|
|
623
|
+
return CheckResolution.noUpdate();
|
|
469
624
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (emitEvents) {
|
|
473
|
-
notifyListeners("updateAvailable", manifestToJSObject(latest, staged != null));
|
|
625
|
+
if ("already_staged".equals(classification.kind)) {
|
|
626
|
+
return CheckResolution.alreadyStaged(latest, classification.bundle);
|
|
474
627
|
}
|
|
628
|
+
return CheckResolution.updateAvailable(latest);
|
|
629
|
+
}
|
|
475
630
|
|
|
476
|
-
|
|
477
|
-
|
|
631
|
+
private JSObject checkResolutionToJSObject(CheckResolution result) {
|
|
632
|
+
JSObject object = new JSObject();
|
|
633
|
+
object.put("kind", result.kind);
|
|
634
|
+
if (result.latest != null) {
|
|
635
|
+
object.put("latest", manifestToJSObject(result.latest));
|
|
478
636
|
}
|
|
637
|
+
return object;
|
|
638
|
+
}
|
|
479
639
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
latest.size,
|
|
486
|
-
latest.runtimeVersion,
|
|
487
|
-
targetChannel,
|
|
488
|
-
latest.releaseId
|
|
489
|
-
);
|
|
490
|
-
} catch (Exception e) {
|
|
491
|
-
if (isExpiredURLError(e)) {
|
|
492
|
-
// Download URL may have expired — re-fetch manifest once and retry
|
|
493
|
-
ManifestClient.LatestManifest refreshed = fetchLatest(targetChannel);
|
|
494
|
-
if (refreshed == null) throw e;
|
|
495
|
-
return downloadAndStage(
|
|
496
|
-
new URL(refreshed.url),
|
|
497
|
-
refreshed.version,
|
|
498
|
-
refreshed.sha256,
|
|
499
|
-
refreshed.size,
|
|
500
|
-
refreshed.runtimeVersion,
|
|
501
|
-
targetChannel,
|
|
502
|
-
refreshed.releaseId
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
throw e;
|
|
640
|
+
private JSObject downloadResolutionToJSObject(DownloadResolution result) {
|
|
641
|
+
JSObject object = new JSObject();
|
|
642
|
+
object.put("kind", result.kind);
|
|
643
|
+
if (result.bundle != null) {
|
|
644
|
+
object.put("bundle", result.bundle.toJSObject());
|
|
506
645
|
}
|
|
646
|
+
return object;
|
|
507
647
|
}
|
|
508
648
|
|
|
509
649
|
private boolean isExpiredURLError(Exception e) {
|
|
@@ -518,11 +658,6 @@ public class UpdaterPlugin extends Plugin {
|
|
|
518
658
|
);
|
|
519
659
|
}
|
|
520
660
|
|
|
521
|
-
private BundleInfo downloadAndStage(URL url, String version, String expectedSha256)
|
|
522
|
-
throws Exception {
|
|
523
|
-
return downloadAndStage(url, version, expectedSha256, 0, null, null, null);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
661
|
private BundleInfo downloadAndStage(
|
|
527
662
|
URL url,
|
|
528
663
|
String version,
|
|
@@ -549,10 +684,6 @@ public class UpdaterPlugin extends Plugin {
|
|
|
549
684
|
}
|
|
550
685
|
}
|
|
551
686
|
|
|
552
|
-
JSObject start = new JSObject();
|
|
553
|
-
start.put("version", version);
|
|
554
|
-
notifyListeners("downloadStarted", start);
|
|
555
|
-
|
|
556
687
|
File downloadedZip = null;
|
|
557
688
|
File extractedDirectory = null;
|
|
558
689
|
|
|
@@ -573,8 +704,8 @@ public class UpdaterPlugin extends Plugin {
|
|
|
573
704
|
zipUtils.extractSecurely(downloadedZip, extractedDirectory);
|
|
574
705
|
File bundleRoot = resolveBundleRoot(extractedDirectory);
|
|
575
706
|
|
|
576
|
-
String bundleId = buildBundleId(version);
|
|
577
|
-
File destination =
|
|
707
|
+
String bundleId = buildBundleId(version, releaseId, expectedSha256);
|
|
708
|
+
File destination = coordinator.bundleDirectory(bundleId);
|
|
578
709
|
if (destination.exists()) {
|
|
579
710
|
deleteRecursively(destination);
|
|
580
711
|
}
|
|
@@ -591,20 +722,20 @@ public class UpdaterPlugin extends Plugin {
|
|
|
591
722
|
channel,
|
|
592
723
|
releaseId
|
|
593
724
|
);
|
|
594
|
-
String
|
|
595
|
-
|
|
596
|
-
store.setStagedBundleId(bundleId);
|
|
597
|
-
cleanupSupersededStagedBundle(previousStagedId, bundleId);
|
|
725
|
+
java.util.List<String> cleanupBundleIds = coordinator.stageDownloadedBundle(info);
|
|
726
|
+
coordinator.cleanupBundles(cleanupBundleIds);
|
|
598
727
|
|
|
599
|
-
notifyListeners("downloadComplete", info.toJSObject());
|
|
600
728
|
sendDeviceEvent("downloaded", version, runtimeVersion, channel, releaseId, null);
|
|
601
729
|
return info;
|
|
602
730
|
} catch (Exception e) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
731
|
+
sendDeviceEvent(
|
|
732
|
+
"download_error",
|
|
733
|
+
version,
|
|
734
|
+
runtimeVersion,
|
|
735
|
+
channel,
|
|
736
|
+
releaseId,
|
|
737
|
+
e.getMessage()
|
|
738
|
+
);
|
|
608
739
|
throw e;
|
|
609
740
|
} finally {
|
|
610
741
|
if (downloadedZip != null && downloadedZip.exists()) {
|
|
@@ -668,71 +799,39 @@ public class UpdaterPlugin extends Plugin {
|
|
|
668
799
|
throw new IllegalStateException("Bundle archive does not contain index.html");
|
|
669
800
|
}
|
|
670
801
|
|
|
671
|
-
private
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
store.setStagedBundleId(null);
|
|
680
|
-
return store.getCurrentBundle();
|
|
681
|
-
}
|
|
682
|
-
if (!isCompatibleRuntime(staged)) {
|
|
683
|
-
try {
|
|
684
|
-
store.deleteBundle(staged.id);
|
|
685
|
-
} catch (Exception ignored) {}
|
|
686
|
-
return store.getCurrentBundle();
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
store.setCurrentBundleId(staged.id);
|
|
690
|
-
store.setStagedBundleId(null);
|
|
691
|
-
return staged;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
private void activateStagedBundleForReload() {
|
|
695
|
-
String stagedId = store.getStagedBundleId();
|
|
696
|
-
if (stagedId == null) {
|
|
697
|
-
return;
|
|
802
|
+
private boolean applyStaged(boolean reloadAfterApply) throws Exception {
|
|
803
|
+
UpdaterCoordinator.ApplyPreparation preparation = coordinator.prepareApplyStaged(
|
|
804
|
+
this::isCompatibleRuntime,
|
|
805
|
+
this::isBundleUsable
|
|
806
|
+
);
|
|
807
|
+
coordinator.cleanupBundles(preparation.cleanupBundleIds);
|
|
808
|
+
if (!preparation.didApply()) {
|
|
809
|
+
return false;
|
|
698
810
|
}
|
|
699
811
|
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
if (!isCompatibleRuntime(staged)) {
|
|
706
|
-
try {
|
|
707
|
-
store.deleteBundle(staged.id);
|
|
708
|
-
} catch (Exception ignored) {}
|
|
709
|
-
return;
|
|
812
|
+
applyServerBasePathSynchronously(preparation.activationPath);
|
|
813
|
+
if (reloadAfterApply) {
|
|
814
|
+
reloadWebViewSynchronously();
|
|
710
815
|
}
|
|
711
816
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (staged.status == BundleStatus.PENDING) {
|
|
716
|
-
store.markStatus(staged.id, BundleStatus.TRIAL);
|
|
717
|
-
staged = store.getBundle(staged.id);
|
|
718
|
-
}
|
|
719
|
-
if (staged != null && staged.status == BundleStatus.TRIAL) {
|
|
720
|
-
scheduleTrialTimeout(staged.id);
|
|
817
|
+
cancelTrialTimeout();
|
|
818
|
+
if (preparation.trialBundleId != null) {
|
|
819
|
+
scheduleTrialTimeout(preparation.trialBundleId);
|
|
721
820
|
}
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
722
823
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
applyServerBasePath(null);
|
|
824
|
+
private void requireApplyStaged(boolean reloadAfterApply) throws Exception {
|
|
825
|
+
if (!applyStaged(reloadAfterApply)) {
|
|
826
|
+
throw new IllegalStateException("Expected a staged bundle to be ready for apply");
|
|
727
827
|
}
|
|
728
828
|
}
|
|
729
829
|
|
|
730
830
|
private void scheduleTrialTimeout(String bundleId) {
|
|
731
831
|
cancelTrialTimeout();
|
|
732
832
|
trialTimeoutRunnable = () -> {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
rollbackCurrentBundle("notify_timeout");
|
|
833
|
+
if (coordinator.isCurrentTrialBundle(bundleId)) {
|
|
834
|
+
rollbackCurrentBundle("notify_timeout", true);
|
|
736
835
|
}
|
|
737
836
|
};
|
|
738
837
|
mainHandler.postDelayed(trialTimeoutRunnable, appReadyTimeoutMs);
|
|
@@ -745,103 +844,93 @@ public class UpdaterPlugin extends Plugin {
|
|
|
745
844
|
}
|
|
746
845
|
}
|
|
747
846
|
|
|
748
|
-
private void rollbackCurrentBundle(String reason) {
|
|
847
|
+
private void rollbackCurrentBundle(String reason, boolean shouldReload) {
|
|
749
848
|
cancelTrialTimeout();
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
849
|
+
UpdaterCoordinator.RollbackPreparation preparation = coordinator.prepareRollback(
|
|
850
|
+
reason,
|
|
851
|
+
this::isBundleUsable
|
|
852
|
+
);
|
|
853
|
+
if (!preparation.didRollback) {
|
|
753
854
|
return;
|
|
754
855
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
store.setFailedBundle(failed);
|
|
759
|
-
store.setStagedBundleId(null);
|
|
760
|
-
|
|
761
|
-
sendDeviceEvent(
|
|
762
|
-
"rollback",
|
|
763
|
-
current.version,
|
|
764
|
-
current.runtimeVersion,
|
|
765
|
-
current.channel,
|
|
766
|
-
current.releaseId,
|
|
767
|
-
reason
|
|
768
|
-
);
|
|
769
|
-
|
|
770
|
-
BundleInfo fallback = store.getFallbackBundle();
|
|
771
|
-
JSObject payload = new JSObject();
|
|
772
|
-
payload.put("from", failed.toJSObject());
|
|
773
|
-
|
|
774
|
-
if (!fallback.isBuiltin() && fallback.path != null) {
|
|
775
|
-
store.setCurrentBundleId(fallback.id);
|
|
776
|
-
applyServerBasePath(fallback.path);
|
|
777
|
-
payload.put("to", fallback.toJSObject());
|
|
778
|
-
} else {
|
|
779
|
-
store.setCurrentBundleId(null);
|
|
780
|
-
applyServerBasePath(null);
|
|
781
|
-
payload.put("to", store.builtinBundle().toJSObject());
|
|
856
|
+
coordinator.cleanupBundles(preparation.cleanupBundleIds);
|
|
857
|
+
if (preparation.eventPayload != null) {
|
|
858
|
+
sendDeviceEvent(preparation.eventPayload);
|
|
782
859
|
}
|
|
783
|
-
payload.put("reason", reason);
|
|
784
|
-
|
|
785
|
-
notifyListeners("rollback", payload);
|
|
786
860
|
|
|
787
861
|
try {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
862
|
+
applyServerBasePathSynchronously(preparation.activationPath);
|
|
863
|
+
if (shouldReload) {
|
|
864
|
+
reloadWebViewSynchronously();
|
|
865
|
+
}
|
|
866
|
+
} catch (Exception e) {
|
|
867
|
+
android.util.Log.w("OtaKit", "rollback activation failed", e);
|
|
868
|
+
}
|
|
792
869
|
}
|
|
793
870
|
|
|
794
|
-
private void
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
871
|
+
private void applyServerBasePathSynchronously(String path) throws Exception {
|
|
872
|
+
runOnMainSynchronously(() -> {
|
|
873
|
+
if (bridge == null) {
|
|
874
|
+
throw new IllegalStateException("Bridge not available for activation");
|
|
875
|
+
}
|
|
876
|
+
if (path == null || path.isEmpty()) {
|
|
877
|
+
bridge.setServerAssetPath(BUILTIN_ASSET_PATH);
|
|
878
|
+
} else {
|
|
879
|
+
bridge.setServerBasePath(path);
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
}
|
|
802
883
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
884
|
+
private void reloadWebViewSynchronously() throws Exception {
|
|
885
|
+
runOnMainSynchronously(() -> {
|
|
886
|
+
if (bridge == null || bridge.getWebView() == null) {
|
|
887
|
+
throw new IllegalStateException("WebView not available for reload");
|
|
888
|
+
}
|
|
889
|
+
bridge.getWebView().reload();
|
|
890
|
+
});
|
|
891
|
+
}
|
|
807
892
|
|
|
808
|
-
|
|
809
|
-
if (
|
|
893
|
+
private void runOnMainSynchronously(ThrowingRunnable work) throws Exception {
|
|
894
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
895
|
+
work.run();
|
|
810
896
|
return;
|
|
811
897
|
}
|
|
812
898
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
} catch (Exception ignored) {}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
private void applyServerBasePath(String path) {
|
|
899
|
+
CountDownLatch latch = new CountDownLatch(1);
|
|
900
|
+
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
819
901
|
mainHandler.post(() -> {
|
|
820
902
|
try {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
} catch (Throwable ignored) {}
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
private void reloadWebView() {
|
|
831
|
-
mainHandler.post(() -> {
|
|
832
|
-
if (bridge != null && bridge.getWebView() != null) {
|
|
833
|
-
bridge.getWebView().reload();
|
|
903
|
+
work.run();
|
|
904
|
+
} catch (Throwable error) {
|
|
905
|
+
failure.set(error);
|
|
906
|
+
} finally {
|
|
907
|
+
latch.countDown();
|
|
834
908
|
}
|
|
835
909
|
});
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
latch.await();
|
|
913
|
+
} catch (InterruptedException e) {
|
|
914
|
+
Thread.currentThread().interrupt();
|
|
915
|
+
throw new IllegalStateException("Interrupted while waiting for main thread activation", e);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
Throwable error = failure.get();
|
|
919
|
+
if (error == null) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (error instanceof Exception) {
|
|
923
|
+
throw (Exception) error;
|
|
924
|
+
}
|
|
925
|
+
throw new RuntimeException(error);
|
|
836
926
|
}
|
|
837
927
|
|
|
838
|
-
private JSObject manifestToJSObject(ManifestClient.LatestManifest latest
|
|
928
|
+
private JSObject manifestToJSObject(ManifestClient.LatestManifest latest) {
|
|
839
929
|
JSObject object = new JSObject();
|
|
840
930
|
object.put("version", latest.version);
|
|
841
931
|
object.put("url", latest.url);
|
|
842
932
|
object.put("sha256", latest.sha256);
|
|
843
933
|
object.put("size", latest.size);
|
|
844
|
-
object.put("downloaded", downloaded);
|
|
845
934
|
if (latest.runtimeVersion != null) {
|
|
846
935
|
object.put("runtimeVersion", latest.runtimeVersion);
|
|
847
936
|
}
|
|
@@ -849,78 +938,19 @@ public class UpdaterPlugin extends Plugin {
|
|
|
849
938
|
return object;
|
|
850
939
|
}
|
|
851
940
|
|
|
852
|
-
private
|
|
853
|
-
ManifestClient.LatestManifest latest,
|
|
854
|
-
String targetChannel
|
|
855
|
-
) {
|
|
856
|
-
return doesBundleMatchLatest(store.getCurrentBundle(), latest, targetChannel);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
private boolean doesBundleMatchLatest(
|
|
860
|
-
BundleInfo bundle,
|
|
861
|
-
ManifestClient.LatestManifest latest,
|
|
862
|
-
String targetChannel
|
|
863
|
-
) {
|
|
864
|
-
if (bundle == null) {
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
if (!java.util.Objects.equals(trimToNull(bundle.channel), targetChannel)) {
|
|
869
|
-
return false;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
if (
|
|
873
|
-
!java.util.Objects.equals(
|
|
874
|
-
trimToNull(bundle.runtimeVersion),
|
|
875
|
-
trimToNull(latest.runtimeVersion)
|
|
876
|
-
)
|
|
877
|
-
) {
|
|
878
|
-
return false;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
if (
|
|
882
|
-
latest.releaseId != null &&
|
|
883
|
-
bundle.releaseId != null &&
|
|
884
|
-
latest.releaseId.equals(bundle.releaseId)
|
|
885
|
-
) {
|
|
886
|
-
return true;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
if (
|
|
890
|
-
latest.sha256 != null &&
|
|
891
|
-
bundle.sha256 != null &&
|
|
892
|
-
latest.sha256.equals(bundle.sha256)
|
|
893
|
-
) {
|
|
894
|
-
return true;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
return latest.version != null && latest.version.equals(bundle.version);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
private BundleInfo findMatchingStagedBundle(
|
|
941
|
+
private BundleInfo downloadLatestManifest(
|
|
901
942
|
ManifestClient.LatestManifest latest,
|
|
902
943
|
String targetChannel
|
|
903
|
-
) {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
if (!java.util.Objects.equals(trimToNull(staged.channel), targetChannel)) {
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (!java.util.Objects.equals(trimToNull(staged.runtimeVersion), trimToNull(latest.runtimeVersion))) {
|
|
920
|
-
return null;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
return doesBundleMatchLatest(staged, latest, targetChannel) ? staged : null;
|
|
944
|
+
) throws Exception {
|
|
945
|
+
return downloadAndStage(
|
|
946
|
+
new URL(latest.url),
|
|
947
|
+
latest.version,
|
|
948
|
+
latest.sha256,
|
|
949
|
+
latest.size,
|
|
950
|
+
latest.runtimeVersion,
|
|
951
|
+
targetChannel,
|
|
952
|
+
latest.releaseId
|
|
953
|
+
);
|
|
924
954
|
}
|
|
925
955
|
|
|
926
956
|
private void moveDirectory(File source, File destination) throws Exception {
|
|
@@ -981,7 +1011,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
981
1011
|
}
|
|
982
1012
|
}
|
|
983
1013
|
|
|
984
|
-
private String buildBundleId(String version) throws Exception {
|
|
1014
|
+
private String buildBundleId(String version, String releaseId, String sha256) throws Exception {
|
|
985
1015
|
String trimmed = version == null ? "" : version.trim();
|
|
986
1016
|
String normalized = trimmed.replaceAll("[^A-Za-z0-9._-]", "-");
|
|
987
1017
|
normalized = normalized.replaceAll("-{2,}", "-");
|
|
@@ -993,8 +1023,16 @@ public class UpdaterPlugin extends Plugin {
|
|
|
993
1023
|
normalized = normalized.substring(0, 64);
|
|
994
1024
|
}
|
|
995
1025
|
|
|
1026
|
+
String identitySource = trimToNull(releaseId);
|
|
1027
|
+
if (identitySource == null) {
|
|
1028
|
+
identitySource = trimToNull(sha256);
|
|
1029
|
+
}
|
|
1030
|
+
if (identitySource == null) {
|
|
1031
|
+
identitySource = trimmed;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
996
1034
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
997
|
-
byte[] hash = digest.digest(
|
|
1035
|
+
byte[] hash = digest.digest(identitySource.getBytes(StandardCharsets.UTF_8));
|
|
998
1036
|
StringBuilder suffix = new StringBuilder();
|
|
999
1037
|
for (int i = 0; i < 6; i++) {
|
|
1000
1038
|
suffix.append(String.format("%02x", hash[i]));
|
|
@@ -1065,20 +1103,33 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1065
1103
|
String releaseId,
|
|
1066
1104
|
String detail
|
|
1067
1105
|
) {
|
|
1106
|
+
sendDeviceEvent(
|
|
1107
|
+
new UpdaterCoordinator.DeviceEventPayload(
|
|
1108
|
+
action,
|
|
1109
|
+
bundleVersion,
|
|
1110
|
+
runtimeVersion,
|
|
1111
|
+
channel,
|
|
1112
|
+
releaseId,
|
|
1113
|
+
detail
|
|
1114
|
+
)
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private void sendDeviceEvent(UpdaterCoordinator.DeviceEventPayload payload) {
|
|
1068
1119
|
if (appId == null) {
|
|
1069
1120
|
return;
|
|
1070
1121
|
}
|
|
1071
|
-
String normalizedBundleVersion = trimToNull(bundleVersion);
|
|
1122
|
+
String normalizedBundleVersion = trimToNull(payload.bundleVersion);
|
|
1072
1123
|
if (normalizedBundleVersion == null) {
|
|
1073
1124
|
android.util.Log.w("OtaKit", "Skipping device event without bundleVersion");
|
|
1074
1125
|
return;
|
|
1075
1126
|
}
|
|
1076
|
-
String normalizedReleaseId = trimToNull(releaseId);
|
|
1127
|
+
String normalizedReleaseId = trimToNull(payload.releaseId);
|
|
1077
1128
|
if (normalizedReleaseId == null) {
|
|
1078
1129
|
android.util.Log.w("OtaKit", "Skipping device event without releaseId");
|
|
1079
1130
|
return;
|
|
1080
1131
|
}
|
|
1081
|
-
String nativeBuild = trimToNull(
|
|
1132
|
+
String nativeBuild = trimToNull(coordinator.getNativeBuild());
|
|
1082
1133
|
if (nativeBuild == null) {
|
|
1083
1134
|
android.util.Log.w("OtaKit", "Skipping device event without nativeBuild");
|
|
1084
1135
|
return;
|
|
@@ -1087,29 +1138,18 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1087
1138
|
ingestUrl,
|
|
1088
1139
|
appId,
|
|
1089
1140
|
"android",
|
|
1090
|
-
action,
|
|
1141
|
+
payload.action,
|
|
1091
1142
|
normalizedBundleVersion,
|
|
1092
|
-
channel,
|
|
1093
|
-
trimToNull(runtimeVersion),
|
|
1143
|
+
payload.channel,
|
|
1144
|
+
trimToNull(payload.runtimeVersion),
|
|
1094
1145
|
normalizedReleaseId,
|
|
1095
1146
|
nativeBuild,
|
|
1096
|
-
detail
|
|
1147
|
+
payload.detail
|
|
1097
1148
|
);
|
|
1098
1149
|
}
|
|
1099
1150
|
|
|
1100
1151
|
private void pruneIncompatibleBundles() {
|
|
1101
|
-
|
|
1102
|
-
if (!isCompatibleRuntime(bundle)) {
|
|
1103
|
-
try {
|
|
1104
|
-
store.deleteBundle(bundle.id);
|
|
1105
|
-
} catch (Exception ignored) {}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
BundleInfo failed = store.getFailedBundle();
|
|
1110
|
-
if (failed != null && !isCompatibleRuntime(failed)) {
|
|
1111
|
-
store.setFailedBundle(null);
|
|
1112
|
-
}
|
|
1152
|
+
coordinator.cleanupBundles(coordinator.pruneIncompatibleBundles(this::isCompatibleRuntime));
|
|
1113
1153
|
}
|
|
1114
1154
|
|
|
1115
1155
|
private boolean isCompatibleRuntime(String bundleRuntimeVersion) {
|
|
@@ -1120,6 +1160,24 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1120
1160
|
return isCompatibleRuntime(bundle.runtimeVersion);
|
|
1121
1161
|
}
|
|
1122
1162
|
|
|
1163
|
+
private boolean isBundleUsable(BundleInfo bundle) {
|
|
1164
|
+
return bundle != null && (bundle.isBuiltin() || isBundlePathUsable(bundle.path));
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
private boolean isBundlePathUsable(String path) {
|
|
1168
|
+
String normalizedPath = trimToNull(path);
|
|
1169
|
+
if (normalizedPath == null) {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
File directory = new File(normalizedPath);
|
|
1174
|
+
if (!directory.exists() || !directory.isDirectory()) {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return new File(directory, "index.html").exists();
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1123
1181
|
private long getFreeDiskSpace() {
|
|
1124
1182
|
try {
|
|
1125
1183
|
android.os.StatFs statFs = new android.os.StatFs(
|