@otakit/capacitor-updater 2.0.1 → 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/{UpdatekitUpdater.podspec → OtaKitUpdater.podspec} +1 -1
- package/README.md +165 -130
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/BundleInfo.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/BundleStatus.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/BundleStore.java +38 -9
- package/android/src/main/java/com/{updatekit → otakit}/updater/DateUtils.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/DeviceEventClient.java +3 -4
- package/android/src/main/java/com/{updatekit → otakit}/updater/HashUtils.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/HostedManifestKeys.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/ManifestClient.java +32 -15
- package/android/src/main/java/com/{updatekit → otakit}/updater/ManifestVerifier.java +1 -1
- package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +726 -0
- package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +1191 -0
- package/android/src/main/java/com/{updatekit → otakit}/updater/ZipUtils.java +1 -1
- package/dist/esm/definitions.d.ts +48 -105
- 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 +6 -60
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +7 -11
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +8 -14
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +14 -74
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +14 -74
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +28 -6
- package/ios/Sources/UpdaterPlugin/Downloader.swift +1 -1
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +8 -4
- package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +635 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +3 -5
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +506 -551
- package/package.json +2 -2
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +0 -1191
|
@@ -1,1191 +0,0 @@
|
|
|
1
|
-
package com.updatekit.updater;
|
|
2
|
-
|
|
3
|
-
import android.content.pm.PackageInfo;
|
|
4
|
-
import android.content.pm.PackageManager;
|
|
5
|
-
import android.os.Build;
|
|
6
|
-
import android.os.Handler;
|
|
7
|
-
import android.os.Looper;
|
|
8
|
-
import com.getcapacitor.JSArray;
|
|
9
|
-
import com.getcapacitor.JSObject;
|
|
10
|
-
import com.getcapacitor.Plugin;
|
|
11
|
-
import com.getcapacitor.PluginCall;
|
|
12
|
-
import com.getcapacitor.PluginMethod;
|
|
13
|
-
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
14
|
-
import java.io.File;
|
|
15
|
-
import java.io.FileInputStream;
|
|
16
|
-
import java.io.FileOutputStream;
|
|
17
|
-
import java.io.InputStream;
|
|
18
|
-
import java.net.HttpURLConnection;
|
|
19
|
-
import java.net.URL;
|
|
20
|
-
import java.nio.charset.StandardCharsets;
|
|
21
|
-
import java.security.MessageDigest;
|
|
22
|
-
import java.util.concurrent.ExecutorService;
|
|
23
|
-
import java.util.concurrent.Executors;
|
|
24
|
-
import java.util.concurrent.atomic.AtomicBoolean;
|
|
25
|
-
|
|
26
|
-
@CapacitorPlugin(name = "OtaKit")
|
|
27
|
-
public class UpdaterPlugin extends Plugin {
|
|
28
|
-
|
|
29
|
-
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
30
|
-
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
31
|
-
private final ZipUtils zipUtils = new ZipUtils();
|
|
32
|
-
|
|
33
|
-
private BundleStore store;
|
|
34
|
-
private Runnable trialTimeoutRunnable;
|
|
35
|
-
|
|
36
|
-
private int appReadyTimeoutMs = 10_000;
|
|
37
|
-
private boolean allowInsecureUrls = false;
|
|
38
|
-
private String updateMode = UPDATE_MODE_NEXT_LAUNCH;
|
|
39
|
-
private String ingestUrl;
|
|
40
|
-
private String cdnUrl;
|
|
41
|
-
private String appId;
|
|
42
|
-
private String channel;
|
|
43
|
-
private String runtimeVersion;
|
|
44
|
-
private java.util.List<ManifestVerifier.KeyEntry> manifestKeys = new java.util.ArrayList<>();
|
|
45
|
-
private long checkIntervalMs = 600_000;
|
|
46
|
-
private final AtomicBoolean checkInProgress = new AtomicBoolean(false);
|
|
47
|
-
private static final String DEFAULT_INGEST_URL = "https://ingest.otakit.app/v1";
|
|
48
|
-
private static final String DEFAULT_CDN_URL = "https://cdn.otakit.app";
|
|
49
|
-
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
|
-
private static final String KEY_LAST_CHECK_TIMESTAMP = "last_check_timestamp";
|
|
55
|
-
private static final String TRIGGER_LAUNCH = "launch";
|
|
56
|
-
private static final String TRIGGER_RESUME = "resume";
|
|
57
|
-
|
|
58
|
-
@Override
|
|
59
|
-
public void load() {
|
|
60
|
-
super.load();
|
|
61
|
-
|
|
62
|
-
String builtinVersion = "0.0.0";
|
|
63
|
-
String nativeBuild = "1";
|
|
64
|
-
try {
|
|
65
|
-
PackageInfo info = getContext()
|
|
66
|
-
.getPackageManager()
|
|
67
|
-
.getPackageInfo(getContext().getPackageName(), 0);
|
|
68
|
-
builtinVersion = info.versionName != null ? info.versionName : builtinVersion;
|
|
69
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
70
|
-
nativeBuild = String.valueOf(info.getLongVersionCode());
|
|
71
|
-
} else {
|
|
72
|
-
//noinspection deprecation
|
|
73
|
-
nativeBuild = String.valueOf(info.versionCode);
|
|
74
|
-
}
|
|
75
|
-
} catch (PackageManager.NameNotFoundException ignored) {}
|
|
76
|
-
|
|
77
|
-
this.ingestUrl = resolveIngestUrl(
|
|
78
|
-
getConfig().getString("ingestUrl"),
|
|
79
|
-
System.getenv("OTAKIT_INGEST_URL")
|
|
80
|
-
);
|
|
81
|
-
this.cdnUrl = resolveCdnUrl(
|
|
82
|
-
getConfig().getString("cdnUrl"),
|
|
83
|
-
System.getenv("OTAKIT_CDN_URL")
|
|
84
|
-
);
|
|
85
|
-
this.appId = getConfig().getString("appId");
|
|
86
|
-
this.channel = trimToNull(getConfig().getString("channel"));
|
|
87
|
-
this.runtimeVersion = trimToNull(getConfig().getString("runtimeVersion"));
|
|
88
|
-
this.store = new BundleStore(getContext(), builtinVersion, nativeBuild, this.runtimeVersion);
|
|
89
|
-
this.allowInsecureUrls = getConfig().getBoolean("allowInsecureUrls", false);
|
|
90
|
-
String configuredUpdateMode = getConfig().getString("updateMode", UPDATE_MODE_NEXT_LAUNCH);
|
|
91
|
-
this.updateMode = resolveUpdateMode(configuredUpdateMode);
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
org.json.JSONArray rawKeys = getConfig().getConfigJSON().optJSONArray("manifestKeys");
|
|
95
|
-
if (rawKeys != null && rawKeys.length() > 0) {
|
|
96
|
-
for (int i = 0; i < rawKeys.length(); i++) {
|
|
97
|
-
org.json.JSONObject entry = rawKeys.optJSONObject(i);
|
|
98
|
-
if (entry == null) {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
String kid = entry.optString("kid", null);
|
|
102
|
-
String keyBase64 = entry.optString("key", null);
|
|
103
|
-
if (kid != null && keyBase64 != null) {
|
|
104
|
-
byte[] keyBytes = android.util.Base64.decode(keyBase64, android.util.Base64.DEFAULT);
|
|
105
|
-
manifestKeys.add(new ManifestVerifier.KeyEntry(kid, keyBytes));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (manifestKeys.isEmpty()) {
|
|
109
|
-
android.util.Log.e(
|
|
110
|
-
"UpdateKit",
|
|
111
|
-
"manifestKeys configured but all entries are invalid. Manifest verification will reject all updates."
|
|
112
|
-
);
|
|
113
|
-
manifestKeys.add(new ManifestVerifier.KeyEntry("_invalid_", new byte[0]));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} catch (Exception e) {
|
|
117
|
-
android.util.Log.e(
|
|
118
|
-
"UpdateKit",
|
|
119
|
-
"Failed to parse manifestKeys. Manifest verification will reject all updates.",
|
|
120
|
-
e
|
|
121
|
-
);
|
|
122
|
-
manifestKeys.add(new ManifestVerifier.KeyEntry("_invalid_", new byte[0]));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (manifestKeys.isEmpty() && HostedManifestKeys.matchesManagedManifestUrl(cdnUrl)) {
|
|
126
|
-
manifestKeys.addAll(HostedManifestKeys.createDefaultKeys());
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
this.appReadyTimeoutMs = Math.max(1000, getConfig().getInt("appReadyTimeout", 10_000));
|
|
130
|
-
this.checkIntervalMs = Math.max(600_000, getConfig().getInt("checkInterval", 600_000));
|
|
131
|
-
|
|
132
|
-
pruneIncompatibleBundles();
|
|
133
|
-
|
|
134
|
-
BundleInfo current = store.getCurrentBundle();
|
|
135
|
-
if (current.status == BundleStatus.TRIAL) {
|
|
136
|
-
rollbackCurrentBundle("app_restarted_before_notify");
|
|
137
|
-
current = store.getCurrentBundle();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (shouldActivateStagedOnLaunch()) {
|
|
141
|
-
current = activateStagedBundleForLaunch();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!current.isBuiltin() && current.path != null) {
|
|
145
|
-
applyServerBasePath(current.path);
|
|
146
|
-
} else {
|
|
147
|
-
applyServerBasePath(null);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (current.status == BundleStatus.PENDING) {
|
|
151
|
-
store.markStatus(current.id, BundleStatus.TRIAL);
|
|
152
|
-
scheduleTrialTimeout(current.id);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (isAutomaticUpdateMode()) {
|
|
156
|
-
runAutomaticUpdate(TRIGGER_LAUNCH);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
@Override
|
|
161
|
-
protected void handleOnResume() {
|
|
162
|
-
super.handleOnResume();
|
|
163
|
-
if (isAutomaticUpdateMode()) {
|
|
164
|
-
runAutomaticUpdate(TRIGGER_RESUME);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private void runAutomaticUpdate(String trigger) {
|
|
169
|
-
// Resume-only guards
|
|
170
|
-
if (TRIGGER_RESUME.equals(trigger)) {
|
|
171
|
-
if (UPDATE_MODE_MANUAL.equals(updateMode)) return;
|
|
172
|
-
|
|
173
|
-
// next-resume and immediate: activate staged bundle on resume without server check
|
|
174
|
-
if ((UPDATE_MODE_NEXT_RESUME.equals(updateMode) || UPDATE_MODE_IMMEDIATE.equals(updateMode))) {
|
|
175
|
-
String stagedId = store.getStagedBundleId();
|
|
176
|
-
if (stagedId != null && store.getBundle(stagedId) != null) {
|
|
177
|
-
activateStagedBundleForReload();
|
|
178
|
-
reloadWebView();
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!UPDATE_MODE_IMMEDIATE.equals(updateMode) && shouldThrottleCheck()) return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
TRIGGER_LAUNCH.equals(trigger) &&
|
|
188
|
-
!UPDATE_MODE_IMMEDIATE.equals(updateMode) &&
|
|
189
|
-
shouldThrottleCheck()
|
|
190
|
-
) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Acquire in-flight guard (submit-time)
|
|
195
|
-
if (!checkInProgress.compareAndSet(false, true)) return;
|
|
196
|
-
|
|
197
|
-
if (UPDATE_MODE_IMMEDIATE.equals(updateMode)) {
|
|
198
|
-
// Immediate: check+download, activate if found
|
|
199
|
-
executor.execute(() -> {
|
|
200
|
-
try {
|
|
201
|
-
BundleInfo result = performCheckAndDownload(null, true);
|
|
202
|
-
if (result != null) {
|
|
203
|
-
activateStagedBundleForReload();
|
|
204
|
-
reloadWebView();
|
|
205
|
-
}
|
|
206
|
-
} catch (Exception e) {
|
|
207
|
-
android.util.Log.w("OtaKit", "immediate update failed (" + trigger + ")", e);
|
|
208
|
-
} finally {
|
|
209
|
-
checkInProgress.set(false);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// next-launch / next-resume: background check+download (fire-and-forget)
|
|
216
|
-
executor.execute(() -> {
|
|
217
|
-
try {
|
|
218
|
-
performCheckAndDownload(null, true);
|
|
219
|
-
recordCheckTimestamp();
|
|
220
|
-
} catch (Exception ignored) {
|
|
221
|
-
// Check failed — timestamp not recorded, will retry on next trigger
|
|
222
|
-
} finally {
|
|
223
|
-
checkInProgress.set(false);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private boolean shouldThrottleCheck() {
|
|
229
|
-
long lastCheck = store.getPrefs().getLong(KEY_LAST_CHECK_TIMESTAMP, 0);
|
|
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();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private boolean shouldActivateStagedOnLaunch() {
|
|
242
|
-
return !UPDATE_MODE_MANUAL.equals(updateMode);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private boolean isAutomaticUpdateMode() {
|
|
246
|
-
return !UPDATE_MODE_MANUAL.equals(updateMode);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private String resolveUpdateMode(String configuredUpdateMode) {
|
|
250
|
-
if (configuredUpdateMode == null) {
|
|
251
|
-
return UPDATE_MODE_NEXT_LAUNCH;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
String raw = configuredUpdateMode.trim().toLowerCase();
|
|
255
|
-
if (raw.isEmpty() || UPDATE_MODE_NEXT_LAUNCH.equals(raw)) {
|
|
256
|
-
return UPDATE_MODE_NEXT_LAUNCH;
|
|
257
|
-
}
|
|
258
|
-
if (UPDATE_MODE_MANUAL.equals(raw)) {
|
|
259
|
-
return UPDATE_MODE_MANUAL;
|
|
260
|
-
}
|
|
261
|
-
if (UPDATE_MODE_NEXT_RESUME.equals(raw)) {
|
|
262
|
-
return UPDATE_MODE_NEXT_RESUME;
|
|
263
|
-
}
|
|
264
|
-
if (UPDATE_MODE_IMMEDIATE.equals(raw)) {
|
|
265
|
-
return UPDATE_MODE_IMMEDIATE;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
android.util.Log.w(
|
|
269
|
-
"OtaKit",
|
|
270
|
-
"Unknown updateMode '" + raw + "', defaulting to 'next-launch'"
|
|
271
|
-
);
|
|
272
|
-
return UPDATE_MODE_NEXT_LAUNCH;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
@PluginMethod
|
|
276
|
-
public void debugGetState(PluginCall call) {
|
|
277
|
-
JSObject result = new JSObject();
|
|
278
|
-
result.put("current", store.getCurrentBundle().toJSObject());
|
|
279
|
-
result.put("fallback", store.getFallbackBundle().toJSObject());
|
|
280
|
-
result.put("builtinVersion", store.getBuiltinVersion());
|
|
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
|
-
|
|
293
|
-
call.resolve(result);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
@PluginMethod
|
|
297
|
-
public void check(PluginCall call) {
|
|
298
|
-
if (!checkInProgress.compareAndSet(false, true)) {
|
|
299
|
-
String stagedId = store.getStagedBundleId();
|
|
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);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
String targetChannel = resolveTargetChannel(null);
|
|
323
|
-
executor.execute(() -> {
|
|
324
|
-
try {
|
|
325
|
-
ManifestClient.LatestManifest latest = fetchLatest(targetChannel);
|
|
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
|
-
}
|
|
334
|
-
} catch (Exception e) {
|
|
335
|
-
call.reject("check failed: " + e.getMessage());
|
|
336
|
-
} finally {
|
|
337
|
-
checkInProgress.set(false);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
@PluginMethod
|
|
343
|
-
public void download(PluginCall call) {
|
|
344
|
-
if (!checkInProgress.compareAndSet(false, true)) {
|
|
345
|
-
String stagedId = store.getStagedBundleId();
|
|
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);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
executor.execute(() -> {
|
|
357
|
-
try {
|
|
358
|
-
BundleInfo bundle = performCheckAndDownload(null, true);
|
|
359
|
-
if (bundle == null) {
|
|
360
|
-
call.resolve((JSObject) null);
|
|
361
|
-
} else {
|
|
362
|
-
call.resolve(bundle.toJSObject());
|
|
363
|
-
}
|
|
364
|
-
} catch (Exception e) {
|
|
365
|
-
call.reject("download failed: " + e.getMessage());
|
|
366
|
-
} finally {
|
|
367
|
-
checkInProgress.set(false);
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
@PluginMethod
|
|
373
|
-
public void apply(PluginCall call) {
|
|
374
|
-
String stagedId = store.getStagedBundleId();
|
|
375
|
-
if (stagedId == null) {
|
|
376
|
-
call.reject("No staged update to apply");
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (!store.bundleExists(stagedId)) {
|
|
380
|
-
store.setStagedBundleId(null);
|
|
381
|
-
call.reject("Staged bundle not found");
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
activateStagedBundleForReload();
|
|
386
|
-
call.resolve();
|
|
387
|
-
reloadWebView();
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
@PluginMethod
|
|
391
|
-
public void notifyAppReady(PluginCall call) {
|
|
392
|
-
BundleInfo current = store.getCurrentBundle();
|
|
393
|
-
if (
|
|
394
|
-
!current.isBuiltin() &&
|
|
395
|
-
(current.status == BundleStatus.TRIAL || current.status == BundleStatus.PENDING)
|
|
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
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
call.resolve();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
@PluginMethod
|
|
424
|
-
public void debugReset(PluginCall call) {
|
|
425
|
-
cancelTrialTimeout();
|
|
426
|
-
store.setCurrentBundleId(null);
|
|
427
|
-
store.setStagedBundleId(null);
|
|
428
|
-
store.setFallbackBundleId(null);
|
|
429
|
-
store.setFailedBundle(null);
|
|
430
|
-
applyServerBasePath(null);
|
|
431
|
-
call.resolve();
|
|
432
|
-
reloadWebView();
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
@PluginMethod
|
|
436
|
-
public void debugListBundles(PluginCall call) {
|
|
437
|
-
JSObject result = new JSObject();
|
|
438
|
-
JSArray bundles = store.listDownloadedBundles();
|
|
439
|
-
result.put("bundles", bundles);
|
|
440
|
-
call.resolve(result);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
@PluginMethod
|
|
444
|
-
public void debugDeleteBundle(PluginCall call) {
|
|
445
|
-
String bundleId = call.getString("bundleId");
|
|
446
|
-
if (bundleId == null) {
|
|
447
|
-
call.reject("Missing bundleId");
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
if (!store.bundleExists(bundleId)) {
|
|
451
|
-
call.reject("Bundle not found");
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
BundleInfo current = store.getCurrentBundle();
|
|
456
|
-
if (bundleId.equals(current.id)) {
|
|
457
|
-
call.reject("Cannot delete current bundle");
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
BundleInfo fallback = store.getFallbackBundle();
|
|
462
|
-
if (bundleId.equals(fallback.id)) {
|
|
463
|
-
call.reject("Cannot delete fallback bundle");
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
String stagedId = store.getStagedBundleId();
|
|
468
|
-
if (bundleId.equals(stagedId)) {
|
|
469
|
-
call.reject("Cannot delete staged bundle");
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
try {
|
|
474
|
-
store.deleteBundle(bundleId);
|
|
475
|
-
call.resolve();
|
|
476
|
-
} catch (Exception e) {
|
|
477
|
-
call.reject("Delete failed: " + e.getMessage());
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
@PluginMethod
|
|
482
|
-
public void debugGetLastFailure(PluginCall call) {
|
|
483
|
-
BundleInfo failed = store.getFailedBundle();
|
|
484
|
-
if (failed == null) {
|
|
485
|
-
call.resolve((JSObject) null);
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
call.resolve(failed.toJSObject());
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
private ManifestClient.LatestManifest fetchLatest(String channel) throws Exception {
|
|
492
|
-
if (appId == null || appId.trim().isEmpty()) {
|
|
493
|
-
throw new IllegalStateException("Missing appId in plugin config");
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return ManifestClient.fetchLatest(
|
|
497
|
-
cdnUrl,
|
|
498
|
-
appId,
|
|
499
|
-
channel,
|
|
500
|
-
runtimeVersion,
|
|
501
|
-
allowInsecureUrls,
|
|
502
|
-
manifestKeys
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private BundleInfo performCheckAndDownload(String channel, boolean emitEvents) throws Exception {
|
|
507
|
-
String targetChannel = resolveTargetChannel(channel);
|
|
508
|
-
ManifestClient.LatestManifest latest = fetchLatest(targetChannel);
|
|
509
|
-
if (latest == null) {
|
|
510
|
-
if (emitEvents) {
|
|
511
|
-
notifyListeners("noUpdateAvailable", new JSObject());
|
|
512
|
-
}
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (!isCompatibleRuntime(latest.runtimeVersion)) {
|
|
517
|
-
throw new IllegalStateException(
|
|
518
|
-
"Manifest runtimeVersion does not match the installed app runtime"
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if (isCurrentBundleLatest(latest, targetChannel)) {
|
|
523
|
-
if (emitEvents) {
|
|
524
|
-
notifyListeners("noUpdateAvailable", new JSObject());
|
|
525
|
-
}
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
BundleInfo staged = findMatchingStagedBundle(latest, targetChannel);
|
|
530
|
-
if (emitEvents) {
|
|
531
|
-
notifyListeners("updateAvailable", manifestToJSObject(latest, staged != null));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (staged != null) {
|
|
535
|
-
return staged;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
try {
|
|
539
|
-
return downloadAndStage(
|
|
540
|
-
new URL(latest.url),
|
|
541
|
-
latest.version,
|
|
542
|
-
latest.sha256,
|
|
543
|
-
latest.size,
|
|
544
|
-
latest.runtimeVersion,
|
|
545
|
-
targetChannel,
|
|
546
|
-
latest.releaseId
|
|
547
|
-
);
|
|
548
|
-
} catch (Exception e) {
|
|
549
|
-
if (isExpiredURLError(e)) {
|
|
550
|
-
// Download URL may have expired — re-fetch manifest once and retry
|
|
551
|
-
ManifestClient.LatestManifest refreshed = fetchLatest(targetChannel);
|
|
552
|
-
if (refreshed == null) throw e;
|
|
553
|
-
return downloadAndStage(
|
|
554
|
-
new URL(refreshed.url),
|
|
555
|
-
refreshed.version,
|
|
556
|
-
refreshed.sha256,
|
|
557
|
-
refreshed.size,
|
|
558
|
-
refreshed.runtimeVersion,
|
|
559
|
-
targetChannel,
|
|
560
|
-
refreshed.releaseId
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
throw e;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
private boolean isExpiredURLError(Exception e) {
|
|
568
|
-
String msg = e.getMessage();
|
|
569
|
-
if (msg == null) return false;
|
|
570
|
-
msg = msg.toLowerCase();
|
|
571
|
-
return (
|
|
572
|
-
msg.contains("403") ||
|
|
573
|
-
msg.contains("410") ||
|
|
574
|
-
msg.contains("forbidden") ||
|
|
575
|
-
msg.contains("expired")
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
private BundleInfo downloadAndStage(URL url, String version, String expectedSha256)
|
|
580
|
-
throws Exception {
|
|
581
|
-
return downloadAndStage(url, version, expectedSha256, 0, null, null, null);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
private BundleInfo downloadAndStage(
|
|
585
|
-
URL url,
|
|
586
|
-
String version,
|
|
587
|
-
String expectedSha256,
|
|
588
|
-
int expectedSize,
|
|
589
|
-
String runtimeVersion,
|
|
590
|
-
String channel,
|
|
591
|
-
String releaseId
|
|
592
|
-
) throws Exception {
|
|
593
|
-
// Check disk space before downloading
|
|
594
|
-
if (expectedSize > 0) {
|
|
595
|
-
long requiredSpace = (long) (expectedSize * 2.5); // zip + extracted + buffer
|
|
596
|
-
long availableSpace = getFreeDiskSpace();
|
|
597
|
-
if (availableSpace < requiredSpace) {
|
|
598
|
-
sendDeviceEvent(
|
|
599
|
-
"download_error",
|
|
600
|
-
version,
|
|
601
|
-
runtimeVersion,
|
|
602
|
-
channel,
|
|
603
|
-
releaseId,
|
|
604
|
-
"insufficient_disk_space"
|
|
605
|
-
);
|
|
606
|
-
throw new IllegalStateException("Insufficient disk space");
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
JSObject start = new JSObject();
|
|
611
|
-
start.put("version", version);
|
|
612
|
-
notifyListeners("downloadStarted", start);
|
|
613
|
-
|
|
614
|
-
File downloadedZip = null;
|
|
615
|
-
File extractedDirectory = null;
|
|
616
|
-
|
|
617
|
-
try {
|
|
618
|
-
downloadedZip = downloadZip(url);
|
|
619
|
-
if (!HashUtils.verify(downloadedZip, expectedSha256)) {
|
|
620
|
-
throw new IllegalStateException("Downloaded bundle hash mismatch");
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
extractedDirectory = new File(
|
|
624
|
-
getContext().getCacheDir(),
|
|
625
|
-
"updatekit-extract-" + System.currentTimeMillis()
|
|
626
|
-
);
|
|
627
|
-
if (!extractedDirectory.exists() && !extractedDirectory.mkdirs()) {
|
|
628
|
-
throw new IllegalStateException("Cannot create temporary extraction directory");
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
zipUtils.extractSecurely(downloadedZip, extractedDirectory);
|
|
632
|
-
File bundleRoot = resolveBundleRoot(extractedDirectory);
|
|
633
|
-
|
|
634
|
-
String bundleId = buildBundleId(version);
|
|
635
|
-
File destination = store.bundleDirectory(bundleId);
|
|
636
|
-
if (destination.exists()) {
|
|
637
|
-
deleteRecursively(destination);
|
|
638
|
-
}
|
|
639
|
-
moveDirectory(bundleRoot, destination);
|
|
640
|
-
|
|
641
|
-
BundleInfo info = new BundleInfo(
|
|
642
|
-
bundleId,
|
|
643
|
-
version,
|
|
644
|
-
runtimeVersion,
|
|
645
|
-
BundleStatus.PENDING,
|
|
646
|
-
System.currentTimeMillis(),
|
|
647
|
-
expectedSha256,
|
|
648
|
-
destination.getAbsolutePath(),
|
|
649
|
-
channel,
|
|
650
|
-
releaseId
|
|
651
|
-
);
|
|
652
|
-
String previousStagedId = store.getStagedBundleId();
|
|
653
|
-
store.saveBundle(info);
|
|
654
|
-
store.setStagedBundleId(bundleId);
|
|
655
|
-
cleanupSupersededStagedBundle(previousStagedId, bundleId);
|
|
656
|
-
|
|
657
|
-
notifyListeners("downloadComplete", info.toJSObject());
|
|
658
|
-
sendDeviceEvent("downloaded", version, runtimeVersion, channel, releaseId, null);
|
|
659
|
-
return info;
|
|
660
|
-
} catch (Exception e) {
|
|
661
|
-
JSObject failed = new JSObject();
|
|
662
|
-
failed.put("version", version);
|
|
663
|
-
failed.put("error", e.getMessage());
|
|
664
|
-
notifyListeners("downloadFailed", failed);
|
|
665
|
-
sendDeviceEvent("download_error", version, runtimeVersion, channel, releaseId, e.getMessage());
|
|
666
|
-
throw e;
|
|
667
|
-
} finally {
|
|
668
|
-
if (downloadedZip != null && downloadedZip.exists()) {
|
|
669
|
-
//noinspection ResultOfMethodCallIgnored
|
|
670
|
-
downloadedZip.delete();
|
|
671
|
-
}
|
|
672
|
-
if (extractedDirectory != null && extractedDirectory.exists()) {
|
|
673
|
-
try {
|
|
674
|
-
deleteRecursively(extractedDirectory);
|
|
675
|
-
} catch (Exception ignored) {}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
private File downloadZip(URL url) throws Exception {
|
|
681
|
-
ManifestClient.requireHTTPS(url, allowInsecureUrls);
|
|
682
|
-
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
683
|
-
try {
|
|
684
|
-
connection.setRequestMethod("GET");
|
|
685
|
-
connection.setConnectTimeout(15_000);
|
|
686
|
-
connection.setReadTimeout(60_000);
|
|
687
|
-
|
|
688
|
-
int status = connection.getResponseCode();
|
|
689
|
-
if (status < 200 || status >= 300) {
|
|
690
|
-
throw new IllegalStateException("Download failed with HTTP " + status);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
File destination = File.createTempFile("updatekit-", ".zip", getContext().getCacheDir());
|
|
694
|
-
|
|
695
|
-
try (
|
|
696
|
-
InputStream input = connection.getInputStream();
|
|
697
|
-
FileOutputStream output = new FileOutputStream(destination)
|
|
698
|
-
) {
|
|
699
|
-
byte[] buffer = new byte[8192];
|
|
700
|
-
int read;
|
|
701
|
-
while ((read = input.read(buffer)) > 0) {
|
|
702
|
-
output.write(buffer, 0, read);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
return destination;
|
|
707
|
-
} finally {
|
|
708
|
-
connection.disconnect();
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
private File resolveBundleRoot(File extractedDirectory) throws Exception {
|
|
713
|
-
File rootIndex = new File(extractedDirectory, "index.html");
|
|
714
|
-
if (rootIndex.exists()) {
|
|
715
|
-
return extractedDirectory;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
File[] children = extractedDirectory.listFiles();
|
|
719
|
-
if (children != null && children.length == 1 && children[0].isDirectory()) {
|
|
720
|
-
File nestedIndex = new File(children[0], "index.html");
|
|
721
|
-
if (nestedIndex.exists()) {
|
|
722
|
-
return children[0];
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
throw new IllegalStateException("Bundle archive does not contain index.html");
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
private BundleInfo activateStagedBundleForLaunch() {
|
|
730
|
-
String stagedId = store.getStagedBundleId();
|
|
731
|
-
if (stagedId == null) {
|
|
732
|
-
return store.getCurrentBundle();
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
BundleInfo staged = store.getBundle(stagedId);
|
|
736
|
-
if (staged == null) {
|
|
737
|
-
store.setStagedBundleId(null);
|
|
738
|
-
return store.getCurrentBundle();
|
|
739
|
-
}
|
|
740
|
-
if (!isCompatibleRuntime(staged)) {
|
|
741
|
-
try {
|
|
742
|
-
store.deleteBundle(staged.id);
|
|
743
|
-
} catch (Exception ignored) {}
|
|
744
|
-
return store.getCurrentBundle();
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
store.setCurrentBundleId(staged.id);
|
|
748
|
-
store.setStagedBundleId(null);
|
|
749
|
-
return staged;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
private void activateStagedBundleForReload() {
|
|
753
|
-
String stagedId = store.getStagedBundleId();
|
|
754
|
-
if (stagedId == null) {
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
BundleInfo staged = store.getBundle(stagedId);
|
|
759
|
-
if (staged == null) {
|
|
760
|
-
store.setStagedBundleId(null);
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
if (!isCompatibleRuntime(staged)) {
|
|
764
|
-
try {
|
|
765
|
-
store.deleteBundle(staged.id);
|
|
766
|
-
} catch (Exception ignored) {}
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
store.setCurrentBundleId(staged.id);
|
|
771
|
-
store.setStagedBundleId(null);
|
|
772
|
-
|
|
773
|
-
if (staged.status == BundleStatus.PENDING) {
|
|
774
|
-
store.markStatus(staged.id, BundleStatus.TRIAL);
|
|
775
|
-
staged = store.getBundle(staged.id);
|
|
776
|
-
}
|
|
777
|
-
if (staged != null && staged.status == BundleStatus.TRIAL) {
|
|
778
|
-
scheduleTrialTimeout(staged.id);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (staged != null && staged.path != null) {
|
|
782
|
-
applyServerBasePath(staged.path);
|
|
783
|
-
} else {
|
|
784
|
-
applyServerBasePath(null);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
private void scheduleTrialTimeout(String bundleId) {
|
|
789
|
-
cancelTrialTimeout();
|
|
790
|
-
trialTimeoutRunnable = () -> {
|
|
791
|
-
BundleInfo current = store.getCurrentBundle();
|
|
792
|
-
if (current.id.equals(bundleId) && current.status == BundleStatus.TRIAL) {
|
|
793
|
-
rollbackCurrentBundle("notify_timeout");
|
|
794
|
-
}
|
|
795
|
-
};
|
|
796
|
-
mainHandler.postDelayed(trialTimeoutRunnable, appReadyTimeoutMs);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
private void cancelTrialTimeout() {
|
|
800
|
-
if (trialTimeoutRunnable != null) {
|
|
801
|
-
mainHandler.removeCallbacks(trialTimeoutRunnable);
|
|
802
|
-
trialTimeoutRunnable = null;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
private void rollbackCurrentBundle(String reason) {
|
|
807
|
-
cancelTrialTimeout();
|
|
808
|
-
|
|
809
|
-
BundleInfo current = store.getCurrentBundle();
|
|
810
|
-
if (current.isBuiltin()) {
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
BundleInfo failed = current.withStatus(BundleStatus.ERROR);
|
|
815
|
-
store.markStatus(current.id, BundleStatus.ERROR);
|
|
816
|
-
store.setFailedBundle(failed);
|
|
817
|
-
store.setStagedBundleId(null);
|
|
818
|
-
|
|
819
|
-
sendDeviceEvent(
|
|
820
|
-
"rollback",
|
|
821
|
-
current.version,
|
|
822
|
-
current.runtimeVersion,
|
|
823
|
-
current.channel,
|
|
824
|
-
current.releaseId,
|
|
825
|
-
reason
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
BundleInfo fallback = store.getFallbackBundle();
|
|
829
|
-
JSObject payload = new JSObject();
|
|
830
|
-
payload.put("from", failed.toJSObject());
|
|
831
|
-
|
|
832
|
-
if (!fallback.isBuiltin() && fallback.path != null) {
|
|
833
|
-
store.setCurrentBundleId(fallback.id);
|
|
834
|
-
applyServerBasePath(fallback.path);
|
|
835
|
-
payload.put("to", fallback.toJSObject());
|
|
836
|
-
} else {
|
|
837
|
-
store.setCurrentBundleId(null);
|
|
838
|
-
applyServerBasePath(null);
|
|
839
|
-
payload.put("to", store.builtinBundle().toJSObject());
|
|
840
|
-
}
|
|
841
|
-
payload.put("reason", reason);
|
|
842
|
-
|
|
843
|
-
notifyListeners("rollback", payload);
|
|
844
|
-
|
|
845
|
-
try {
|
|
846
|
-
store.deleteBundle(current.id);
|
|
847
|
-
} catch (Exception ignored) {}
|
|
848
|
-
|
|
849
|
-
reloadWebView();
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
private void cleanupSupersededStagedBundle(String previousStagedId, String replacementId) {
|
|
853
|
-
if (
|
|
854
|
-
previousStagedId == null ||
|
|
855
|
-
previousStagedId.equals(replacementId) ||
|
|
856
|
-
"builtin".equals(previousStagedId)
|
|
857
|
-
) {
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
BundleInfo current = store.getCurrentBundle();
|
|
862
|
-
if (previousStagedId.equals(current.id)) {
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
BundleInfo fallback = store.getFallbackBundle();
|
|
867
|
-
if (previousStagedId.equals(fallback.id)) {
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
try {
|
|
872
|
-
store.deleteBundle(previousStagedId);
|
|
873
|
-
} catch (Exception ignored) {}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
private void applyServerBasePath(String path) {
|
|
877
|
-
mainHandler.post(() -> {
|
|
878
|
-
try {
|
|
879
|
-
if (path == null || path.isEmpty()) {
|
|
880
|
-
bridge.setServerBasePath("");
|
|
881
|
-
} else {
|
|
882
|
-
bridge.setServerBasePath(path);
|
|
883
|
-
}
|
|
884
|
-
} catch (Throwable ignored) {}
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
private void reloadWebView() {
|
|
889
|
-
mainHandler.post(() -> {
|
|
890
|
-
if (bridge != null && bridge.getWebView() != null) {
|
|
891
|
-
bridge.getWebView().reload();
|
|
892
|
-
}
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
private JSObject manifestToJSObject(ManifestClient.LatestManifest latest, boolean downloaded) {
|
|
897
|
-
JSObject object = new JSObject();
|
|
898
|
-
object.put("version", latest.version);
|
|
899
|
-
object.put("url", latest.url);
|
|
900
|
-
object.put("sha256", latest.sha256);
|
|
901
|
-
object.put("size", latest.size);
|
|
902
|
-
object.put("downloaded", downloaded);
|
|
903
|
-
if (latest.runtimeVersion != null) {
|
|
904
|
-
object.put("runtimeVersion", latest.runtimeVersion);
|
|
905
|
-
}
|
|
906
|
-
object.put("releaseId", latest.releaseId);
|
|
907
|
-
return object;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
private boolean isCurrentBundleLatest(
|
|
911
|
-
ManifestClient.LatestManifest latest,
|
|
912
|
-
String targetChannel
|
|
913
|
-
) {
|
|
914
|
-
return doesBundleMatchLatest(store.getCurrentBundle(), latest, targetChannel);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
private boolean doesBundleMatchLatest(
|
|
918
|
-
BundleInfo bundle,
|
|
919
|
-
ManifestClient.LatestManifest latest,
|
|
920
|
-
String targetChannel
|
|
921
|
-
) {
|
|
922
|
-
if (bundle == null) {
|
|
923
|
-
return false;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
if (!java.util.Objects.equals(trimToNull(bundle.channel), targetChannel)) {
|
|
927
|
-
return false;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
if (
|
|
931
|
-
!java.util.Objects.equals(
|
|
932
|
-
trimToNull(bundle.runtimeVersion),
|
|
933
|
-
trimToNull(latest.runtimeVersion)
|
|
934
|
-
)
|
|
935
|
-
) {
|
|
936
|
-
return false;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if (
|
|
940
|
-
latest.releaseId != null &&
|
|
941
|
-
bundle.releaseId != null &&
|
|
942
|
-
latest.releaseId.equals(bundle.releaseId)
|
|
943
|
-
) {
|
|
944
|
-
return true;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
if (
|
|
948
|
-
latest.sha256 != null &&
|
|
949
|
-
bundle.sha256 != null &&
|
|
950
|
-
latest.sha256.equals(bundle.sha256)
|
|
951
|
-
) {
|
|
952
|
-
return true;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
return latest.version != null && latest.version.equals(bundle.version);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
private BundleInfo findMatchingStagedBundle(
|
|
959
|
-
ManifestClient.LatestManifest latest,
|
|
960
|
-
String targetChannel
|
|
961
|
-
) {
|
|
962
|
-
String stagedId = store.getStagedBundleId();
|
|
963
|
-
if (stagedId == null) {
|
|
964
|
-
return null;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
BundleInfo staged = store.getBundle(stagedId);
|
|
968
|
-
if (staged == null) {
|
|
969
|
-
store.setStagedBundleId(null);
|
|
970
|
-
return null;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (!java.util.Objects.equals(trimToNull(staged.channel), targetChannel)) {
|
|
974
|
-
return null;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
if (!java.util.Objects.equals(trimToNull(staged.runtimeVersion), trimToNull(latest.runtimeVersion))) {
|
|
978
|
-
return null;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
return doesBundleMatchLatest(staged, latest, targetChannel) ? staged : null;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
private void moveDirectory(File source, File destination) throws Exception {
|
|
985
|
-
if (source.renameTo(destination)) {
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
copyRecursively(source, destination);
|
|
989
|
-
deleteRecursively(source);
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
private void copyRecursively(File source, File destination) throws Exception {
|
|
993
|
-
if (source.isDirectory()) {
|
|
994
|
-
if (!destination.exists() && !destination.mkdirs()) {
|
|
995
|
-
throw new IllegalStateException(
|
|
996
|
-
"Cannot create directory: " + destination.getAbsolutePath()
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
File[] children = source.listFiles();
|
|
1000
|
-
if (children != null) {
|
|
1001
|
-
for (File child : children) {
|
|
1002
|
-
copyRecursively(child, new File(destination, child.getName()));
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
File parent = destination.getParentFile();
|
|
1009
|
-
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
|
1010
|
-
throw new IllegalStateException("Cannot create parent: " + parent.getAbsolutePath());
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
try (
|
|
1014
|
-
FileInputStream input = new FileInputStream(source);
|
|
1015
|
-
FileOutputStream output = new FileOutputStream(destination)
|
|
1016
|
-
) {
|
|
1017
|
-
byte[] buffer = new byte[8192];
|
|
1018
|
-
int read;
|
|
1019
|
-
while ((read = input.read(buffer)) > 0) {
|
|
1020
|
-
output.write(buffer, 0, read);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
private void deleteRecursively(File target) throws Exception {
|
|
1026
|
-
if (!target.exists()) {
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
if (target.isDirectory()) {
|
|
1030
|
-
File[] children = target.listFiles();
|
|
1031
|
-
if (children != null) {
|
|
1032
|
-
for (File child : children) {
|
|
1033
|
-
deleteRecursively(child);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
if (!target.delete()) {
|
|
1038
|
-
throw new IllegalStateException("Failed to delete: " + target.getAbsolutePath());
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
private String buildBundleId(String version) throws Exception {
|
|
1043
|
-
String trimmed = version == null ? "" : version.trim();
|
|
1044
|
-
String normalized = trimmed.replaceAll("[^A-Za-z0-9._-]", "-");
|
|
1045
|
-
normalized = normalized.replaceAll("-{2,}", "-");
|
|
1046
|
-
normalized = normalized.replaceAll("^[\\-.]+|[\\-.]+$", "");
|
|
1047
|
-
if (normalized.isEmpty()) {
|
|
1048
|
-
normalized = "bundle";
|
|
1049
|
-
}
|
|
1050
|
-
if (normalized.length() > 64) {
|
|
1051
|
-
normalized = normalized.substring(0, 64);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
1055
|
-
byte[] hash = digest.digest(trimmed.getBytes(StandardCharsets.UTF_8));
|
|
1056
|
-
StringBuilder suffix = new StringBuilder();
|
|
1057
|
-
for (int i = 0; i < 6; i++) {
|
|
1058
|
-
suffix.append(String.format("%02x", hash[i]));
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
return normalized + "-" + suffix;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
private String resolveIngestUrl(String configured, String env) {
|
|
1065
|
-
String configuredValue = trimToNull(configured);
|
|
1066
|
-
if (configuredValue != null) {
|
|
1067
|
-
return normalizeIngestUrl(configuredValue);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
String envValue = trimToNull(env);
|
|
1071
|
-
if (envValue != null) {
|
|
1072
|
-
return normalizeIngestUrl(envValue);
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
return DEFAULT_INGEST_URL;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
private String resolveCdnUrl(String configured, String env) {
|
|
1079
|
-
String configuredValue = trimToNull(configured);
|
|
1080
|
-
if (configuredValue != null) {
|
|
1081
|
-
return normalizeCdnUrl(configuredValue);
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
String envValue = trimToNull(env);
|
|
1085
|
-
if (envValue != null) {
|
|
1086
|
-
return normalizeCdnUrl(envValue);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
return DEFAULT_CDN_URL;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
private String normalizeIngestUrl(String raw) {
|
|
1093
|
-
String trimmed = raw.trim().replaceAll("/+$", "");
|
|
1094
|
-
if (trimmed.toLowerCase(java.util.Locale.ROOT).endsWith(INGEST_PATH_SUFFIX)) {
|
|
1095
|
-
return trimmed;
|
|
1096
|
-
}
|
|
1097
|
-
return trimmed + INGEST_PATH_SUFFIX;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
private String normalizeCdnUrl(String raw) {
|
|
1101
|
-
return raw.trim().replaceAll("/+$", "");
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
private String trimToNull(String value) {
|
|
1105
|
-
if (value == null) {
|
|
1106
|
-
return null;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
String trimmed = value.trim();
|
|
1110
|
-
return trimmed.isEmpty() ? null : trimmed;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
private String resolveTargetChannel(String channel) {
|
|
1114
|
-
String resolved = trimToNull(channel);
|
|
1115
|
-
return resolved != null ? resolved : this.channel;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
private void sendDeviceEvent(
|
|
1119
|
-
String action,
|
|
1120
|
-
String bundleVersion,
|
|
1121
|
-
String runtimeVersion,
|
|
1122
|
-
String channel,
|
|
1123
|
-
String releaseId,
|
|
1124
|
-
String detail
|
|
1125
|
-
) {
|
|
1126
|
-
if (appId == null) {
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
String normalizedBundleVersion = trimToNull(bundleVersion);
|
|
1130
|
-
if (normalizedBundleVersion == null) {
|
|
1131
|
-
android.util.Log.w("UpdateKit", "Skipping device event without bundleVersion");
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
String normalizedReleaseId = trimToNull(releaseId);
|
|
1135
|
-
if (normalizedReleaseId == null) {
|
|
1136
|
-
android.util.Log.w("UpdateKit", "Skipping device event without releaseId");
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
String nativeBuild = trimToNull(store.getNativeBuild());
|
|
1140
|
-
if (nativeBuild == null) {
|
|
1141
|
-
android.util.Log.w("UpdateKit", "Skipping device event without nativeBuild");
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
DeviceEventClient.send(
|
|
1145
|
-
ingestUrl,
|
|
1146
|
-
appId,
|
|
1147
|
-
"android",
|
|
1148
|
-
action,
|
|
1149
|
-
normalizedBundleVersion,
|
|
1150
|
-
channel,
|
|
1151
|
-
trimToNull(runtimeVersion),
|
|
1152
|
-
normalizedReleaseId,
|
|
1153
|
-
nativeBuild,
|
|
1154
|
-
detail
|
|
1155
|
-
);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
private void pruneIncompatibleBundles() {
|
|
1159
|
-
for (BundleInfo bundle : store.listDownloadedBundleInfos()) {
|
|
1160
|
-
if (!isCompatibleRuntime(bundle)) {
|
|
1161
|
-
try {
|
|
1162
|
-
store.deleteBundle(bundle.id);
|
|
1163
|
-
} catch (Exception ignored) {}
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
BundleInfo failed = store.getFailedBundle();
|
|
1168
|
-
if (failed != null && !isCompatibleRuntime(failed)) {
|
|
1169
|
-
store.setFailedBundle(null);
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
private boolean isCompatibleRuntime(String bundleRuntimeVersion) {
|
|
1174
|
-
return java.util.Objects.equals(trimToNull(bundleRuntimeVersion), runtimeVersion);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
private boolean isCompatibleRuntime(BundleInfo bundle) {
|
|
1178
|
-
return isCompatibleRuntime(bundle.runtimeVersion);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
private long getFreeDiskSpace() {
|
|
1182
|
-
try {
|
|
1183
|
-
android.os.StatFs statFs = new android.os.StatFs(
|
|
1184
|
-
getContext().getFilesDir().getAbsolutePath()
|
|
1185
|
-
);
|
|
1186
|
-
return statFs.getAvailableBlocksLong() * statFs.getBlockSizeLong();
|
|
1187
|
-
} catch (Exception e) {
|
|
1188
|
-
return Long.MAX_VALUE; // If we can't determine, allow download
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
}
|