@capawesome/capacitor-live-update 6.4.2 → 6.5.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.
@@ -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,6 +12,8 @@ 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;
@@ -25,32 +28,33 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdR
25
28
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
26
29
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
27
30
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
31
+ import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
28
32
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
29
33
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
30
34
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
31
35
  import java.io.File;
36
+ import java.io.FileInputStream;
37
+ import java.io.FileOutputStream;
32
38
  import java.io.IOException;
39
+ import java.io.InputStream;
33
40
  import java.security.KeyFactory;
34
41
  import java.security.MessageDigest;
35
42
  import java.security.PublicKey;
36
43
  import java.security.Signature;
37
44
  import java.security.spec.X509EncodedKeySpec;
45
+ import java.util.ArrayList;
38
46
  import java.util.Arrays;
47
+ import java.util.List;
39
48
  import java.util.UUID;
40
- import java.util.concurrent.TimeUnit;
49
+ import java.util.concurrent.atomic.AtomicReference;
41
50
  import net.lingala.zip4j.ZipFile;
42
- import okhttp3.Call;
43
- import okhttp3.Callback;
44
51
  import okhttp3.HttpUrl;
45
- import okhttp3.OkHttpClient;
46
- import okhttp3.Request;
47
52
  import okhttp3.Response;
48
53
  import okhttp3.ResponseBody;
49
54
  import okio.Buffer;
50
- import okio.BufferedSink;
51
55
  import okio.BufferedSource;
52
56
  import okio.Okio;
53
- import org.json.JSONException;
57
+ import org.json.JSONArray;
54
58
  import org.json.JSONObject;
55
59
 
