@capawesome/capacitor-live-update 8.0.1 → 8.1.0-dev.c088829.1767373807
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapawesomeCapacitorLiveUpdate.podspec +2 -2
- package/Package.swift +3 -3
- package/README.md +0 -15
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +74 -56
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +23 -4
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +11 -17
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/CustomException.java +20 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/CustomExceptions.java +33 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/LiveUpdateEventEmitter.java +13 -0
- package/ios/Plugin/LiveUpdate.swift +42 -40
- package/ios/Plugin/LiveUpdatePlugin.swift +29 -5
- package/ios/Plugin/LiveUpdatePreferences.swift +3 -1
- package/ios/Plugin/Protocols/LiveUpdateEventEmitter.swift +8 -0
- package/package.json +1 -1
|
@@ -11,10 +11,10 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.author = package['author']
|
|
12
12
|
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
13
|
s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
-
s.ios.deployment_target = '
|
|
14
|
+
s.ios.deployment_target = '15.0'
|
|
15
15
|
s.dependency 'Capacitor'
|
|
16
16
|
s.dependency 'Alamofire', '~> 5.10.2'
|
|
17
|
-
s.dependency '
|
|
17
|
+
s.dependency 'ZIPFoundation', '~> 0.9.0'
|
|
18
18
|
s.swift_version = '5.1'
|
|
19
19
|
s.static_framework = true
|
|
20
20
|
end
|
package/Package.swift
CHANGED
|
@@ -3,7 +3,7 @@ import PackageDescription
|
|
|
3
3
|
|
|
4
4
|
let package = Package(
|
|
5
5
|
name: "CapawesomeCapacitorLiveUpdate",
|
|
6
|
-
platforms: [.iOS(.
|
|
6
|
+
platforms: [.iOS(.v15)],
|
|
7
7
|
products: [
|
|
8
8
|
.library(
|
|
9
9
|
name: "CapawesomeCapacitorLiveUpdate",
|
|
@@ -12,7 +12,7 @@ let package = Package(
|
|
|
12
12
|
dependencies: [
|
|
13
13
|
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
|
|
14
14
|
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMinor(from: "5.10.2")),
|
|
15
|
-
.package(url: "https://github.com/
|
|
15
|
+
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.0"))
|
|
16
16
|
],
|
|
17
17
|
targets: [
|
|
18
18
|
.target(
|
|
@@ -21,7 +21,7 @@ let package = Package(
|
|
|
21
21
|
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
22
22
|
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
|
23
23
|
.product(name: "Alamofire", package: "Alamofire"),
|
|
24
|
-
.product(name: "
|
|
24
|
+
.product(name: "ZIPFoundation", package: "ZIPFoundation")
|
|
25
25
|
],
|
|
26
26
|
path: "ios/Plugin"),
|
|
27
27
|
.testTarget(
|
package/README.md
CHANGED
|
@@ -93,21 +93,6 @@ Add the `NSPrivacyAccessedAPICategoryUserDefaults` dictionary key to your [Priva
|
|
|
93
93
|
|
|
94
94
|
We recommend to declare [`CA92.1`](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401) as the reason for accessing the [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults) API.
|
|
95
95
|
|
|
96
|
-
#### Minimum Deployment Target
|
|
97
|
-
|
|
98
|
-
If you are using **Swift Package Manager**, make sure that your iOS deployment target is set to at least `16.0` in your Xcode project settings (usually in `ios/App/App.xcodeproj`):
|
|
99
|
-
|
|
100
|
-
```diff
|
|
101
|
-
-IPHONEOS_DEPLOYMENT_TARGET = 15.0
|
|
102
|
-
+IPHONEOS_DEPLOYMENT_TARGET = 16.0
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
If you are using **CocoaPods**, make sure that your iOS deployment target is set to at least `16.0` in your `Podfile`:
|
|
106
|
-
|
|
107
|
-
```ruby
|
|
108
|
-
platform :ios, '16.0'
|
|
109
|
-
```
|
|
110
|
-
|
|
111
96
|
## Configuration
|
|
112
97
|
|
|
113
98
|
<docgen-config>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package io.capawesome.capacitorjs.plugins.liveupdate;
|
|
2
2
|
|
|
3
3
|
import android.app.Activity;
|
|
4
|
+
import android.content.Context;
|
|
4
5
|
import android.content.SharedPreferences;
|
|
5
6
|
import android.content.pm.PackageInfo;
|
|
6
7
|
import android.content.pm.PackageManager;
|
|
@@ -13,6 +14,7 @@ import androidx.annotation.Nullable;
|
|
|
13
14
|
import com.getcapacitor.Bridge;
|
|
14
15
|
import com.getcapacitor.Logger;
|
|
15
16
|
import com.getcapacitor.plugin.WebView;
|
|
17
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.CustomExceptions;
|
|
16
18
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.Manifest;
|
|
17
19
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.ManifestItem;
|
|
18
20
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
|
|
@@ -44,6 +46,7 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
|
|
|
44
46
|
import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
|
|
45
47
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.DownloadProgressCallback;
|
|
46
48
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
|
|
49
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.LiveUpdateEventEmitter;
|
|
47
50
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
48
51
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
49
52
|
import java.io.File;
|
|
@@ -77,18 +80,27 @@ import org.json.JSONObject;
|
|
|
77
80
|
|
|
78
81
|
public class LiveUpdate {
|
|
79
82
|
|
|
83
|
+
public static final String TAG = "LiveUpdate";
|
|
84
|
+
public static final String VERSION = "8.1.1";
|
|
85
|
+
|
|
80
86
|
private final long autoUpdateIntervalMs = 15 * 60 * 1000; // 15 minutes
|
|
81
87
|
|
|
88
|
+
@NonNull
|
|
89
|
+
private final Bridge bridge;
|
|
90
|
+
|
|
82
91
|
@NonNull
|
|
83
92
|
private final LiveUpdateConfig config;
|
|
84
93
|
|
|
94
|
+
@NonNull
|
|
95
|
+
private final Context context;
|
|
96
|
+
|
|
85
97
|
private final String defaultWebAssetDir = Bridge.DEFAULT_WEB_ASSET_DIR;
|
|
86
98
|
|
|
87
99
|
@NonNull
|
|
88
|
-
private final
|
|
100
|
+
private final LiveUpdateEventEmitter eventEmitter;
|
|
89
101
|
|
|
90
102
|
@NonNull
|
|
91
|
-
private final
|
|
103
|
+
private final LiveUpdateHttpClient httpClient;
|
|
92
104
|
|
|
93
105
|
@NonNull
|
|
94
106
|
private final LiveUpdatePreferences preferences;
|
|
@@ -105,12 +117,19 @@ public class LiveUpdate {
|
|
|
105
117
|
private boolean rollbackPerformed = false;
|
|
106
118
|
private boolean syncInProgress = false;
|
|
107
119
|
|
|
108
|
-
public LiveUpdate(
|
|
120
|
+
public LiveUpdate(
|
|
121
|
+
@NonNull Context context,
|
|
122
|
+
@NonNull Bridge bridge,
|
|
123
|
+
@NonNull LiveUpdateConfig config,
|
|
124
|
+
@NonNull LiveUpdateEventEmitter eventEmitter
|
|
125
|
+
) throws PackageManager.NameNotFoundException {
|
|
126
|
+
this.context = context;
|
|
127
|
+
this.bridge = bridge;
|
|
109
128
|
this.config = config;
|
|
129
|
+
this.eventEmitter = eventEmitter;
|
|
110
130
|
this.httpClient = new LiveUpdateHttpClient(config);
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
113
|
-
this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
|
|
131
|
+
this.preferences = new LiveUpdatePreferences(context);
|
|
132
|
+
this.webViewSettingsEditor = context.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
|
|
114
133
|
|
|
115
134
|
// Check version and reset config if version changed
|
|
116
135
|
checkAndResetConfigIfVersionChanged();
|
|
@@ -128,7 +147,7 @@ public class LiveUpdate {
|
|
|
128
147
|
String bundleId = options.getBundleId();
|
|
129
148
|
|
|
130
149
|
if (!hasBundleById(bundleId)) {
|
|
131
|
-
Exception exception =
|
|
150
|
+
Exception exception = CustomExceptions.BUNDLE_NOT_FOUND;
|
|
132
151
|
callback.error(exception);
|
|
133
152
|
return;
|
|
134
153
|
}
|
|
@@ -284,9 +303,9 @@ public class LiveUpdate {
|
|
|
284
303
|
}
|
|
285
304
|
|
|
286
305
|
public void ready(@NonNull NonEmptyCallback callback) {
|
|
287
|
-
Logger.debug(
|
|
306
|
+
Logger.debug(LiveUpdate.TAG, "App is ready.");
|
|
288
307
|
if (config.getReadyTimeout() <= 0) {
|
|
289
|
-
Logger.warn(
|
|
308
|
+
Logger.warn(LiveUpdate.TAG, "Ready timeout is set to 0. Automatic rollback is disabled.");
|
|
290
309
|
}
|
|
291
310
|
// Stop the rollback timer
|
|
292
311
|
stopRollbackTimer();
|
|
@@ -352,7 +371,7 @@ public class LiveUpdate {
|
|
|
352
371
|
if (hasBundleById(bundleId)) {
|
|
353
372
|
setNextBundleById(bundleId);
|
|
354
373
|
} else {
|
|
355
|
-
Exception exception =
|
|
374
|
+
Exception exception = CustomExceptions.BUNDLE_NOT_FOUND;
|
|
356
375
|
callback.error(exception);
|
|
357
376
|
return;
|
|
358
377
|
}
|
|
@@ -362,7 +381,7 @@ public class LiveUpdate {
|
|
|
362
381
|
|
|
363
382
|
public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback) {
|
|
364
383
|
if (syncInProgress) {
|
|
365
|
-
Exception exception =
|
|
384
|
+
Exception exception = CustomExceptions.SYNC_IN_PROGRESS;
|
|
366
385
|
callback.error(exception);
|
|
367
386
|
return;
|
|
368
387
|
}
|
|
@@ -378,7 +397,7 @@ public class LiveUpdate {
|
|
|
378
397
|
public void success(@Nullable GetLatestBundleResponse response) {
|
|
379
398
|
try {
|
|
380
399
|
if (response == null) {
|
|
381
|
-
Logger.debug(
|
|
400
|
+
Logger.debug(LiveUpdate.TAG, "No update available.");
|
|
382
401
|
syncInProgress = false;
|
|
383
402
|
SyncResult syncResult = new SyncResult(null);
|
|
384
403
|
callback.success(syncResult);
|
|
@@ -391,7 +410,7 @@ public class LiveUpdate {
|
|
|
391
410
|
String url = response.getUrl();
|
|
392
411
|
// Check if the bundle is blocked
|
|
393
412
|
if (isBlockedBundleId(latestBundleId)) {
|
|
394
|
-
Logger.warn(
|
|
413
|
+
Logger.warn(LiveUpdate.TAG, "Bundle is blocked and will not be downloaded.");
|
|
395
414
|
syncInProgress = false;
|
|
396
415
|
SyncResult syncResult = new SyncResult(null);
|
|
397
416
|
callback.success(syncResult);
|
|
@@ -459,7 +478,7 @@ public class LiveUpdate {
|
|
|
459
478
|
// Search folder with index.html file
|
|
460
479
|
File indexHtmlFile = searchIndexHtmlFile(sourceDirectory);
|
|
461
480
|
if (indexHtmlFile == null) {
|
|
462
|
-
throw
|
|
481
|
+
throw CustomExceptions.BUNDLE_INDEX_HTML_MISSING;
|
|
463
482
|
}
|
|
464
483
|
|
|
465
484
|
// Create the bundles directory if it does not exist
|
|
@@ -482,21 +501,21 @@ public class LiveUpdate {
|
|
|
482
501
|
}
|
|
483
502
|
|
|
484
503
|
private File buildBundlesDirectory() {
|
|
485
|
-
return new File(
|
|
504
|
+
return new File(context.getFilesDir(), bundlesDirectory);
|
|
486
505
|
}
|
|
487
506
|
|
|
488
507
|
private File buildBundleDirectoryFor(@NonNull String bundleId) {
|
|
489
|
-
return new File(
|
|
508
|
+
return new File(context.getFilesDir(), bundlesDirectory + "/" + bundleId);
|
|
490
509
|
}
|
|
491
510
|
|
|
492
511
|
private File buildTemporaryDirectory() {
|
|
493
512
|
String fileName = UUID.randomUUID().toString();
|
|
494
|
-
return new File(
|
|
513
|
+
return new File(context.getCacheDir(), fileName);
|
|
495
514
|
}
|
|
496
515
|
|
|
497
516
|
private File buildTemporaryZipFile() {
|
|
498
517
|
String fileName = UUID.randomUUID().toString() + ".zip";
|
|
499
|
-
return new File(
|
|
518
|
+
return new File(context.getCacheDir(), fileName);
|
|
500
519
|
}
|
|
501
520
|
|
|
502
521
|
private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
|
|
@@ -504,7 +523,7 @@ public class LiveUpdate {
|
|
|
504
523
|
String currentBundleId = getCurrentBundleId();
|
|
505
524
|
if (currentBundleId == null) {
|
|
506
525
|
// Create the source input stream
|
|
507
|
-
AssetManager assets =
|
|
526
|
+
AssetManager assets = context.getAssets();
|
|
508
527
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
|
|
509
528
|
// Create the destination file
|
|
510
529
|
File destination = new File(destinationDirectory, href);
|
|
@@ -532,7 +551,7 @@ public class LiveUpdate {
|
|
|
532
551
|
for (ManifestItem fileToCopy : filesToCopy) {
|
|
533
552
|
boolean success = tryCopyCurrentBundleFile(fileToCopy, destinationDirectory);
|
|
534
553
|
if (!success) {
|
|
535
|
-
Logger.warn(
|
|
554
|
+
Logger.warn(LiveUpdate.TAG, "Failed to copy file: " + fileToCopy.getHref());
|
|
536
555
|
// If the file could not be copied, add it to the list of missing items
|
|
537
556
|
missingItems.add(fileToCopy);
|
|
538
557
|
}
|
|
@@ -578,8 +597,8 @@ public class LiveUpdate {
|
|
|
578
597
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
|
579
598
|
return keyFactory.generatePublic(X509publicKey);
|
|
580
599
|
} catch (Exception exception) {
|
|
581
|
-
Logger.error(
|
|
582
|
-
throw
|
|
600
|
+
Logger.error(LiveUpdate.TAG, exception.getMessage(), exception);
|
|
601
|
+
throw CustomExceptions.PUBLIC_KEY_INVALID;
|
|
583
602
|
}
|
|
584
603
|
}
|
|
585
604
|
|
|
@@ -645,8 +664,8 @@ public class LiveUpdate {
|
|
|
645
664
|
completionCallback.success();
|
|
646
665
|
} else {
|
|
647
666
|
String errorMessage = response.body().string();
|
|
648
|
-
Exception exception =
|
|
649
|
-
Logger.error(
|
|
667
|
+
Exception exception = CustomExceptions.DOWNLOAD_FAILED;
|
|
668
|
+
Logger.error(LiveUpdate.TAG, errorMessage, exception);
|
|
650
669
|
completionCallback.error(exception);
|
|
651
670
|
}
|
|
652
671
|
} catch (Exception e) {
|
|
@@ -749,7 +768,7 @@ public class LiveUpdate {
|
|
|
749
768
|
public void error(@NonNull Exception e) {
|
|
750
769
|
// Capture first error and cancel all remaining downloads
|
|
751
770
|
if (firstError.compareAndSet(null, e)) {
|
|
752
|
-
Logger.error(
|
|
771
|
+
Logger.error(LiveUpdate.TAG, "Failed to download file: " + item.getHref(), e);
|
|
753
772
|
// Cancel all in-flight downloads (fail-fast)
|
|
754
773
|
synchronized (activeCalls) {
|
|
755
774
|
for (Call activeCall : activeCalls) {
|
|
@@ -924,10 +943,10 @@ public class LiveUpdate {
|
|
|
924
943
|
.addQueryParameter("deviceId", getDeviceId())
|
|
925
944
|
.addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
|
|
926
945
|
.addQueryParameter("platform", "0")
|
|
927
|
-
.addQueryParameter("pluginVersion",
|
|
946
|
+
.addQueryParameter("pluginVersion", LiveUpdate.VERSION)
|
|
928
947
|
.build()
|
|
929
948
|
.toString();
|
|
930
|
-
Logger.debug(
|
|
949
|
+
Logger.debug(LiveUpdate.TAG, "Fetching latest bundle: " + url);
|
|
931
950
|
|
|
932
951
|
httpClient.enqueue(
|
|
933
952
|
url,
|
|
@@ -936,7 +955,7 @@ public class LiveUpdate {
|
|
|
936
955
|
public void success(@NonNull Response response) {
|
|
937
956
|
try {
|
|
938
957
|
String responseBodyString = response.body().string();
|
|
939
|
-
Logger.debug(
|
|
958
|
+
Logger.debug(LiveUpdate.TAG, "Latest bundle response: " + responseBodyString);
|
|
940
959
|
if (response.isSuccessful()) {
|
|
941
960
|
JSONObject responseJson = new JSONObject(responseBodyString);
|
|
942
961
|
GetLatestBundleResponse result = new GetLatestBundleResponse(responseJson);
|
|
@@ -986,8 +1005,8 @@ public class LiveUpdate {
|
|
|
986
1005
|
source.close();
|
|
987
1006
|
return digest.digest();
|
|
988
1007
|
} catch (IOException exception) {
|
|
989
|
-
Logger.error(
|
|
990
|
-
throw
|
|
1008
|
+
Logger.error(LiveUpdate.TAG, exception.getMessage(), exception);
|
|
1009
|
+
throw CustomExceptions.CHECKSUM_CALCULATION_FAILED;
|
|
991
1010
|
}
|
|
992
1011
|
}
|
|
993
1012
|
|
|
@@ -1037,7 +1056,7 @@ public class LiveUpdate {
|
|
|
1037
1056
|
* @return The absolute path to the current bundle directory (`public` for the built-in bundle).
|
|
1038
1057
|
*/
|
|
1039
1058
|
private String getCurrentCapacitorServerPath() {
|
|
1040
|
-
return
|
|
1059
|
+
return bridge.getServerBasePath();
|
|
1041
1060
|
}
|
|
1042
1061
|
|
|
1043
1062
|
@NonNull
|
|
@@ -1067,8 +1086,7 @@ public class LiveUpdate {
|
|
|
1067
1086
|
*/
|
|
1068
1087
|
@NonNull
|
|
1069
1088
|
private String getNextCapacitorServerPath() {
|
|
1070
|
-
String path =
|
|
1071
|
-
.getContext()
|
|
1089
|
+
String path = context
|
|
1072
1090
|
.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
|
|
1073
1091
|
.getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
|
|
1074
1092
|
// Empty path means default path
|
|
@@ -1113,7 +1131,7 @@ public class LiveUpdate {
|
|
|
1113
1131
|
private Manifest loadCurrentManifest() throws Exception {
|
|
1114
1132
|
String currentBundleId = getCurrentBundleId();
|
|
1115
1133
|
if (currentBundleId == null) {
|
|
1116
|
-
AssetManager assets =
|
|
1134
|
+
AssetManager assets = context.getAssets();
|
|
1117
1135
|
boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
1118
1136
|
if (manifestFileExists) {
|
|
1119
1137
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
@@ -1145,14 +1163,14 @@ public class LiveUpdate {
|
|
|
1145
1163
|
}
|
|
1146
1164
|
|
|
1147
1165
|
private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
|
|
1148
|
-
|
|
1166
|
+
eventEmitter.onDownloadBundleProgress(event);
|
|
1149
1167
|
}
|
|
1150
1168
|
|
|
1151
1169
|
private void performAutoUpdate() {
|
|
1152
1170
|
// Check if enough time has passed since the last check
|
|
1153
1171
|
long now = System.currentTimeMillis();
|
|
1154
1172
|
if (lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs) {
|
|
1155
|
-
Logger.debug(
|
|
1173
|
+
Logger.debug(LiveUpdate.TAG, "Auto-update skipped. Last check was less than 15 minutes ago.");
|
|
1156
1174
|
return;
|
|
1157
1175
|
}
|
|
1158
1176
|
|
|
@@ -1160,17 +1178,17 @@ public class LiveUpdate {
|
|
|
1160
1178
|
lastAutoUpdateCheckTimestamp = now;
|
|
1161
1179
|
|
|
1162
1180
|
// Run sync
|
|
1163
|
-
Logger.debug(
|
|
1181
|
+
Logger.debug(LiveUpdate.TAG, "Auto-update started.");
|
|
1164
1182
|
SyncOptions options = new SyncOptions((String) null);
|
|
1165
1183
|
NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
|
|
1166
1184
|
@Override
|
|
1167
1185
|
public void success(@NonNull Result result) {
|
|
1168
|
-
Logger.debug(
|
|
1186
|
+
Logger.debug(LiveUpdate.TAG, "Auto-update completed successfully.");
|
|
1169
1187
|
}
|
|
1170
1188
|
|
|
1171
1189
|
@Override
|
|
1172
1190
|
public void error(@NonNull Exception exception) {
|
|
1173
|
-
Logger.error(
|
|
1191
|
+
Logger.error(LiveUpdate.TAG, "Auto-update failed: " + exception.getMessage(), exception);
|
|
1174
1192
|
}
|
|
1175
1193
|
};
|
|
1176
1194
|
sync(options, callback);
|
|
@@ -1184,9 +1202,9 @@ public class LiveUpdate {
|
|
|
1184
1202
|
setPreviousBundleId(currentBundleId);
|
|
1185
1203
|
// Log the rollback result
|
|
1186
1204
|
if (currentBundleId == null) {
|
|
1187
|
-
Logger.debug(
|
|
1205
|
+
Logger.debug(LiveUpdate.TAG, "App is not ready. Default bundle is already in use.");
|
|
1188
1206
|
} else {
|
|
1189
|
-
Logger.debug(
|
|
1207
|
+
Logger.debug(LiveUpdate.TAG, "App is not ready. Rolling back to default bundle.");
|
|
1190
1208
|
// Rollback to the default bundle
|
|
1191
1209
|
setNextBundleById(null);
|
|
1192
1210
|
setCurrentBundleById(null);
|
|
@@ -1232,11 +1250,11 @@ public class LiveUpdate {
|
|
|
1232
1250
|
|
|
1233
1251
|
private void setCurrentCapacitorServerPath(@NonNull String path) {
|
|
1234
1252
|
if (path.equals(defaultWebAssetDir)) {
|
|
1235
|
-
this.
|
|
1253
|
+
this.bridge.setServerAssetPath(path);
|
|
1236
1254
|
} else {
|
|
1237
|
-
this.
|
|
1255
|
+
this.bridge.setServerBasePath(path);
|
|
1238
1256
|
}
|
|
1239
|
-
this.
|
|
1257
|
+
this.bridge.reload();
|
|
1240
1258
|
// Notify listeners
|
|
1241
1259
|
notifyReloadedListeners();
|
|
1242
1260
|
}
|
|
@@ -1258,11 +1276,11 @@ public class LiveUpdate {
|
|
|
1258
1276
|
|
|
1259
1277
|
private void notifyNextBundleSetListeners(@Nullable String bundleId) {
|
|
1260
1278
|
NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
|
|
1261
|
-
|
|
1279
|
+
eventEmitter.onNextBundleSet(event);
|
|
1262
1280
|
}
|
|
1263
1281
|
|
|
1264
1282
|
private void notifyReloadedListeners() {
|
|
1265
|
-
|
|
1283
|
+
eventEmitter.onReloaded();
|
|
1266
1284
|
}
|
|
1267
1285
|
|
|
1268
1286
|
private void addBlockedBundleId(@NonNull String bundleId) {
|
|
@@ -1292,7 +1310,7 @@ public class LiveUpdate {
|
|
|
1292
1310
|
String newBlockedIds = String.join(",", blockedList);
|
|
1293
1311
|
preferences.setBlockedBundleIds(newBlockedIds);
|
|
1294
1312
|
|
|
1295
|
-
Logger.debug(
|
|
1313
|
+
Logger.debug(LiveUpdate.TAG, "Bundle blocked: " + bundleId);
|
|
1296
1314
|
}
|
|
1297
1315
|
|
|
1298
1316
|
private boolean isBlockedBundleId(@NonNull String bundleId) {
|
|
@@ -1316,7 +1334,7 @@ public class LiveUpdate {
|
|
|
1316
1334
|
|
|
1317
1335
|
if (lastVersionCode == -1 || lastVersionCode != currentVersionCode) {
|
|
1318
1336
|
Logger.debug(
|
|
1319
|
-
|
|
1337
|
+
LiveUpdate.TAG,
|
|
1320
1338
|
"App version changed (last: " + lastVersionCode + ", current: " + currentVersionCode + "), resetting config."
|
|
1321
1339
|
);
|
|
1322
1340
|
resetConfig();
|
|
@@ -1366,12 +1384,12 @@ public class LiveUpdate {
|
|
|
1366
1384
|
if (publicKey != null) {
|
|
1367
1385
|
// Verify the signature
|
|
1368
1386
|
if (signature == null) {
|
|
1369
|
-
throw
|
|
1387
|
+
throw CustomExceptions.SIGNATURE_MISSING;
|
|
1370
1388
|
}
|
|
1371
1389
|
// Verify the signature
|
|
1372
1390
|
boolean verified = verifySignatureForFile(file, signature, publicKey);
|
|
1373
1391
|
if (!verified) {
|
|
1374
|
-
throw
|
|
1392
|
+
throw CustomExceptions.SIGNATURE_VERIFICATION_FAILED;
|
|
1375
1393
|
}
|
|
1376
1394
|
}
|
|
1377
1395
|
// Verify the checksum
|
|
@@ -1379,7 +1397,7 @@ public class LiveUpdate {
|
|
|
1379
1397
|
// Calculate the checksum
|
|
1380
1398
|
boolean verified = verifyChecksumForFile(file, checksum);
|
|
1381
1399
|
if (!verified) {
|
|
1382
|
-
throw
|
|
1400
|
+
throw CustomExceptions.CHECKSUM_MISMATCH;
|
|
1383
1401
|
}
|
|
1384
1402
|
}
|
|
1385
1403
|
}
|
|
@@ -1402,17 +1420,17 @@ public class LiveUpdate {
|
|
|
1402
1420
|
source.close();
|
|
1403
1421
|
return sig.verify(signatureBytes);
|
|
1404
1422
|
} catch (Exception exception) {
|
|
1405
|
-
Logger.error(
|
|
1406
|
-
throw
|
|
1423
|
+
Logger.error(LiveUpdate.TAG, exception.getMessage(), exception);
|
|
1424
|
+
throw CustomExceptions.SIGNATURE_VERIFICATION_FAILED;
|
|
1407
1425
|
}
|
|
1408
1426
|
}
|
|
1409
1427
|
|
|
1410
1428
|
private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
|
1411
|
-
String packageName = this.
|
|
1429
|
+
String packageName = this.context.getPackageName();
|
|
1412
1430
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1413
|
-
return this.
|
|
1431
|
+
return this.context.getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
|
|
1414
1432
|
} else {
|
|
1415
|
-
return this.
|
|
1433
|
+
return this.context.getPackageManager().getPackageInfo(packageName, 0);
|
|
1416
1434
|
}
|
|
1417
1435
|
}
|
|
1418
1436
|
}
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java
CHANGED
|
@@ -28,15 +28,15 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDownloade
|
|
|
28
28
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetNextBundleResult;
|
|
29
29
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingResult;
|
|
30
30
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
|
|
31
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.LiveUpdateEventEmitter;
|
|
31
32
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
32
33
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
33
34
|
|
|
34
35
|
@CapacitorPlugin(name = "LiveUpdate")
|
|
35
36
|
public class LiveUpdatePlugin extends Plugin {
|
|
36
37
|
|
|
37
|
-
public static final String TAG =
|
|
38
|
-
public static final String VERSION =
|
|
39
|
-
public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
|
|
38
|
+
public static final String TAG = LiveUpdate.TAG;
|
|
39
|
+
public static final String VERSION = LiveUpdate.VERSION;
|
|
40
40
|
public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
|
|
41
41
|
public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
|
|
42
42
|
public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
|
|
@@ -68,7 +68,8 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
68
68
|
public void load() {
|
|
69
69
|
try {
|
|
70
70
|
config = getLiveUpdateConfig();
|
|
71
|
-
|
|
71
|
+
LiveUpdateEventEmitter eventEmitter = new PluginLiveUpdateEventEmitter();
|
|
72
|
+
implementation = new LiveUpdate(getContext(), getBridge(), config, eventEmitter);
|
|
72
73
|
} catch (Exception exception) {
|
|
73
74
|
Logger.error(TAG, exception.getMessage(), exception);
|
|
74
75
|
}
|
|
@@ -684,4 +685,22 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
684
685
|
Logger.error(TAG, message, exception);
|
|
685
686
|
call.reject(message);
|
|
686
687
|
}
|
|
688
|
+
|
|
689
|
+
private class PluginLiveUpdateEventEmitter implements LiveUpdateEventEmitter {
|
|
690
|
+
|
|
691
|
+
@Override
|
|
692
|
+
public void onDownloadBundleProgress(@NonNull DownloadBundleProgressEvent event) {
|
|
693
|
+
notifyDownloadBundleProgressListeners(event);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
@Override
|
|
697
|
+
public void onNextBundleSet(@NonNull NextBundleSetEvent event) {
|
|
698
|
+
notifyNextBundleSetListeners(event);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
@Override
|
|
702
|
+
public void onReloaded() {
|
|
703
|
+
notifyReloadedListeners();
|
|
704
|
+
}
|
|
705
|
+
}
|
|
687
706
|
}
|
|
@@ -9,6 +9,8 @@ import java.util.UUID;
|
|
|
9
9
|
|
|
10
10
|
public class LiveUpdatePreferences {
|
|
11
11
|
|
|
12
|
+
private static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
|
|
13
|
+
|
|
12
14
|
@NonNull
|
|
13
15
|
private final Context context;
|
|
14
16
|
|
|
@@ -25,53 +27,45 @@ public class LiveUpdatePreferences {
|
|
|
25
27
|
|
|
26
28
|
public LiveUpdatePreferences(@NonNull Context context) {
|
|
27
29
|
this.context = context;
|
|
28
|
-
this.settingsEditor = context.getSharedPreferences(
|
|
30
|
+
this.settingsEditor = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).edit();
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
@Nullable
|
|
32
34
|
public String getAppId() {
|
|
33
|
-
return context.getSharedPreferences(
|
|
35
|
+
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(appIdKey, null);
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
@Nullable
|
|
37
39
|
public String getChannel() {
|
|
38
|
-
return context.getSharedPreferences(
|
|
40
|
+
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(channelKey, null);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
@Nullable
|
|
42
44
|
public String getCustomId() {
|
|
43
|
-
return context.getSharedPreferences(
|
|
45
|
+
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(customIdKey, null);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
@Nullable
|
|
47
49
|
public String getDeviceIdForApp(@Nullable String appId) {
|
|
48
50
|
if (appId == null) {
|
|
49
|
-
return context
|
|
50
|
-
.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE)
|
|
51
|
-
.getString(deviceIdKey, null);
|
|
51
|
+
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(deviceIdKey, null);
|
|
52
52
|
} else {
|
|
53
|
-
return context
|
|
54
|
-
.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE)
|
|
55
|
-
.getString(deviceIdKey + "_" + appId, null);
|
|
53
|
+
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(deviceIdKey + "_" + appId, null);
|
|
56
54
|
}
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
public int getLastVersionCode() {
|
|
60
|
-
return context.getSharedPreferences(
|
|
58
|
+
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getInt(lastVersionCodeKey, -1);
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
@Nullable
|
|
64
62
|
public String getPreviousBundleId() {
|
|
65
|
-
return context
|
|
66
|
-
.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE)
|
|
67
|
-
.getString(previousBundleIdKey, null);
|
|
63
|
+
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(previousBundleIdKey, null);
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
@Nullable
|
|
71
67
|
public String getBlockedBundleIds() {
|
|
72
|
-
return context
|
|
73
|
-
.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE)
|
|
74
|
-
.getString(blockedBundleIdsKey, null);
|
|
68
|
+
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(blockedBundleIdsKey, null);
|
|
75
69
|
}
|
|
76
70
|
|
|
77
71
|
public void setAppId(@Nullable String appId) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package io.capawesome.capacitorjs.plugins.liveupdate.classes;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
import androidx.annotation.Nullable;
|
|
5
|
+
|
|
6
|
+
public class CustomException extends Exception {
|
|
7
|
+
|
|
8
|
+
@Nullable
|
|
9
|
+
private final String code;
|
|
10
|
+
|
|
11
|
+
public CustomException(@Nullable String code, @NonNull String message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Nullable
|
|
17
|
+
public String getCode() {
|
|
18
|
+
return code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package io.capawesome.capacitorjs.plugins.liveupdate.classes;
|
|
2
|
+
|
|
3
|
+
public class CustomExceptions {
|
|
4
|
+
|
|
5
|
+
public static final CustomException APP_ID_MISSING = new CustomException("APP_ID_MISSING", "appId must be configured.");
|
|
6
|
+
public static final CustomException BUNDLE_EXISTS = new CustomException("BUNDLE_EXISTS", "bundle already exists.");
|
|
7
|
+
public static final CustomException BUNDLE_ID_MISSING = new CustomException(null, "bundleId must be provided.");
|
|
8
|
+
public static final CustomException BUNDLE_INDEX_HTML_MISSING = new CustomException(
|
|
9
|
+
"BUNDLE_INDEX_HTML_MISSING",
|
|
10
|
+
"The bundle does not contain an index.html file."
|
|
11
|
+
);
|
|
12
|
+
public static final CustomException BUNDLE_NOT_FOUND = new CustomException("BUNDLE_NOT_FOUND", "bundle not found.");
|
|
13
|
+
public static final CustomException CHECKSUM_CALCULATION_FAILED = new CustomException(
|
|
14
|
+
"CHECKSUM_CALCULATION_FAILED",
|
|
15
|
+
"Failed to calculate checksum."
|
|
16
|
+
);
|
|
17
|
+
public static final CustomException CHECKSUM_MISMATCH = new CustomException("CHECKSUM_MISMATCH", "Checksum mismatch.");
|
|
18
|
+
public static final CustomException CUSTOM_ID_MISSING = new CustomException(null, "customId must be provided.");
|
|
19
|
+
public static final CustomException DOWNLOAD_FAILED = new CustomException("DOWNLOAD_FAILED", "Bundle could not be downloaded.");
|
|
20
|
+
public static final CustomException HTTP_TIMEOUT = new CustomException("HTTP_TIMEOUT", "Request timed out.");
|
|
21
|
+
public static final CustomException URL_MISSING = new CustomException(null, "url must be provided.");
|
|
22
|
+
public static final CustomException SIGNATURE_VERIFICATION_FAILED = new CustomException(
|
|
23
|
+
"SIGNATURE_VERIFICATION_FAILED",
|
|
24
|
+
"Signature verification failed."
|
|
25
|
+
);
|
|
26
|
+
public static final CustomException PUBLIC_KEY_INVALID = new CustomException("PUBLIC_KEY_INVALID", "Invalid public key.");
|
|
27
|
+
public static final CustomException SIGNATURE_MISSING = new CustomException(
|
|
28
|
+
"SIGNATURE_MISSING",
|
|
29
|
+
"Bundle does not contain a signature."
|
|
30
|
+
);
|
|
31
|
+
public static final CustomException SYNC_IN_PROGRESS = new CustomException("SYNC_IN_PROGRESS", "Sync is already in progress.");
|
|
32
|
+
public static final CustomException UNKNOWN_ERROR = new CustomException("UNKNOWN_ERROR", "An unknown error has occurred.");
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package io.capawesome.capacitorjs.plugins.liveupdate.interfaces;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.DownloadBundleProgressEvent;
|
|
5
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.NextBundleSetEvent;
|
|
6
|
+
|
|
7
|
+
public interface LiveUpdateEventEmitter {
|
|
8
|
+
void onDownloadBundleProgress(@NonNull DownloadBundleProgressEvent event);
|
|
9
|
+
|
|
10
|
+
void onNextBundleSet(@NonNull NextBundleSetEvent event);
|
|
11
|
+
|
|
12
|
+
void onReloaded();
|
|
13
|
+
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import CryptoKit
|
|
3
|
-
|
|
4
|
-
import ZipArchive
|
|
5
|
-
#else
|
|
6
|
-
import SSZipArchive
|
|
7
|
-
#endif
|
|
3
|
+
import ZIPFoundation
|
|
8
4
|
import Capacitor
|
|
9
5
|
import Alamofire
|
|
10
6
|
import CommonCrypto
|
|
11
7
|
|
|
12
8
|
// swiftlint:disable type_body_length
|
|
13
9
|
@objc public class LiveUpdate: NSObject {
|
|
10
|
+
public static let tag = "LiveUpdate"
|
|
11
|
+
public static let version = "8.1.1"
|
|
12
|
+
|
|
14
13
|
private let autoUpdateIntervalMs: Int64 = 15 * 60 * 1000 // 15 minutes
|
|
14
|
+
private let bridge: CAPBridgeProtocol
|
|
15
15
|
private let bundlesDirectory = "NoCloud/ionic_built_snapshots" // DO NOT CHANGE! (See https://dub.sh/BLluidt)
|
|
16
16
|
private let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
|
17
17
|
private let config: LiveUpdateConfig
|
|
18
18
|
private let defaultWebAssetDir = "public" // DO NOT CHANGE! (See https://dub.sh/Buvz4yj)
|
|
19
19
|
private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE! (See https://dub.sh/ceDl0zT)
|
|
20
|
+
private let eventEmitter: LiveUpdateEventEmitter
|
|
20
21
|
private let httpClient: LiveUpdateHttpClient
|
|
21
22
|
private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
|
|
22
23
|
private let manifestFileName = "capawesome-live-update-manifest.json" // DO NOT CHANGE!
|
|
23
|
-
private let plugin: LiveUpdatePlugin
|
|
24
24
|
private let preferences: LiveUpdatePreferences
|
|
25
25
|
|
|
26
26
|
private var rollbackDispatchWorkItem: DispatchWorkItem?
|
|
@@ -28,10 +28,11 @@ import CommonCrypto
|
|
|
28
28
|
private var lastAutoUpdateCheckTimestamp: Int64 = 0
|
|
29
29
|
private var syncInProgress = false
|
|
30
30
|
|
|
31
|
-
init(config: LiveUpdateConfig,
|
|
31
|
+
init(bridge: CAPBridgeProtocol, config: LiveUpdateConfig, eventEmitter: LiveUpdateEventEmitter) {
|
|
32
|
+
self.bridge = bridge
|
|
32
33
|
self.config = config
|
|
34
|
+
self.eventEmitter = eventEmitter
|
|
33
35
|
self.httpClient = LiveUpdateHttpClient(config: config)
|
|
34
|
-
self.plugin = plugin
|
|
35
36
|
self.preferences = LiveUpdatePreferences()
|
|
36
37
|
super.init()
|
|
37
38
|
|
|
@@ -178,9 +179,9 @@ import CommonCrypto
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
@objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
|
|
181
|
-
CAPLog.print("[",
|
|
182
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "App is ready.")
|
|
182
183
|
if config.readyTimeout <= 0 {
|
|
183
|
-
CAPLog.print("[",
|
|
184
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Ready timeout is set to 0. Automatic rollback is disabled.")
|
|
184
185
|
}
|
|
185
186
|
// Stop the rollback timer
|
|
186
187
|
stopRollbackTimer()
|
|
@@ -268,7 +269,7 @@ import CommonCrypto
|
|
|
268
269
|
// Fetch the latest bundle
|
|
269
270
|
let fetchLatestBundleOptions = FetchLatestBundleOptions(channel: channel)
|
|
270
271
|
guard let response = try await fetchLatestBundle(fetchLatestBundleOptions) else {
|
|
271
|
-
CAPLog.print("[",
|
|
272
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "No update available.")
|
|
272
273
|
return SyncResult(nextBundleId: nil)
|
|
273
274
|
}
|
|
274
275
|
let artifactType = response.artifactType
|
|
@@ -278,7 +279,7 @@ import CommonCrypto
|
|
|
278
279
|
let downloadUrl = response.url
|
|
279
280
|
// Check if the bundle is blocked
|
|
280
281
|
if isBlockedBundleId(latestBundleId) {
|
|
281
|
-
CAPLog.print("[",
|
|
282
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Bundle is blocked and will not be downloaded.")
|
|
282
283
|
return SyncResult(nextBundleId: nil)
|
|
283
284
|
}
|
|
284
285
|
// Check if bundle already exists
|
|
@@ -323,7 +324,7 @@ import CommonCrypto
|
|
|
323
324
|
|
|
324
325
|
private func addBundleOfTypeZip(bundleId: String, zipFile: URL) async throws {
|
|
325
326
|
// Unzip the bundle
|
|
326
|
-
let unzippedDirectory = self.unzipFile(zipFile: zipFile)
|
|
327
|
+
let unzippedDirectory = try self.unzipFile(zipFile: zipFile)
|
|
327
328
|
// Add the bundle
|
|
328
329
|
try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
|
|
329
330
|
}
|
|
@@ -376,7 +377,7 @@ import CommonCrypto
|
|
|
376
377
|
for fileToCopy in filesToCopy {
|
|
377
378
|
let success = tryCopyCurrentBundleFile(fileToCopy: fileToCopy, toDirectory: toDirectory)
|
|
378
379
|
if !success {
|
|
379
|
-
CAPLog.print("[",
|
|
380
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to copy file: \(fileToCopy.href)")
|
|
380
381
|
// If the file could not be copied, add it to the list of missing items
|
|
381
382
|
missingItems.append(fileToCopy)
|
|
382
383
|
}
|
|
@@ -391,7 +392,7 @@ import CommonCrypto
|
|
|
391
392
|
do {
|
|
392
393
|
try FileManager.default.createDirectory(at: bundlesDirectoryUrl, withIntermediateDirectories: true, attributes: nil)
|
|
393
394
|
} catch {
|
|
394
|
-
CAPLog.print("[",
|
|
395
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to create bundles directory.")
|
|
395
396
|
}
|
|
396
397
|
}
|
|
397
398
|
}
|
|
@@ -423,7 +424,7 @@ import CommonCrypto
|
|
|
423
424
|
do {
|
|
424
425
|
try deleteBundleById(bundleId)
|
|
425
426
|
} catch {
|
|
426
|
-
CAPLog.print("[",
|
|
427
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
|
|
427
428
|
}
|
|
428
429
|
}
|
|
429
430
|
}
|
|
@@ -436,7 +437,7 @@ import CommonCrypto
|
|
|
436
437
|
let urlComponents = URLComponents(string: url)!
|
|
437
438
|
let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
|
|
438
439
|
if let error = result.error {
|
|
439
|
-
CAPLog.print("[",
|
|
440
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to download file: \(error)")
|
|
440
441
|
if let urlError = error.underlyingError as? URLError {
|
|
441
442
|
if urlError.code == .timedOut {
|
|
442
443
|
throw urlError
|
|
@@ -558,14 +559,14 @@ import CommonCrypto
|
|
|
558
559
|
parameters["deviceId"] = getDeviceId()
|
|
559
560
|
parameters["osVersion"] = await UIDevice.current.systemVersion
|
|
560
561
|
parameters["platform"] = "1"
|
|
561
|
-
parameters["pluginVersion"] =
|
|
562
|
+
parameters["pluginVersion"] = LiveUpdate.version
|
|
562
563
|
var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(getAppId() ?? "")/bundles/latest")!
|
|
563
564
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
564
565
|
let url = try urlComponents.asURL()
|
|
565
|
-
CAPLog.print("[",
|
|
566
|
+
CAPLog.print("[", LiveUpdate.tag, "] Fetching latest bundle: ", url)
|
|
566
567
|
let response = try await self.httpClient.request(url: url, type: GetLatestBundleResponse.self)
|
|
567
568
|
if let data = response.data {
|
|
568
|
-
CAPLog.print("[",
|
|
569
|
+
CAPLog.print("[", LiveUpdate.tag, "] Latest bundle response: ", String(decoding: data, as: UTF8.self))
|
|
569
570
|
}
|
|
570
571
|
if let error = response.error {
|
|
571
572
|
if let urlError = error.underlyingError as? URLError {
|
|
@@ -642,7 +643,7 @@ import CommonCrypto
|
|
|
642
643
|
|
|
643
644
|
/// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
|
|
644
645
|
private func getCurrentCapacitorServerPath() -> String? {
|
|
645
|
-
guard let viewController = self.
|
|
646
|
+
guard let viewController = self.bridge.viewController as? CAPBridgeViewController else {
|
|
646
647
|
return nil
|
|
647
648
|
}
|
|
648
649
|
return viewController.getServerBasePath()
|
|
@@ -724,14 +725,14 @@ import CommonCrypto
|
|
|
724
725
|
}
|
|
725
726
|
|
|
726
727
|
private func notifyDownloadBundleProgressListeners(_ event: DownloadBundleProgressEvent) {
|
|
727
|
-
|
|
728
|
+
eventEmitter.onDownloadBundleProgress(event)
|
|
728
729
|
}
|
|
729
730
|
|
|
730
731
|
private func performAutoUpdate() {
|
|
731
732
|
// Check if enough time has passed since the last check
|
|
732
733
|
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
|
733
734
|
if lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs {
|
|
734
|
-
CAPLog.print("[",
|
|
735
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Auto-update skipped. Last check was less than 15 minutes ago.")
|
|
735
736
|
return
|
|
736
737
|
}
|
|
737
738
|
|
|
@@ -741,12 +742,12 @@ import CommonCrypto
|
|
|
741
742
|
// Run sync in background task
|
|
742
743
|
Task {
|
|
743
744
|
do {
|
|
744
|
-
CAPLog.print("[",
|
|
745
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Auto-update started.")
|
|
745
746
|
let options = SyncOptions(channel: nil)
|
|
746
747
|
_ = try await sync(options)
|
|
747
|
-
CAPLog.print("[",
|
|
748
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Auto-update completed successfully.")
|
|
748
749
|
} catch {
|
|
749
|
-
CAPLog.print("[",
|
|
750
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Auto-update failed: ", error.localizedDescription)
|
|
750
751
|
}
|
|
751
752
|
}
|
|
752
753
|
}
|
|
@@ -759,11 +760,11 @@ import CommonCrypto
|
|
|
759
760
|
setPreviousBundleId(bundleId: currentBundleId)
|
|
760
761
|
// Perform the rollback
|
|
761
762
|
if let _ = currentBundleId {
|
|
762
|
-
CAPLog.print("[",
|
|
763
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "App is not ready. Rolling back to default bundle.")
|
|
763
764
|
setNextBundleById(nil)
|
|
764
765
|
setCurrentBundleById(nil)
|
|
765
766
|
} else {
|
|
766
|
-
CAPLog.print("[",
|
|
767
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "App is not ready. Default bundle is already in use.")
|
|
767
768
|
}
|
|
768
769
|
}
|
|
769
770
|
|
|
@@ -789,7 +790,7 @@ import CommonCrypto
|
|
|
789
790
|
}
|
|
790
791
|
}
|
|
791
792
|
} catch {
|
|
792
|
-
CAPLog.print("[",
|
|
793
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to search index.html file: \(error.localizedDescription)")
|
|
793
794
|
}
|
|
794
795
|
return nil
|
|
795
796
|
}
|
|
@@ -801,7 +802,7 @@ import CommonCrypto
|
|
|
801
802
|
}
|
|
802
803
|
|
|
803
804
|
private func setCurrentCapacitorServerPath(path: String) {
|
|
804
|
-
guard let viewController = self.
|
|
805
|
+
guard let viewController = self.bridge.viewController as? CAPBridgeViewController else {
|
|
805
806
|
return
|
|
806
807
|
}
|
|
807
808
|
viewController.setServerBasePath(path: path)
|
|
@@ -820,11 +821,11 @@ import CommonCrypto
|
|
|
820
821
|
|
|
821
822
|
private func notifyNextBundleSetListeners(_ bundleId: String?) {
|
|
822
823
|
let event = NextBundleSetEvent(bundleId: bundleId)
|
|
823
|
-
|
|
824
|
+
eventEmitter.onNextBundleSet(event)
|
|
824
825
|
}
|
|
825
826
|
|
|
826
827
|
private func notifyReloadedListeners() {
|
|
827
|
-
|
|
828
|
+
eventEmitter.onReloaded()
|
|
828
829
|
}
|
|
829
830
|
|
|
830
831
|
private func addBlockedBundleId(_ bundleId: String) {
|
|
@@ -852,7 +853,7 @@ import CommonCrypto
|
|
|
852
853
|
let newBlockedIds = blockedList.joined(separator: ",")
|
|
853
854
|
preferences.setBlockedBundleIds(newBlockedIds)
|
|
854
855
|
|
|
855
|
-
CAPLog.print("[",
|
|
856
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Bundle blocked: ", bundleId)
|
|
856
857
|
}
|
|
857
858
|
|
|
858
859
|
private func isBlockedBundleId(_ bundleId: String) -> Bool {
|
|
@@ -882,7 +883,7 @@ import CommonCrypto
|
|
|
882
883
|
|
|
883
884
|
if lastVersionCode == nil || lastVersionName == nil || lastVersionCode != currentVersionCode || lastVersionName != currentVersionName {
|
|
884
885
|
CAPLog.print(
|
|
885
|
-
"[",
|
|
886
|
+
"[", LiveUpdate.tag, "] ",
|
|
886
887
|
"App version changed (last: \(lastVersionName ?? "nil")/\(lastVersionCode ?? "nil"), current: \(currentVersionName)/\(currentVersionCode)), resetting config."
|
|
887
888
|
)
|
|
888
889
|
resetConfig()
|
|
@@ -923,9 +924,10 @@ import CommonCrypto
|
|
|
923
924
|
}
|
|
924
925
|
}
|
|
925
926
|
|
|
926
|
-
private func unzipFile(zipFile: URL) -> URL {
|
|
927
|
+
private func unzipFile(zipFile: URL) throws -> URL {
|
|
927
928
|
let destinationDirectory = zipFile.deletingPathExtension()
|
|
928
|
-
|
|
929
|
+
try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
930
|
+
try FileManager.default.unzipItem(at: zipFile, to: destinationDirectory)
|
|
929
931
|
return destinationDirectory
|
|
930
932
|
}
|
|
931
933
|
|
|
@@ -967,7 +969,7 @@ import CommonCrypto
|
|
|
967
969
|
.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
|
|
968
970
|
.replacingOccurrences(of: "\n", with: "")
|
|
969
971
|
guard let publicKeyData = Data(base64Encoded: publicKeyAsBase64) else {
|
|
970
|
-
CAPLog.print("[",
|
|
972
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to decode public key.")
|
|
971
973
|
return false
|
|
972
974
|
}
|
|
973
975
|
let publicKeyAttributes: [CFString: Any] = [
|
|
@@ -979,12 +981,12 @@ import CommonCrypto
|
|
|
979
981
|
var secKeyCreateWithDataError: Unmanaged<CFError>?
|
|
980
982
|
guard let publicKey = SecKeyCreateWithData(publicKeyData as CFData, publicKeyAttributes as CFDictionary, &secKeyCreateWithDataError) else {
|
|
981
983
|
if let error = secKeyCreateWithDataError?.takeRetainedValue() {
|
|
982
|
-
CAPLog.print("[",
|
|
984
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to create public key with error: \(error)")
|
|
983
985
|
}
|
|
984
986
|
return false
|
|
985
987
|
}
|
|
986
988
|
guard let signatureData = Data(base64Encoded: signature) else {
|
|
987
|
-
CAPLog.print("[",
|
|
989
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to decode signature.")
|
|
988
990
|
return false
|
|
989
991
|
}
|
|
990
992
|
|
|
@@ -1014,7 +1016,7 @@ import CommonCrypto
|
|
|
1014
1016
|
let signatureAlgorithm = SecKeyAlgorithm.rsaSignatureDigestPKCS1v15SHA256
|
|
1015
1017
|
let verificationResult = SecKeyVerifySignature(publicKey, signatureAlgorithm, digest as CFData, signatureData as CFData, &secKeyVerifySignatureError)
|
|
1016
1018
|
if let error = secKeyVerifySignatureError?.takeRetainedValue() {
|
|
1017
|
-
CAPLog.print("[",
|
|
1019
|
+
CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to verify signature with error: \(error)")
|
|
1018
1020
|
}
|
|
1019
1021
|
return verificationResult
|
|
1020
1022
|
}
|
|
@@ -7,9 +7,8 @@ import Capacitor
|
|
|
7
7
|
*/
|
|
8
8
|
@objc(LiveUpdatePlugin)
|
|
9
9
|
public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
10
|
-
public static let tag =
|
|
11
|
-
public static let version =
|
|
12
|
-
public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
|
|
10
|
+
public static let tag = LiveUpdate.tag
|
|
11
|
+
public static let version = LiveUpdate.version
|
|
13
12
|
|
|
14
13
|
public let identifier = "LiveUpdatePlugin"
|
|
15
14
|
public let jsName = "LiveUpdate"
|
|
@@ -49,8 +48,13 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
49
48
|
private var implementation: LiveUpdate?
|
|
50
49
|
|
|
51
50
|
override public func load() {
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
guard let bridge = bridge else {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
let config = liveUpdateConfig()
|
|
55
|
+
self.config = config
|
|
56
|
+
let eventEmitter = PluginLiveUpdateEventEmitter(plugin: self)
|
|
57
|
+
self.implementation = LiveUpdate(bridge: bridge, config: config, eventEmitter: eventEmitter)
|
|
54
58
|
|
|
55
59
|
// Notify implementation about load
|
|
56
60
|
implementation?.handleLoad()
|
|
@@ -422,4 +426,24 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
422
426
|
private func resolveCall(_ call: CAPPluginCall, _ result: JSObject) {
|
|
423
427
|
call.resolve(result)
|
|
424
428
|
}
|
|
429
|
+
|
|
430
|
+
private class PluginLiveUpdateEventEmitter: NSObject, LiveUpdateEventEmitter {
|
|
431
|
+
weak var plugin: LiveUpdatePlugin?
|
|
432
|
+
|
|
433
|
+
init(plugin: LiveUpdatePlugin) {
|
|
434
|
+
self.plugin = plugin
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
func onDownloadBundleProgress(_ event: DownloadBundleProgressEvent) {
|
|
438
|
+
plugin?.notifyDownloadBundleProgressListeners(event)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
func onNextBundleSet(_ event: NextBundleSetEvent) {
|
|
442
|
+
plugin?.notifyNextBundleSetListeners(event)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func onReloaded() {
|
|
446
|
+
plugin?.notifyReloadedListeners()
|
|
447
|
+
}
|
|
448
|
+
}
|
|
425
449
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
public class LiveUpdatePreferences: NSObject {
|
|
4
|
+
private static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
|
|
5
|
+
|
|
4
6
|
private let appIdKey = "appId" // DO NOT CHANGE
|
|
5
7
|
private let blockedBundleIdsKey = "blockedBundleIds" // DO NOT CHANGE
|
|
6
8
|
private let channelKey = "channel" // DO NOT CHANGE
|
|
@@ -97,6 +99,6 @@ public class LiveUpdatePreferences: NSObject {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
private func applyPrefix(to key: String) -> String {
|
|
100
|
-
return
|
|
102
|
+
return LiveUpdatePreferences.userDefaultsPrefix + "." + key
|
|
101
103
|
}
|
|
102
104
|
}
|
package/package.json
CHANGED