@capgo/capacitor-updater 6.14.0 → 6.14.2

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.
Files changed (30) hide show
  1. package/README.md +11 -11
  2. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +134 -194
  3. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +23 -23
  4. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +1 -1
  5. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +960 -1165
  6. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1259 -1629
  7. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +161 -197
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +202 -256
  9. package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +16 -16
  10. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +44 -48
  11. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +378 -467
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +71 -83
  14. package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +19 -28
  15. package/dist/docs.json +17 -17
  16. package/dist/esm/definitions.d.ts +13 -13
  17. package/dist/esm/definitions.js.map +1 -1
  18. package/dist/esm/index.d.ts +2 -2
  19. package/dist/esm/index.js +4 -4
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/web.d.ts +2 -2
  22. package/dist/esm/web.js +43 -43
  23. package/dist/esm/web.js.map +1 -1
  24. package/dist/plugin.cjs.js +43 -43
  25. package/dist/plugin.cjs.js.map +1 -1
  26. package/dist/plugin.js +43 -43
  27. package/dist/plugin.js.map +1 -1
  28. package/ios/Plugin/CapacitorUpdater.swift +8 -8
  29. package/ios/Plugin/CapacitorUpdaterPlugin.swift +1 -1
  30. package/package.json +5 -7
@@ -50,1253 +50,1048 @@ import org.json.JSONObject;
50
50
 