56
60
  public class LiveUpdate {
@@ -60,6 +64,12 @@ public class LiveUpdate {
60
64
 
61
65
  private final String defaultWebAssetDir;
62
66
 
67
+ @NonNull
68
+ private final String host;
69
+
70
+ @NonNull
71
+ private final LiveUpdateHttpClient httpClient;
72
+
63
73
  @NonNull
64
74
  private final LiveUpdatePlugin plugin;
65
75
 
@@ -69,12 +79,19 @@ public class LiveUpdate {
69
79
  @NonNull
70
80
  private final SharedPreferences.Editor webViewSettingsEditor;
71
81
 
72
- private final String bundlesDirectory = "_capacitor_live_update_bundles";
82
+ private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
73
83
  private final Handler rollbackHandler = new Handler();
84
+ private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
74
85
 
75
86
  public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
76
87
  this.config = config;
77
88
  this.defaultWebAssetDir = plugin.getBridge().DEFAULT_WEB_ASSET_DIR;
89
+ if (config.getLocation() != null && config.getLocation().equals("eu")) {
90
+ this.host = "api.cloud.capawesome.eu";
91
+ } else {
92
+ this.host = "api.cloud.capawesome.io";
93
+ }
94
+ this.httpClient = new LiveUpdateHttpClient(config);
78
95
  this.plugin = plugin;
79
96
  this.preferences = new LiveUpdatePreferences(plugin.getContext());
80
97
  this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
@@ -115,7 +132,8 @@ public class LiveUpdate {
115
132
  }
116
133
 
117
134
  // Download the bundle
118
- downloadBundle(bundleId, checksum, null, url, callback);
135
+ downloadBundleOfTypeZip(bundleId, url, checksum);
136
+ callback.success();
119
137
  }
120
138
 
121
139
  public void getBundle(@NonNull NonEmptyCallback callback) {
@@ -165,7 +183,9 @@ public class LiveUpdate {
165
183
 
166
184
  public void ready() {
167
185
  Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
186
+ // Stop the rollback timer
168
187
  stopRollbackTimer();
188
+ // Delete unused bundles
169
189
  if (config.getAutoDeleteBundles()) {
170
190
  deleteUnusedBundles();
171
191
  }
@@ -210,73 +230,44 @@ public class LiveUpdate {
210
230
 
211
231
  public void sync(@NonNull NonEmptyCallback<Result> callback) throws Exception {
212
232
  // 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
- }
233
+ GetLatestBundleResponse response = fetchLatestBundle();
234
+ if (response == null) {
235
+ Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
236
+ SyncResult syncResult = new SyncResult(null);
237
+ callback.success(syncResult);
238
+ return;
239
+ }
240
+ ArtifactType artifactType = response.getArtifactType();
241
+ String latestBundleId = response.getBundleId();
242
+ String url = response.getUrl();
243
+ // Check if the bundle already exists
244
+ if (hasBundle(latestBundleId)) {
245
+ String nextBundleId = null;
246
+ String currentBundleId = getCurrentBundleId();
247
+ if (!latestBundleId.equals(currentBundleId)) {
248
+ // Set the next bundle
249
+ setNextBundle(latestBundleId);
250
+ nextBundleId = latestBundleId;
270
251
  }
271
- );
252
+ SyncResult syncResult = new SyncResult(nextBundleId);
253
+ callback.success(syncResult);
254
+ return;
255
+ }
256
+ // Download the bundle
257
+ if (artifactType == ArtifactType.MANIFEST) {
258
+ downloadBundleOfTypeManifest(latestBundleId, url);
259
+ } else {
260
+ downloadBundleOfTypeZip(latestBundleId, url, null);
261
+ }
262
+ // Set the next bundle
263
+ setNextBundle(latestBundleId);
264
+ SyncResult syncResult = new SyncResult(latestBundleId);
265
+ callback.success(syncResult);
272
266
  }
273
267
 
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
-
268
+ private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
278
269
  // Search folder with index.html file
279
- File indexHtmlFile = searchIndexHtmlFile(unzippedDirectory);
270
+ File indexHtmlFile = searchIndexHtmlFile(sourceDirectory);
280
271
  if (indexHtmlFile == null) {
281
272
  throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_INDEX_HTML_MISSING);
282
273
  }
@@ -289,6 +280,25 @@ public class LiveUpdate {
289
280
  indexHtmlFile.getParentFile().renameTo(bundleDirectory);
290
281
  }
291
282
 
283
+ private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
284
+ addBundle(bundleId, directory);
285
+ }
286
+
287
+ private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
288
+ // Unzip the file to the bundle directory
289
+ File unzippedDirectory = unzipFile(zipFile);
290
+ // Add the bundle
291
+ addBundle(bundleId, unzippedDirectory);
292
+ }
293
+
294
+ private File buildBundlesDirectory() {
295
+ return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
296
+ }
297
+
298
+ private File buildBundleDirectoryFor(@NonNull String bundleId) {
299
+ return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
300
+ }
301
+
292
302
  private File buildTemporaryDirectory() {
293
303
  String fileName = UUID.randomUUID().toString();
294
304
  return new File(plugin.getContext().getCacheDir(), fileName);
@@ -299,43 +309,63 @@ public class LiveUpdate {
299
309
  return new File(plugin.getContext().getCacheDir(), fileName);
300
310
  }
301
311
 
302
- private File buildBundlesDirectory() {
303
- return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
312
+ private void copyCurrentBundleFile(@NonNull String href, @NonNull File destinationDirectory) throws IOException {
313
+ String currentBundleId = getCurrentBundleId();
314
+ if (currentBundleId.equals(defaultWebAssetDir)) {
315
+ // Create the source input stream
316
+ AssetManager assets = plugin.getContext().getAssets();
317
+ InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
318
+ // Create the destination file
319
+ File destination = new File(destinationDirectory, href);
320
+ // Create all destination directories if they do not exist
321
+ destination.getParentFile().mkdirs();
322
+ // Copy the file
323
+ copyFile(inputStream, destination);
324
+ } else {
325
+ File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
326
+ // Create the source and destination files
327
+ File source = new File(currentBundleDirectory, href);
328
+ File destination = new File(destinationDirectory, href);
329
+ // Create all destination directories if they do not exist
330
+ destination.getParentFile().mkdirs();
331
+ // Copy the file
332
+ copyFile(source, destination);
333
+ }
304
334
  }
305
335
 
306
- private File buildBundleDirectoryFor(@NonNull String bundle) {
307
- return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundle);
336
+ private void copyCurrentBundleFiles(@NonNull List<String> hrefs, @NonNull File destinationDirectory) throws IOException {
337
+ for (String href : hrefs) {
338
+ copyCurrentBundleFile(href, destinationDirectory);
339
+ }
308
340
  }
309
341
 
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);
342
+ private void copyFile(File input, File output) throws IOException {
343
+ FileInputStream in = new FileInputStream(input);
344
+ FileOutputStream out = new FileOutputStream(output);
345
+ byte[] buffer = new byte[1024];
346
+ int length;
347
+ while ((length = in.read(buffer)) > 0) {
348
+ out.write(buffer, 0, length);
323
349
  }
350
+ out.close();
351
+ in.close();
324
352
  }
325
353
 
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));
354
+ private void copyFile(InputStream input, File output) throws IOException {
355
+ FileOutputStream out = new FileOutputStream(output);
356
+ byte[] buffer = new byte[1024];
357
+ int length;
358
+ while ((length = input.read(buffer)) > 0) {
359
+ out.write(buffer, 0, length);
331
360
  }
332
- return checksum.toString();
361
+ out.close();
362
+ input.close();
333
363
  }
