@capawesome/capacitor-live-update 7.5.0 → 7.5.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.
@@ -7,9 +7,11 @@ import android.content.pm.PackageManager;
7
7
  import android.content.res.AssetManager;
8
8
  import android.os.Build;
9
9
  import android.os.Handler;
10
+ import android.os.Looper;
10
11
  import android.util.Base64;
11
12
  import androidx.annotation.NonNull;
12
13
  import androidx.annotation.Nullable;
14
+ import androidx.core.content.pm.PackageInfoCompat;
13
15
  import com.getcapacitor.Bridge;
14
16
  import com.getcapacitor.Logger;
15
17
  import com.getcapacitor.plugin.WebView;
@@ -102,7 +104,8 @@ public class LiveUpdate {
102
104
  private final SharedPreferences.Editor webViewSettingsEditor;
103
105
 
104
106
  private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
105
- private final Handler rollbackHandler = new Handler();
107
+ private final String downloadsDirectory = "capawesome_capacitor_live_update_downloads";
108
+ private final Handler rollbackHandler = new Handler(Looper.getMainLooper());
106
109
  private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
107
110
 
108
111
  private boolean initialPageLoaded = false;
@@ -123,6 +126,9 @@ public class LiveUpdate {
123
126
  // Set the device ID on the HTTP client (after any potential config reset)
124
127
  this.httpClient.setDeviceId(getDeviceId());
125
128
 
129
+ // Delete the leftovers of downloads that were interrupted by a process kill
130
+ tryDeleteDownloadsDirectory();
131
+
126
132
  // Start the rollback timer to rollback to the default bundle
127
133
  // if the app is not ready after a certain time
128
134
  startRollbackTimer();
@@ -530,7 +536,10 @@ public class LiveUpdate {
530
536
 
531
537
  // Move the bundle directory to the bundles directory
532
538
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
533
- indexHtmlFile.getParentFile().renameTo(bundleDirectory);
539
+ boolean moved = indexHtmlFile.getParentFile().renameTo(bundleDirectory);
540
+ if (!moved) {
541
+ throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_MOVE_FAILED);
542
+ }
534
543
  }
535
544
 
536
545
  private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
@@ -539,7 +548,7 @@ public class LiveUpdate {
539
548
 
540
549
  private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
541
550
  // Unzip the file to the bundle directory
542
- File unzippedDirectory = unzipFile(zipFile);
551
+ File unzippedDirectory = unzipDownloadedBundle(zipFile);
543
552
  // Add the bundle
544
553
  addBundle(bundleId, unzippedDirectory);
545
554
  }
@@ -552,14 +561,8 @@ public class LiveUpdate {
552
561
  return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
553
562
  }
554
563
 
555
- private File buildTemporaryDirectory() {
556
- String fileName = UUID.randomUUID().toString();
557
- return new File(plugin.getContext().getCacheDir(), fileName);
558
- }
559
-
560
- private File buildTemporaryZipFile() {
561
- String fileName = UUID.randomUUID().toString() + ".zip";
562
- return new File(plugin.getContext().getCacheDir(), fileName);
564
+ private File buildDownloadsDirectory() {
565
+ return new File(plugin.getContext().getCacheDir(), downloadsDirectory);
563
566
  }
564
567
 
565
568
  private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
@@ -646,10 +649,10 @@ public class LiveUpdate {
646
649
  }
647
650
  }
648
651
 
649
- private File createTemporaryDirectory() {
650
- File file = buildTemporaryDirectory();
651
- file.mkdir();
652
- return file;
652
+ private File createTemporaryDownloadDirectory() {
653
+ File temporaryDirectory = new File(buildDownloadsDirectory(), UUID.randomUUID().toString());
654
+ temporaryDirectory.mkdirs();
655
+ return temporaryDirectory;
653
656
  }
654
657
 
655
658
  private void deleteBundleById(@NonNull String bundleId) {
@@ -675,6 +678,23 @@ public class LiveUpdate {
675
678
  file.delete();
676
679
  }
677
680
 
681
+ @NonNull
682
+ private EmptyCallback deleteTemporaryDirectoryOnCompletion(@NonNull File temporaryDirectory, @NonNull EmptyCallback callback) {
683
+ return new EmptyCallback() {
684
+ @Override
685
+ public void success() {
686
+ deleteFileRecursively(temporaryDirectory);
687
+ callback.success();
688
+ }
689
+
690
+ @Override
691
+ public void error(@NonNull Exception exception) {
692
+ deleteFileRecursively(temporaryDirectory);
693
+ callback.error(exception);
694
+ }
695
+ };
696
+ }
697
+
678
698
  private void deleteUnusedBundles() {
679
699
  String[] bundleIds = getDownloadedBundleIds();
680
700
  for (String bundleId : bundleIds) {
@@ -845,10 +865,10 @@ public class LiveUpdate {
845
865
  @NonNull String downloadUrl,
846
866
  @NonNull EmptyCallback completionCallback
847
867
  ) {
868
+ // Create a temporary directory that is deleted once the download completed or failed
869
+ File temporaryDirectory = createTemporaryDownloadDirectory();
870
+ EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
848
871
  try {
849
- // Create a temporary directory
850
- File temporaryDirectory = createTemporaryDirectory();
851
-
852
872
  // Download the latest manifest
853
873
  downloadBundleFile(
854
874
  downloadUrl,
@@ -898,31 +918,31 @@ public class LiveUpdate {
898
918
  try {
899
919
  // Add the bundle
900
920
  addBundleOfTypeManifest(bundleId, temporaryDirectory);
901
- completionCallback.success();
921
+ callback.success();
902
922
  } catch (Exception e) {
903
- completionCallback.error(e);
923
+ callback.error(e);
904
924
  }
905
925
  }
906
926
 
907
927
  @Override
908
928
  public void error(@NonNull Exception exception) {
909
- completionCallback.error(exception);
929
+ callback.error(exception);
910
930
  }
911
931
  }
912
932
  );
913
933
  } catch (Exception e) {
914
- completionCallback.error(e);
934
+ callback.error(e);
915
935
  }
916
936
  }
917
937
 
918
938
  @Override
919
939
  public void error(@NonNull Exception exception) {
920
- completionCallback.error(exception);
940
+ callback.error(exception);
921
941
  }
922
942
  }
923
943
  );
924
944
  } catch (Exception e) {
925
- completionCallback.error(e);
945
+ callback.error(e);
926
946
  }
927
947
  }