51
51
  public class CapacitorUpdater {
52
52
 
53
- private static final String AB =
54
- "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
55
- private static final SecureRandom rnd = new SecureRandom();
56
-
57
- private static final String INFO_SUFFIX = "_info";
58
-
59
- private static final String FALLBACK_VERSION = "pastVersion";
60
- private static final String NEXT_VERSION = "nextVersion";
61
- private static final String bundleDirectory = "versions";
62
-
63
- public static final String TAG = "Capacitor-updater";
64
- public SharedPreferences.Editor editor;
65
- public SharedPreferences prefs;
66
-
67
- public OkHttpClient client;
68
-
69
- public File documentsDir;
70
- public Boolean directUpdate = false;
71
- public Activity activity;
72
- public String PLUGIN_VERSION = "";
73
- public String versionBuild = "";
74
- public String versionCode = "";
75
- public String versionOs = "";
76
-
77
- public String customId = "";
78
- public String statsUrl = "";
79
- public String channelUrl = "";
80
- public String defaultChannel = "";
81
- public String appId = "";
82
- public String privateKey = "";
83
- public String publicKey = "";
84
- public boolean hasOldPrivateKeyPropertyInConfig = false;
85
- public String deviceID = "";
86
- public int timeout = 20000;
87
-
88
- private final FilenameFilter filter = (f, name) -> {
89
- // ignore directories generated by mac os x
90
- return (
91
- !name.startsWith("__MACOSX") &&
92
- !name.startsWith(".") &&
93
- !name.startsWith(".DS_Store")
94
- );
95
- };
96
-
97
- private boolean isProd() {
98
- try {
99
- return !Objects.requireNonNull(getClass().getPackage())
100
- .getName()
101
- .contains(".debug");
102
- } catch (Exception e) {
103
- return true; // Default to production if we can't determine
104
- }
105
- }
106
-
107
- private boolean isEmulator() {
108
- return (
109
- (Build.BRAND.startsWith("generic") &&
110
- Build.DEVICE.startsWith("generic")) ||
111
- Build.FINGERPRINT.startsWith("generic") ||
112
- Build.FINGERPRINT.startsWith("unknown") ||
113
- Build.HARDWARE.contains("goldfish") ||
114
- Build.HARDWARE.contains("ranchu") ||
115
- Build.MODEL.contains("google_sdk") ||
116
- Build.MODEL.contains("Emulator") ||
117
- Build.MODEL.contains("Android SDK built for x86") ||
118
- Build.MANUFACTURER.contains("Genymotion") ||
119
- Build.PRODUCT.contains("sdk_google") ||
120
- Build.PRODUCT.contains("google_sdk") ||
121
- Build.PRODUCT.contains("sdk") ||
122
- Build.PRODUCT.contains("sdk_x86") ||
123
- Build.PRODUCT.contains("sdk_gphone64_arm64") ||
124
- Build.PRODUCT.contains("vbox86p") ||
125
- Build.PRODUCT.contains("emulator") ||
126
- Build.PRODUCT.contains("simulator")
127
- );
128
- }
129
-
130
- private int calcTotalPercent(
131
- final int percent,
132
- final int min,
133
- final int max
134
- ) {
135
- return (percent * (max - min)) / 100 + min;
136
- }
137
-
138
- void notifyDownload(final String id, final int percent) {}
139
-
140
- void directUpdateFinish(final BundleInfo latest) {}
141
-
142
- void notifyListeners(final String id, final JSObject res) {}
143
-
144
- private String randomString() {
145
- final StringBuilder sb = new StringBuilder(10);
146
- for (int i = 0; i < 10; i++) sb.append(AB.charAt(rnd.nextInt(AB.length())));
147
- return sb.toString();
148
- }
149
-
150
- private File unzip(final String id, final File zipFile, final String dest)
151
- throws IOException {
152
- final File targetDirectory = new File(this.documentsDir, dest);
153
- try (
154
- final BufferedInputStream bis = new BufferedInputStream(
155
- new FileInputStream(zipFile)
156
- );
157
- final ZipInputStream zis = new ZipInputStream(bis)
158
- ) {
159
- int count;
160
- final int bufferSize = 8192;
161
- final byte[] buffer = new byte[bufferSize];
162
- final long lengthTotal = zipFile.length();
163
- long lengthRead = bufferSize;
164
- int percent = 0;
165
- this.notifyDownload(id, 75);
166
-
167
- ZipEntry entry;
168
- while ((entry = zis.getNextEntry()) != null) {
169
- if (entry.getName().contains("\\")) {
170
- Log.e(
171
- TAG,
172
- "unzip: Windows path is not supported, please use unix path as require by zip RFC: " +
173
- entry.getName()
174
- );
175
- this.sendStats("windows_path_fail");
176
- }
177
- final File file = new File(targetDirectory, entry.getName());
178
- final String canonicalPath = file.getCanonicalPath();
179
- final String canonicalDir = targetDirectory.getCanonicalPath();
180
- final File dir = entry.isDirectory() ? file : file.getParentFile();
181
-
182
- if (!canonicalPath.startsWith(canonicalDir)) {
183
- this.sendStats("canonical_path_fail");
184
- throw new FileNotFoundException(
185
- "SecurityException, Failed to ensure directory is the start path : " +
186
- canonicalDir +
187
- " of " +
188
- canonicalPath
189
- );
53
+ private static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
54
+ private static final SecureRandom rnd = new SecureRandom();
55
+
56
+ private static final String INFO_SUFFIX = "_info";
57
+
58
+ private static final String FALLBACK_VERSION = "pastVersion";
59
+ private static final String NEXT_VERSION = "nextVersion";
60
+ private static final String bundleDirectory = "versions";
61
+
62
+ public static final String TAG = "Capacitor-updater";
63
+ public SharedPreferences.Editor editor;
64
+ public SharedPreferences prefs;
65
+
66
+ public OkHttpClient client;
67
+
68
+ public File documentsDir;
69
+ public Boolean directUpdate = false;
70
+ public Activity activity;
71
+ public String PLUGIN_VERSION = "";
72
+ public String versionBuild = "";
73
+ public String versionCode = "";
74
+ public String versionOs = "";
75
+
76
+ public String customId = "";
77
+ public String statsUrl = "";
78
+ public String channelUrl = "";
79
+ public String defaultChannel = "";
80
+ public String appId = "";
81
+ public String privateKey = "";
82
+ public String publicKey = "";
83
+ public boolean hasOldPrivateKeyPropertyInConfig = false;
84
+ public String deviceID = "";
85
+ public int timeout = 20000;
86
+
87
+ private final FilenameFilter filter = (f, name) -> {
88
+ // ignore directories generated by mac os x
89
+ return (!name.startsWith("__MACOSX") && !name.startsWith(".") && !name.startsWith(".DS_Store"));
90
+ };
91
+
92
+ private boolean isProd() {
93
+ try {
94
+ return !Objects.requireNonNull(getClass().getPackage()).getName().contains(".debug");
95
+ } catch (Exception e) {
96
+ return true; // Default to production if we can't determine
190
97
  }
98
+ }
191
99
 
192
- assert dir != null;
193
- if (!dir.isDirectory() && !dir.mkdirs()) {
194
- this.sendStats("directory_path_fail");
195
- throw new FileNotFoundException(
196
- "Failed to ensure directory: " + dir.getAbsolutePath()
197
- );
198
- }
100
+ private boolean isEmulator() {
101
+ return (
102
+ (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) ||
103
+ Build.FINGERPRINT.startsWith("generic") ||
104
+ Build.FINGERPRINT.startsWith("unknown") ||
105
+ Build.HARDWARE.contains("goldfish") ||
106
+ Build.HARDWARE.contains("ranchu") ||
107
+ Build.MODEL.contains("google_sdk") ||
108
+ Build.MODEL.contains("Emulator") ||
109
+ Build.MODEL.contains("Android SDK built for x86") ||
110
+ Build.MANUFACTURER.contains("Genymotion") ||
111
+ Build.PRODUCT.contains("sdk_google") ||
112
+ Build.PRODUCT.contains("google_sdk") ||
113
+ Build.PRODUCT.contains("sdk") ||
114
+ Build.PRODUCT.contains("sdk_x86") ||
115
+ Build.PRODUCT.contains("sdk_gphone64_arm64") ||
116
+ Build.PRODUCT.contains("vbox86p") ||
117
+ Build.PRODUCT.contains("emulator") ||
118
+ Build.PRODUCT.contains("simulator")
119
+ );
120
+ }
199
121
 
200
- if (entry.isDirectory()) {
201
- continue;
202
- }
122
+ private int calcTotalPercent(final int percent, final int min, final int max) {
123
+ return (percent * (max - min)) / 100 + min;
124
+ }
203
125
 
204
- try (final FileOutputStream outputStream = new FileOutputStream(file)) {
205
- while ((count = zis.read(buffer)) != -1) outputStream.write(
206
- buffer,
207
- 0,
208
- count
209
- );
210
- }
126
+ void notifyDownload(final String id, final int percent) {}
211
127
 
212
- final int newPercent = (int) ((lengthRead / (float) lengthTotal) * 100);
213
- if (lengthTotal > 1 && newPercent != percent) {
214
- percent = newPercent;
215
- this.notifyDownload(id, this.calcTotalPercent(percent, 75, 90));
216
- }
128
+ void directUpdateFinish(final BundleInfo latest) {}
217
129
 
218
- lengthRead += entry.getCompressedSize();
219
- }
220
- return targetDirectory;
221
- } catch (IOException e) {
222
- this.sendStats("unzip_fail");
223
- throw new IOException("Failed to unzip: " + zipFile.getPath());
224
- }
225
- }
226
-
227
- private void flattenAssets(final File sourceFile, final String dest)
228
- throws IOException {
229
- if (!sourceFile.exists()) {
230
- throw new FileNotFoundException(
231
- "Source file not found: " + sourceFile.getPath()
232
- );
233
- }
234
- final File destinationFile = new File(this.documentsDir, dest);
235
- Objects.requireNonNull(destinationFile.getParentFile()).mkdirs();
236
- final String[] entries = sourceFile.list(this.filter);
237
- if (entries == null || entries.length == 0) {
238
- throw new IOException(
239
- "Source file was not a directory or was empty: " + sourceFile.getPath()
240
- );
241
- }
242
- if (entries.length == 1 && !"index.html".equals(entries[0])) {
243
- final File child = new File(sourceFile, entries[0]);
244
- child.renameTo(destinationFile);
245
- } else {
246
- sourceFile.renameTo(destinationFile);
247
- }
248
- sourceFile.delete();
249
- }
250
-
251
- private void observeWorkProgress(Context context, String id) {
252
- if (!(context instanceof LifecycleOwner)) {
253
- Log.e(
254
- TAG,
255
- "Context is not a LifecycleOwner, cannot observe work progress"
256
- );
257
- return;
258
- }
130
+ void notifyListeners(final String id, final JSObject res) {}
259
131
 
260
- activity.runOnUiThread(() -> {
261
- WorkManager.getInstance(context)
262
- .getWorkInfosByTagLiveData(id)
263
- .observe((LifecycleOwner) context, workInfos -> {
264
- if (workInfos == null || workInfos.isEmpty()) return;
265
-
266
- WorkInfo workInfo = workInfos.get(0);
267
- Data progress = workInfo.getProgress();
268
-
269
- switch (workInfo.getState()) {
270
- case RUNNING:
271
- int percent = progress.getInt(DownloadService.PERCENT, 0);
272
- notifyDownload(id, percent);
273
- break;
274
- case SUCCEEDED:
275
- Data outputData = workInfo.getOutputData();
276
- String dest = outputData.getString(DownloadService.FILEDEST);
277
- String version = outputData.getString(DownloadService.VERSION);
278
- String sessionKey = outputData.getString(
279
- DownloadService.SESSIONKEY
280
- );
281
- String checksum = outputData.getString(DownloadService.CHECKSUM);
282
- boolean isManifest = outputData.getBoolean(
283
- DownloadService.IS_MANIFEST,
284
- false
285
- );
286
-
287
- boolean success = finishDownload(
288
- id,
289
- dest,
290
- version,
291
- sessionKey,
292
- checksum,
293
- true,
294
- isManifest
295
- );
296
- if (!success) {
297
- saveBundleInfo(
298
- id,
299
- new BundleInfo(
300
- id,
301
- version,
302
- BundleStatus.ERROR,
303
- new Date(System.currentTimeMillis()),
304
- ""
305
- )
306
- );
307
- JSObject ret = new JSObject();
308
- ret.put("version", getCurrentBundle().getVersionName());
309
- ret.put("error", "finish_download_fail");
310
- sendStats("finish_download_fail", version);
311
- notifyListeners("downloadFailed", ret);
312
- }
313
- break;
314
- case FAILED:
315
- Data failedData = workInfo.getOutputData();
316
- String error = failedData.getString(DownloadService.ERROR);
317
- String failedVersion = failedData.getString(
318
- DownloadService.VERSION
319
- );
320
- saveBundleInfo(
321
- id,
322
- new BundleInfo(
323
- id,
324
- failedVersion,
325
- BundleStatus.ERROR,
326
- new Date(System.currentTimeMillis()),
327
- ""
328
- )
329
- );
330
- JSObject ret = new JSObject();
331
- ret.put("version", getCurrentBundle().getVersionName());
332
- if ("low_mem_fail".equals(error)) {
333
- sendStats("low_mem_fail", failedVersion);
334
- }
335
- ret.put("error", error != null ? error : "download_fail");
336
- sendStats("download_fail", failedVersion);
337
- notifyListeners("downloadFailed", ret);
338
- break;
339
- }
340
- });
341
- });
342
- }
343
-
344
- private void download(
345
- final String id,
346
- final String url,
347
- final String dest,
348
- final String version,
349
- final String sessionKey,
350
- final String checksum,
351
- final JSONArray manifest
352
- ) {
353
- if (this.activity == null) {
354
- Log.e(TAG, "Activity is null, cannot observe work progress");
355
- return;
356
- }
357
- observeWorkProgress(this.activity, id);
358
-
359
- DownloadWorkerManager.enqueueDownload(
360
- this.activity,
361
- url,
362
- id,
363
- this.documentsDir.getAbsolutePath(),
364
- dest,
365
- version,
366
- sessionKey,
367
- checksum,
368
- this.publicKey,
369
- manifest != null
370
- );
371
-
372
- if (manifest != null) {
373
- DataManager.getInstance().setManifest(manifest);
132
+ private String randomString() {
133
+ final StringBuilder sb = new StringBuilder(10);
134
+ for (int i = 0; i < 10; i++) sb.append(AB.charAt(rnd.nextInt(AB.length())));
135
+ return sb.toString();
374
136
  }
375
- }
376
-
377
- public Boolean finishDownload(
378
- String id,
379
- String dest,
380
- String version,
381
- String sessionKey,
382
- String checksumRes,
383
- Boolean setNext,
384
- Boolean isManifest
385
- ) {
386
- File downloaded = null;
387
- String checksum = "";
388
-
389
- try {
390
- this.notifyDownload(id, 71);
391
- downloaded = new File(this.documentsDir, dest);
392
-
393
- if (!isManifest) {
394
- String checksumDecrypted = Objects.requireNonNullElse(checksumRes, "");
395
- if (!this.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty()) {
396
- CryptoCipherV2.decryptFile(downloaded, publicKey, sessionKey);
397
- checksumDecrypted = CryptoCipherV2.decryptChecksum(
398
- checksumRes,
399
- publicKey
400
- );
401
- checksum = CryptoCipherV2.calcChecksum(downloaded);
402
- } else {
403
- CryptoCipher.decryptFile(downloaded, privateKey, sessionKey, version);
404
- checksum = CryptoCipher.calcChecksum(downloaded);
405
- }
406
- if (
407
- (!checksumDecrypted.isEmpty() || !this.publicKey.isEmpty()) &&
408
- !checksumDecrypted.equals(checksum)
137
+
138
+ private File unzip(final String id, final File zipFile, final String dest) throws IOException {
139
+ final File targetDirectory = new File(this.documentsDir, dest);
140
+ try (
141
+ final BufferedInputStream bis = new BufferedInputStream(new FileInputStream(zipFile));
142
+ final ZipInputStream zis = new ZipInputStream(bis)
409
143
  ) {
410
- Log.e(
411
- CapacitorUpdater.TAG,
412
- "Error checksum '" + checksumDecrypted + "' '" + checksum + "' '"
413
- );
414
- this.sendStats("checksum_fail");
415
- throw new IOException("Checksum failed: " + id);
144
+ int count;
145
+ final int bufferSize = 8192;
146
+ final byte[] buffer = new byte[bufferSize];
147
+ final long lengthTotal = zipFile.length();
148
+ long lengthRead = bufferSize;
149
+ int percent = 0;
150
+ this.notifyDownload(id, 75);
151
+
152
+ ZipEntry entry;
153
+ while ((entry = zis.getNextEntry()) != null) {
154
+ if (entry.getName().contains("\\")) {
155
+ Log.e(TAG, "unzip: Windows path is not supported, please use unix path as require by zip RFC: " + entry.getName());
156
+ this.sendStats("windows_path_fail");
157
+ }
158
+ final File file = new File(targetDirectory, entry.getName());
159
+ final String canonicalPath = file.getCanonicalPath();
160
+ final String canonicalDir = targetDirectory.getCanonicalPath();
161
+ final File dir = entry.isDirectory() ? file : file.getParentFile();
162
+
163
+ if (!canonicalPath.startsWith(canonicalDir)) {
164
+ this.sendStats("canonical_path_fail");
165
+ throw new FileNotFoundException(
166
+ "SecurityException, Failed to ensure directory is the start path : " + canonicalDir + " of " + canonicalPath
167
+ );
168
+ }
169
+
170
+ assert dir != null;
171
+ if (!dir.isDirectory() && !dir.mkdirs()) {
172
+ this.sendStats("directory_path_fail");
173
+ throw new FileNotFoundException("Failed to ensure directory: " + dir.getAbsolutePath());
174
+ }
175
+
176
+ if (entry.isDirectory()) {
177
+ continue;
178
+ }
179
+
180
+ try (final FileOutputStream outputStream = new FileOutputStream(file)) {
181
+ while ((count = zis.read(buffer)) != -1) outputStream.write(buffer, 0, count);
182
+ }
183
+
184
+ final int newPercent = (int) ((lengthRead / (float) lengthTotal) * 100);
185
+ if (lengthTotal > 1 && newPercent != percent) {
186
+ percent = newPercent;
187
+ this.notifyDownload(id, this.calcTotalPercent(percent, 75, 90));
188
+ }
189
+
190
+ lengthRead += entry.getCompressedSize();
191
+ }
192
+ return targetDirectory;
193
+ } catch (IOException e) {
194
+ this.sendStats("unzip_fail");
195
+ throw new IOException("Failed to unzip: " + zipFile.getPath());
416
196
  }
417
- }
418
- // Remove the decryption for manifest downloads
419
- } catch (IOException e) {
420
- final Boolean res = this.delete(id);
421
- if (!res) {
422
- Log.i(CapacitorUpdater.TAG, "Double error, cannot cleanup: " + version);
423
- }
424
-
425
- final JSObject ret = new JSObject();
426
- ret.put(
427
- "version",
428
- CapacitorUpdater.this.getCurrentBundle().getVersionName()
429
- );
430
-
431
- CapacitorUpdater.this.notifyListeners("downloadFailed", ret);
432
- CapacitorUpdater.this.sendStats("download_fail");
433
- return false;
434
197
  }
435
198
 
436
- try {
437
- if (!isManifest) {
438
- final File unzipped = this.unzip(id, downloaded, this.randomString());
439
- this.notifyDownload(id, 91);
440
- final String idName = bundleDirectory + "/" + id;
441
- this.flattenAssets(unzipped, idName);
442
- } else {
443
- this.notifyDownload(id, 91);
444
- final String idName = bundleDirectory + "/" + id;
445
- this.flattenAssets(downloaded, idName);
446
- downloaded.delete();
447
- }
448
- this.notifyDownload(id, 100);
449
- this.saveBundleInfo(id, null);
450
- BundleInfo next = new BundleInfo(
451
- id,
452
- version,
453
- BundleStatus.PENDING,
454
- new Date(System.currentTimeMillis()),
455
- checksum
456
- );
457
- this.saveBundleInfo(id, next);
458
-
459
- final JSObject ret = new JSObject();
460
- ret.put("bundle", next.toJSON());
461
- CapacitorUpdater.this.notifyListeners("updateAvailable", ret);
462
- if (setNext) {
463
- if (this.directUpdate) {
464
- CapacitorUpdater.this.directUpdateFinish(next);
465
- this.directUpdate = false;
199
+ private void flattenAssets(final File sourceFile, final String dest) throws IOException {
200
+ if (!sourceFile.exists()) {
201
+ throw new FileNotFoundException("Source file not found: " + sourceFile.getPath());
202
+ }
203
+ final File destinationFile = new File(this.documentsDir, dest);
204
+ Objects.requireNonNull(destinationFile.getParentFile()).mkdirs();
205
+ final String[] entries = sourceFile.list(this.filter);
206
+ if (entries == null || entries.length == 0) {
207
+ throw new IOException("Source file was not a directory or was empty: " + sourceFile.getPath());
208
+ }
209
+ if (entries.length == 1 && !"index.html".equals(entries[0])) {
210
+ final File child = new File(sourceFile, entries[0]);
211
+ child.renameTo(destinationFile);
466
212
  } else {
467
- this.setNextBundle(next.getId());
213
+ sourceFile.renameTo(destinationFile);
468
214
  }
469
- }
470
- } catch (IOException e) {
471
- e.printStackTrace();
472
- final JSObject ret = new JSObject();
473
- ret.put(
474
- "version",
475
- CapacitorUpdater.this.getCurrentBundle().getVersionName()
476
- );
477
- CapacitorUpdater.this.notifyListeners("downloadFailed", ret);
478
- CapacitorUpdater.this.sendStats("download_fail");
479
- return false;
215
+ sourceFile.delete();
480
216
  }
481
- return true;
482
- }
483
-
484
- private void deleteDirectory(final File file) throws IOException {
485
- if (file.isDirectory()) {
486
- final File[] entries = file.listFiles();
487
- if (entries != null) {
488
- for (final File entry : entries) {
489
- this.deleteDirectory(entry);
217
+
218
+ private void observeWorkProgress(Context context, String id) {
219
+ if (!(context instanceof LifecycleOwner)) {
220
+ Log.e(TAG, "Context is not a LifecycleOwner, cannot observe work progress");
221
+ return;
490
222
  }
491
- }
492
- }
493
- if (!file.delete()) {
494
- throw new IOException("Failed to delete: " + file);
495
- }
496
- }
497
-
498
- private void setCurrentBundle(final File bundle) {
499
- this.editor.putString(WebView.CAP_SERVER_PATH, bundle.getPath());
500
- Log.i(TAG, "Current bundle set to: " + bundle);
501
- this.editor.commit();
502
- }
503
-
504
- public void downloadBackground(
505
- final String url,
506
- final String version,
507
- final String sessionKey,
508
- final String checksum,
509
- final JSONArray manifest
510
- ) {
511
- final String id = this.randomString();
512
-
513
- // Check if version is already downloading
514
- if (
515
- this.activity != null &&
516
- DownloadWorkerManager.isVersionDownloading(version)
517
- ) {
518
- Log.i(TAG, "Version already downloading: " + version);
519
- return;
223
+
224
+ activity.runOnUiThread(() -> {
225
+ WorkManager.getInstance(context)
226
+ .getWorkInfosByTagLiveData(id)
227
+ .observe((LifecycleOwner) context, workInfos -> {
228
+ if (workInfos == null || workInfos.isEmpty()) return;
229
+
230
+ WorkInfo workInfo = workInfos.get(0);
231
+ Data progress = workInfo.getProgress();
232
+
233
+ switch (workInfo.getState()) {
234
+ case RUNNING:
235
+ int percent = progress.getInt(DownloadService.PERCENT, 0);
236
+ notifyDownload(id, percent);
237
+ break;
238
+ case SUCCEEDED:
239
+ Data outputData = workInfo.getOutputData();
240
+ String dest = outputData.getString(DownloadService.FILEDEST);
241
+ String version = outputData.getString(DownloadService.VERSION);
242
+ String sessionKey = outputData.getString(DownloadService.SESSIONKEY);
243
+ String checksum = outputData.getString(DownloadService.CHECKSUM);
244
+ boolean isManifest = outputData.getBoolean(DownloadService.IS_MANIFEST, false);
245
+
246
+ boolean success = finishDownload(id, dest, version, sessionKey, checksum, true, isManifest);
247
+ if (!success) {
248
+ saveBundleInfo(
249
+ id,
250
+ new BundleInfo(id, version, BundleStatus.ERROR, new Date(System.currentTimeMillis()), "")
251
+ );
252
+ JSObject ret = new JSObject();
253
+ ret.put("version", getCurrentBundle().getVersionName());
254
+ ret.put("error", "finish_download_fail");
255
+ sendStats("finish_download_fail", version);
256
+ notifyListeners("downloadFailed", ret);
257
+ }
258
+ break;
259
+ case FAILED:
260
+ Data failedData = workInfo.getOutputData();
261
+ String error = failedData.getString(DownloadService.ERROR);
262
+ String failedVersion = failedData.getString(DownloadService.VERSION);
263
+ saveBundleInfo(
264
+ id,
265
+ new BundleInfo(id, failedVersion, BundleStatus.ERROR, new Date(System.currentTimeMillis()), "")
266
+ );
267
+ JSObject ret = new JSObject();
268
+ ret.put("version", getCurrentBundle().getVersionName());
269
+ if ("low_mem_fail".equals(error)) {
270
+ sendStats("low_mem_fail", failedVersion);
271
+ }
272
+ ret.put("error", error != null ? error : "download_fail");
273
+ sendStats("download_fail", failedVersion);
274
+ notifyListeners("downloadFailed", ret);
275
+ break;
276
+ }
277
+ });
278
+ });
520
279
  }
521
280
 
522
- this.saveBundleInfo(
523
- id,
524
- new BundleInfo(
525
- id,
526
- version,
527
- BundleStatus.DOWNLOADING,
528
- new Date(System.currentTimeMillis()),
529
- ""
530
- )
531
- );
532
- this.notifyDownload(id, 0);
533
- this.notifyDownload(id, 5);
534
-
535
- this.download(
536
- id,
537
- url,
538
- this.randomString(),
539
- version,
540
- sessionKey,
541
- checksum,
542
- manifest
543
- );
544
- }
545
-
546
- public BundleInfo download(
547
- final String url,
548
- final String version,
549
- final String sessionKey,
550
- final String checksum
551
- ) throws IOException {
552
- final String id = this.randomString();
553
- this.saveBundleInfo(
554
- id,
555
- new BundleInfo(
556
- id,
557
- version,
558
- BundleStatus.DOWNLOADING,
559
- new Date(System.currentTimeMillis()),
560
- ""
561
- )
562
- );
563
- this.notifyDownload(id, 0);
564
- this.notifyDownload(id, 5);
565
- final String dest = this.randomString();
566
-
567
- // Use the new WorkManager-based download
568
- this.download(id, url, dest, version, sessionKey, checksum, null);
569
-
570
- // Wait for completion
571
- try {
572
- ListenableFuture<List<WorkInfo>> future = WorkManager.getInstance(
573
- activity
574
- ).getWorkInfosByTag(id);
575
-
576
- List<WorkInfo> workInfos = Futures.getChecked(future, IOException.class);
577
-
578
- if (workInfos != null && !workInfos.isEmpty()) {
579
- WorkInfo workInfo = workInfos.get(0);
580
- while (!workInfo.getState().isFinished()) {
581
- Thread.sleep(100);
582
- workInfos = Futures.getChecked(
583
- WorkManager.getInstance(activity).getWorkInfosByTag(id),
584
- IOException.class
585
- );
586
- if (workInfos != null && !workInfos.isEmpty()) {
587
- workInfo = workInfos.get(0);
588
- }
281
+ private void download(
282
+ final String id,
283
+ final String url,
284
+ final String dest,
285
+ final String version,
286
+ final String sessionKey,
287
+ final String checksum,
288
+ final JSONArray manifest
289
+ ) {
290
+ if (this.activity == null) {
291
+ Log.e(TAG, "Activity is null, cannot observe work progress");
292
+ return;
589
293
  }
294
+ observeWorkProgress(this.activity, id);
590
295
 
591
- if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
592
- Data outputData = workInfo.getOutputData();
593
- boolean success = finishDownload(
296
+ DownloadWorkerManager.enqueueDownload(
297
+ this.activity,
298
+ url,
594
299
  id,
595
- outputData.getString(DownloadService.FILEDEST),
300
+ this.documentsDir.getAbsolutePath(),
301
+ dest,
596
302
  version,
597
303
  sessionKey,
598
304
  checksum,
599
- true,
600
- false
601
- );
602
- if (!success) {
603
- throw new IOException("Failed to finish download");
604
- }
605
- } else {
606
- Data outputData = workInfo.getOutputData();
607
- String error = outputData.getString(DownloadService.ERROR);
608
- throw new IOException(
609
- error != null ? error : "Download failed: " + workInfo.getState()
610
- );
305
+ this.publicKey,
306
+ manifest != null
307
+ );
308
+
309
+ if (manifest != null) {
310
+ DataManager.getInstance().setManifest(manifest);
611
311
  }
612
- }
613
- return getBundleInfo(id);
614
- } catch (Exception e) {
615
- Log.e(TAG, "Error waiting for download", e);
616
- saveBundleInfo(
617
- id,
618
- new BundleInfo(
619
- id,
620
- version,
621
- BundleStatus.ERROR,
622
- new Date(System.currentTimeMillis()),
623
- ""
624
- )
625
- );
626
- throw new IOException("Error waiting for download: " + e.getMessage());
627
312
  }
628
- }
629
-
630
- public List<BundleInfo> list(boolean rawList) {
631
- if (!rawList) {
632
- final List<BundleInfo> res = new ArrayList<>();
633
- final File destHot = new File(this.documentsDir, bundleDirectory);
634
- Log.d(TAG, "list File : " + destHot.getPath());
635
- if (destHot.exists()) {
636
- for (final File i : Objects.requireNonNull(destHot.listFiles())) {
637
- final String id = i.getName();
638
- res.add(this.getBundleInfo(id));
313
+
314
+ public Boolean finishDownload(
315
+ String id,
316
+ String dest,
317
+ String version,
318
+ String sessionKey,
319
+ String checksumRes,
320
+ Boolean setNext,
321
+ Boolean isManifest
322
+ ) {
323
+ File downloaded = null;
324
+ String checksum = "";
325
+
326
+ try {
327
+ this.notifyDownload(id, 71);
328
+ downloaded = new File(this.documentsDir, dest);
329
+
330
+ if (!isManifest) {
331
+ String checksumDecrypted = Objects.requireNonNullElse(checksumRes, "");
332
+ if (!this.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty()) {
333
+ CryptoCipherV2.decryptFile(downloaded, publicKey, sessionKey);
334
+ checksumDecrypted = CryptoCipherV2.decryptChecksum(checksumRes, publicKey);
335
+ checksum = CryptoCipherV2.calcChecksum(downloaded);
336
+ } else {
337
+ CryptoCipher.decryptFile(downloaded, privateKey, sessionKey, version);
338
+ checksum = CryptoCipher.calcChecksum(downloaded);
339
+ }
340
+ if ((!checksumDecrypted.isEmpty() || !this.publicKey.isEmpty()) && !checksumDecrypted.equals(checksum)) {
341
+ Log.e(CapacitorUpdater.TAG, "Error checksum '" + checksumDecrypted + "' '" + checksum + "' '");
342
+ this.sendStats("checksum_fail");
343
+ throw new IOException("Checksum failed: " + id);
344
+ }
345
+ }
346
+ // Remove the decryption for manifest downloads
347
+ } catch (IOException e) {
348
+ final Boolean res = this.delete(id);
349
+ if (!res) {
350
+ Log.i(CapacitorUpdater.TAG, "Double error, cannot cleanup: " + version);
351
+ }
352
+
353
+ final JSObject ret = new JSObject();
354
+ ret.put("version", CapacitorUpdater.this.getCurrentBundle().getVersionName());
355
+
356
+ CapacitorUpdater.this.notifyListeners("downloadFailed", ret);
357
+ CapacitorUpdater.this.sendStats("download_fail");
358
+ return false;
639
359
  }
640
- } else {
641
- Log.i(TAG, "No versions available to list" + destHot);
642
- }
643
- return res;
644
- } else {
645
- final List<BundleInfo> res = new ArrayList<>();
646
- for (String value : this.prefs.getAll().keySet()) {
647
- if (!value.matches("^[0-9A-Za-z]{10}_info$")) {
648
- continue;
360
+
361
+ try {
362
+ if (!isManifest) {
363
+ final File unzipped = this.unzip(id, downloaded, this.randomString());
364
+ this.notifyDownload(id, 91);
365
+ final String idName = bundleDirectory + "/" + id;
366
+ this.flattenAssets(unzipped, idName);
367
+ } else {
368
+ this.notifyDownload(id, 91);
369
+ final String idName = bundleDirectory + "/" + id;
370
+ this.flattenAssets(downloaded, idName);
371
+ downloaded.delete();
372
+ }
373
+ this.notifyDownload(id, 100);
374
+ this.saveBundleInfo(id, null);
375
+ BundleInfo next = new BundleInfo(id, version, BundleStatus.PENDING, new Date(System.currentTimeMillis()), checksum);
376
+ this.saveBundleInfo(id, next);
377
+
378
+ final JSObject ret = new JSObject();
379
+ ret.put("bundle", next.toJSON());
380
+ CapacitorUpdater.this.notifyListeners("updateAvailable", ret);
381
+ if (setNext) {
382
+ if (this.directUpdate) {
383
+ CapacitorUpdater.this.directUpdateFinish(next);
384
+ this.directUpdate = false;
385
+ } else {
386
+ this.setNextBundle(next.getId());
387
+ }
388
+ }
389
+ } catch (IOException e) {
390
+ e.printStackTrace();
391
+ final JSObject ret = new JSObject();
392
+ ret.put("version", CapacitorUpdater.this.getCurrentBundle().getVersionName());
393
+ CapacitorUpdater.this.notifyListeners("downloadFailed", ret);
394
+ CapacitorUpdater.this.sendStats("download_fail");
395
+ return false;
649
396
  }
397
+ return true;
398
+ }
650
399
 
651
- res.add(this.getBundleInfo(value.split("_")[0]));
652
- }
653
- return res;
400
+ private void deleteDirectory(final File file) throws IOException {
401
+ if (file.isDirectory()) {
402
+ final File[] entries = file.listFiles();
403
+ if (entries != null) {
404
+ for (final File entry : entries) {
405
+ this.deleteDirectory(entry);
406
+ }
407
+ }
408
+ }
409
+ if (!file.delete()) {
410
+ throw new IOException("Failed to delete: " + file);
411
+ }
654
412
  }
655
- }
656
-
657
- public Boolean delete(final String id, final Boolean removeInfo)
658
- throws IOException {
659
- final BundleInfo deleted = this.getBundleInfo(id);
660
- if (deleted.isBuiltin() || this.getCurrentBundleId().equals(id)) {
661
- Log.e(TAG, "Cannot delete " + id);
662
- return false;
413
+
414
+ private void setCurrentBundle(final File bundle) {
415
+ this.editor.putString(WebView.CAP_SERVER_PATH, bundle.getPath());
416
+ Log.i(TAG, "Current bundle set to: " + bundle);
417
+ this.editor.commit();
663
418
  }
664
- final BundleInfo next = this.getNextBundle();
665
- if (
666
- next != null &&
667
- !next.isDeleted() &&
668
- !next.isErrorStatus() &&
669
- next.getId().equals(id)
419
+
420
+ public void downloadBackground(
421
+ final String url,
422
+ final String version,
423
+ final String sessionKey,
424
+ final String checksum,
425
+ final JSONArray manifest
670
426
  ) {
671
- Log.e(TAG, "Cannot delete the next bundle" + id);
672
- return false;
427
+ final String id = this.randomString();
428
+
429
+ // Check if version is already downloading
430
+ if (this.activity != null && DownloadWorkerManager.isVersionDownloading(version)) {
431
+ Log.i(TAG, "Version already downloading: " + version);
432
+ return;
433
+ }
434
+
435
+ this.saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), ""));
436
+ this.notifyDownload(id, 0);
437
+ this.notifyDownload(id, 5);
438
+
439
+ this.download(id, url, this.randomString(), version, sessionKey, checksum, manifest);
673
440
  }
674
- // Cancel download for this version if active
675
- if (this.activity != null) {
676
- DownloadWorkerManager.cancelVersionDownload(
677
- this.activity,
678
- deleted.getVersionName()
679
- );
441
+
442
+ public BundleInfo download(final String url, final String version, final String sessionKey, final String checksum) throws IOException {
443
+ final String id = this.randomString();
444
+ this.saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), ""));
445
+ this.notifyDownload(id, 0);
446
+ this.notifyDownload(id, 5);
447
+ final String dest = this.randomString();
448
+
449
+ // Use the new WorkManager-based download
450
+ this.download(id, url, dest, version, sessionKey, checksum, null);
451
+
452
+ // Wait for completion
453
+ try {
454
+ ListenableFuture<List<WorkInfo>> future = WorkManager.getInstance(activity).getWorkInfosByTag(id);
455
+
456
+ List<WorkInfo> workInfos = Futures.getChecked(future, IOException.class);
457
+
458
+ if (workInfos != null && !workInfos.isEmpty()) {
459
+ WorkInfo workInfo = workInfos.get(0);
460
+ while (!workInfo.getState().isFinished()) {
461
+ Thread.sleep(100);
462
+ workInfos = Futures.getChecked(WorkManager.getInstance(activity).getWorkInfosByTag(id), IOException.class);
463
+ if (workInfos != null && !workInfos.isEmpty()) {
464
+ workInfo = workInfos.get(0);
465
+ }
466
+ }
467
+
468
+ if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
469
+ Data outputData = workInfo.getOutputData();
470
+ boolean success = finishDownload(
471
+ id,
472
+ outputData.getString(DownloadService.FILEDEST),
473
+ version,
474
+ sessionKey,
475
+ checksum,
476
+ true,
477
+ false
478
+ );
479
+ if (!success) {
480
+ throw new IOException("Failed to finish download");
481
+ }
482
+ } else {
483
+ Data outputData = workInfo.getOutputData();
484
+ String error = outputData.getString(DownloadService.ERROR);
485
+ throw new IOException(error != null ? error : "Download failed: " + workInfo.getState());
486
+ }
487
+ }
488
+ return getBundleInfo(id);
489
+ } catch (Exception e) {
490
+ Log.e(TAG, "Error waiting for download", e);
491
+ saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.ERROR, new Date(System.currentTimeMillis()), ""));
492
+ throw new IOException("Error waiting for download: " + e.getMessage());
493
+ }
680
494
  }
681
- final File bundle = new File(this.documentsDir, bundleDirectory + "/" + id);
682
- if (bundle.exists()) {
683
- this.deleteDirectory(bundle);
684
- if (!removeInfo) {
685
- this.saveBundleInfo(id, deleted.setStatus(BundleStatus.DELETED));
686
- } else {
687
- this.removeBundleInfo(id);
688
- }
689
- return true;
495
+
496
+ public List<BundleInfo> list(boolean rawList) {
497
+ if (!rawList) {
498
+ final List<BundleInfo> res = new ArrayList<>();
499
+ final File destHot = new File(this.documentsDir, bundleDirectory);
500
+ Log.d(TAG, "list File : " + destHot.getPath());
501
+ if (destHot.exists()) {
502
+ for (final File i : Objects.requireNonNull(destHot.listFiles())) {
503
+ final String id = i.getName();
504
+ res.add(this.getBundleInfo(id));
505
+ }
506
+ } else {
507
+ Log.i(TAG, "No versions available to list" + destHot);
508
+ }
509
+ return res;
510
+ } else {
511
+ final List<BundleInfo> res = new ArrayList<>();
512
+ for (String value : this.prefs.getAll().keySet()) {
513
+ if (!value.matches("^[0-9A-Za-z]{10}_info$")) {
514
+ continue;
515
+ }
516
+
517
+ res.add(this.getBundleInfo(value.split("_")[0]));
518
+ }
519
+ return res;
520
+ }
690
521
  }
691
- Log.e(TAG, "bundle removed: " + deleted.getVersionName());
692
- // perhaps we did not find the bundle in the files, but if the user requested a delete, we delete
693
- if (removeInfo) {
694
- this.removeBundleInfo(id);
522
+
523
+ public Boolean delete(final String id, final Boolean removeInfo) throws IOException {
524
+ final BundleInfo deleted = this.getBundleInfo(id);
525
+ if (deleted.isBuiltin() || this.getCurrentBundleId().equals(id)) {
526
+ Log.e(TAG, "Cannot delete " + id);
527
+ return false;
528
+ }
529
+ final BundleInfo next = this.getNextBundle();
530
+ if (next != null && !next.isDeleted() && !next.isErrorStatus() && next.getId().equals(id)) {
531
+ Log.e(TAG, "Cannot delete the next bundle" + id);
532
+ return false;
533
+ }
534
+ // Cancel download for this version if active
535
+ if (this.activity != null) {
536
+ DownloadWorkerManager.cancelVersionDownload(this.activity, deleted.getVersionName());
537
+ }
538
+ final File bundle = new File(this.documentsDir, bundleDirectory + "/" + id);
539
+ if (bundle.exists()) {
540
+ this.deleteDirectory(bundle);
541
+ if (!removeInfo) {
542
+ this.saveBundleInfo(id, deleted.setStatus(BundleStatus.DELETED));
543
+ } else {
544
+ this.removeBundleInfo(id);
545
+ }
546
+ return true;
547
+ }
548
+ Log.e(TAG, "bundle removed: " + deleted.getVersionName());
549
+ // perhaps we did not find the bundle in the files, but if the user requested a delete, we delete
550
+ if (removeInfo) {
551
+ this.removeBundleInfo(id);
552
+ }
553
+ this.sendStats("delete", deleted.getVersionName());
554
+ return false;
695
555
  }
696
- this.sendStats("delete", deleted.getVersionName());
697
- return false;
698
- }
699
-
700
- public Boolean delete(final String id) {
701
- try {
702
- return this.delete(id, true);
703
- } catch (IOException e) {
704
- e.printStackTrace();
705
- Log.i(
706
- CapacitorUpdater.TAG,
707
- "Failed to delete bundle (" + id + ")" + "\nError:\n" + e.toString()
708
- );
709
- return false;
556
+
557
+ public Boolean delete(final String id) {
558
+ try {
559
+ return this.delete(id, true);
560
+ } catch (IOException e) {
561
+ e.printStackTrace();
562
+ Log.i(CapacitorUpdater.TAG, "Failed to delete bundle (" + id + ")" + "\nError:\n" + e.toString());
563
+ return false;
564
+ }
710
565
  }
711
- }
712
-
713
- private File getBundleDirectory(final String id) {
714
- return new File(this.documentsDir, bundleDirectory + "/" + id);
715
- }
716
-
717
- private boolean bundleExists(final String id) {
718
- final File bundle = this.getBundleDirectory(id);
719
- final BundleInfo bundleInfo = this.getBundleInfo(id);
720
- return (
721
- bundle.isDirectory() &&
722
- bundle.exists() &&
723
- new File(bundle.getPath(), "/index.html").exists() &&
724
- !bundleInfo.isDeleted()
725
- );
726
- }
727
-
728
- public Boolean set(final BundleInfo bundle) {
729
- return this.set(bundle.getId());
730
- }
731
-
732
- public Boolean set(final String id) {
733
- final BundleInfo newBundle = this.getBundleInfo(id);
734
- if (newBundle.isBuiltin()) {
735
- this.reset();
736
- return true;
566
+
567
+ private File getBundleDirectory(final String id) {
568
+ return new File(this.documentsDir, bundleDirectory + "/" + id);
737
569
  }
738
- final File bundle = this.getBundleDirectory(id);
739
- Log.i(TAG, "Setting next active bundle: " + id);
740
- if (this.bundleExists(id)) {
741
- var currentBundleName = this.getCurrentBundle().getVersionName();
742
- this.setCurrentBundle(bundle);
743
- this.setBundleStatus(id, BundleStatus.PENDING);
744
- this.sendStats("set", newBundle.getVersionName(), currentBundleName);
745
- return true;
570
+
571
+ private boolean bundleExists(final String id) {
572
+ final File bundle = this.getBundleDirectory(id);
573
+ final BundleInfo bundleInfo = this.getBundleInfo(id);
574
+ return (bundle.isDirectory() && bundle.exists() && new File(bundle.getPath(), "/index.html").exists() && !bundleInfo.isDeleted());
746
575
  }
747
- this.setBundleStatus(id, BundleStatus.ERROR);
748
- this.sendStats("set_fail", newBundle.getVersionName());
749
- return false;
750
- }
751
-
752
- public void autoReset() {
753
- final BundleInfo currentBundle = this.getCurrentBundle();
754
- if (
755
- !currentBundle.isBuiltin() && !this.bundleExists(currentBundle.getId())
756
- ) {
757
- Log.i(TAG, "Folder at bundle path does not exist. Triggering reset.");
758
- this.reset();
576
+
577
+ public Boolean set(final BundleInfo bundle) {
578
+ return this.set(bundle.getId());
759
579
  }
760
- }
761
-
762
- public void reset() {
763
- this.reset(false);
764
- }
765
-
766
- public void setSuccess(final BundleInfo bundle, Boolean autoDeletePrevious) {
767
- this.setBundleStatus(bundle.getId(), BundleStatus.SUCCESS);
768
- final BundleInfo fallback = this.getFallbackBundle();
769
- Log.d(CapacitorUpdater.TAG, "Fallback bundle is: " + fallback);
770
- Log.i(
771
- CapacitorUpdater.TAG,
772
- "Version successfully loaded: " + bundle.getVersionName()
773
- );
774
- if (autoDeletePrevious && !fallback.isBuiltin()) {
775
- final Boolean res = this.delete(fallback.getId());
776
- if (res) {
777
- Log.i(
778
- CapacitorUpdater.TAG,
779
- "Deleted previous bundle: " + fallback.getVersionName()
780
- );
781
- }
580
+
581
+ public Boolean set(final String id) {
582
+ final BundleInfo newBundle = this.getBundleInfo(id);
583
+ if (newBundle.isBuiltin()) {
584
+ this.reset();
585
+ return true;
586
+ }
587
+ final File bundle = this.getBundleDirectory(id);
588
+ Log.i(TAG, "Setting next active bundle: " + id);
589
+ if (this.bundleExists(id)) {
590
+ var currentBundleName = this.getCurrentBundle().getVersionName();
591
+ this.setCurrentBundle(bundle);
592
+ this.setBundleStatus(id, BundleStatus.PENDING);
593
+ this.sendStats("set", newBundle.getVersionName(), currentBundleName);
594
+ return true;
595
+ }
596
+ this.setBundleStatus(id, BundleStatus.ERROR);
597
+ this.sendStats("set_fail", newBundle.getVersionName());
598
+ return false;
782
599
  }
783
- this.setFallbackBundle(bundle);
784
- }
785
-
786
- public void setError(final BundleInfo bundle) {
787
- this.setBundleStatus(bundle.getId(), BundleStatus.ERROR);
788
- }
789
-
790
- public void reset(final boolean internal) {
791
- Log.d(CapacitorUpdater.TAG, "reset: " + internal);
792
- var currentBundleName = this.getCurrentBundle().getVersionName();
793
- this.setCurrentBundle(new File("public"));
794
- this.setFallbackBundle(null);
795
- this.setNextBundle(null);
796
- // Cancel any ongoing downloads
797
- if (this.activity != null) {
798
- DownloadWorkerManager.cancelAllDownloads(this.activity);
600
+
601
+ public void autoReset() {
602
+ final BundleInfo currentBundle = this.getCurrentBundle();
603
+ if (!currentBundle.isBuiltin() && !this.bundleExists(currentBundle.getId())) {
604
+ Log.i(TAG, "Folder at bundle path does not exist. Triggering reset.");
605
+ this.reset();
606
+ }
799
607
  }
800
- if (!internal) {
801
- this.sendStats(
802
- "reset",
803
- this.getCurrentBundle().getVersionName(),
804
- currentBundleName
805
- );
608
+
609
+ public void reset() {
610
+ this.reset(false);
806
611
  }
807
- }
808
-
809
- private JSONObject createInfoObject() throws JSONException {
810
- JSONObject json = new JSONObject();
811
- json.put("platform", "android");
812
- json.put("device_id", this.deviceID);
813
- json.put("app_id", this.appId);
814
- json.put("custom_id", this.customId);
815
- json.put("version_build", this.versionBuild);
816
- json.put("version_code", this.versionCode);
817
- json.put("version_os", this.versionOs);
818
- json.put("version_name", this.getCurrentBundle().getVersionName());
819
- json.put("plugin_version", this.PLUGIN_VERSION);
820
- json.put("is_emulator", this.isEmulator());
821
- json.put("is_prod", this.isProd());
822
- json.put("defaultChannel", this.defaultChannel);
823
- return json;
824
- }
825
-
826
- private void makeJsonRequest(
827
- String url,
828
- JSONObject jsonBody,
829
- Callback callback
830
- ) {
831
- MediaType JSON = MediaType.get("application/json; charset=utf-8");
832
- RequestBody body = RequestBody.create(jsonBody.toString(), JSON);
833
-
834
- Request request = new Request.Builder().url(url).post(body).build();
835
-
836
- client
837
- .newCall(request)
838
- .enqueue(
839
- new okhttp3.Callback() {
840
- @Override
841
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
842
- JSObject retError = new JSObject();
843
- retError.put("message", "Request failed: " + e.getMessage());
844
- retError.put("error", "network_error");
845
- callback.callback(retError);
846
- }
847
-
848
- @Override
849
- public void onResponse(
850
- @NonNull Call call,
851
- @NonNull Response response
852
- ) throws IOException {
853
- try (ResponseBody responseBody = response.body()) {
854
- if (!response.isSuccessful()) {
855
- JSObject retError = new JSObject();
856
- retError.put("message", "Server error: " + response.code());
857
- retError.put("error", "response_error");
858
- callback.callback(retError);
859
- return;
860
- }
861
-
862
- assert responseBody != null;
863
- String responseData = responseBody.string();
864
- JSONObject jsonResponse = new JSONObject(responseData);
865
- JSObject ret = new JSObject();
866
-
867
- Iterator<String> keys = jsonResponse.keys();
868
- while (keys.hasNext()) {
869
- String key = keys.next();
870
- if (jsonResponse.has(key)) {
871
- if ("session_key".equals(key)) {
872
- ret.put("sessionKey", jsonResponse.get(key));
873
- } else {
874
- ret.put(key, jsonResponse.get(key));
875
- }
876
- }
877
- }
878
- callback.callback(ret);
879
- } catch (JSONException e) {
880
- JSObject retError = new JSObject();
881
- retError.put("message", "JSON parse error: " + e.getMessage());
882
- retError.put("error", "parse_error");
883
- callback.callback(retError);
612
+
613
+ public void setSuccess(final BundleInfo bundle, Boolean autoDeletePrevious) {
614
+ this.setBundleStatus(bundle.getId(), BundleStatus.SUCCESS);
615
+ final BundleInfo fallback = this.getFallbackBundle();
616
+ Log.d(CapacitorUpdater.TAG, "Fallback bundle is: " + fallback);
617
+ Log.i(CapacitorUpdater.TAG, "Version successfully loaded: " + bundle.getVersionName());
618
+ if (autoDeletePrevious && !fallback.isBuiltin()) {
619
+ final Boolean res = this.delete(fallback.getId());
620
+ if (res) {
621
+ Log.i(CapacitorUpdater.TAG, "Deleted previous bundle: " + fallback.getVersionName());
884
622
  }
885
- }
886
623
  }
887
- );
888
- }
889
-
890
- public void getLatest(
891
- final String updateUrl,
892
- final String channel,
893
- final Callback callback
894
- ) {
895
- JSONObject json;
896
- try {
897
- json = this.createInfoObject();
898
- if (channel != null && json != null) {
899
- json.put("defaultChannel", channel);
900
- }
901
- } catch (JSONException e) {
902
- Log.e(TAG, "Error getLatest JSONException", e);
903
- final JSObject retError = new JSObject();
904
- retError.put("message", "Cannot get info: " + e);
905
- retError.put("error", "json_error");
906
- callback.callback(retError);
907
- return;
624
+ this.setFallbackBundle(bundle);
908
625
  }
909
626
 
910
- Log.i(CapacitorUpdater.TAG, "Auto-update parameters: " + json);
911
-
912
- makeJsonRequest(updateUrl, json, callback);
913
- }
627
+ public void setError(final BundleInfo bundle) {
628
+ this.setBundleStatus(bundle.getId(), BundleStatus.ERROR);
629
+ }
914
630
 
915
- public void unsetChannel(final Callback callback) {
916
- String channelUrl = this.channelUrl;
917
- if (channelUrl == null || channelUrl.isEmpty()) {
918
- Log.e(TAG, "Channel URL is not set");
919
- final JSObject retError = new JSObject();
920
- retError.put("message", "channelUrl missing");
921
- retError.put("error", "missing_config");
922
- callback.callback(retError);
923
- return;
631
+ public void reset(final boolean internal) {
632
+ Log.d(CapacitorUpdater.TAG, "reset: " + internal);
633
+ var currentBundleName = this.getCurrentBundle().getVersionName();
634
+ this.setCurrentBundle(new File("public"));
635
+ this.setFallbackBundle(null);
636
+ this.setNextBundle(null);
637
+ // Cancel any ongoing downloads
638
+ if (this.activity != null) {
639
+ DownloadWorkerManager.cancelAllDownloads(this.activity);
640
+ }
641
+ if (!internal) {
642
+ this.sendStats("reset", this.getCurrentBundle().getVersionName(), currentBundleName);
643
+ }
924
644
  }
925
- JSONObject json;
926
- try {
927
- json = this.createInfoObject();
928
- } catch (JSONException e) {
929
- Log.e(TAG, "Error unsetChannel JSONException", e);
930
- final JSObject retError = new JSObject();
931
- retError.put("message", "Cannot get info: " + e);
932
- retError.put("error", "json_error");
933
- callback.callback(retError);
934
- return;
645
+
646
+ private JSONObject createInfoObject() throws JSONException {
647
+ JSONObject json = new JSONObject();
648
+ json.put("platform", "android");
649
+ json.put("device_id", this.deviceID);
650
+ json.put("app_id", this.appId);
651
+ json.put("custom_id", this.customId);
652
+ json.put("version_build", this.versionBuild);
653
+ json.put("version_code", this.versionCode);
654
+ json.put("version_os", this.versionOs);
655
+ json.put("version_name", this.getCurrentBundle().getVersionName());
656
+ json.put("plugin_version", this.PLUGIN_VERSION);
657
+ json.put("is_emulator", this.isEmulator());
658
+ json.put("is_prod", this.isProd());
659
+ json.put("defaultChannel", this.defaultChannel);
660
+ return json;
935
661
  }
936
662
 
937
- Request request = new Request.Builder()
938
- .url(channelUrl)
939
- .delete(
940
- RequestBody.create(json.toString(), MediaType.get("application/json"))
941
- )
942
- .build();
943
-
944
- client
945
- .newCall(request)
946
- .enqueue(
947
- new okhttp3.Callback() {
948
- @Override
949
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
950
- JSObject retError = new JSObject();
951
- retError.put("message", "Request failed: " + e.getMessage());
952
- retError.put("error", "network_error");
953
- callback.callback(retError);
954
- }
955
-
956
- @Override
957
- public void onResponse(
958
- @NonNull Call call,
959
- @NonNull Response response
960
- ) throws IOException {
961
- try (ResponseBody responseBody = response.body()) {
962
- if (!response.isSuccessful()) {
963
- JSObject retError = new JSObject();
964
- retError.put("message", "Server error: " + response.code());
965
- retError.put("error", "response_error");
966
- callback.callback(retError);
967
- return;
968
- }
969
-
970
- assert responseBody != null;
971
- String responseData = responseBody.string();
972
- JSONObject jsonResponse = new JSONObject(responseData);
973
- JSObject ret = new JSObject();
974
-
975
- Iterator<String> keys = jsonResponse.keys();
976
- while (keys.hasNext()) {
977
- String key = keys.next();
978
- if (jsonResponse.has(key)) {
979
- ret.put(key, jsonResponse.get(key));
663
+ private void makeJsonRequest(String url, JSONObject jsonBody, Callback callback) {
664
+ MediaType JSON = MediaType.get("application/json; charset=utf-8");
665
+ RequestBody body = RequestBody.create(jsonBody.toString(), JSON);
666
+
667
+ Request request = new Request.Builder().url(url).post(body).build();
668
+
669
+ client
670
+ .newCall(request)
671
+ .enqueue(
672
+ new okhttp3.Callback() {
673
+ @Override
674
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
675
+ JSObject retError = new JSObject();
676
+ retError.put("message", "Request failed: " + e.getMessage());
677
+ retError.put("error", "network_error");
678
+ callback.callback(retError);
679
+ }
680
+
681
+ @Override
682
+ public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
683
+ try (ResponseBody responseBody = response.body()) {
684
+ if (!response.isSuccessful()) {
685
+ JSObject retError = new JSObject();
686
+ retError.put("message", "Server error: " + response.code());
687
+ retError.put("error", "response_error");
688
+ callback.callback(retError);
689
+ return;
690
+ }
691
+
692
+ assert responseBody != null;
693
+ String responseData = responseBody.string();
694
+ JSONObject jsonResponse = new JSONObject(responseData);
695
+ JSObject ret = new JSObject();
696
+
697
+ Iterator<String> keys = jsonResponse.keys();
698
+ while (keys.hasNext()) {
699
+ String key = keys.next();
700
+ if (jsonResponse.has(key)) {
701
+ if ("session_key".equals(key)) {
702
+ ret.put("sessionKey", jsonResponse.get(key));
703
+ } else {
704
+ ret.put(key, jsonResponse.get(key));
705
+ }
706
+ }
707
+ }
708
+ callback.callback(ret);
709
+ } catch (JSONException e) {
710
+ JSObject retError = new JSObject();
711
+ retError.put("message", "JSON parse error: " + e.getMessage());
712
+ retError.put("error", "parse_error");
713
+ callback.callback(retError);
714
+ }
715
+ }
980
716
  }
981
- }
982
- Log.i(TAG, "Channel unset");
983
- callback.callback(ret);
984
- } catch (JSONException e) {
985
- JSObject retError = new JSObject();
986
- retError.put("message", "JSON parse error: " + e.getMessage());
987
- retError.put("error", "parse_error");
988
- callback.callback(retError);
717
+ );
718
+ }
719
+
720
+ public void getLatest(final String updateUrl, final String channel, final Callback callback) {
721
+ JSONObject json;
722
+ try {
723
+ json = this.createInfoObject();
724
+ if (channel != null && json != null) {
725
+ json.put("defaultChannel", channel);
989
726
  }
990
- }
727
+ } catch (JSONException e) {
728
+ Log.e(TAG, "Error getLatest JSONException", e);
729
+ final JSObject retError = new JSObject();
730
+ retError.put("message", "Cannot get info: " + e);
731
+ retError.put("error", "json_error");
732
+ callback.callback(retError);
733
+ return;
991
734
  }
992
- );
993
- }
994
-
995
- public void setChannel(final String channel, final Callback callback) {
996
- String channelUrl = this.channelUrl;
997
- if (channelUrl == null || channelUrl.isEmpty()) {
998
- Log.e(TAG, "Channel URL is not set");
999
- final JSObject retError = new JSObject();
1000
- retError.put("message", "channelUrl missing");
1001
- retError.put("error", "missing_config");
1002
- callback.callback(retError);
1003
- return;
1004
- }
1005
- JSONObject json;
1006
- try {
1007
- json = this.createInfoObject();
1008
- json.put("channel", channel);
1009
- } catch (JSONException e) {
1010
- Log.e(TAG, "Error setChannel JSONException", e);
1011
- final JSObject retError = new JSObject();
1012
- retError.put("message", "Cannot get info: " + e);
1013
- retError.put("error", "json_error");
1014
- callback.callback(retError);
1015
- return;
735
+
736
+ Log.i(CapacitorUpdater.TAG, "Auto-update parameters: " + json);
737
+
738
+ makeJsonRequest(updateUrl, json, callback);
1016
739
  }
1017
740
 
1018
- makeJsonRequest(channelUrl, json, callback);
1019
- }
1020
-
1021
- public void getChannel(final Callback callback) {
1022
- String channelUrl = this.channelUrl;
1023
- if (channelUrl == null || channelUrl.isEmpty()) {
1024
- Log.e(TAG, "Channel URL is not set");
1025
- final JSObject retError = new JSObject();
1026
- retError.put("message", "Channel URL is not set");
1027
- retError.put("error", "missing_config");
1028
- callback.callback(retError);
1029
- return;
741
+ public void unsetChannel(final Callback callback) {
742
+ String channelUrl = this.channelUrl;
743
+ if (channelUrl == null || channelUrl.isEmpty()) {
744
+ Log.e(TAG, "Channel URL is not set");
745
+ final JSObject retError = new JSObject();
746
+ retError.put("message", "channelUrl missing");
747
+ retError.put("error", "missing_config");
748
+ callback.callback(retError);
749
+ return;
750
+ }
751
+ JSONObject json;
752
+ try {
753
+ json = this.createInfoObject();
754
+ } catch (JSONException e) {
755
+ Log.e(TAG, "Error unsetChannel JSONException", e);
756
+ final JSObject retError = new JSObject();
757
+ retError.put("message", "Cannot get info: " + e);
758
+ retError.put("error", "json_error");
759
+ callback.callback(retError);
760
+ return;
761
+ }
762
+
763
+ Request request = new Request.Builder()
764
+ .url(channelUrl)
765
+ .delete(RequestBody.create(json.toString(), MediaType.get("application/json")))
766
+ .build();
767
+
768
+ client
769
+ .newCall(request)
770
+ .enqueue(
771
+ new okhttp3.Callback() {
772
+ @Override
773
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
774
+ JSObject retError = new JSObject();
775
+ retError.put("message", "Request failed: " + e.getMessage());
776
+ retError.put("error", "network_error");
777
+ callback.callback(retError);
778
+ }
779
+
780
+ @Override
781
+ public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
782
+ try (ResponseBody responseBody = response.body()) {
783
+ if (!response.isSuccessful()) {
784
+ JSObject retError = new JSObject();
785
+ retError.put("message", "Server error: " + response.code());
786
+ retError.put("error", "response_error");
787
+ callback.callback(retError);
788
+ return;
789
+ }
790
+
791
+ assert responseBody != null;
792
+ String responseData = responseBody.string();
793
+ JSONObject jsonResponse = new JSONObject(responseData);
794
+ JSObject ret = new JSObject();
795
+
796
+ Iterator<String> keys = jsonResponse.keys();
797
+ while (keys.hasNext()) {
798
+ String key = keys.next();
799
+ if (jsonResponse.has(key)) {
800
+ ret.put(key, jsonResponse.get(key));
801
+ }
802
+ }
803
+ Log.i(TAG, "Channel unset");
804
+ callback.callback(ret);
805
+ } catch (JSONException e) {
806
+ JSObject retError = new JSObject();
807
+ retError.put("message", "JSON parse error: " + e.getMessage());
808
+ retError.put("error", "parse_error");
809
+ callback.callback(retError);
810
+ }
811
+ }
812
+ }
813
+ );
1030
814
  }
1031
- JSONObject json;
1032
- try {
1033
- json = this.createInfoObject();
1034
- } catch (JSONException e) {
1035
- Log.e(TAG, "Error getChannel JSONException", e);
1036
- final JSObject retError = new JSObject();
1037
- retError.put("message", "Cannot get info: " + e);
1038
- retError.put("error", "json_error");
1039
- callback.callback(retError);
1040
- return;
815
+
816
+ public void setChannel(final String channel, final Callback callback) {
817
+ String channelUrl = this.channelUrl;
818
+ if (channelUrl == null || channelUrl.isEmpty()) {
819
+ Log.e(TAG, "Channel URL is not set");
820
+ final JSObject retError = new JSObject();
821
+ retError.put("message", "channelUrl missing");
822
+ retError.put("error", "missing_config");
823
+ callback.callback(retError);
824
+ return;
825
+ }
826
+ JSONObject json;
827
+ try {
828
+ json = this.createInfoObject();
829
+ json.put("channel", channel);
830
+ } catch (JSONException e) {
831
+ Log.e(TAG, "Error setChannel JSONException", e);
832
+ final JSObject retError = new JSObject();
833
+ retError.put("message", "Cannot get info: " + e);
834
+ retError.put("error", "json_error");
835
+ callback.callback(retError);
836
+ return;
837
+ }
838
+
839
+ makeJsonRequest(channelUrl, json, callback);
1041
840
  }
1042
841
 
1043
- Request request = new Request.Builder()
1044
- .url(channelUrl)
1045
- .put(
1046
- RequestBody.create(json.toString(), MediaType.get("application/json"))
1047
- )
1048
- .build();
1049
-
1050
- client
1051
- .newCall(request)
1052
- .enqueue(
1053
- new okhttp3.Callback() {
1054
- @Override
1055
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
1056
- JSObject retError = new JSObject();
1057
- retError.put("message", "Request failed: " + e.getMessage());
1058
- retError.put("error", "network_error");
842
+ public void getChannel(final Callback callback) {
843
+ String channelUrl = this.channelUrl;
844
+ if (channelUrl == null || channelUrl.isEmpty()) {
845
+ Log.e(TAG, "Channel URL is not set");
846
+ final JSObject retError = new JSObject();
847
+ retError.put("message", "Channel URL is not set");
848
+ retError.put("error", "missing_config");
849
+ callback.callback(retError);
850
+ return;
851
+ }
852
+ JSONObject json;
853
+ try {
854
+ json = this.createInfoObject();
855
+ } catch (JSONException e) {
856
+ Log.e(TAG, "Error getChannel JSONException", e);
857
+ final JSObject retError = new JSObject();
858
+ retError.put("message", "Cannot get info: " + e);
859
+ retError.put("error", "json_error");
1059
860
  callback.callback(retError);
1060
- }
1061
-
1062
- @Override
1063
- public void onResponse(
1064
- @NonNull Call call,
1065
- @NonNull Response response
1066
- ) throws IOException {
1067
- try (ResponseBody responseBody = response.body()) {
1068
- if (response.code() == 400) {
1069
- assert responseBody != null;
1070
- String data = responseBody.string();
1071
- if (
1072
- data.contains("channel_not_found") &&
1073
- !defaultChannel.isEmpty()
1074
- ) {
1075
- JSObject ret = new JSObject();
1076
- ret.put("channel", defaultChannel);
1077
- ret.put("status", "default");
1078
- Log.i(TAG, "Channel get to \"" + ret);
1079
- callback.callback(ret);
1080
- return;
861
+ return;
862
+ }
863
+
864
+ Request request = new Request.Builder()
865
+ .url(channelUrl)
866
+ .put(RequestBody.create(json.toString(), MediaType.get("application/json")))
867
+ .build();
868
+
869
+ client
870
+ .newCall(request)
871
+ .enqueue(
872
+ new okhttp3.Callback() {
873
+ @Override
874
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
875
+ JSObject retError = new JSObject();
876
+ retError.put("message", "Request failed: " + e.getMessage());
877
+ retError.put("error", "network_error");
878
+ callback.callback(retError);
879
+ }
880
+
881
+ @Override
882
+ public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
883
+ try (ResponseBody responseBody = response.body()) {
884
+ if (response.code() == 400) {
885
+ assert responseBody != null;
886
+ String data = responseBody.string();
887
+ if (data.contains("channel_not_found") && !defaultChannel.isEmpty()) {
888
+ JSObject ret = new JSObject();
889
+ ret.put("channel", defaultChannel);
890
+ ret.put("status", "default");
891
+ Log.i(TAG, "Channel get to \"" + ret);
892
+ callback.callback(ret);
893
+ return;
894
+ }
895
+ }
896
+
897
+ if (!response.isSuccessful()) {
898
+ JSObject retError = new JSObject();
899
+ retError.put("message", "Server error: " + response.code());
900
+ retError.put("error", "response_error");
901
+ callback.callback(retError);
902
+ return;
903
+ }
904
+
905
+ assert responseBody != null;
906
+ String responseData = responseBody.string();
907
+ JSONObject jsonResponse = new JSONObject(responseData);
908
+ JSObject ret = new JSObject();
909
+
910
+ Iterator<String> keys = jsonResponse.keys();
911
+ while (keys.hasNext()) {
912
+ String key = keys.next();
913
+ if (jsonResponse.has(key)) {
914
+ ret.put(key, jsonResponse.get(key));
915
+ }
916
+ }
917
+ Log.i(TAG, "Channel get to \"" + ret);
918
+ callback.callback(ret);
919
+ } catch (JSONException e) {
920
+ JSObject retError = new JSObject();
921
+ retError.put("message", "JSON parse error: " + e.getMessage());
922
+ retError.put("error", "parse_error");
923
+ callback.callback(retError);
924
+ }
925
+ }
1081
926
  }
1082
- }
1083
-
1084
- if (!response.isSuccessful()) {
1085
- JSObject retError = new JSObject();
1086
- retError.put("message", "Server error: " + response.code());
1087
- retError.put("error", "response_error");
1088
- callback.callback(retError);
1089
- return;
1090
- }
1091
-
1092
- assert responseBody != null;
1093
- String responseData = responseBody.string();
1094
- JSONObject jsonResponse = new JSONObject(responseData);
1095
- JSObject ret = new JSObject();
1096
-
1097
- Iterator<String> keys = jsonResponse.keys();
1098
- while (keys.hasNext()) {
1099
- String key = keys.next();
1100
- if (jsonResponse.has(key)) {
1101
- ret.put(key, jsonResponse.get(key));
927
+ );
928
+ }
929
+
930
+ public void sendStats(final String action) {
931
+ this.sendStats(action, this.getCurrentBundle().getVersionName());
932
+ }
933
+
934
+ public void sendStats(final String action, final String versionName) {
935
+ this.sendStats(action, versionName, "");
936
+ }
937
+
938
+ public void sendStats(final String action, final String versionName, final String oldVersionName) {
939
+ String statsUrl = this.statsUrl;
940
+ if (statsUrl == null || statsUrl.isEmpty()) {
941
+ return;
942
+ }
943
+ JSONObject json;
944
+ try {
945
+ json = this.createInfoObject();
946
+ json.put("version_name", versionName);
947
+ json.put("old_version_name", oldVersionName);
948
+ json.put("action", action);
949
+ } catch (JSONException e) {
950
+ Log.e(TAG, "Error sendStats JSONException", e);
951
+ return;
952
+ }
953
+
954
+ Request request = new Request.Builder()
955
+ .url(statsUrl)
956
+ .post(RequestBody.create(json.toString(), MediaType.get("application/json")))
957
+ .build();
958
+
959
+ client
960
+ .newCall(request)
961
+ .enqueue(
962
+ new okhttp3.Callback() {
963
+ @Override
964
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
965
+ Log.e(TAG, "Failed to send stats: " + e.getMessage());
966
+ }
967
+
968
+ @Override
969
+ public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
970
+ if (response.isSuccessful()) {
971
+ Log.i(TAG, "Stats send for \"" + action + "\", version " + versionName);
972
+ } else {
973
+ Log.e(TAG, "Error sending stats: " + response.code());
974
+ }
975
+ }
1102
976
  }
1103
- }
1104
- Log.i(TAG, "Channel get to \"" + ret);
1105
- callback.callback(ret);
977
+ );
978
+ }
979
+
980
+ public BundleInfo getBundleInfo(final String id) {
981
+ String trueId = BundleInfo.VERSION_UNKNOWN;
982
+ if (id != null) {
983
+ trueId = id;
984
+ }
985
+ BundleInfo result;
986
+ if (BundleInfo.ID_BUILTIN.equals(trueId)) {
987
+ result = new BundleInfo(trueId, null, BundleStatus.SUCCESS, "", "");
988
+ } else if (BundleInfo.VERSION_UNKNOWN.equals(trueId)) {
989
+ result = new BundleInfo(trueId, null, BundleStatus.ERROR, "", "");
990
+ } else {
991
+ try {
992
+ String stored = this.prefs.getString(trueId + INFO_SUFFIX, "");
993
+ result = BundleInfo.fromJSON(stored);
1106
994
  } catch (JSONException e) {
1107
- JSObject retError = new JSObject();
1108
- retError.put("message", "JSON parse error: " + e.getMessage());
1109
- retError.put("error", "parse_error");
1110
- callback.callback(retError);
995
+ Log.e(TAG, "Failed to parse info for bundle [" + trueId + "] ", e);
996
+ result = new BundleInfo(trueId, null, BundleStatus.PENDING, "", "");
1111
997
  }
1112
- }
1113
998
  }
1114
- );
1115
- }
1116
-
1117
- public void sendStats(final String action) {
1118
- this.sendStats(action, this.getCurrentBundle().getVersionName());
1119
- }
1120
-
1121
- public void sendStats(final String action, final String versionName) {
1122
- this.sendStats(action, versionName, "");
1123
- }
1124
-
1125
- public void sendStats(
1126
- final String action,
1127
- final String versionName,
1128
- final String oldVersionName
1129
- ) {
1130
- String statsUrl = this.statsUrl;
1131
- if (statsUrl == null || statsUrl.isEmpty()) {
1132
- return;
1133
- }
1134
- JSONObject json;
1135
- try {
1136
- json = this.createInfoObject();
1137
- json.put("version_name", versionName);
1138
- json.put("old_version_name", oldVersionName);
1139
- json.put("action", action);
1140
- } catch (JSONException e) {
1141
- Log.e(TAG, "Error sendStats JSONException", e);
1142
- return;
999
+ // Log.d(TAG, "Returning info [" + trueId + "] " + result);
1000
+ return result;
1143
1001
  }
1144
1002
 
1145
- Request request = new Request.Builder()
1146
- .url(statsUrl)
1147
- .post(
1148
- RequestBody.create(json.toString(), MediaType.get("application/json"))
1149
- )
1150
- .build();
1151
-
1152
- client
1153
- .newCall(request)
1154
- .enqueue(
1155
- new okhttp3.Callback() {
1156
- @Override
1157
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
1158
- Log.e(TAG, "Failed to send stats: " + e.getMessage());
1159
- }
1160
-
1161
- @Override
1162
- public void onResponse(
1163
- @NonNull Call call,
1164
- @NonNull Response response
1165
- ) throws IOException {
1166
- if (response.isSuccessful()) {
1167
- Log.i(
1168
- TAG,
1169
- "Stats send for \"" + action + "\", version " + versionName
1170
- );
1171
- } else {
1172
- Log.e(TAG, "Error sending stats: " + response.code());
1003
+ public BundleInfo getBundleInfoByName(final String versionName) {
1004
+ final List<BundleInfo> installed = this.list(false);
1005
+ for (final BundleInfo i : installed) {
1006
+ if (i.getVersionName().equals(versionName)) {
1007
+ return i;
1173
1008
  }
1174
- }
1175
1009
  }
1176
- );
1177
- }
1010
+ return null;
1011
+ }
1178
1012
 
1179
- public BundleInfo getBundleInfo(final String id) {
1180
- String trueId = BundleInfo.VERSION_UNKNOWN;
1181
- if (id != null) {
1182
- trueId = id;
1013
+ private void removeBundleInfo(final String id) {
1014
+ this.saveBundleInfo(id, null);
1183
1015
  }
1184
- BundleInfo result;
1185
- if (BundleInfo.ID_BUILTIN.equals(trueId)) {
1186
- result = new BundleInfo(trueId, null, BundleStatus.SUCCESS, "", "");
1187
- } else if (BundleInfo.VERSION_UNKNOWN.equals(trueId)) {
1188
- result = new BundleInfo(trueId, null, BundleStatus.ERROR, "", "");
1189
- } else {
1190
- try {
1191
- String stored = this.prefs.getString(trueId + INFO_SUFFIX, "");
1192
- result = BundleInfo.fromJSON(stored);
1193
- } catch (JSONException e) {
1194
- Log.e(TAG, "Failed to parse info for bundle [" + trueId + "] ", e);
1195
- result = new BundleInfo(trueId, null, BundleStatus.PENDING, "", "");
1196
- }
1016
+
1017
+ public void saveBundleInfo(final String id, final BundleInfo info) {
1018
+ if (id == null || (info != null && (info.isBuiltin() || info.isUnknown()))) {
1019
+ Log.d(TAG, "Not saving info for bundle: [" + id + "] " + info);
1020
+ return;
1021
+ }
1022
+
1023
+ if (info == null) {
1024
+ Log.d(TAG, "Removing info for bundle [" + id + "]");
1025
+ this.editor.remove(id + INFO_SUFFIX);
1026
+ } else {
1027
+ final BundleInfo update = info.setId(id);
1028
+ Log.d(TAG, "Storing info for bundle [" + id + "] " + update.toString());
1029
+ this.editor.putString(id + INFO_SUFFIX, update.toString());
1030
+ }
1031
+ this.editor.commit();
1197
1032
  }
1198
- // Log.d(TAG, "Returning info [" + trueId + "] " + result);
1199
- return result;
1200
- }
1201
-
1202
- public BundleInfo getBundleInfoByName(final String versionName) {
1203
- final List<BundleInfo> installed = this.list(false);
1204
- for (final BundleInfo i : installed) {
1205
- if (i.getVersionName().equals(versionName)) {
1206
- return i;
1207
- }
1033
+
1034
+ private void setBundleStatus(final String id, final BundleStatus status) {
1035
+ if (id != null && status != null) {
1036
+ BundleInfo info = this.getBundleInfo(id);
1037
+ Log.d(TAG, "Setting status for bundle [" + id + "] to " + status);
1038
+ this.saveBundleInfo(id, info.setStatus(status));
1039
+ }
1208
1040
  }
1209
- return null;
1210
- }
1211
1041
 
1212
- private void removeBundleInfo(final String id) {
1213
- this.saveBundleInfo(id, null);
1214
- }
1042
+ private String getCurrentBundleId() {
1043
+ if (this.isUsingBuiltin()) {
1044
+ return BundleInfo.ID_BUILTIN;
1045
+ } else {
1046
+ final String path = this.getCurrentBundlePath();
1047
+ return path.substring(path.lastIndexOf('/') + 1);
1048
+ }
1049
+ }
1215
1050
 
1216
- public void saveBundleInfo(final String id, final BundleInfo info) {
1217
- if (
1218
- id == null || (info != null && (info.isBuiltin() || info.isUnknown()))
1219
- ) {
1220
- Log.d(TAG, "Not saving info for bundle: [" + id + "] " + info);
1221
- return;
1051
+ public BundleInfo getCurrentBundle() {
1052
+ return this.getBundleInfo(this.getCurrentBundleId());
1222
1053
  }
1223
1054
 
1224
- if (info == null) {
1225
- Log.d(TAG, "Removing info for bundle [" + id + "]");
1226
- this.editor.remove(id + INFO_SUFFIX);
1227
- } else {
1228
- final BundleInfo update = info.setId(id);
1229
- Log.d(TAG, "Storing info for bundle [" + id + "] " + update.toString());
1230
- this.editor.putString(id + INFO_SUFFIX, update.toString());
1055
+ public String getCurrentBundlePath() {
1056
+ String path = this.prefs.getString(WebView.CAP_SERVER_PATH, "public");
1057
+ if (path.trim().isEmpty()) {
1058
+ return "public";
1059
+ }
1060
+ return path;
1231
1061
  }
1232
- this.editor.commit();
1233
- }
1234
-
1235
- private void setBundleStatus(final String id, final BundleStatus status) {
1236
- if (id != null && status != null) {
1237
- BundleInfo info = this.getBundleInfo(id);
1238
- Log.d(TAG, "Setting status for bundle [" + id + "] to " + status);
1239
- this.saveBundleInfo(id, info.setStatus(status));
1062
+
1063
+ public Boolean isUsingBuiltin() {
1064
+ return this.getCurrentBundlePath().equals("public");
1240
1065
  }
1241
- }
1242
-
1243
- private String getCurrentBundleId() {
1244
- if (this.isUsingBuiltin()) {
1245
- return BundleInfo.ID_BUILTIN;
1246
- } else {
1247
- final String path = this.getCurrentBundlePath();
1248
- return path.substring(path.lastIndexOf('/') + 1);
1066
+
1067
+ public BundleInfo getFallbackBundle() {
1068
+ final String id = this.prefs.getString(FALLBACK_VERSION, BundleInfo.ID_BUILTIN);
1069
+ return this.getBundleInfo(id);
1249
1070
  }
1250
- }
1251
1071
 
1252
- public BundleInfo getCurrentBundle() {
1253
- return this.getBundleInfo(this.getCurrentBundleId());
1254
- }
1072
+ private void setFallbackBundle(final BundleInfo fallback) {
1073
+ this.editor.putString(FALLBACK_VERSION, fallback == null ? BundleInfo.ID_BUILTIN : fallback.getId());
1074
+ this.editor.commit();
1075
+ }
1255
1076
 
1256
- public String getCurrentBundlePath() {
1257
- String path = this.prefs.getString(WebView.CAP_SERVER_PATH, "public");
1258
- if (path.trim().isEmpty()) {
1259
- return "public";
1077
+ public BundleInfo getNextBundle() {
1078
+ final String id = this.prefs.getString(NEXT_VERSION, null);
1079
+ if (id == null) return null;
1080
+ return this.getBundleInfo(id);
1260
1081
  }
1261
- return path;
1262
- }
1263
-
1264
- public Boolean isUsingBuiltin() {
1265
- return this.getCurrentBundlePath().equals("public");
1266
- }
1267
-
1268
- public BundleInfo getFallbackBundle() {
1269
- final String id =
1270
- this.prefs.getString(FALLBACK_VERSION, BundleInfo.ID_BUILTIN);
1271
- return this.getBundleInfo(id);
1272
- }
1273
-
1274
- private void setFallbackBundle(final BundleInfo fallback) {
1275
- this.editor.putString(
1276
- FALLBACK_VERSION,
1277
- fallback == null ? BundleInfo.ID_BUILTIN : fallback.getId()
1278
- );
1279
- this.editor.commit();
1280
- }
1281
-
1282
- public BundleInfo getNextBundle() {
1283
- final String id = this.prefs.getString(NEXT_VERSION, null);
1284
- if (id == null) return null;
1285
- return this.getBundleInfo(id);
1286
- }
1287
-
1288
- public boolean setNextBundle(final String next) {
1289
- if (next == null) {
1290
- this.editor.remove(NEXT_VERSION);
1291
- } else {
1292
- final BundleInfo newBundle = this.getBundleInfo(next);
1293
- if (!newBundle.isBuiltin() && !this.bundleExists(next)) {
1294
- return false;
1295
- }
1296
- this.editor.putString(NEXT_VERSION, next);
1297
- this.setBundleStatus(next, BundleStatus.PENDING);
1082
+
1083
+ public boolean setNextBundle(final String next) {
1084
+ if (next == null) {
1085
+ this.editor.remove(NEXT_VERSION);
1086
+ } else {
1087
+ final BundleInfo newBundle = this.getBundleInfo(next);
1088
+ if (!newBundle.isBuiltin() && !this.bundleExists(next)) {
1089
+ return false;
1090
+ }
1091
+ this.editor.putString(NEXT_VERSION, next);
1092
+ this.setBundleStatus(next, BundleStatus.PENDING);
1093
+ }
1094
+ this.editor.commit();
1095
+ return true;
1298
1096
  }
1299
- this.editor.commit();
1300
- return true;
1301
- }
1302
1097
  }