334
364
 
335
365
  private void createBundlesDirectory() {
336
- File bundlesDirectory = buildBundlesDirectory();
337
- if (!bundlesDirectory.exists()) {
338
- bundlesDirectory.mkdir();
366
+ File file = buildBundlesDirectory();
367
+ if (!file.exists()) {
368
+ file.mkdir();
339
369
  }
340
370
  }
341
371
 
@@ -352,14 +382,19 @@ public class LiveUpdate {
352
382
  }
353
383
  }
354
384
 
385
+ private File createTemporaryDirectory() {
386
+ File file = buildTemporaryDirectory();
387
+ file.mkdir();
388
+ return file;
389
+ }
390
+
355
391
  private void deleteBundle(@NonNull String bundleId) {
356
392
  // Delete the bundle directory
357
393
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
358
394
  deleteFileRecursively(bundleDirectory);
359
395
  // Reset the next bundle if it is the deleted bundle
360
- String currentBundleId = getCurrentBundleId();
361
396
  String nextBundleId = getNextBundleId();
362
- if (bundleId.equals(currentBundleId) && bundleId.equals(nextBundleId)) {
397
+ if (bundleId.equals(nextBundleId)) {
363
398
  setNextCapacitorServerPathToDefaultWebAssetDir();
364
399
  }
365
400
  }
@@ -382,124 +417,125 @@ public class LiveUpdate {
382
417
  }
383
418
  }
384
419
 
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
- }
420
+ private void downloadAndVerifyFile(@NonNull String url, @NonNull File file, @Nullable String checksum) throws Exception {
421
+ Response response = httpClient.execute(url);
422
+ ResponseBody responseBody = response.body();
423
+ if (response.isSuccessful()) {
424
+ LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file);
425
+ // Verify the file
426
+ checksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
427
+ String signature = LiveUpdateHttpClient.getSignatureFromResponse(response);
428
+ verifyFile(file, checksum, signature);
429
+ } else {
430
+ Exception exception = new Exception(responseBody.string());
431
+ Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
432
+ throw new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
433
+ }
434
+ }
404
435
 
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);
436
+ private File downloadBundleFile(@NonNull String baseUrl, @NonNull String href, @NonNull File destinationDirectory) throws Exception {
437
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
438
+ urlBuilder.addQueryParameter("href", href);
439
+ String url = urlBuilder.build().toString();
440
+
441
+ File destinationFile = new File(destinationDirectory, href);
442
+ destinationFile.getParentFile().mkdirs();
443
+ downloadAndVerifyFile(url, destinationFile, null);
444
+ return destinationFile;
445
+ }
446
+
447
+ private void downloadBundleFiles(@NonNull String baseUrl, @NonNull List<String> hrefs, @NonNull File destinationDirectory)
448
+ throws Exception {
449
+ // Limit the number of threads to twice the number of processors
450
+ int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
451
+ List<Thread> threads = new ArrayList<>();
452
+ AtomicReference<Exception> exceptionReference = new AtomicReference<>();
453
+ for (String href : hrefs) {
454
+ // Wait until a thread is available
455
+ while (threads.size() >= maxThreads) {
456
+ for (Thread thread : threads) {
457
+ if (!thread.isAlive()) {
458
+ threads.remove(thread);
459
+ break;
439
460
  }
440
461
  }
462
+ // Wait a bit before checking again
463
+ Thread.sleep(10);
441
464
  }
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
- }
465
+ // Stop if an exception occurred
466
+ if (exceptionReference.get() != null) {
467
+ break;
468
+ }
469
+ // Start a new thread
470
+ Thread thread = new Thread(() -> {
471
+ try {
472
+ downloadBundleFile(baseUrl, href, destinationDirectory);
473
+ } catch (Exception exception) {
474
+ exceptionReference.set(exception);
475
+ Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + href, exception);
491
476
  }
492
- );
477
+ });
478
+ threads.add(thread);
479
+ thread.start();
480
+ }
481
+ // Wait for all threads to finish
482
+ for (Thread thread : threads) {
483
+ thread.join();
484
+ }
485
+ // Throw the exception if one occurred
486
+ Exception exception = exceptionReference.get();
487
+ if (exception != null) {
488
+ throw exception;
489
+ }
493
490
  }