928
948
 
@@ -933,39 +953,42 @@ public class LiveUpdate {
933
953
  @NonNull String downloadUrl,
934
954
  @NonNull EmptyCallback completionCallback
935
955
  ) {
936
- File file = buildTemporaryZipFile();
937
- // Download the bundle
938
- downloadAndVerifyFile(
939
- downloadUrl,
940
- file,
941
- checksum,
942
- signature,
943
- (downloadedBytes, totalBytes) -> {
944
- DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
945
- notifyDownloadBundleProgressListeners(event);
946
- },
947
- new EmptyCallback() {
948
- @Override
949
- public void success() {
950
- try {
951
- // Add the bundle
952
- addBundleOfTypeZip(bundleId, file);
953
- // Delete the temporary file
954
- file.delete();
955
- completionCallback.success();
956
- } catch (Exception e) {
957
- completionCallback.error(e);
956
+ // Create a temporary directory that is deleted once the download completed or failed
957
+ File temporaryDirectory = createTemporaryDownloadDirectory();
958
+ EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
959
+ File zipFile = new File(temporaryDirectory, "bundle.zip");
960
+ try {
961
+ // Download the bundle
962
+ downloadAndVerifyFile(
963
+ downloadUrl,
964
+ zipFile,
965
+ checksum,
966
+ signature,
967
+ (downloadedBytes, totalBytes) -> {
968
+ DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
969
+ notifyDownloadBundleProgressListeners(event);
970
+ },
971
+ new EmptyCallback() {
972
+ @Override
973
+ public void success() {
974
+ try {
975
+ // Add the bundle
976
+ addBundleOfTypeZip(bundleId, zipFile);
977
+ callback.success();
978
+ } catch (Exception e) {
979
+ callback.error(e);
980
+ }
958
981
  }
959
- }
960
982
 
961
- @Override
962
- public void error(@NonNull Exception exception) {
963
- // Delete the temporary file on error
964
- file.delete();
965
- completionCallback.error(exception);
983
+ @Override
984
+ public void error(@NonNull Exception exception) {
985
+ callback.error(exception);
986
+ }
966
987
  }
967
- }
968
- );
988
+ );
989
+ } catch (Exception e) {
990
+ callback.error(e);
991
+ }
969
992
  }
970
993
 
971
994
  private void fetchLatestBundleInternal(
@@ -1169,7 +1192,7 @@ public class LiveUpdate {
1169
1192
  }
1170
1193
 
1171
1194
  private int getVersionCodeAsInt() throws PackageManager.NameNotFoundException {
1172
- return getPackageInfo().versionCode;
1195
+ return (int) PackageInfoCompat.getLongVersionCode(getPackageInfo());
1173
1196
  }
1174
1197
 
1175
1198
  private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
@@ -1431,8 +1454,12 @@ public class LiveUpdate {
1431
1454
  }
1432
1455
  }
1433
1456
 
1434
- private File unzipFile(@NonNull File zipFile) throws IOException {
1435
- File destination = buildTemporaryDirectory();
1457
+ private void tryDeleteDownloadsDirectory() {
1458
+ deleteFileRecursively(buildDownloadsDirectory());
1459
+ }
1460
+
1461
+ private File unzipDownloadedBundle(@NonNull File zipFile) throws IOException {
1462
+ File destination = new File(zipFile.getParentFile(), "bundle");
1436
1463
  String destinationPath = destination.getPath();
1437
1464
  ZipFile zip = new ZipFile(zipFile);
1438
1465
  // Clear stored Unix permissions to prevent EACCES errors on newer Android versions
@@ -37,12 +37,13 @@ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
37
37
  public class LiveUpdatePlugin extends Plugin {
38
38
 
39
39
  public static final String TAG = "LiveUpdate";
40
- public static final String VERSION = "7.5.0";
40
+ public static final String VERSION = "7.5.2";
41
41
  public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
42
42
  public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
43
43
  public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
44
44
  public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
45
45
  public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
46
+ public static final String ERROR_BUNDLE_MOVE_FAILED = "Bundle could not be moved.";
46
47
  public static final String ERROR_BUNDLE_NOT_FOUND = "bundle not found.";
47
48
  public static final String ERROR_CHECKSUM_CALCULATION_FAILED = "Failed to calculate checksum.";
48
49
  public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
@@ -13,6 +13,7 @@ import CommonCrypto
13
13
  private let config: LiveUpdateConfig
14
14
  private let defaultWebAssetDir = "public" // DO NOT CHANGE! (See https://dub.sh/Buvz4yj)
15
15
  private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE! (See https://dub.sh/ceDl0zT)
16
+ private let downloadsDirectory = "capawesome_capacitor_live_update_downloads"
16
17
  private let httpClient: LiveUpdateHttpClient
17
18
  private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
18
19
  private let manifestFileName = "capawesome-live-update-manifest.json" // DO NOT CHANGE!
@@ -37,6 +38,9 @@ import CommonCrypto
37
38
  // Set the device ID on the HTTP client (after any potential config reset)
38
39
  self.httpClient.setDeviceId(getDeviceId())
39
40
 
41
+ // Delete the leftovers of downloads that were interrupted by a process kill
42
+ tryDeleteDownloadsDirectory()
43
+
40
44
  // Start the rollback timer to rollback to the default bundle
41
45
  // if the app is not ready after a certain time
42
46
  startRollbackTimer()
@@ -352,7 +356,7 @@ import CommonCrypto
352
356
 
353
357
  private func addBundleOfTypeZip(bundleId: String, zipFile: URL) async throws {
354
358
  // Unzip the bundle
355
- let unzippedDirectory = try self.unzipFile(zipFile: zipFile)
359
+ let unzippedDirectory = try self.unzipDownloadedBundle(zipFile: zipFile)
356
360
  // Add the bundle
357
361
  try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
358
362
  }
@@ -376,6 +380,10 @@ import CommonCrypto
376
380
  return url
377
381
  }
378
382
 
383
+ private func buildDownloadsDirectoryUrl() -> URL {
384
+ return cachesDirectoryUrl.appendingPathComponent(downloadsDirectory)
385
+ }
386
+
379
387
  private func copyCurrentBundleFile(fileToCopy: ManifestItem, toDirectory: URL) throws {
380
388
  let currentBundleId = getCurrentBundleId()
381
389
  let destination = toDirectory.appendingPathComponent(fileToCopy.href)
@@ -425,8 +433,8 @@ import CommonCrypto
425
433
  }
426
434
  }
427
435
 
428
- private func createTemporaryDirectory() throws -> URL {
429
- let temporaryDirectory = cachesDirectoryUrl.appendingPathComponent(UUID().uuidString)
436
+ private func createTemporaryDownloadDirectory() throws -> URL {
437
+ let temporaryDirectory = buildDownloadsDirectoryUrl().appendingPathComponent(UUID().uuidString)
430
438
  try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
431
439
  return temporaryDirectory
432
440
  }
@@ -460,7 +468,8 @@ import CommonCrypto
460
468
 
461
469
  private func downloadAndVerifyFile(url: String, file: URL, checksum: String?, signature: String?, callback: ((Progress) -> Void)?) async throws {
462
470
  let destination: DownloadRequest.Destination = { _, _ in
463
- return (file, [.createIntermediateDirectories])
471
+ // `removePreviousFile` ensures that a leftover file from a failed attempt does not fail the download
472
+ return (file, [.createIntermediateDirectories, .removePreviousFile])
464
473
  }
465
474
  let urlComponents = URLComponents(string: url)!
466
475
  let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
@@ -533,8 +542,9 @@ import CommonCrypto
533
542
  }
534
543
 
535
544
  private func downloadBundleOfTypeManifest(bundleId: String, url: String) async throws {
536
- // Create a temporary directory
537
- let temporaryDirectory = try createTemporaryDirectory()
545
+ // Create a temporary directory that is deleted once the download completed or failed
546
+ let temporaryDirectory = try createTemporaryDownloadDirectory()
547
+ defer { tryDeleteDirectory(temporaryDirectory) }
538
548
  // Download the latest manifest
539
549
  let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory, callback: nil)
540
550
  let latestManifest = try loadManifest(file: latestManifestFile)
@@ -565,15 +575,17 @@ import CommonCrypto
565
575
  }
566
576
 
567
577
  private func downloadBundleOfTypeZip(bundleId: String, checksum: String?, signature: String?, url: String) async throws {
568
- let timestamp = String(Int(Date().timeIntervalSince1970))
569
- let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
578
+ // Create a temporary directory that is deleted once the download completed or failed
579
+ let temporaryDirectory = try createTemporaryDownloadDirectory()
580
+ defer { tryDeleteDirectory(temporaryDirectory) }
581
+ let zipFile = temporaryDirectory.appendingPathComponent("bundle.zip")
570
582
  // Download the bundle
571
- try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum, signature: signature, callback: { progress in
583
+ try await downloadAndVerifyFile(url: url, file: zipFile, checksum: checksum, signature: signature, callback: { progress in
572
584
  let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
573
585
  self.notifyDownloadBundleProgressListeners(event)
574
586
  })
575
587
  // Add the bundle
576
- try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
588
+ try await addBundleOfTypeZip(bundleId: bundleId, zipFile: zipFile)
577
589
  }
578
590
 
579
591
  private func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> GetLatestBundleResponse? {
@@ -959,8 +971,23 @@ import CommonCrypto
959
971
  }
960
972
  }
961
973
 
962
- private func unzipFile(zipFile: URL) throws -> URL {
963
- let destinationDirectory = zipFile.deletingPathExtension()
974
+ private func tryDeleteDirectory(_ url: URL) {
975
+ guard FileManager.default.fileExists(atPath: url.path) else {
976
+ return
977
+ }
978
+ do {
979
+ try FileManager.default.removeItem(at: url)
980
+ } catch {
981
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete directory: \(url.path)")
982
+ }
983
+ }
984
+
985
+ private func tryDeleteDownloadsDirectory() {
986
+ tryDeleteDirectory(buildDownloadsDirectoryUrl())
987
+ }
988
+
989
+ private func unzipDownloadedBundle(zipFile: URL) throws -> URL {
990
+ let destinationDirectory = zipFile.deletingLastPathComponent().appendingPathComponent("bundle")
964
991
  try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true, attributes: nil)
965
992
  try FileManager.default.unzipItem(at: zipFile, to: destinationDirectory)
966
993
  return destinationDirectory
@@ -27,13 +27,16 @@ public class LiveUpdateHttpClient: NSObject {
27
27
 
28
28
  public func download(url: URL, destination: @escaping DownloadRequest.Destination, callback: ((Progress) -> Void)?) async throws -> AFDownloadResponse<Data> {
29
29
  var request = URLRequest(url: url)
30
+ // Ignore the URL cache so that a cached response is never replayed for a deterministic request URL
31
+ request.cachePolicy = .reloadIgnoringLocalCacheData
30
32
  request.httpMethod = HTTPMethod.get.rawValue
31
33
  request.timeoutInterval = Double(config.httpTimeout) / 1000.0
32
34
  if let deviceId = deviceId, !deviceId.isEmpty {
33
35
  request.setValue(deviceId, forHTTPHeaderField: "X-Device-Id")
34
36
  }
35
37
  return try await withCheckedThrowingContinuation { continuation in
36
- AF.download(request, to: destination).downloadProgress { progress in
38
+ // `validate()` treats non-2xx responses as errors so that an error body is never saved as a bundle file
39
+ AF.download(request, to: destination).validate().downloadProgress { progress in
37
40
  if let callback = callback {
38
41
  callback(progress)
39
42
  }
@@ -45,6 +48,8 @@ public class LiveUpdateHttpClient: NSObject {
45
48
 
46
49
  public func request<T: Decodable>(url: URL, type: T.Type) async throws -> AFDataResponse<T> {
47
50
  var request = URLRequest(url: url)
51
+ // Ignore the URL cache so that a cached response is never replayed for a deterministic request URL
52
+ request.cachePolicy = .reloadIgnoringLocalCacheData
48
53
  request.httpMethod = HTTPMethod.get.rawValue
49
54
  request.timeoutInterval = Double(config.httpTimeout) / 1000.0
50
55
  if let deviceId = deviceId, !deviceId.isEmpty {
@@ -8,7 +8,7 @@ import Capacitor
8
8
  @objc(LiveUpdatePlugin)
9
9
  public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
10
10
  public static let tag = "LiveUpdate"
11
- public static let version = "7.5.0"
11
+ public static let version = "7.5.2"
12
12
  public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
13
13
 
14
14
  public let identifier = "LiveUpdatePlugin"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/capacitor-live-update",
3
- "version": "7.5.0",
3
+ "version": "7.5.2",
4
4
  "description": "Capacitor plugin to update your app remotely in real-time.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",