@otakit/capacitor-updater 2.3.2 → 3.0.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 +46 -5
- package/android/build.gradle +16 -0
- package/android/src/main/java/com/otakit/updater/BundleCrypto.java +38 -17
- package/android/src/main/java/com/otakit/updater/BundleInfo.java +10 -0
- package/android/src/main/java/com/otakit/updater/BundleStore.java +134 -86
- package/android/src/main/java/com/otakit/updater/CheckFailure.java +53 -0
- package/android/src/main/java/com/otakit/updater/DeltaAssembler.java +46 -41
- package/android/src/main/java/com/otakit/updater/DeviceEventClient.java +125 -26
- package/android/src/main/java/com/otakit/updater/DocumentReadyBridge.java +71 -0
- package/android/src/main/java/com/otakit/updater/DownloadRetry.java +144 -0
- package/android/src/main/java/com/otakit/updater/EventOutbox.java +174 -0
- package/android/src/main/java/com/otakit/updater/FileDownloader.java +80 -0
- package/android/src/main/java/com/otakit/updater/ForegroundDeadline.java +84 -0
- package/android/src/main/java/com/otakit/updater/HashUtils.java +26 -0
- package/android/src/main/java/com/otakit/updater/ManifestClient.java +12 -9
- package/android/src/main/java/com/otakit/updater/ManifestKeyConfig.java +47 -0
- package/android/src/main/java/com/otakit/updater/ManifestVerifier.java +22 -4
- package/android/src/main/java/com/otakit/updater/SDKVersion.java +9 -0
- package/android/src/main/java/com/otakit/updater/UpdateOwner.java +27 -0
- package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +154 -59
- package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +284 -209
- package/android/src/main/java/com/otakit/updater/WebViewActivation.java +26 -0
- package/dist/esm/definitions.d.ts +3 -3
- package/dist/esm/definitions.d.ts.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleCrypto.swift +35 -3
- package/ios/Sources/UpdaterPlugin/BundleInfo.swift +13 -0
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +103 -77
- package/ios/Sources/UpdaterPlugin/CheckFailure.swift +41 -0
- package/ios/Sources/UpdaterPlugin/DeltaAssembler.swift +29 -17
- package/ios/Sources/UpdaterPlugin/DeviceEventClient.swift +12 -10
- package/ios/Sources/UpdaterPlugin/DocumentReadyBridge.swift +43 -0
- package/ios/Sources/UpdaterPlugin/DownloadRetry.swift +51 -0
- package/ios/Sources/UpdaterPlugin/Downloader.swift +97 -39
- package/ios/Sources/UpdaterPlugin/EventOutbox.swift +183 -0
- package/ios/Sources/UpdaterPlugin/ForegroundDeadline.swift +94 -0
- package/ios/Sources/UpdaterPlugin/HashUtils.swift +16 -0
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +2 -4
- package/ios/Sources/UpdaterPlugin/ManifestKeyConfig.swift +20 -0
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +7 -1
- package/ios/Sources/UpdaterPlugin/SDKVersion.swift +4 -0
- package/ios/Sources/UpdaterPlugin/UpdateOwner.swift +12 -0
- package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +146 -110
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +206 -202
- package/ios/Tests/UpdaterPluginTests/BundleCryptoTests.swift +80 -0
- package/ios/Tests/UpdaterPluginTests/BundlePersistenceTests.swift +102 -0
- package/ios/Tests/UpdaterPluginTests/CheckFailureTests.swift +55 -0
- package/ios/Tests/UpdaterPluginTests/DeltaCacheIntegrityTests.swift +69 -0
- package/ios/Tests/UpdaterPluginTests/DocumentReadyBridgeTests.swift +102 -0
- package/ios/Tests/UpdaterPluginTests/DownloadIntegrityTests.swift +40 -0
- package/ios/Tests/UpdaterPluginTests/DownloadRetryTests.swift +189 -0
- package/ios/Tests/UpdaterPluginTests/EventDeliveryTests.swift +56 -0
- package/ios/Tests/UpdaterPluginTests/EventOutboxTests.swift +86 -0
- package/ios/Tests/UpdaterPluginTests/ForegroundDeadlineTests.swift +144 -0
- package/ios/Tests/UpdaterPluginTests/ManifestKeyConfigTests.swift +48 -0
- package/ios/Tests/UpdaterPluginTests/UpdateOwnerTests.swift +34 -0
- package/ios/Tests/UpdaterPluginTests/UpdaterCoordinatorTests.swift +394 -0
- package/package.json +7 -3
|
@@ -13,11 +13,8 @@ import com.getcapacitor.annotation.CapacitorPlugin;
|
|
|
13
13
|
import java.io.File;
|
|
14
14
|
import java.io.FileInputStream;
|
|
15
15
|
import java.io.FileOutputStream;
|
|
16
|
-
import java.io.InputStream;
|
|
17
|
-
import java.net.HttpURLConnection;
|
|
18
16
|
import java.net.URL;
|
|
19
17
|
import java.nio.charset.StandardCharsets;
|
|
20
|
-
import java.security.MessageDigest;
|
|
21
18
|
import java.util.concurrent.CountDownLatch;
|
|
22
19
|
import java.util.concurrent.ExecutorService;
|
|
23
20
|
import java.util.concurrent.Executors;
|
|
@@ -100,9 +97,13 @@ public class UpdaterPlugin extends Plugin {
|
|
|
100
97
|
|
|
101
98
|
private BundleStore store;
|
|
102
99
|
private UpdaterCoordinator coordinator;
|
|
103
|
-
private
|
|
100
|
+
private final UpdateOwner updateOwner;
|
|
101
|
+
private final DocumentReadyBridge documentReadyBridge = new DocumentReadyBridge();
|
|
102
|
+
private final ForegroundDeadline trialDeadline;
|
|
104
103
|
|
|
105
104
|
private int appReadyTimeoutMs = 10_000;
|
|
105
|
+
private boolean storageReady;
|
|
106
|
+
private volatile String lifecycleState = "unknown";
|
|
106
107
|
private boolean allowInsecureUrls = false;
|
|
107
108
|
private Policy launchPolicy = Policy.APPLY_STAGED;
|
|
108
109
|
private Policy resumePolicy = Policy.SHADOW;
|
|
@@ -126,6 +127,21 @@ public class UpdaterPlugin extends Plugin {
|
|
|
126
127
|
java.util.regex.Pattern.compile("^[A-Za-z0-9._-]{1,64}$");
|
|
127
128
|
private UpdaterCoordinator.StartupPreparation pendingStartupPreparation;
|
|
128
129
|
|
|
130
|
+
public UpdaterPlugin() {
|
|
131
|
+
this.trialDeadline = new ForegroundDeadline(mainHandler);
|
|
132
|
+
this.updateOwner = new UpdateOwner(() -> bridge != null && bridge.getWebView() != null);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
UpdaterPlugin(ForegroundDeadline trialDeadline) {
|
|
136
|
+
this.trialDeadline = trialDeadline;
|
|
137
|
+
this.updateOwner = new UpdateOwner(() -> bridge != null && bridge.getWebView() != null);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
UpdaterPlugin(ForegroundDeadline trialDeadline, UpdateOwner updateOwner) {
|
|
141
|
+
this.trialDeadline = trialDeadline;
|
|
142
|
+
this.updateOwner = updateOwner;
|
|
143
|
+
}
|
|
144
|
+
|
|
129
145
|
@Override
|
|
130
146
|
public void load() {
|
|
131
147
|
super.load();
|
|
@@ -151,6 +167,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
151
167
|
);
|
|
152
168
|
this.cdnUrl = resolveCdnUrl(getConfig().getString("cdnUrl"), System.getenv("OTAKIT_CDN_URL"));
|
|
153
169
|
this.appId = getConfig().getString("appId");
|
|
170
|
+
DeviceEventClient.resume(new File(getContext().getNoBackupFilesDir(), "otakit-events"));
|
|
154
171
|
this.channel = trimToNull(getConfig().getString("channel"));
|
|
155
172
|
this.runtimeVersion = trimToNull(getConfig().getString("runtimeVersion"));
|
|
156
173
|
this.store = new BundleStore(getContext(), builtinVersion, nativeBuild, this.runtimeVersion);
|
|
@@ -160,37 +177,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
160
177
|
this.resumePolicy = resolvePolicy(getConfig().getString("resumePolicy"), Policy.SHADOW);
|
|
161
178
|
this.runtimePolicy = resolvePolicy(getConfig().getString("runtimePolicy"), Policy.IMMEDIATE);
|
|
162
179
|
|
|
163
|
-
|
|
164
|
-
org.json.JSONArray rawKeys = getConfig().getConfigJSON().optJSONArray("manifestKeys");
|
|
165
|
-
if (rawKeys != null && rawKeys.length() > 0) {
|
|
166
|
-
for (int i = 0; i < rawKeys.length(); i++) {
|
|
167
|
-
org.json.JSONObject entry = rawKeys.optJSONObject(i);
|
|
168
|
-
if (entry == null) {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
String kid = entry.optString("kid", null);
|
|
172
|
-
String keyBase64 = entry.optString("key", null);
|
|
173
|
-
if (kid != null && keyBase64 != null) {
|
|
174
|
-
byte[] keyBytes = android.util.Base64.decode(keyBase64, android.util.Base64.DEFAULT);
|
|
175
|
-
manifestKeys.add(new ManifestVerifier.KeyEntry(kid, keyBytes));
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (manifestKeys.isEmpty()) {
|
|
179
|
-
android.util.Log.e(
|
|
180
|
-
"OtaKit",
|
|
181
|
-
"manifestKeys configured but all entries are invalid. Manifest verification will reject all updates."
|
|
182
|
-
);
|
|
183
|
-
manifestKeys.add(new ManifestVerifier.KeyEntry("_invalid_", new byte[0]));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
} catch (Exception e) {
|
|
187
|
-
android.util.Log.e(
|
|
188
|
-
"OtaKit",
|
|
189
|
-
"Failed to parse manifestKeys. Manifest verification will reject all updates.",
|
|
190
|
-
e
|
|
191
|
-
);
|
|
192
|
-
manifestKeys.add(new ManifestVerifier.KeyEntry("_invalid_", new byte[0]));
|
|
193
|
-
}
|
|
180
|
+
manifestKeys = ManifestKeyConfig.parse(getConfig().getConfigJSON());
|
|
194
181
|
|
|
195
182
|
if (manifestKeys.isEmpty() && HostedManifestKeys.matchesManagedManifestUrl(cdnUrl)) {
|
|
196
183
|
manifestKeys.addAll(HostedManifestKeys.createDefaultKeys());
|
|
@@ -231,13 +218,17 @@ public class UpdaterPlugin extends Plugin {
|
|
|
231
218
|
this.appReadyTimeoutMs = Math.max(1000, getConfig().getInt("appReadyTimeout", 10_000));
|
|
232
219
|
this.checkIntervalMs = getConfig().getInt("checkInterval", 600_000);
|
|
233
220
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
221
|
+
try {
|
|
222
|
+
pruneIncompatibleBundles();
|
|
223
|
+
UpdaterCoordinator.StartupPreparation startup = coordinator.normalizeStartupState(
|
|
224
|
+
this::isBundleUsable
|
|
225
|
+
);
|
|
226
|
+
cleanupInBackground(startup.cleanupBundleIds);
|
|
227
|
+
pendingStartupPreparation = startup;
|
|
228
|
+
storageReady = true;
|
|
229
|
+
} catch (Exception error) {
|
|
230
|
+
android.util.Log.e("OtaKit", "Cannot persist startup recovery; keeping builtin", error);
|
|
231
|
+
}
|
|
241
232
|
}
|
|
242
233
|
|
|
243
234
|
@Override
|
|
@@ -249,6 +240,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
249
240
|
@Override
|
|
250
241
|
protected void handleOnResume() {
|
|
251
242
|
super.handleOnResume();
|
|
243
|
+
lifecycleState = "foreground";
|
|
244
|
+
trialDeadline.setForeground(true);
|
|
245
|
+
if (bridge != null) DeviceEventClient.resume(
|
|
246
|
+
new File(getContext().getNoBackupFilesDir(), "otakit-events")
|
|
247
|
+
);
|
|
248
|
+
if (!storageReady) return;
|
|
252
249
|
if (coldStartInProgress) {
|
|
253
250
|
coldStartInProgress = false;
|
|
254
251
|
return;
|
|
@@ -256,6 +253,20 @@ public class UpdaterPlugin extends Plugin {
|
|
|
256
253
|
handleResume();
|
|
257
254
|
}
|
|
258
255
|
|
|
256
|
+
@Override
|
|
257
|
+
protected void handleOnPause() {
|
|
258
|
+
lifecycleState = "background";
|
|
259
|
+
trialDeadline.setForeground(false);
|
|
260
|
+
super.handleOnPause();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@Override
|
|
264
|
+
protected void handleOnDestroy() {
|
|
265
|
+
updateOwner.close();
|
|
266
|
+
trialDeadline.cancel();
|
|
267
|
+
super.handleOnDestroy();
|
|
268
|
+
}
|
|
269
|
+
|
|
259
270
|
private boolean shouldSkipCheckInterval() {
|
|
260
271
|
if (checkIntervalMs <= 0) return false;
|
|
261
272
|
long lastCheck = store.getPrefs().getLong(KEY_LAST_CHECK_TIMESTAMP, 0);
|
|
@@ -285,9 +296,14 @@ public class UpdaterPlugin extends Plugin {
|
|
|
285
296
|
|
|
286
297
|
if (startup.activationPath != null && !startup.activationPath.isEmpty()) {
|
|
287
298
|
try {
|
|
299
|
+
if (startup.trial != null) installReadinessBridge(startup.trial);
|
|
288
300
|
applyServerBasePathSynchronously(startup.activationPath);
|
|
289
301
|
} catch (Exception e) {
|
|
290
302
|
android.util.Log.w("OtaKit", "startup activation failed", e);
|
|
303
|
+
if (startup.trial != null) {
|
|
304
|
+
rollbackCurrentBundle(startup.trial, "activation_failed");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
291
307
|
}
|
|
292
308
|
}
|
|
293
309
|
|
|
@@ -295,8 +311,8 @@ public class UpdaterPlugin extends Plugin {
|
|
|
295
311
|
sendDeviceEvent(startup.eventPayload);
|
|
296
312
|
}
|
|
297
313
|
|
|
298
|
-
if (startup.
|
|
299
|
-
scheduleTrialTimeout(startup.
|
|
314
|
+
if (startup.trial != null) {
|
|
315
|
+
scheduleTrialTimeout(startup.trial);
|
|
300
316
|
} else {
|
|
301
317
|
cancelTrialTimeout();
|
|
302
318
|
}
|
|
@@ -319,7 +335,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
319
335
|
if (hasStagedBundle) {
|
|
320
336
|
resolveCurrentRuntimeKey();
|
|
321
337
|
try {
|
|
322
|
-
if (!applyStaged(
|
|
338
|
+
if (!applyStaged()) {
|
|
323
339
|
android.util.Log.w(
|
|
324
340
|
"OtaKit",
|
|
325
341
|
"Failed to apply a valid staged bundle during runtime handling"
|
|
@@ -334,7 +350,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
334
350
|
DownloadResolution result = downloadLatest(false, null);
|
|
335
351
|
resolveCurrentRuntimeKey();
|
|
336
352
|
if (result.isForcedStaged()) {
|
|
337
|
-
requireApplyStaged(
|
|
353
|
+
requireApplyStaged();
|
|
338
354
|
}
|
|
339
355
|
});
|
|
340
356
|
return;
|
|
@@ -343,7 +359,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
343
359
|
DownloadResolution result = downloadLatest(false, null);
|
|
344
360
|
resolveCurrentRuntimeKey();
|
|
345
361
|
if (result.isForcedStaged()) {
|
|
346
|
-
requireApplyStaged(
|
|
362
|
+
requireApplyStaged();
|
|
347
363
|
}
|
|
348
364
|
});
|
|
349
365
|
return;
|
|
@@ -355,7 +371,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
355
371
|
return;
|
|
356
372
|
}
|
|
357
373
|
resolveCurrentRuntimeKey();
|
|
358
|
-
requireApplyStaged(
|
|
374
|
+
requireApplyStaged();
|
|
359
375
|
});
|
|
360
376
|
return;
|
|
361
377
|
}
|
|
@@ -367,7 +383,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
367
383
|
return;
|
|
368
384
|
case APPLY_STAGED:
|
|
369
385
|
try {
|
|
370
|
-
if (applyStaged(
|
|
386
|
+
if (applyStaged()) {
|
|
371
387
|
return;
|
|
372
388
|
}
|
|
373
389
|
} catch (Exception e) {
|
|
@@ -377,7 +393,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
377
393
|
executeAutomaticUpdate("launch apply-staged fallback", () -> {
|
|
378
394
|
DownloadResolution result = downloadLatest(false, null);
|
|
379
395
|
if (result.isForcedStaged()) {
|
|
380
|
-
requireApplyStaged(
|
|
396
|
+
requireApplyStaged();
|
|
381
397
|
}
|
|
382
398
|
});
|
|
383
399
|
return;
|
|
@@ -385,7 +401,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
385
401
|
executeAutomaticUpdate("launch shadow", () -> {
|
|
386
402
|
DownloadResolution result = downloadLatest(false, null);
|
|
387
403
|
if (result.isForcedStaged()) {
|
|
388
|
-
requireApplyStaged(
|
|
404
|
+
requireApplyStaged();
|
|
389
405
|
}
|
|
390
406
|
});
|
|
391
407
|
return;
|
|
@@ -393,7 +409,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
393
409
|
executeAutomaticUpdate("launch immediate", () -> {
|
|
394
410
|
DownloadResolution result = downloadLatest(false, null);
|
|
395
411
|
if ("staged".equals(result.kind)) {
|
|
396
|
-
requireApplyStaged(
|
|
412
|
+
requireApplyStaged();
|
|
397
413
|
}
|
|
398
414
|
});
|
|
399
415
|
return;
|
|
@@ -406,12 +422,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
406
422
|
return;
|
|
407
423
|
case APPLY_STAGED:
|
|
408
424
|
executeAutomaticUpdate("resume apply-staged", () -> {
|
|
409
|
-
if (applyStaged(
|
|
425
|
+
if (applyStaged()) {
|
|
410
426
|
return;
|
|
411
427
|
}
|
|
412
428
|
DownloadResolution result = downloadLatest(true, null);
|
|
413
429
|
if (result.isForcedStaged()) {
|
|
414
|
-
requireApplyStaged(
|
|
430
|
+
requireApplyStaged();
|
|
415
431
|
}
|
|
416
432
|
});
|
|
417
433
|
return;
|
|
@@ -419,7 +435,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
419
435
|
executeAutomaticUpdate("resume shadow", () -> {
|
|
420
436
|
DownloadResolution result = downloadLatest(true, null);
|
|
421
437
|
if (result.isForcedStaged()) {
|
|
422
|
-
requireApplyStaged(
|
|
438
|
+
requireApplyStaged();
|
|
423
439
|
}
|
|
424
440
|
});
|
|
425
441
|
return;
|
|
@@ -427,7 +443,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
427
443
|
executeAutomaticUpdate("resume immediate", () -> {
|
|
428
444
|
DownloadResolution result = downloadLatest(false, null);
|
|
429
445
|
if ("staged".equals(result.kind)) {
|
|
430
|
-
requireApplyStaged(
|
|
446
|
+
requireApplyStaged();
|
|
431
447
|
}
|
|
432
448
|
});
|
|
433
449
|
return;
|
|
@@ -441,7 +457,10 @@ public class UpdaterPlugin extends Plugin {
|
|
|
441
457
|
}
|
|
442
458
|
executor.execute(() -> {
|
|
443
459
|
try {
|
|
460
|
+
ensureOwnerActive();
|
|
444
461
|
operation.run();
|
|
462
|
+
} catch (java.util.concurrent.CancellationException ignored) {
|
|
463
|
+
// The owning bridge went away; this is not a failed update attempt.
|
|
445
464
|
} catch (Exception e) {
|
|
446
465
|
android.util.Log.w("OtaKit", label + " failed", e);
|
|
447
466
|
} finally {
|
|
@@ -539,7 +558,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
539
558
|
}
|
|
540
559
|
executor.execute(() -> {
|
|
541
560
|
try {
|
|
542
|
-
requireApplyStaged(
|
|
561
|
+
requireApplyStaged();
|
|
543
562
|
} catch (Exception e) {
|
|
544
563
|
call.reject("apply failed: " + e.getMessage());
|
|
545
564
|
} finally {
|
|
@@ -558,7 +577,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
558
577
|
try {
|
|
559
578
|
DownloadResolution result = downloadLatest(false, null);
|
|
560
579
|
if ("staged".equals(result.kind)) {
|
|
561
|
-
requireApplyStaged(
|
|
580
|
+
requireApplyStaged();
|
|
562
581
|
return;
|
|
563
582
|
}
|
|
564
583
|
call.resolve();
|
|
@@ -572,18 +591,24 @@ public class UpdaterPlugin extends Plugin {
|
|
|
572
591
|
|
|
573
592
|
@PluginMethod
|
|
574
593
|
public void notifyAppReady(PluginCall call) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
594
|
+
mainHandler.post(() -> {
|
|
595
|
+
try {
|
|
596
|
+
UpdaterCoordinator.NotifyReadyPreparation preparation = updateOwner.run(() ->
|
|
597
|
+
coordinator.prepareNotifyAppReady(call.getString("_otakitActivationId"))
|
|
598
|
+
);
|
|
599
|
+
cleanupInBackground(preparation.cleanupBundleIds);
|
|
600
|
+
if (preparation.eventPayload != null) {
|
|
601
|
+
cancelTrialTimeout();
|
|
602
|
+
sendDeviceEvent(preparation.eventPayload);
|
|
603
|
+
JSObject appliedData = new JSObject();
|
|
604
|
+
appliedData.put("bundle", store.getCurrentBundle().toJSObject());
|
|
605
|
+
emitEvent("updateApplied", appliedData);
|
|
606
|
+
}
|
|
607
|
+
call.resolve();
|
|
608
|
+
} catch (Exception error) {
|
|
609
|
+
call.reject("Could not persist update readiness: " + error.getMessage(), error);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
587
612
|
}
|
|
588
613
|
|
|
589
614
|
@PluginMethod
|
|
@@ -658,17 +683,38 @@ public class UpdaterPlugin extends Plugin {
|
|
|
658
683
|
throw new IllegalStateException("Missing appId in plugin config");
|
|
659
684
|
}
|
|
660
685
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
686
|
+
String checkId = "check-" + java.util.UUID.randomUUID();
|
|
687
|
+
return CheckFailure.observe(
|
|
688
|
+
() -> {
|
|
689
|
+
ManifestClient.LatestManifest latest = ManifestClient.fetchLatest(
|
|
690
|
+
cdnUrl,
|
|
691
|
+
appId,
|
|
692
|
+
channel,
|
|
693
|
+
runtimeVersion,
|
|
694
|
+
allowInsecureUrls,
|
|
695
|
+
manifestKeys
|
|
696
|
+
);
|
|
697
|
+
ensureOwnerActive();
|
|
698
|
+
return latest;
|
|
699
|
+
},
|
|
700
|
+
failure -> {
|
|
701
|
+
ensureOwnerActive();
|
|
702
|
+
sendDeviceEvent(
|
|
703
|
+
"check_error",
|
|
704
|
+
null,
|
|
705
|
+
runtimeVersion,
|
|
706
|
+
channel,
|
|
707
|
+
null,
|
|
708
|
+
failure.detail,
|
|
709
|
+
checkId,
|
|
710
|
+
failure.phase
|
|
711
|
+
);
|
|
712
|
+
}
|
|
668
713
|
);
|
|
669
714
|
}
|
|
670
715
|
|
|
671
716
|
private CheckResolution checkLatest(boolean respectInterval, String channel) throws Exception {
|
|
717
|
+
ensureOwnerActive();
|
|
672
718
|
String targetChannel = resolveTargetChannel(channel);
|
|
673
719
|
if (respectInterval && shouldSkipCheckInterval()) {
|
|
674
720
|
android.util.Log.d("OtaKit", "Skipping resume check: checkInterval has not elapsed");
|
|
@@ -750,6 +796,17 @@ public class UpdaterPlugin extends Plugin {
|
|
|
750
796
|
String targetChannel
|
|
751
797
|
) throws Exception {
|
|
752
798
|
if (!isCompatibleRuntime(latest.runtimeVersion)) {
|
|
799
|
+
ensureOwnerActive();
|
|
800
|
+
sendDeviceEvent(
|
|
801
|
+
"check_error",
|
|
802
|
+
latest.version,
|
|
803
|
+
runtimeVersion,
|
|
804
|
+
targetChannel,
|
|
805
|
+
latest.releaseId,
|
|
806
|
+
"runtime_mismatch",
|
|
807
|
+
"check-" + java.util.UUID.randomUUID(),
|
|
808
|
+
"check"
|
|
809
|
+
);
|
|
753
810
|
throw new IllegalStateException(
|
|
754
811
|
"Manifest runtimeVersion does not match the installed app runtime"
|
|
755
812
|
);
|
|
@@ -785,15 +842,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
785
842
|
}
|
|
786
843
|
|
|
787
844
|
private boolean isExpiredURLError(Exception e) {
|
|
788
|
-
|
|
789
|
-
if (msg == null) return false;
|
|
790
|
-
msg = msg.toLowerCase();
|
|
791
|
-
return (
|
|
792
|
-
msg.contains("403") ||
|
|
793
|
-
msg.contains("410") ||
|
|
794
|
-
msg.contains("forbidden") ||
|
|
795
|
-
msg.contains("expired")
|
|
796
|
-
);
|
|
845
|
+
return DownloadRetry.isExpiredUrlFailure(e);
|
|
797
846
|
}
|
|
798
847
|
|
|
799
848
|
private BundleInfo downloadAndStage(
|
|
@@ -806,6 +855,9 @@ public class UpdaterPlugin extends Plugin {
|
|
|
806
855
|
String releaseId,
|
|
807
856
|
ManifestClient.ManifestEncryption encryption
|
|
808
857
|
) throws Exception {
|
|
858
|
+
ensureOwnerActive();
|
|
859
|
+
String installationId = BundleStore.newInstallationId();
|
|
860
|
+
String phase = "admission";
|
|
809
861
|
// Check disk space before downloading
|
|
810
862
|
if (expectedSize > 0) {
|
|
811
863
|
// zip + extracted + buffer; encrypted bundles keep an extra decrypted
|
|
@@ -820,7 +872,9 @@ public class UpdaterPlugin extends Plugin {
|
|
|
820
872
|
runtimeVersion,
|
|
821
873
|
channel,
|
|
822
874
|
releaseId,
|
|
823
|
-
"insufficient_disk_space"
|
|
875
|
+
"insufficient_disk_space",
|
|
876
|
+
installationId,
|
|
877
|
+
phase
|
|
824
878
|
);
|
|
825
879
|
emitEvent(
|
|
826
880
|
"downloadFailed",
|
|
@@ -835,15 +889,22 @@ public class UpdaterPlugin extends Plugin {
|
|
|
835
889
|
File extractedDirectory = null;
|
|
836
890
|
|
|
837
891
|
try {
|
|
892
|
+
phase = "transfer";
|
|
838
893
|
downloadedZip = downloadZip(url);
|
|
894
|
+
ensureOwnerActive();
|
|
839
895
|
// The manifest sha256 covers the downloaded object as-is — the
|
|
840
896
|
// ciphertext when the bundle is encrypted.
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
897
|
+
phase = "integrity";
|
|
898
|
+
HashUtils.verifyDownload(
|
|
899
|
+
downloadedZip,
|
|
900
|
+
expectedSha256,
|
|
901
|
+
expectedSize > 0 ? expectedSize : -1,
|
|
902
|
+
"bundle"
|
|
903
|
+
);
|
|
844
904
|
|
|
845
905
|
File zipToExtract = downloadedZip;
|
|
846
906
|
if (encryption != null) {
|
|
907
|
+
phase = "decrypt";
|
|
847
908
|
byte[] kek = bundleKeys.get(encryption.kid);
|
|
848
909
|
if (kek == null) {
|
|
849
910
|
throw new IllegalStateException("no matching bundle key (kid " + encryption.kid + ")");
|
|
@@ -866,6 +927,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
866
927
|
zipToExtract = decryptedZip;
|
|
867
928
|
}
|
|
868
929
|
|
|
930
|
+
phase = "extract";
|
|
869
931
|
extractedDirectory = new File(
|
|
870
932
|
getContext().getCacheDir(),
|
|
871
933
|
"otakit-extract-" + System.currentTimeMillis()
|
|
@@ -877,10 +939,11 @@ public class UpdaterPlugin extends Plugin {
|
|
|
877
939
|
zipUtils.extractSecurely(zipToExtract, extractedDirectory);
|
|
878
940
|
File bundleRoot = resolveBundleRoot(extractedDirectory);
|
|
879
941
|
|
|
880
|
-
|
|
942
|
+
phase = "install";
|
|
943
|
+
String bundleId = installationId;
|
|
881
944
|
File destination = coordinator.bundleDirectory(bundleId);
|
|
882
945
|
if (destination.exists()) {
|
|
883
|
-
|
|
946
|
+
throw new IllegalStateException("Bundle installation directory already exists");
|
|
884
947
|
}
|
|
885
948
|
moveDirectory(bundleRoot, destination);
|
|
886
949
|
|
|
@@ -895,22 +958,35 @@ public class UpdaterPlugin extends Plugin {
|
|
|
895
958
|
channel,
|
|
896
959
|
releaseId
|
|
897
960
|
);
|
|
898
|
-
|
|
961
|
+
phase = "stage";
|
|
962
|
+
java.util.List<String> cleanupBundleIds = stageOwnedBundle(info);
|
|
899
963
|
coordinator.cleanupBundles(cleanupBundleIds);
|
|
900
964
|
|
|
901
|
-
sendDeviceEvent(
|
|
965
|
+
sendDeviceEvent(
|
|
966
|
+
"downloaded",
|
|
967
|
+
version,
|
|
968
|
+
runtimeVersion,
|
|
969
|
+
channel,
|
|
970
|
+
releaseId,
|
|
971
|
+
null,
|
|
972
|
+
installationId,
|
|
973
|
+
phase
|
|
974
|
+
);
|
|
902
975
|
JSObject stagedData = new JSObject();
|
|
903
976
|
stagedData.put("bundle", info.toJSObject());
|
|
904
977
|
emitEvent("updateStaged", stagedData);
|
|
905
978
|
return info;
|
|
906
979
|
} catch (Exception e) {
|
|
980
|
+
if (e instanceof java.util.concurrent.CancellationException) throw e;
|
|
907
981
|
sendDeviceEvent(
|
|
908
982
|
"download_error",
|
|
909
983
|
version,
|
|
910
984
|
runtimeVersion,
|
|
911
985
|
channel,
|
|
912
986
|
releaseId,
|
|
913
|
-
e.getMessage()
|
|
987
|
+
e.getMessage(),
|
|
988
|
+
installationId,
|
|
989
|
+
phase
|
|
914
990
|
);
|
|
915
991
|
emitEvent(
|
|
916
992
|
"downloadFailed",
|
|
@@ -918,6 +994,9 @@ public class UpdaterPlugin extends Plugin {
|
|
|
918
994
|
);
|
|
919
995
|
throw e;
|
|
920
996
|
} finally {
|
|
997
|
+
if (installationId != null) coordinator.cleanupBundles(
|
|
998
|
+
java.util.Collections.singletonList(installationId)
|
|
999
|
+
);
|
|
921
1000
|
if (downloadedZip != null && downloadedZip.exists()) {
|
|
922
1001
|
//noinspection ResultOfMethodCallIgnored
|
|
923
1002
|
downloadedZip.delete();
|
|
@@ -935,35 +1014,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
935
1014
|
}
|
|
936
1015
|
|
|
937
1016
|
private File downloadZip(URL url) throws Exception {
|
|
938
|
-
|
|
939
|
-
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
940
|
-
try {
|
|
941
|
-
connection.setRequestMethod("GET");
|
|
942
|
-
connection.setConnectTimeout(15_000);
|
|
943
|
-
connection.setReadTimeout(60_000);
|
|
944
|
-
|
|
945
|
-
int status = connection.getResponseCode();
|
|
946
|
-
if (status < 200 || status >= 300) {
|
|
947
|
-
throw new IllegalStateException("Download failed with HTTP " + status);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
File destination = File.createTempFile("otakit-", ".zip", getContext().getCacheDir());
|
|
951
|
-
|
|
952
|
-
try (
|
|
953
|
-
InputStream input = connection.getInputStream();
|
|
954
|
-
FileOutputStream output = new FileOutputStream(destination)
|
|
955
|
-
) {
|
|
956
|
-
byte[] buffer = new byte[8192];
|
|
957
|
-
int read;
|
|
958
|
-
while ((read = input.read(buffer)) > 0) {
|
|
959
|
-
output.write(buffer, 0, read);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
return destination;
|
|
964
|
-
} finally {
|
|
965
|
-
connection.disconnect();
|
|
966
|
-
}
|
|
1017
|
+
return FileDownloader.download(url, getContext().getCacheDir(), allowInsecureUrls);
|
|
967
1018
|
}
|
|
968
1019
|
|
|
969
1020
|
private File resolveBundleRoot(File extractedDirectory) throws Exception {
|
|
@@ -983,61 +1034,92 @@ public class UpdaterPlugin extends Plugin {
|
|
|
983
1034
|
throw new IllegalStateException("Bundle archive does not contain index.html");
|
|
984
1035
|
}
|
|
985
1036
|
|
|
986
|
-
private boolean applyStaged(
|
|
1037
|
+
private boolean applyStaged() throws Exception {
|
|
1038
|
+
java.util.concurrent.atomic.AtomicBoolean applied =
|
|
1039
|
+
new java.util.concurrent.atomic.AtomicBoolean();
|
|
1040
|
+
runOnMainSynchronously(() -> applied.set(updateOwner.run(this::applyStagedOnMain)));
|
|
1041
|
+
return applied.get();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
private java.util.List<String> stageOwnedBundle(BundleInfo bundle) throws Exception {
|
|
1045
|
+
AtomicReference<java.util.List<String>> cleanup = new AtomicReference<>();
|
|
1046
|
+
runOnMainSynchronously(() ->
|
|
1047
|
+
cleanup.set(updateOwner.run(() -> coordinator.stageDownloadedBundle(bundle)))
|
|
1048
|
+
);
|
|
1049
|
+
return cleanup.get();
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
private void ensureOwnerActive() throws Exception {
|
|
1053
|
+
runOnMainSynchronously(() -> updateOwner.run(() -> null));
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
private void cleanupInBackground(java.util.List<String> ids) {
|
|
1057
|
+
if (!ids.isEmpty()) executor.execute(() -> coordinator.cleanupBundles(ids));
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private void installReadinessBridge(UpdaterCoordinator.Trial trial) {
|
|
1061
|
+
documentReadyBridge.install(bridge.getWebView(), bridge.getAppUrl(), trial.activationId);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private boolean applyStagedOnMain() throws Exception {
|
|
1065
|
+
DocumentReadyBridge.preflight(bridge.getWebView(), bridge.getAppUrl());
|
|
987
1066
|
UpdaterCoordinator.ApplyPreparation preparation = coordinator.prepareApplyStaged(
|
|
988
1067
|
this::isCompatibleRuntime,
|
|
989
1068
|
this::isBundleUsable
|
|
990
1069
|
);
|
|
991
|
-
|
|
1070
|
+
cleanupInBackground(preparation.cleanupBundleIds);
|
|
992
1071
|
if (!preparation.didApply()) {
|
|
993
1072
|
return false;
|
|
994
1073
|
}
|
|
995
1074
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1075
|
+
try {
|
|
1076
|
+
if (preparation.trial != null) installReadinessBridge(preparation.trial);
|
|
1077
|
+
applyServerBasePathSynchronously(preparation.activationPath);
|
|
1078
|
+
} catch (Exception error) {
|
|
1079
|
+
if (preparation.trial != null) rollbackCurrentBundle(preparation.trial, "activation_failed");
|
|
1080
|
+
throw error;
|
|
999
1081
|
}
|
|
1000
1082
|
|
|
1001
1083
|
cancelTrialTimeout();
|
|
1002
|
-
if (preparation.
|
|
1003
|
-
scheduleTrialTimeout(preparation.
|
|
1084
|
+
if (preparation.trial != null) {
|
|
1085
|
+
scheduleTrialTimeout(preparation.trial);
|
|
1004
1086
|
}
|
|
1005
1087
|
return true;
|
|
1006
1088
|
}
|
|
1007
1089
|
|
|
1008
|
-
private void requireApplyStaged(
|
|
1009
|
-
if (!applyStaged(
|
|
1090
|
+
private void requireApplyStaged() throws Exception {
|
|
1091
|
+
if (!applyStaged()) {
|
|
1010
1092
|
throw new IllegalStateException("Expected a staged bundle to be ready for apply");
|
|
1011
1093
|
}
|
|
1012
1094
|
}
|
|
1013
1095
|
|
|
1014
|
-
private void scheduleTrialTimeout(
|
|
1015
|
-
|
|
1016
|
-
trialTimeoutRunnable = () -> {
|
|
1017
|
-
if (coordinator.isCurrentTrialBundle(bundleId)) {
|
|
1018
|
-
rollbackCurrentBundle("notify_timeout", true);
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
mainHandler.postDelayed(trialTimeoutRunnable, appReadyTimeoutMs);
|
|
1096
|
+
private void scheduleTrialTimeout(UpdaterCoordinator.Trial trial) {
|
|
1097
|
+
trialDeadline.start(appReadyTimeoutMs, () -> rollbackCurrentBundle(trial, "notify_timeout"));
|
|
1022
1098
|
}
|
|
1023
1099
|
|
|
1024
1100
|
private void cancelTrialTimeout() {
|
|
1025
|
-
|
|
1026
|
-
mainHandler.removeCallbacks(trialTimeoutRunnable);
|
|
1027
|
-
trialTimeoutRunnable = null;
|
|
1028
|
-
}
|
|
1101
|
+
trialDeadline.cancel();
|
|
1029
1102
|
}
|
|
1030
1103
|
|
|
1031
|
-
private void rollbackCurrentBundle(
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1104
|
+
private void rollbackCurrentBundle(UpdaterCoordinator.Trial expectedTrial, String reason) {
|
|
1105
|
+
UpdaterCoordinator.RollbackPreparation preparation;
|
|
1106
|
+
try {
|
|
1107
|
+
preparation = updateOwner.run(() ->
|
|
1108
|
+
coordinator.prepareRollback(expectedTrial, reason, this::isBundleUsable)
|
|
1109
|
+
);
|
|
1110
|
+
} catch (java.util.concurrent.CancellationException ignored) {
|
|
1111
|
+
cancelTrialTimeout();
|
|
1112
|
+
return;
|
|
1113
|
+
} catch (Exception error) {
|
|
1114
|
+
android.util.Log.e("OtaKit", "Cannot persist rollback", error);
|
|
1115
|
+
scheduleTrialTimeout(expectedTrial);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1037
1118
|
if (!preparation.didRollback) {
|
|
1038
1119
|
return;
|
|
1039
1120
|
}
|
|
1040
|
-
|
|
1121
|
+
cancelTrialTimeout();
|
|
1122
|
+
cleanupInBackground(preparation.cleanupBundleIds);
|
|
1041
1123
|
if (preparation.eventPayload != null) {
|
|
1042
1124
|
sendDeviceEvent(preparation.eventPayload);
|
|
1043
1125
|
emitEvent(
|
|
@@ -1056,9 +1138,6 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1056
1138
|
|
|
1057
1139
|
try {
|
|
1058
1140
|
applyServerBasePathSynchronously(preparation.activationPath);
|
|
1059
|
-
if (shouldReload) {
|
|
1060
|
-
reloadWebViewSynchronously();
|
|
1061
|
-
}
|
|
1062
1141
|
} catch (Exception e) {
|
|
1063
1142
|
android.util.Log.w("OtaKit", "rollback activation failed", e);
|
|
1064
1143
|
}
|
|
@@ -1069,20 +1148,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1069
1148
|
if (bridge == null) {
|
|
1070
1149
|
throw new IllegalStateException("Bridge not available for activation");
|
|
1071
1150
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
bridge
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
private void reloadWebViewSynchronously() throws Exception {
|
|
1081
|
-
runOnMainSynchronously(() -> {
|
|
1082
|
-
if (bridge == null || bridge.getWebView() == null) {
|
|
1083
|
-
throw new IllegalStateException("WebView not available for reload");
|
|
1084
|
-
}
|
|
1085
|
-
bridge.getWebView().reload();
|
|
1151
|
+
WebViewActivation.activate(
|
|
1152
|
+
path,
|
|
1153
|
+
BUILTIN_ASSET_PATH,
|
|
1154
|
+
bridge::setServerBasePath,
|
|
1155
|
+
bridge::setServerAssetPath
|
|
1156
|
+
);
|
|
1086
1157
|
});
|
|
1087
1158
|
}
|
|
1088
1159
|
|
|
@@ -1170,6 +1241,9 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1170
1241
|
*/
|
|
1171
1242
|
private BundleInfo assembleAndStage(ManifestClient.LatestManifest manifest, String targetChannel)
|
|
1172
1243
|
throws Exception {
|
|
1244
|
+
ensureOwnerActive();
|
|
1245
|
+
String installationId = BundleStore.newInstallationId();
|
|
1246
|
+
String phase = "admission";
|
|
1173
1247
|
// Same conservative disk-space guard as the zip path.
|
|
1174
1248
|
if (manifest.size > 0) {
|
|
1175
1249
|
long requiredSpace = (long) (manifest.size * 2.5);
|
|
@@ -1180,7 +1254,9 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1180
1254
|
manifest.runtimeVersion,
|
|
1181
1255
|
targetChannel,
|
|
1182
1256
|
manifest.releaseId,
|
|
1183
|
-
"insufficient_disk_space"
|
|
1257
|
+
"insufficient_disk_space",
|
|
1258
|
+
installationId,
|
|
1259
|
+
phase
|
|
1184
1260
|
);
|
|
1185
1261
|
emitEvent(
|
|
1186
1262
|
"downloadFailed",
|
|
@@ -1202,6 +1278,7 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1202
1278
|
);
|
|
1203
1279
|
|
|
1204
1280
|
try {
|
|
1281
|
+
phase = "delta";
|
|
1205
1282
|
if (manifest.files == null || manifest.files.isEmpty()) {
|
|
1206
1283
|
throw new IllegalStateException("Delta manifest is missing its file list");
|
|
1207
1284
|
}
|
|
@@ -1214,10 +1291,11 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1214
1291
|
assembler.seedFromBuiltinIfNeeded(getContext(), BUILTIN_ASSET_PATH, store.getNativeBuild());
|
|
1215
1292
|
assembler.assemble(manifest.files, assembleDirectory, getContext());
|
|
1216
1293
|
|
|
1217
|
-
|
|
1294
|
+
phase = "install";
|
|
1295
|
+
String bundleId = installationId;
|
|
1218
1296
|
File destination = coordinator.bundleDirectory(bundleId);
|
|
1219
1297
|
if (destination.exists()) {
|
|
1220
|
-
|
|
1298
|
+
throw new IllegalStateException("Bundle installation directory already exists");
|
|
1221
1299
|
}
|
|
1222
1300
|
moveDirectory(assembleDirectory, destination);
|
|
1223
1301
|
|
|
@@ -1247,7 +1325,8 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1247
1325
|
targetChannel,
|
|
1248
1326
|
manifest.releaseId
|
|
1249
1327
|
);
|
|
1250
|
-
|
|
1328
|
+
phase = "stage";
|
|
1329
|
+
java.util.List<String> cleanupBundleIds = stageOwnedBundle(info);
|
|
1251
1330
|
coordinator.cleanupBundles(cleanupBundleIds);
|
|
1252
1331
|
|
|
1253
1332
|
pruneDeltaCache(assembler);
|
|
@@ -1258,20 +1337,25 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1258
1337
|
manifest.runtimeVersion,
|
|
1259
1338
|
targetChannel,
|
|
1260
1339
|
manifest.releaseId,
|
|
1261
|
-
null
|
|
1340
|
+
null,
|
|
1341
|
+
installationId,
|
|
1342
|
+
phase
|
|
1262
1343
|
);
|
|
1263
1344
|
JSObject stagedData = new JSObject();
|
|
1264
1345
|
stagedData.put("bundle", info.toJSObject());
|
|
1265
1346
|
emitEvent("updateStaged", stagedData);
|
|
1266
1347
|
return info;
|
|
1267
1348
|
} catch (Exception e) {
|
|
1349
|
+
if (e instanceof java.util.concurrent.CancellationException) throw e;
|
|
1268
1350
|
sendDeviceEvent(
|
|
1269
1351
|
"download_error",
|
|
1270
1352
|
manifest.version,
|
|
1271
1353
|
manifest.runtimeVersion,
|
|
1272
1354
|
targetChannel,
|
|
1273
1355
|
manifest.releaseId,
|
|
1274
|
-
e.getMessage()
|
|
1356
|
+
e.getMessage(),
|
|
1357
|
+
installationId,
|
|
1358
|
+
phase
|
|
1275
1359
|
);
|
|
1276
1360
|
emitEvent(
|
|
1277
1361
|
"downloadFailed",
|
|
@@ -1285,6 +1369,9 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1285
1369
|
);
|
|
1286
1370
|
throw e;
|
|
1287
1371
|
} finally {
|
|
1372
|
+
if (installationId != null) coordinator.cleanupBundles(
|
|
1373
|
+
java.util.Collections.singletonList(installationId)
|
|
1374
|
+
);
|
|
1288
1375
|
if (assembleDirectory.exists()) {
|
|
1289
1376
|
try {
|
|
1290
1377
|
deleteRecursively(assembleDirectory);
|
|
@@ -1388,36 +1475,6 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1388
1475
|
}
|
|
1389
1476
|
}
|
|
1390
1477
|
|
|
1391
|
-
private String buildBundleId(String version, String releaseId, String sha256) throws Exception {
|
|
1392
|
-
String trimmed = version == null ? "" : version.trim();
|
|
1393
|
-
String normalized = trimmed.replaceAll("[^A-Za-z0-9._-]", "-");
|
|
1394
|
-
normalized = normalized.replaceAll("-{2,}", "-");
|
|
1395
|
-
normalized = normalized.replaceAll("^[\\-.]+|[\\-.]+$", "");
|
|
1396
|
-
if (normalized.isEmpty()) {
|
|
1397
|
-
normalized = "bundle";
|
|
1398
|
-
}
|
|
1399
|
-
if (normalized.length() > 64) {
|
|
1400
|
-
normalized = normalized.substring(0, 64);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
String identitySource = trimToNull(releaseId);
|
|
1404
|
-
if (identitySource == null) {
|
|
1405
|
-
identitySource = trimToNull(sha256);
|
|
1406
|
-
}
|
|
1407
|
-
if (identitySource == null) {
|
|
1408
|
-
identitySource = trimmed;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
1412
|
-
byte[] hash = digest.digest(identitySource.getBytes(StandardCharsets.UTF_8));
|
|
1413
|
-
StringBuilder suffix = new StringBuilder();
|
|
1414
|
-
for (int i = 0; i < 6; i++) {
|
|
1415
|
-
suffix.append(String.format("%02x", hash[i]));
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
return normalized + "-" + suffix;
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
1478
|
private String resolveIngestUrl(String configured, String env) {
|
|
1422
1479
|
String configuredValue = trimToNull(configured);
|
|
1423
1480
|
if (configuredValue != null) {
|
|
@@ -1539,6 +1596,19 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1539
1596
|
String channel,
|
|
1540
1597
|
String releaseId,
|
|
1541
1598
|
String detail
|
|
1599
|
+
) {
|
|
1600
|
+
sendDeviceEvent(action, bundleVersion, runtimeVersion, channel, releaseId, detail, null, null);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
private void sendDeviceEvent(
|
|
1604
|
+
String action,
|
|
1605
|
+
String bundleVersion,
|
|
1606
|
+
String runtimeVersion,
|
|
1607
|
+
String channel,
|
|
1608
|
+
String releaseId,
|
|
1609
|
+
String detail,
|
|
1610
|
+
String attemptId,
|
|
1611
|
+
String phase
|
|
1542
1612
|
) {
|
|
1543
1613
|
sendDeviceEvent(
|
|
1544
1614
|
new UpdaterCoordinator.DeviceEventPayload(
|
|
@@ -1547,7 +1617,9 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1547
1617
|
runtimeVersion,
|
|
1548
1618
|
channel,
|
|
1549
1619
|
releaseId,
|
|
1550
|
-
detail
|
|
1620
|
+
detail,
|
|
1621
|
+
attemptId,
|
|
1622
|
+
phase
|
|
1551
1623
|
)
|
|
1552
1624
|
);
|
|
1553
1625
|
}
|
|
@@ -1557,12 +1629,12 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1557
1629
|
return;
|
|
1558
1630
|
}
|
|
1559
1631
|
String normalizedBundleVersion = trimToNull(payload.bundleVersion);
|
|
1560
|
-
if (normalizedBundleVersion == null) {
|
|
1632
|
+
if (normalizedBundleVersion == null && !"check_error".equals(payload.action)) {
|
|
1561
1633
|
android.util.Log.w("OtaKit", "Skipping device event without bundleVersion");
|
|
1562
1634
|
return;
|
|
1563
1635
|
}
|
|
1564
1636
|
String normalizedReleaseId = trimToNull(payload.releaseId);
|
|
1565
|
-
if (normalizedReleaseId == null) {
|
|
1637
|
+
if (normalizedReleaseId == null && !"check_error".equals(payload.action)) {
|
|
1566
1638
|
android.util.Log.w("OtaKit", "Skipping device event without releaseId");
|
|
1567
1639
|
return;
|
|
1568
1640
|
}
|
|
@@ -1581,7 +1653,10 @@ public class UpdaterPlugin extends Plugin {
|
|
|
1581
1653
|
trimToNull(payload.runtimeVersion),
|
|
1582
1654
|
normalizedReleaseId,
|
|
1583
1655
|
nativeBuild,
|
|
1584
|
-
payload.detail
|
|
1656
|
+
payload.detail,
|
|
1657
|
+
payload.attemptId,
|
|
1658
|
+
payload.phase,
|
|
1659
|
+
lifecycleState
|
|
1585
1660
|
);
|
|
1586
1661
|
}
|
|
1587
1662
|
|