494
491
 
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";
492
+ private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
493
+ // Create a temporary directory
494
+ File temporaryDirectory = createTemporaryDirectory();
495
+ // Download the latest manifest
496
+ File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory);
497
+ Manifest latestManifest = loadManifest(latestManifestFile);
498
+ // Load the current manifest
499
+ Manifest currentManifest = loadCurrentManifest();
500
+ // Compare the manifests
501
+ List<ManifestItem> itemsToCopy = new ArrayList<>();
502
+ List<ManifestItem> itemsToDownload = new ArrayList<>();
503
+ if (currentManifest == null) {
504
+ itemsToDownload.addAll(latestManifest.getItems());
505
+ } else {
506
+ itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
507
+ itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
508
+ }
509
+ // Copy the files
510
+ List<String> hrefsToCopy = new ArrayList<>();
511
+ for (ManifestItem item : itemsToCopy) {
512
+ hrefsToCopy.add(item.getHref());
501
513
  }
502
- HttpUrl url = new HttpUrl.Builder()
514
+ copyCurrentBundleFiles(hrefsToCopy, temporaryDirectory);
515
+ // Download the files
516
+ List<String> hrefsToDownload = new ArrayList<>();
517
+ for (ManifestItem item : itemsToDownload) {
518
+ hrefsToDownload.add(item.getHref());
519
+ }
520
+ downloadBundleFiles(downloadUrl, hrefsToDownload, temporaryDirectory);
521
+ // Add the bundle
522
+ addBundleOfTypeManifest(bundleId, temporaryDirectory);
523
+ }
524
+
525
+ private void downloadBundleOfTypeZip(@NonNull String bundleId, @NonNull String downloadUrl, @Nullable String checksum)
526
+ throws Exception {
527
+ File file = buildTemporaryZipFile();
528
+ // Download the bundle
529
+ downloadAndVerifyFile(downloadUrl, file, checksum);
530
+ // Add the bundle
531
+ addBundleOfTypeZip(bundleId, file);
532
+ // Delete the temporary file
533
+ file.delete();
534
+ }
535
+
536
+ @Nullable
537
+ private GetLatestBundleResponse fetchLatestBundle() throws Exception {
538
+ String url = new HttpUrl.Builder()
503
539
  .scheme("https")
504
540
  .host(host)
505
541
  .addPathSegment("v1")
@@ -516,47 +552,18 @@ public class LiveUpdate {
516
552
  .addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
517
553
  .addQueryParameter("platform", "0")
518
554
  .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
- );
555
+ .build()
556
+ .toString();
557
+ Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
558
+ Response response = httpClient.execute(url);
559
+ String responseBodyString = response.body().string();
560
+ Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
561
+ if (response.isSuccessful()) {
562
+ JSONObject responseJson = new JSONObject(responseBodyString);
563
+ return new GetLatestBundleResponse(responseJson);
564
+ } else {
565
+ return null;
566
+ }
560
567
  }
561
568
 
