@capawesome/capacitor-live-update 6.4.2 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +46 -5
  2. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +363 -251
  3. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +71 -0
  4. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +65 -60
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/Manifest.java +58 -0
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/ManifestItem.java +37 -0
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +19 -27
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +15 -1
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +22 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/enums/ArtifactType.java +6 -0
  11. package/dist/docs.json +67 -4
  12. package/dist/esm/definitions.d.ts +42 -3
  13. package/dist/esm/definitions.js.map +1 -1
  14. package/dist/esm/web.d.ts +2 -1
  15. package/dist/esm/web.js +3 -0
  16. package/dist/esm/web.js.map +1 -1
  17. package/dist/plugin.cjs.js +3 -0
  18. package/dist/plugin.cjs.js.map +1 -1
  19. package/dist/plugin.js +3 -0
  20. package/dist/plugin.js.map +1 -1
  21. package/ios/Plugin/Classes/Manifest.swift +20 -0
  22. package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +11 -1
  23. package/ios/Plugin/Classes/Results/FetchLatestBundle.swift +16 -0
  24. package/ios/Plugin/Enums/ArtifactType.swift +4 -0
  25. package/ios/Plugin/{Classes → Enums}/CustomError.swift +3 -0
  26. package/ios/Plugin/LiveUpdate.swift +279 -198
  27. package/ios/Plugin/LiveUpdateHttpClient.swift +42 -0
  28. package/ios/Plugin/LiveUpdatePlugin.m +1 -0
  29. package/ios/Plugin/LiveUpdatePlugin.swift +77 -61
  30. package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +2 -3
  31. package/ios/Plugin/Protocols/Api/ManifestItem.swift +5 -0
  32. package/package.json +1 -1
@@ -4,6 +4,7 @@ import android.app.Activity;
4
4
  import android.content.SharedPreferences;
5
5
  import android.content.pm.PackageInfo;
6
6
  import android.content.pm.PackageManager;
7
+ import android.content.res.AssetManager;
7
8
  import android.os.Build;
8
9
  import android.os.Handler;
9
10
  import android.util.Base64;
@@ -11,12 +12,15 @@ import androidx.annotation.NonNull;
11
12
  import androidx.annotation.Nullable;
12
13
  import com.getcapacitor.Logger;
13
14
  import com.getcapacitor.plugin.WebView;
15
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.Manifest;
16
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.ManifestItem;
14
17
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
15
18
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundleOptions;
16
19
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
17
20
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetBundleOptions;
18
21
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
19
22
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
23
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
20
24
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundleResult;
21
25
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundlesResult;
22
26
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetChannelResult;
@@ -25,32 +29,33 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdR
25
29
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
26
30
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
27
31
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
32
+ import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
28
33
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
29
34
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
30
35
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
31
36
  import java.io.File;
37
+ import java.io.FileInputStream;
38
+ import java.io.FileOutputStream;
32
39
  import java.io.IOException;
40
+ import java.io.InputStream;
33
41
  import java.security.KeyFactory;
34
42
  import java.security.MessageDigest;
35
43
  import java.security.PublicKey;
36
44
  import java.security.Signature;
37
45
  import java.security.spec.X509EncodedKeySpec;
46
+ import java.util.ArrayList;
38
47
  import java.util.Arrays;
48
+ import java.util.List;
39
49
  import java.util.UUID;
40
- import java.util.concurrent.TimeUnit;
50
+ import java.util.concurrent.atomic.AtomicReference;
41
51
  import net.lingala.zip4j.ZipFile;
42
- import okhttp3.Call;
43
- import okhttp3.Callback;
44
52
  import okhttp3.HttpUrl;
45
- import okhttp3.OkHttpClient;
46
- import okhttp3.Request;
47
53
  import okhttp3.Response;
48
54
  import okhttp3.ResponseBody;
49
55
  import okio.Buffer;
50
- import okio.BufferedSink;
51
56
  import okio.BufferedSource;
52
57
  import okio.Okio;
53
- import org.json.JSONException;
58
+ import org.json.JSONArray;
54
59
  import org.json.JSONObject;
55
60
 
56
61
  public class LiveUpdate {
@@ -60,6 +65,12 @@ public class LiveUpdate {
60
65
 
61
66
  private final String defaultWebAssetDir;
62
67
 
68
+ @NonNull
69
+ private final String host;
70
+
71
+ @NonNull
72
+ private final LiveUpdateHttpClient httpClient;
73
+
63
74
  @NonNull
64
75
  private final LiveUpdatePlugin plugin;
65
76
 
@@ -69,12 +80,19 @@ public class LiveUpdate {
69
80
  @NonNull
70
81
  private final SharedPreferences.Editor webViewSettingsEditor;
71
82
 
72
- private final String bundlesDirectory = "_capacitor_live_update_bundles";
83
+ private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
73
84
  private final Handler rollbackHandler = new Handler();
85
+ private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
74
86
 
75
87
  public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
76
88
  this.config = config;
77
89
  this.defaultWebAssetDir = plugin.getBridge().DEFAULT_WEB_ASSET_DIR;
90
+ if (config.getLocation() != null && config.getLocation().equals("eu")) {
91
+ this.host = "api.cloud.capawesome.eu";
92
+ } else {
93
+ this.host = "api.cloud.capawesome.io";
94
+ }
95
+ this.httpClient = new LiveUpdateHttpClient(config);
78
96
  this.plugin = plugin;
79
97
  this.preferences = new LiveUpdatePreferences(plugin.getContext());
80
98
  this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
@@ -103,6 +121,7 @@ public class LiveUpdate {
103
121
  }
104
122
 
105
123
  public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) throws Exception {
124
+ ArtifactType artifactType = options.getArtifactType();
106
125
  String bundleId = options.getBundleId();
107
126
  String checksum = options.getChecksum();
108
127
  String url = options.getUrl();
@@ -115,7 +134,18 @@ public class LiveUpdate {
115
134
  }
116
135
 
117
136
  // Download the bundle
118
- downloadBundle(bundleId, checksum, null, url, callback);
137
+ if (artifactType == ArtifactType.MANIFEST) {
138
+ downloadBundleOfTypeManifest(bundleId, url);
139
+ } else {
140
+ downloadBundleOfTypeZip(bundleId, url, checksum);
141
+ }
142
+ callback.success();
143
+ }
144
+
145
+ public void fetchLatestBundle(@NonNull NonEmptyCallback callback) throws Exception {
146
+ GetLatestBundleResponse response = fetchLatestBundle();
147
+ FetchLatestBundleResult result = new FetchLatestBundleResult(response == null ? null : response.getBundleId());
148
+ callback.success(result);
119
149
  }
120
150
 
121
151
  public void getBundle(@NonNull NonEmptyCallback callback) {
@@ -165,7 +195,9 @@ public class LiveUpdate {
165
195
 
166
196
  public void ready() {
167
197
  Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
198
+ // Stop the rollback timer
168
199
  stopRollbackTimer();
200
+ // Delete unused bundles
169
201
  if (config.getAutoDeleteBundles()) {
170
202
  deleteUnusedBundles();
171
203
  }
@@ -210,73 +242,44 @@ public class LiveUpdate {
210
242
 
211
243
  public void sync(@NonNull NonEmptyCallback<Result> callback) throws Exception {
212
244
  // Fetch the latest bundle
213
- fetchLatestBundle(
214
- new NonEmptyCallback<GetLatestBundleResponse>() {
215
- @Override
216
- public void error(Exception exception) {
217
- // Throw error if the exception is a timeout
218
- if (exception instanceof java.net.SocketTimeoutException) {
219
- callback.error(new Exception(LiveUpdatePlugin.ERROR_HTTP_TIMEOUT));
220
- return;
221
- }
222
- // No update available
223
- Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
224
- SyncResult syncResult = new SyncResult(null);
225
- callback.success(syncResult);
226
- }
227
-
228
- @Override
229
- public void success(@NonNull GetLatestBundleResponse result) {
230
- String latestBundleId = result.getBundleId();
231
- String checksum = result.getChecksum();
232
- String signature = result.getSignature();
233
- String url = result.getUrl();
234
-
235
- // Check if the bundle already exists
236
- if (hasBundle(latestBundleId)) {
237
- String nextBundleId = null;
238
- String currentBundleId = getCurrentBundleId();
239
- if (!latestBundleId.equals(currentBundleId)) {
240
- // Set the next bundle
241
- setNextBundle(latestBundleId);
242
- nextBundleId = latestBundleId;
243
- }
244
- SyncResult syncResult = new SyncResult(nextBundleId);
245
- callback.success(syncResult);
246
- return;
247
- }
248
- // Download and unzip the bundle
249
- downloadBundle(
250
- latestBundleId,
251
- checksum,
252
- signature,
253
- url,
254
- new EmptyCallback() {
255
- @Override
256
- public void success() {
257
- // Set the next bundle
258
- setNextBundle(latestBundleId);
259
- SyncResult syncResult = new SyncResult(latestBundleId);
260
- callback.success(syncResult);
261
- }
262
-
263
- @Override
264
- public void error(Exception exception) {
265
- callback.error(exception);
266
- }
267
- }
268
- );
269
- }
245
+ GetLatestBundleResponse response = fetchLatestBundle();
246
+ if (response == null) {
247
+ Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
248
+ SyncResult syncResult = new SyncResult(null);
249
+ callback.success(syncResult);
250
+ return;
251
+ }
252
+ ArtifactType artifactType = response.getArtifactType();
253
+ String latestBundleId = response.getBundleId();
254
+ String url = response.getUrl();
255
+ // Check if the bundle already exists
256
+ if (hasBundle(latestBundleId)) {
257
+ String nextBundleId = null;
258
+ String currentBundleId = getCurrentBundleId();
259
+ if (!latestBundleId.equals(currentBundleId)) {
260
+ // Set the next bundle
261
+ setNextBundle(latestBundleId);
262
+ nextBundleId = latestBundleId;
270
263
  }
271
- );
264
+ SyncResult syncResult = new SyncResult(nextBundleId);
265
+ callback.success(syncResult);
266
+ return;
267
+ }
268
+ // Download the bundle
269
+ if (artifactType == ArtifactType.MANIFEST) {
270
+ downloadBundleOfTypeManifest(latestBundleId, url);
271
+ } else {
272
+ downloadBundleOfTypeZip(latestBundleId, url, null);
273
+ }
274
+ // Set the next bundle
275
+ setNextBundle(latestBundleId);
276
+ SyncResult syncResult = new SyncResult(latestBundleId);
277
+ callback.success(syncResult);
272
278
  }
273
279
 
274
- private void addBundle(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
275
- // Unzip the file to the bundle directory
276
- File unzippedDirectory = unzipFile(zipFile);
277
-
280
+ private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
278
281
  // Search folder with index.html file
279
- File indexHtmlFile = searchIndexHtmlFile(unzippedDirectory);
282
+ File indexHtmlFile = searchIndexHtmlFile(sourceDirectory);
280
283
  if (indexHtmlFile == null) {
281
284
  throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_INDEX_HTML_MISSING);
282
285
  }
@@ -289,6 +292,25 @@ public class LiveUpdate {
289
292
  indexHtmlFile.getParentFile().renameTo(bundleDirectory);
290
293
  }
291
294
 
295
+ private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
296
+ addBundle(bundleId, directory);
297
+ }
298
+
299
+ private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
300
+ // Unzip the file to the bundle directory
301
+ File unzippedDirectory = unzipFile(zipFile);
302
+ // Add the bundle
303
+ addBundle(bundleId, unzippedDirectory);
304
+ }
305
+
306
+ private File buildBundlesDirectory() {
307
+ return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
308
+ }
309
+
310
+ private File buildBundleDirectoryFor(@NonNull String bundleId) {
311
+ return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
312
+ }
313
+
292
314
  private File buildTemporaryDirectory() {
293
315
  String fileName = UUID.randomUUID().toString();
294
316
  return new File(plugin.getContext().getCacheDir(), fileName);
@@ -299,43 +321,63 @@ public class LiveUpdate {
299
321
  return new File(plugin.getContext().getCacheDir(), fileName);
300
322
  }
301
323
 
302
- private File buildBundlesDirectory() {
303
- return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
324
+ private void copyCurrentBundleFile(@NonNull String href, @NonNull File destinationDirectory) throws IOException {
325
+ String currentBundleId = getCurrentBundleId();
326
+ if (currentBundleId.equals(defaultWebAssetDir)) {
327
+ // Create the source input stream
328
+ AssetManager assets = plugin.getContext().getAssets();
329
+ InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
330
+ // Create the destination file
331
+ File destination = new File(destinationDirectory, href);
332
+ // Create all destination directories if they do not exist
333
+ destination.getParentFile().mkdirs();
334
+ // Copy the file
335
+ copyFile(inputStream, destination);
336
+ } else {
337
+ File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
338
+ // Create the source and destination files
339
+ File source = new File(currentBundleDirectory, href);
340
+ File destination = new File(destinationDirectory, href);
341
+ // Create all destination directories if they do not exist
342
+ destination.getParentFile().mkdirs();
343
+ // Copy the file
344
+ copyFile(source, destination);
345
+ }
304
346
  }
305
347
 
306
- private File buildBundleDirectoryFor(@NonNull String bundle) {
307
- return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundle);
348
+ private void copyCurrentBundleFiles(@NonNull List<String> hrefs, @NonNull File destinationDirectory) throws IOException {
349
+ for (String href : hrefs) {
350
+ copyCurrentBundleFile(href, destinationDirectory);
351
+ }
308
352
  }
309
353
 
310
- private byte[] getChecksumForFileAsBytes(@NonNull File file) throws Exception {
311
- try {
312
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
313
- BufferedSource source = Okio.buffer(Okio.source(file));
314
- Buffer buffer = new Buffer();
315
- for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
316
- digest.update(buffer.readByteArray());
317
- }
318
- source.close();
319
- return digest.digest();
320
- } catch (IOException exception) {
321
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
322
- throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
354
+ private void copyFile(File input, File output) throws IOException {
355
+ FileInputStream in = new FileInputStream(input);
356
+ FileOutputStream out = new FileOutputStream(output);
357
+ byte[] buffer = new byte[1024];
358
+ int length;
359
+ while ((length = in.read(buffer)) > 0) {
360
+ out.write(buffer, 0, length);
323
361
  }
362
+ out.close();
363
+ in.close();
324
364
  }
325
365
 
326
- private String getChecksumForFileAsString(@NonNull File file) throws Exception {
327
- byte[] checksumBytes = getChecksumForFileAsBytes(file);
328
- StringBuilder checksum = new StringBuilder();
329
- for (byte checksumByte : checksumBytes) {
330
- checksum.append(Integer.toString((checksumByte & 0xff) + 0x100, 16).substring(1));
366
+ private void copyFile(InputStream input, File output) throws IOException {
367
+ FileOutputStream out = new FileOutputStream(output);
368
+ byte[] buffer = new byte[1024];
369
+ int length;
370
+ while ((length = input.read(buffer)) > 0) {
371
+ out.write(buffer, 0, length);
331
372
  }
332
- return checksum.toString();
373
+ out.close();
374
+ input.close();
333
375
  }
334
376
 
335
377
  private void createBundlesDirectory() {
336
- File bundlesDirectory = buildBundlesDirectory();
337
- if (!bundlesDirectory.exists()) {
338
- bundlesDirectory.mkdir();
378
+ File file = buildBundlesDirectory();
379
+ if (!file.exists()) {
380
+ file.mkdir();
339
381
  }
340
382
  }
341
383
 
@@ -352,14 +394,19 @@ public class LiveUpdate {
352
394
  }
353
395
  }
354
396
 
397
+ private File createTemporaryDirectory() {
398
+ File file = buildTemporaryDirectory();
399
+ file.mkdir();
400
+ return file;
401
+ }
402
+
355
403
  private void deleteBundle(@NonNull String bundleId) {
356
404
  // Delete the bundle directory
357
405
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
358
406
  deleteFileRecursively(bundleDirectory);
359
407
  // Reset the next bundle if it is the deleted bundle
360
- String currentBundleId = getCurrentBundleId();
361
408
  String nextBundleId = getNextBundleId();
362
- if (bundleId.equals(currentBundleId) && bundleId.equals(nextBundleId)) {
409
+ if (bundleId.equals(nextBundleId)) {
363
410
  setNextCapacitorServerPathToDefaultWebAssetDir();
364
411
  }
365
412
  }
@@ -382,124 +429,125 @@ public class LiveUpdate {
382
429
  }
383
430
  }
384
431
 
385
- private void downloadBundle(
386
- @NonNull String bundleId,
387
- @Nullable String expectedChecksum,
388
- @Nullable String signature,
389
- @NonNull String url,
390
- @NonNull EmptyCallback callback
391
- ) {
392
- downloadFile(
393
- url,
394
- new NonEmptyCallback<File>() {
395
- @Override
396
- public void error(Exception exception) {
397
- // Throw error if the exception is a timeout
398
- if (exception instanceof java.net.SocketTimeoutException) {
399
- callback.error(new Exception(LiveUpdatePlugin.ERROR_HTTP_TIMEOUT));
400
- return;
401
- }
402
- callback.error(exception);
403
- }
432
+ private void downloadAndVerifyFile(@NonNull String url, @NonNull File file, @Nullable String checksum) throws Exception {
433
+ Response response = httpClient.execute(url);
434
+ ResponseBody responseBody = response.body();
435
+ if (response.isSuccessful()) {
436
+ LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file);
437
+ // Verify the file
438
+ checksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
439
+ String signature = LiveUpdateHttpClient.getSignatureFromResponse(response);
440
+ verifyFile(file, checksum, signature);
441
+ } else {
442
+ Exception exception = new Exception(responseBody.string());
443
+ Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
444
+ throw new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
445
+ }
446
+ }
404
447
 
405
- @Override
406
- public void success(@NonNull File result) {
407
- try {
408
- // Verify the signature
409
- String publicKey = config.getPublicKey();
410
- if (publicKey != null) {
411
- if (signature == null) {
412
- throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_MISSING);
413
- }
414
- PublicKey key = createPublicKeyFromString(publicKey);
415
- // Verify the signature
416
- boolean verified = verifySignatureForFile(result, signature, key);
417
- if (!verified) {
418
- throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
419
- }
420
- }
421
- // Verify the checksum
422
- else if (expectedChecksum != null) {
423
- // Calculate the checksum
424
- String receivedChecksum = getChecksumForFileAsString(result);
425
- if (!expectedChecksum.equals(receivedChecksum)) {
426
- throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_MISMATCH);
427
- }
428
- }
429
-
430
- // Add the bundle
431
- addBundle(bundleId, result);
432
-
433
- // Delete the temporary file
434
- result.delete();
435
-
436
- callback.success();
437
- } catch (Exception exception) {
438
- callback.error(exception);
448
+ private File downloadBundleFile(@NonNull String baseUrl, @NonNull String href, @NonNull File destinationDirectory) throws Exception {
449
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
450
+ urlBuilder.addQueryParameter("href", href);
451
+ String url = urlBuilder.build().toString();
452
+
453
+ File destinationFile = new File(destinationDirectory, href);
454
+ destinationFile.getParentFile().mkdirs();
455
+ downloadAndVerifyFile(url, destinationFile, null);
456
+ return destinationFile;
457
+ }
458
+
459
+ private void downloadBundleFiles(@NonNull String baseUrl, @NonNull List<String> hrefs, @NonNull File destinationDirectory)
460
+ throws Exception {
461
+ // Limit the number of threads to twice the number of processors
462
+ int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
463
+ List<Thread> threads = new ArrayList<>();
464
+ AtomicReference<Exception> exceptionReference = new AtomicReference<>();
465
+ for (String href : hrefs) {
466
+ // Wait until a thread is available
467
+ while (threads.size() >= maxThreads) {
468
+ for (Thread thread : threads) {
469
+ if (!thread.isAlive()) {
470
+ threads.remove(thread);
471
+ break;
439
472
  }
440
473
  }
474
+ // Wait a bit before checking again
475
+ Thread.sleep(10);
441
476
  }
442
- );
443
- }
444
-
445
- private void downloadFile(@NonNull String url, @NonNull NonEmptyCallback<File> callback) {
446
- int httpTimeout = config.getHttpTimeout();
447
- OkHttpClient okHttpClient = new OkHttpClient.Builder()
448
- .connectTimeout(httpTimeout, TimeUnit.MILLISECONDS)
449
- .readTimeout(httpTimeout, TimeUnit.MILLISECONDS)
450
- .writeTimeout(httpTimeout, TimeUnit.MILLISECONDS)
451
- .build();
452
- Request request = new Request.Builder().url(url).build();
453
- okHttpClient
454
- .newCall(request)
455
- .enqueue(
456
- new Callback() {
457
- @Override
458
- public void onFailure(@NonNull Call call, @NonNull IOException exception) {
459
- callback.error(exception);
460
- }
461
-
462
- @Override
463
- public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
464
- ResponseBody responseBody = response.body();
465
- if (response.isSuccessful()) {
466
- File destinationFile = buildTemporaryZipFile();
467
-
468
- long contentLength = responseBody.contentLength();
469
- BufferedSource source = responseBody.source();
470
-
471
- BufferedSink sink = Okio.buffer(Okio.sink(destinationFile));
472
- Buffer sinkBuffer = sink.getBuffer();
473
-
474
- long totalBytesRead = 0;
475
- int bufferSize = 8 * 1024;
476
- for (long bytesRead; (bytesRead = source.read(sinkBuffer, bufferSize)) != -1;) {
477
- sink.emit();
478
- totalBytesRead += bytesRead;
479
- int progress = (int) ((totalBytesRead * 100) / contentLength);
480
- }
481
- sink.flush();
482
- sink.close();
483
- source.close();
484
- callback.success(destinationFile);
485
- } else {
486
- Exception exception = new Exception(responseBody.string());
487
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
488
- callback.error(new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED));
489
- }
490
- }
477
+ // Stop if an exception occurred
478
+ if (exceptionReference.get() != null) {
479
+ break;
480
+ }
481
+ // Start a new thread
482
+ Thread thread = new Thread(() -> {
483
+ try {
484
+ downloadBundleFile(baseUrl, href, destinationDirectory);
485
+ } catch (Exception exception) {
486
+ exceptionReference.set(exception);
487
+ Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + href, exception);
491
488
  }
492
- );
489
+ });
490
+ threads.add(thread);
491
+ thread.start();
492
+ }
493
+ // Wait for all threads to finish
494
+ for (Thread thread : threads) {
495
+ thread.join();
496
+ }
497
+ // Throw the exception if one occurred
498
+ Exception exception = exceptionReference.get();
499
+ if (exception != null) {
500
+ throw exception;
501
+ }
493
502
  }
494
503
 
495
- @Nullable
496
- private void fetchLatestBundle(@NonNull NonEmptyCallback<GetLatestBundleResponse> callback)
497
- throws PackageManager.NameNotFoundException {
498
- String host = "api.cloud.capawesome.io";
499
- if (config.getLocation() != null && config.getLocation().equals("eu")) {
500
- host = "api.cloud.capawesome.eu";
504
+ private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
505
+ // Create a temporary directory
506
+ File temporaryDirectory = createTemporaryDirectory();
507
+ // Download the latest manifest
508
+ File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory);
509
+ Manifest latestManifest = loadManifest(latestManifestFile);
510
+ // Load the current manifest
511
+ Manifest currentManifest = loadCurrentManifest();
512
+ // Compare the manifests
513
+ List<ManifestItem> itemsToCopy = new ArrayList<>();
514
+ List<ManifestItem> itemsToDownload = new ArrayList<>();
515
+ if (currentManifest == null) {
516
+ itemsToDownload.addAll(latestManifest.getItems());
517
+ } else {
518
+ itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
519
+ itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
501
520
  }
502
- HttpUrl url = new HttpUrl.Builder()
521
+ // Copy the files
522
+ List<String> hrefsToCopy = new ArrayList<>();
523
+ for (ManifestItem item : itemsToCopy) {
524
+ hrefsToCopy.add(item.getHref());
525
+ }
526
+ copyCurrentBundleFiles(hrefsToCopy, temporaryDirectory);
527
+ // Download the files
528
+ List<String> hrefsToDownload = new ArrayList<>();
529
+ for (ManifestItem item : itemsToDownload) {
530
+ hrefsToDownload.add(item.getHref());
531
+ }
532
+ downloadBundleFiles(downloadUrl, hrefsToDownload, temporaryDirectory);
533
+ // Add the bundle
534
+ addBundleOfTypeManifest(bundleId, temporaryDirectory);
535
+ }
536
+
537
+ private void downloadBundleOfTypeZip(@NonNull String bundleId, @NonNull String downloadUrl, @Nullable String checksum)
538
+ throws Exception {
539
+ File file = buildTemporaryZipFile();
540
+ // Download the bundle
541
+ downloadAndVerifyFile(downloadUrl, file, checksum);
542
+ // Add the bundle
543
+ addBundleOfTypeZip(bundleId, file);
544
+ // Delete the temporary file
545
+ file.delete();
546
+ }
547
+
548
+ @Nullable
549
+ private GetLatestBundleResponse fetchLatestBundle() throws Exception {
550
+ String url = new HttpUrl.Builder()
503
551
  .scheme("https")
504
552
  .host(host)
505
553
  .addPathSegment("v1")
@@ -516,47 +564,18 @@ public class LiveUpdate {
516
564
  .addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
517
565
  .addQueryParameter("platform", "0")
518
566
  .addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
519
- .build();
520
- Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle from " + url);
521
- int httpTimeout = config.getHttpTimeout();
522
- OkHttpClient client = new OkHttpClient()
523
- .newBuilder()
524
- .connectTimeout(httpTimeout, TimeUnit.MILLISECONDS)
525
- .readTimeout(httpTimeout, TimeUnit.MILLISECONDS)
526
- .writeTimeout(httpTimeout, TimeUnit.MILLISECONDS)
527
- .build();
528
- okhttp3.Request request = new okhttp3.Request.Builder().url(url).build();
529
- client
530
- .newCall(request)
531
- .enqueue(
532
- new okhttp3.Callback() {
533
- @Override
534
- public void onFailure(@NonNull okhttp3.Call call, @NonNull IOException exception) {
535
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
536
- callback.error(exception);
537
- }
538
-
539
- @Override
540
- public void onResponse(@NonNull okhttp3.Call call, @NonNull okhttp3.Response response) throws IOException {
541
- ResponseBody responseBody = response.body();
542
- if (response.isSuccessful()) {
543
- String responseBodyString = responseBody.string();
544
- JSONObject responseJson = null;
545
- try {
546
- responseJson = new JSONObject(responseBodyString);
547
- } catch (JSONException exception) {
548
- callback.error(exception);
549
- }
550
- GetLatestBundleResponse getLatestBundleResponse = new GetLatestBundleResponse(responseJson);
551
- callback.success(getLatestBundleResponse);
552
- } else {
553
- Exception exception = new Exception(responseBody.string());
554
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
555
- callback.error(exception);
556
- }
557
- }
558
- }
559
- );
567
+ .build()
568
+ .toString();
569
+ Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
570
+ Response response = httpClient.execute(url);
571
+ String responseBodyString = response.body().string();
572
+ Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
573
+ if (response.isSuccessful()) {
574
+ JSONObject responseJson = new JSONObject(responseBodyString);
575
+ return new GetLatestBundleResponse(responseJson);
576
+ } else {
577
+ return null;
578
+ }
560
579
  }
561
580
 
562
581
  private String[] getBundleIds() {
@@ -574,6 +593,31 @@ public class LiveUpdate {
574
593
  return bundleIds;
575
594
  }
576
595
 
596
+ private byte[] getChecksumForFileAsBytes(@NonNull File file) throws Exception {
597
+ try {
598
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
599
+ BufferedSource source = Okio.buffer(Okio.source(file));
600
+ Buffer buffer = new Buffer();
601
+ for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
602
+ digest.update(buffer.readByteArray());
603
+ }
604
+ source.close();
605
+ return digest.digest();
606
+ } catch (IOException exception) {
607
+ Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
608
+ throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
609
+ }
610
+ }
611
+
612
+ private String getChecksumForFileAsString(@NonNull File file) throws Exception {
613
+ byte[] checksumBytes = getChecksumForFileAsBytes(file);
614
+ StringBuilder checksum = new StringBuilder();
615
+ for (byte checksumByte : checksumBytes) {
616
+ checksum.append(Integer.toString((checksumByte & 0xff) + 0x100, 16).substring(1));
617
+ }
618
+ return checksum.toString();
619
+ }
620
+
577
621
  @Nullable
578
622
  private String getChannel() {
579
623
  String channel = null;
@@ -650,6 +694,41 @@ public class LiveUpdate {
650
694
  return bundleDirectory.exists();
651
695
  }
652
696
 
697
+ @Nullable
698
+ private Manifest loadCurrentManifest() throws Exception {
699
+ String currentBundleId = getCurrentBundleId();
700
+ if (currentBundleId.equals(defaultWebAssetDir)) {
701
+ AssetManager assets = plugin.getContext().getAssets();
702
+ Boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
703
+ if (manifestFileExists) {
704
+ InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
705
+ BufferedSource source = Okio.buffer(Okio.source(inputStream));
706
+ return loadManifest(source);
707
+ } else {
708
+ return null;
709
+ }
710
+ } else {
711
+ File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
712
+ File manifestFile = new File(currentBundleDirectory, manifestFileName);
713
+ if (manifestFile.exists()) {
714
+ return loadManifest(manifestFile);
715
+ } else {
716
+ return null;
717
+ }
718
+ }
719
+ }
720
+
721
+ private Manifest loadManifest(@NonNull BufferedSource source) throws Exception {
722
+ String jsonAsString = source.readUtf8();
723
+ JSONArray jsonArray = new JSONArray(jsonAsString);
724
+ return new Manifest(jsonArray);
725
+ }
726
+
727
+ private Manifest loadManifest(@NonNull File file) throws Exception {
728
+ BufferedSource source = Okio.buffer(Okio.source(file));
729
+ return loadManifest(source);
730
+ }
731
+
653
732
  private boolean isBundleInUse(@NonNull String bundleId) {
654
733
  String currentBundleId = getCurrentBundleId();
655
734
  String nextBundleId = getNextBundleId();
@@ -739,6 +818,39 @@ public class LiveUpdate {
739
818
  return destination;
740
819
  }
741
820
 
821
+ private boolean verifyChecksumForFile(@NonNull File file, @NonNull String checksum) throws Exception {
822
+ String receivedChecksum = getChecksumForFileAsString(file);
823
+ return checksum.equals(receivedChecksum);
824
+ }
825
+
826
+ private void verifyFile(@NonNull File file, @Nullable String checksum, @Nullable String signature) throws Exception {
827
+ String publicKey = config.getPublicKey();
828
+ if (publicKey != null) {
829
+ // Verify the signature
830
+ if (signature == null) {
831
+ throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_MISSING);
832
+ }
833
+ // Verify the signature
834
+ boolean verified = verifySignatureForFile(file, signature, publicKey);
835
+ if (!verified) {
836
+ throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
837
+ }
838
+ }
839
+ // Verify the checksum
840
+ else if (checksum != null) {
841
+ // Calculate the checksum
842
+ boolean verified = verifyChecksumForFile(file, checksum);
843
+ if (!verified) {
844
+ throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_MISMATCH);
845
+ }
846
+ }
847
+ }
848
+
849
+ private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull String keyAsString) throws Exception {
850
+ PublicKey key = createPublicKeyFromString(keyAsString);
851
+ return verifySignatureForFile(file, signature, key);
852
+ }
853
+
742
854
  private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull PublicKey key) throws Exception {
743
855
  try {
744
856
  byte[] signatureBytes = Base64.decode(signature, Base64.DEFAULT);