562
569
  private String[] getBundleIds() {
@@ -574,6 +581,31 @@ public class LiveUpdate {
574
581
  return bundleIds;
575
582
  }
576
583
 
584
+ private byte[] getChecksumForFileAsBytes(@NonNull File file) throws Exception {
585
+ try {
586
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
587
+ BufferedSource source = Okio.buffer(Okio.source(file));
588
+ Buffer buffer = new Buffer();
589
+ for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
590
+ digest.update(buffer.readByteArray());
591
+ }
592
+ source.close();
593
+ return digest.digest();
594
+ } catch (IOException exception) {
595
+ Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
596
+ throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
597
+ }
598
+ }
599
+
600
+ private String getChecksumForFileAsString(@NonNull File file) throws Exception {
601
+ byte[] checksumBytes = getChecksumForFileAsBytes(file);
602
+ StringBuilder checksum = new StringBuilder();
603
+ for (byte checksumByte : checksumBytes) {
604
+ checksum.append(Integer.toString((checksumByte & 0xff) + 0x100, 16).substring(1));
605
+ }
606
+ return checksum.toString();
607
+ }
608
+
577
609
  @Nullable
578
610
  private String getChannel() {
579
611
  String channel = null;
@@ -650,6 +682,41 @@ public class LiveUpdate {
650
682
  return bundleDirectory.exists();
651
683
  }
652
684
 
685
+ @Nullable
686
+ private Manifest loadCurrentManifest() throws Exception {
687
+ String currentBundleId = getCurrentBundleId();
688
+ if (currentBundleId.equals(defaultWebAssetDir)) {
689
+ AssetManager assets = plugin.getContext().getAssets();
690
+ Boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
691
+ if (manifestFileExists) {
692
+ InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
693
+ BufferedSource source = Okio.buffer(Okio.source(inputStream));
694
+ return loadManifest(source);
695
+ } else {
696
+ return null;
697
+ }
698
+ } else {
699
+ File currentBundleDirectory = buildBundleDirectoryFor(currentBundleId);
700
+ File manifestFile = new File(currentBundleDirectory, manifestFileName);
701
+ if (manifestFile.exists()) {
702
+ return loadManifest(manifestFile);
703
+ } else {
704
+ return null;
705
+ }
706
+ }
707
+ }
708
+
709
+ private Manifest loadManifest(@NonNull BufferedSource source) throws Exception {
710
+ String jsonAsString = source.readUtf8();
711
+ JSONArray jsonArray = new JSONArray(jsonAsString);
712
+ return new Manifest(jsonArray);
713
+ }
714
+
715
+ private Manifest loadManifest(@NonNull File file) throws Exception {
716
+ BufferedSource source = Okio.buffer(Okio.source(file));
717
+ return loadManifest(source);
718
+ }
719
+
653
720
  private boolean isBundleInUse(@NonNull String bundleId) {
654
721
  String currentBundleId = getCurrentBundleId();
655
722
  String nextBundleId = getNextBundleId();
@@ -739,6 +806,39 @@ public class LiveUpdate {
739
806
  return destination;
740
807
  }
741
808
 
809
+ private boolean verifyChecksumForFile(@NonNull File file, @NonNull String checksum) throws Exception {
810
+ String receivedChecksum = getChecksumForFileAsString(file);
811
+ return checksum.equals(receivedChecksum);
812
+ }
813
+
814
+ private void verifyFile(@NonNull File file, @Nullable String checksum, @Nullable String signature) throws Exception {
815
+ String publicKey = config.getPublicKey();
816
+ if (publicKey != null) {
817
+ // Verify the signature
818
+ if (signature == null) {
819
+ throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_MISSING);
820
+ }
821
+ // Verify the signature
822
+ boolean verified = verifySignatureForFile(file, signature, publicKey);
823
+ if (!verified) {
824
+ throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
825
+ }
826
+ }
827
+ // Verify the checksum
828
+ else if (checksum != null) {
829
+ // Calculate the checksum
830
+ boolean verified = verifyChecksumForFile(file, checksum);
831
+ if (!verified) {
832
+ throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_MISMATCH);
833
+ }
834
+ }
835
+ }
836
+
837
+ private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull String keyAsString) throws Exception {
838
+ PublicKey key = createPublicKeyFromString(keyAsString);
839
+ return verifySignatureForFile(file, signature, key);
840
+ }
841
+
742
842
  private boolean verifySignatureForFile(@NonNull File file, @NonNull String signature, @NonNull PublicKey key) throws Exception {
743
843
  try {
744
844
  byte[] signatureBytes = Base64.decode(signature, Base64.DEFAULT);