@capawesome/capacitor-live-update 8.2.3 → 8.3.0-dev.f9cc1d15.1783929784
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 +16 -1
- package/Package.swift +22 -6
- package/README.md +213 -84
- package/android/build.gradle +17 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +69 -20
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +30 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +9 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchLatestBundleOptions.java +15 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +38 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicManager.kt +169 -0
- package/dist/docs.json +14 -2
- package/dist/esm/definitions.d.ts +17 -2
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Plugin/Classes/Options/FetchLatestBundleOptions.swift +12 -0
- package/ios/Plugin/Classes/Results/FetchLatestBundleResult.swift +30 -1
- package/ios/Plugin/Enums/CustomError.swift +12 -0
- package/ios/Plugin/LiveUpdate.swift +23 -9
- package/ios/Plugin/LiveUpdatePlugin.swift +2 -2
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +1 -0
- package/ios/Plugin/Providers/Ionic/LiveUpdateIonicManager.swift +108 -0
- package/ios/Plugin/Providers/Ionic/LiveUpdatePlugin+LiveUpdateProvider.swift +15 -0
- package/package.json +11 -5
|
@@ -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;
|
|
@@ -87,12 +88,15 @@ public class LiveUpdate {
|
|
|
87
88
|
@NonNull
|
|
88
89
|
private final LiveUpdateConfig config;
|
|
89
90
|
|
|
91
|
+
@NonNull
|
|
92
|
+
private final Context context;
|
|
93
|
+
|
|
90
94
|
private final String defaultWebAssetDir = Bridge.DEFAULT_WEB_ASSET_DIR;
|
|
91
95
|
|
|
92
96
|
@NonNull
|
|
93
97
|
private final LiveUpdateHttpClient httpClient;
|
|
94
98
|
|
|
95
|
-
@
|
|
99
|
+
@Nullable
|
|
96
100
|
private final LiveUpdatePlugin plugin;
|
|
97
101
|
|
|
98
102
|
@NonNull
|
|
@@ -111,11 +115,26 @@ public class LiveUpdate {
|
|
|
111
115
|
private boolean syncInProgress = false;
|
|
112
116
|
|
|
113
117
|
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
|
|
118
|
+
this(config, plugin.getContext(), plugin);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates a headless instance without a Capacitor plugin, e.g. for Ionic Portals hosts
|
|
123
|
+
* that construct a provider manager directly. WebView-related operations (server path
|
|
124
|
+
* changes, reload) and plugin events are skipped in this mode.
|
|
125
|
+
*/
|
|
126
|
+
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull Context context) throws PackageManager.NameNotFoundException {
|
|
127
|
+
this(config, context, null);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull Context context, @Nullable LiveUpdatePlugin plugin)
|
|
131
|
+
throws PackageManager.NameNotFoundException {
|
|
114
132
|
this.config = config;
|
|
133
|
+
this.context = context;
|
|
115
134
|
this.httpClient = new LiveUpdateHttpClient(config);
|
|
116
135
|
this.plugin = plugin;
|
|
117
|
-
this.preferences = new LiveUpdatePreferences(
|
|
118
|
-
this.webViewSettingsEditor =
|
|
136
|
+
this.preferences = new LiveUpdatePreferences(context);
|
|
137
|
+
this.webViewSettingsEditor = context.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
|
|
119
138
|
|
|
120
139
|
// Check version and reset config if version changed
|
|
121
140
|
checkAndResetConfigIfVersionChanged();
|
|
@@ -232,6 +251,7 @@ public class LiveUpdate {
|
|
|
232
251
|
public void success(@Nullable GetLatestBundleResponse response) {
|
|
233
252
|
ArtifactType artifactType = response == null ? null : response.getArtifactType();
|
|
234
253
|
String bundleId = response == null ? null : response.getBundleId();
|
|
254
|
+
String channel = response == null ? null : response.getChannelName();
|
|
235
255
|
String checksum = response == null ? null : response.getChecksum();
|
|
236
256
|
JSONObject customProperties = response == null ? null : response.getCustomProperties();
|
|
237
257
|
String downloadUrl = response == null ? null : response.getUrl();
|
|
@@ -239,6 +259,7 @@ public class LiveUpdate {
|
|
|
239
259
|
FetchLatestBundleResult result = new FetchLatestBundleResult(
|
|
240
260
|
artifactType,
|
|
241
261
|
bundleId,
|
|
262
|
+
channel,
|
|
242
263
|
checksum,
|
|
243
264
|
customProperties,
|
|
244
265
|
downloadUrl,
|
|
@@ -298,6 +319,17 @@ public class LiveUpdate {
|
|
|
298
319
|
callback.success(result);
|
|
299
320
|
}
|
|
300
321
|
|
|
322
|
+
/**
|
|
323
|
+
* @return The on-disk directory of a downloaded bundle, or `null` if the bundle does not exist.
|
|
324
|
+
*/
|
|
325
|
+
@Nullable
|
|
326
|
+
public File getBundleDirectory(@NonNull String bundleId) {
|
|
327
|
+
if (!hasBundleById(bundleId)) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return buildBundleDirectoryFor(bundleId);
|
|
331
|
+
}
|
|
332
|
+
|
|
301
333
|
public void getCustomId(@NonNull NonEmptyCallback callback) {
|
|
302
334
|
String customId = preferences.getCustomId();
|
|
303
335
|
GetCustomIdResult result = new GetCustomIdResult(customId);
|
|
@@ -547,21 +579,21 @@ public class LiveUpdate {
|
|
|
547
579
|
}
|
|
548
580
|
|
|
549
581
|
private File buildBundlesDirectory() {
|
|
550
|
-
return new File(
|
|
582
|
+
return new File(context.getFilesDir(), bundlesDirectory);
|
|
551
583
|
}
|
|
552
584
|
|
|
553
585
|
private File buildBundleDirectoryFor(@NonNull String bundleId) {
|
|
554
|
-
return new File(
|
|
586
|
+
return new File(context.getFilesDir(), bundlesDirectory + "/" + bundleId);
|
|
555
587
|
}
|
|
556
588
|
|
|
557
589
|
private File buildTemporaryDirectory() {
|
|
558
590
|
String fileName = UUID.randomUUID().toString();
|
|
559
|
-
return new File(
|
|
591
|
+
return new File(context.getCacheDir(), fileName);
|
|
560
592
|
}
|
|
561
593
|
|
|
562
594
|
private File buildTemporaryZipFile() {
|
|
563
595
|
String fileName = UUID.randomUUID().toString() + ".zip";
|
|
564
|
-
return new File(
|
|
596
|
+
return new File(context.getCacheDir(), fileName);
|
|
565
597
|
}
|
|
566
598
|
|
|
567
599
|
private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
|
|
@@ -569,7 +601,7 @@ public class LiveUpdate {
|
|
|
569
601
|
String currentBundleId = getCurrentBundleId();
|
|
570
602
|
if (currentBundleId == null) {
|
|
571
603
|
// Create the source input stream
|
|
572
|
-
AssetManager assets =
|
|
604
|
+
AssetManager assets = context.getAssets();
|
|
573
605
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
|
|
574
606
|
// Create the destination file
|
|
575
607
|
File destination = new File(destinationDirectory, href);
|
|
@@ -975,13 +1007,17 @@ public class LiveUpdate {
|
|
|
975
1007
|
@NonNull NonEmptyCallback<GetLatestBundleResponse> callback
|
|
976
1008
|
) {
|
|
977
1009
|
try {
|
|
1010
|
+
String appId = options.getAppId() == null ? getAppId() : options.getAppId();
|
|
1011
|
+
if (appId == null || appId.isEmpty()) {
|
|
1012
|
+
throw new Exception(LiveUpdatePlugin.ERROR_APP_ID_MISSING);
|
|
1013
|
+
}
|
|
978
1014
|
String channel = options.getChannel() == null ? getChannel() : options.getChannel();
|
|
979
1015
|
String url = new HttpUrl.Builder()
|
|
980
1016
|
.scheme("https")
|
|
981
1017
|
.host(config.getServerDomain())
|
|
982
1018
|
.addPathSegment("v1")
|
|
983
1019
|
.addPathSegment("apps")
|
|
984
|
-
.addPathSegment(
|
|
1020
|
+
.addPathSegment(appId)
|
|
985
1021
|
.addPathSegment("bundles")
|
|
986
1022
|
.addPathSegment("latest")
|
|
987
1023
|
.addQueryParameter("appVersionCode", getVersionCodeAsString())
|
|
@@ -1095,14 +1131,11 @@ public class LiveUpdate {
|
|
|
1095
1131
|
|
|
1096
1132
|
@Nullable
|
|
1097
1133
|
private String getNativeChannel() {
|
|
1098
|
-
int resId =
|
|
1099
|
-
.getContext()
|
|
1100
|
-
.getResources()
|
|
1101
|
-
.getIdentifier("capawesome_live_update_default_channel", "string", plugin.getContext().getPackageName());
|
|
1134
|
+
int resId = context.getResources().getIdentifier("capawesome_live_update_default_channel", "string", context.getPackageName());
|
|
1102
1135
|
if (resId == 0) {
|
|
1103
1136
|
return null;
|
|
1104
1137
|
}
|
|
1105
|
-
return
|
|
1138
|
+
return context.getResources().getString(resId);
|
|
1106
1139
|
}
|
|
1107
1140
|
|
|
1108
1141
|
/**
|
|
@@ -1121,6 +1154,10 @@ public class LiveUpdate {
|
|
|
1121
1154
|
* @return The absolute path to the current bundle directory (`public` for the built-in bundle).
|
|
1122
1155
|
*/
|
|
1123
1156
|
private String getCurrentCapacitorServerPath() {
|
|
1157
|
+
if (plugin == null) {
|
|
1158
|
+
// Headless mode: there is no WebView, so the default bundle is always current.
|
|
1159
|
+
return defaultWebAssetDir;
|
|
1160
|
+
}
|
|
1124
1161
|
return plugin.getBridge().getServerBasePath();
|
|
1125
1162
|
}
|
|
1126
1163
|
|
|
@@ -1151,8 +1188,7 @@ public class LiveUpdate {
|
|
|
1151
1188
|
*/
|
|
1152
1189
|
@NonNull
|
|
1153
1190
|
private String getNextCapacitorServerPath() {
|
|
1154
|
-
String path =
|
|
1155
|
-
.getContext()
|
|
1191
|
+
String path = context
|
|
1156
1192
|
.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
|
|
1157
1193
|
.getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
|
|
1158
1194
|
// Empty path means default path
|
|
@@ -1197,7 +1233,7 @@ public class LiveUpdate {
|
|
|
1197
1233
|
private Manifest loadCurrentManifest() throws Exception {
|
|
1198
1234
|
String currentBundleId = getCurrentBundleId();
|
|
1199
1235
|
if (currentBundleId == null) {
|
|
1200
|
-
AssetManager assets =
|
|
1236
|
+
AssetManager assets = context.getAssets();
|
|
1201
1237
|
boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
1202
1238
|
if (manifestFileExists) {
|
|
1203
1239
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
@@ -1229,6 +1265,9 @@ public class LiveUpdate {
|
|
|
1229
1265
|
}
|
|
1230
1266
|
|
|
1231
1267
|
private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
|
|
1268
|
+
if (plugin == null) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1232
1271
|
plugin.notifyDownloadBundleProgressListeners(event);
|
|
1233
1272
|
}
|
|
1234
1273
|
|
|
@@ -1315,6 +1354,10 @@ public class LiveUpdate {
|
|
|
1315
1354
|
}
|
|
1316
1355
|
|
|
1317
1356
|
private void setCurrentCapacitorServerPath(@NonNull String path) {
|
|
1357
|
+
if (plugin == null) {
|
|
1358
|
+
// Headless mode: there is no WebView to point at the new path.
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1318
1361
|
if (path.equals(defaultWebAssetDir)) {
|
|
1319
1362
|
this.plugin.getBridge().setServerAssetPath(path);
|
|
1320
1363
|
} else {
|
|
@@ -1341,11 +1384,17 @@ public class LiveUpdate {
|
|
|
1341
1384
|
}
|
|
1342
1385
|
|
|
1343
1386
|
private void notifyNextBundleSetListeners(@Nullable String bundleId) {
|
|
1387
|
+
if (plugin == null) {
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1344
1390
|
NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
|
|
1345
1391
|
plugin.notifyNextBundleSetListeners(event);
|
|
1346
1392
|
}
|
|
1347
1393
|
|
|
1348
1394
|
private void notifyReloadedListeners() {
|
|
1395
|
+
if (plugin == null) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1349
1398
|
plugin.notifyReloadedListeners();
|
|
1350
1399
|
}
|
|
1351
1400
|
|
|
@@ -1498,11 +1547,11 @@ public class LiveUpdate {
|
|
|
1498
1547
|
}
|
|
1499
1548
|
|
|
1500
1549
|
private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
|
1501
|
-
String packageName = this.
|
|
1550
|
+
String packageName = this.context.getPackageName();
|
|
1502
1551
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1503
|
-
return this.
|
|
1552
|
+
return this.context.getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
|
|
1504
1553
|
} else {
|
|
1505
|
-
return this.
|
|
1554
|
+
return this.context.getPackageManager().getPackageInfo(packageName, 0);
|
|
1506
1555
|
}
|
|
1507
1556
|
}
|
|
1508
1557
|
}
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package io.capawesome.capacitorjs.plugins.liveupdate;
|
|
2
2
|
|
|
3
|
+
import android.content.Context;
|
|
3
4
|
import android.webkit.WebView;
|
|
4
5
|
import androidx.annotation.NonNull;
|
|
5
6
|
import androidx.annotation.Nullable;
|
|
@@ -32,14 +33,19 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingRes
|
|
|
32
33
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
|
|
33
34
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
34
35
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
36
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic.LiveUpdateIonicManager;
|
|
37
|
+
import io.ionic.liveupdateprovider.ProviderError;
|
|
38
|
+
import io.ionic.liveupdateprovider.ProviderManager;
|
|
39
|
+
import java.util.Map;
|
|
35
40
|
|
|
36
41
|
@CapacitorPlugin(name = "LiveUpdate")
|
|
37
42
|
public class LiveUpdatePlugin extends Plugin {
|
|
38
43
|
|
|
39
44
|
public static final String TAG = "LiveUpdate";
|
|
40
|
-
public static final String VERSION = "8.
|
|
45
|
+
public static final String VERSION = "8.3.0";
|
|
41
46
|
public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
|
|
42
47
|
public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
|
|
48
|
+
public static final String ERROR_BUNDLE_DIRECTORY_NOT_FOUND = "Bundle directory could not be resolved.";
|
|
43
49
|
public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
|
|
44
50
|
public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
|
|
45
51
|
public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
|
|
@@ -48,7 +54,10 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
48
54
|
public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
|
|
49
55
|
public static final String ERROR_CUSTOM_ID_MISSING = "customId must be provided.";
|
|
50
56
|
public static final String ERROR_DOWNLOAD_FAILED = "Bundle could not be downloaded.";
|
|
57
|
+
public static final String ERROR_DOWNLOAD_URL_MISSING = "Bundle does not have a valid download URL.";
|
|
51
58
|
public static final String ERROR_HTTP_TIMEOUT = "Request timed out.";
|
|
59
|
+
public static final String ERROR_MANAGER_KEY_MISSING = "managerKey must be provided.";
|
|
60
|
+
public static final String ERROR_PLUGIN_NOT_INITIALIZED = "Plugin is not initialized.";
|
|
52
61
|
public static final String ERROR_URL_MISSING = "url must be provided.";
|
|
53
62
|
public static final String ERROR_SIGNATURE_VERIFICATION_FAILED = "Signature verification failed.";
|
|
54
63
|
public static final String ERROR_PUBLIC_KEY_INVALID = "Invalid public key.";
|
|
@@ -76,6 +85,26 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
76
85
|
}
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Creates a manager for the Ionic Live Update Provider SDK. Invoked natively by
|
|
90
|
+
* Federated Capacitor after resolving this plugin by its Capacitor plugin name.
|
|
91
|
+
*
|
|
92
|
+
* This class deliberately does NOT implement the SDK's `LiveUpdateProvider` interface:
|
|
93
|
+
* superinterfaces are resolved eagerly at class-definition time, which would crash the
|
|
94
|
+
* plugin when the compileOnly SDK is absent at runtime. Instead, this method matches the
|
|
95
|
+
* interface's exact signature and Federated Capacitor invokes it via reflection when the
|
|
96
|
+
* cast to `LiveUpdateProvider` fails. Method references are resolved lazily, so this
|
|
97
|
+
* class loads fine without the SDK. Do NOT change the signature.
|
|
98
|
+
*/
|
|
99
|
+
@NonNull
|
|
100
|
+
public ProviderManager createManager(@NonNull Context context, @NonNull Map<String, ?> configuration)
|
|
101
|
+
throws ProviderError.InvalidConfiguration {
|
|
102
|
+
if (implementation == null) {
|
|
103
|
+
throw new ProviderError.InvalidConfiguration(ERROR_PLUGIN_NOT_INITIALIZED, null);
|
|
104
|
+
}
|
|
105
|
+
return new LiveUpdateIonicManager(context, configuration, implementation);
|
|
106
|
+
}
|
|
107
|
+
|
|
79
108
|
@Override
|
|
80
109
|
protected void handleOnResume() {
|
|
81
110
|
super.handleOnResume();
|
|
@@ -16,6 +16,9 @@ public class GetLatestBundleResponse {
|
|
|
16
16
|
@NonNull
|
|
17
17
|
private String bundleId;
|
|
18
18
|
|
|
19
|
+
@Nullable
|
|
20
|
+
private String channelName;
|
|
21
|
+
|
|
19
22
|
@Nullable
|
|
20
23
|
private String checksum;
|
|
21
24
|
|
|
@@ -36,6 +39,7 @@ public class GetLatestBundleResponse {
|
|
|
36
39
|
this.artifactType = ArtifactType.ZIP;
|
|
37
40
|
}
|
|
38
41
|
this.bundleId = responseJson.optString("bundleId");
|
|
42
|
+
this.channelName = responseJson.isNull("channelName") ? null : responseJson.optString("channelName");
|
|
39
43
|
this.checksum = responseJson.isNull("checksum") ? null : responseJson.optString("checksum");
|
|
40
44
|
this.customProperties = responseJson.optJSONObject("customProperties");
|
|
41
45
|
this.signature = responseJson.isNull("signature") ? null : responseJson.optString("signature");
|
|
@@ -52,6 +56,11 @@ public class GetLatestBundleResponse {
|
|
|
52
56
|
return bundleId;
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
@Nullable
|
|
60
|
+
public String getChannelName() {
|
|
61
|
+
return channelName;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
@Nullable
|
|
56
65
|
public JSONObject getCustomProperties() {
|
|
57
66
|
return customProperties;
|
|
@@ -6,17 +6,32 @@ import com.getcapacitor.PluginCall;
|
|
|
6
6
|
|
|
7
7
|
public class FetchLatestBundleOptions {
|
|
8
8
|
|
|
9
|
+
@Nullable
|
|
10
|
+
private final String appId;
|
|
11
|
+
|
|
9
12
|
@Nullable
|
|
10
13
|
private final String channel;
|
|
11
14
|
|
|
12
15
|
public FetchLatestBundleOptions(@NonNull PluginCall call) {
|
|
16
|
+
this.appId = null;
|
|
13
17
|
this.channel = call.getString("channel", null);
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
public FetchLatestBundleOptions(@Nullable String channel) {
|
|
21
|
+
this.appId = null;
|
|
22
|
+
this.channel = channel;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public FetchLatestBundleOptions(@Nullable String appId, @Nullable String channel) {
|
|
26
|
+
this.appId = appId;
|
|
17
27
|
this.channel = channel;
|
|
18
28
|
}
|
|
19
29
|
|
|
30
|
+
@Nullable
|
|
31
|
+
public String getAppId() {
|
|
32
|
+
return appId;
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
@Nullable
|
|
21
36
|
public String getChannel() {
|
|
22
37
|
return channel;
|
|
@@ -15,6 +15,9 @@ public class FetchLatestBundleResult implements Result {
|
|
|
15
15
|
@Nullable
|
|
16
16
|
private final String bundleId;
|
|
17
17
|
|
|
18
|
+
@Nullable
|
|
19
|
+
private final String channel;
|
|
20
|
+
|
|
18
21
|
@Nullable
|
|
19
22
|
private final String checksum;
|
|
20
23
|
|
|
@@ -30,6 +33,7 @@ public class FetchLatestBundleResult implements Result {
|
|
|
30
33
|
public FetchLatestBundleResult(
|
|
31
34
|
@Nullable ArtifactType artifactType,
|
|
32
35
|
@Nullable String bundleId,
|
|
36
|
+
@Nullable String channel,
|
|
33
37
|
@Nullable String checksum,
|
|
34
38
|
@Nullable JSONObject customProperties,
|
|
35
39
|
@Nullable String downloadUrl,
|
|
@@ -37,12 +41,43 @@ public class FetchLatestBundleResult implements Result {
|
|
|
37
41
|
) {
|
|
38
42
|
this.artifactType = artifactType;
|
|
39
43
|
this.bundleId = bundleId;
|
|
44
|
+
this.channel = channel;
|
|
40
45
|
this.checksum = checksum;
|
|
41
46
|
this.customProperties = customProperties;
|
|
42
47
|
this.downloadUrl = downloadUrl;
|
|
43
48
|
this.signature = signature;
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
@Nullable
|
|
52
|
+
public ArtifactType getArtifactType() {
|
|
53
|
+
return artifactType;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Nullable
|
|
57
|
+
public String getBundleId() {
|
|
58
|
+
return bundleId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Nullable
|
|
62
|
+
public String getChecksum() {
|
|
63
|
+
return checksum;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Nullable
|
|
67
|
+
public JSONObject getCustomProperties() {
|
|
68
|
+
return customProperties;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Nullable
|
|
72
|
+
public String getDownloadUrl() {
|
|
73
|
+
return downloadUrl;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Nullable
|
|
77
|
+
public String getSignature() {
|
|
78
|
+
return signature;
|
|
79
|
+
}
|
|
80
|
+
|
|
46
81
|
@NonNull
|
|
47
82
|
public JSObject toJSObject() {
|
|
48
83
|
JSObject result = new JSObject();
|
|
@@ -52,6 +87,9 @@ public class FetchLatestBundleResult implements Result {
|
|
|
52
87
|
result.put("artifactType", "zip");
|
|
53
88
|
}
|
|
54
89
|
result.put("bundleId", bundleId == null ? JSONObject.NULL : bundleId);
|
|
90
|
+
if (channel != null) {
|
|
91
|
+
result.put("channel", channel);
|
|
92
|
+
}
|
|
55
93
|
if (checksum != null) {
|
|
56
94
|
result.put("checksum", checksum);
|
|
57
95
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
package io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import com.getcapacitor.Logger
|
|
6
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdate
|
|
7
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdateConfig
|
|
8
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdatePlugin
|
|
9
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions
|
|
10
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions
|
|
11
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult
|
|
12
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType
|
|
13
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback
|
|
14
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback
|
|
15
|
+
import io.ionic.liveupdateprovider.MetadataSyncResult
|
|
16
|
+
import io.ionic.liveupdateprovider.ProviderError
|
|
17
|
+
import io.ionic.liveupdateprovider.ProviderManager
|
|
18
|
+
import io.ionic.liveupdateprovider.ProviderSyncResult
|
|
19
|
+
import java.io.File
|
|
20
|
+
import kotlin.coroutines.resume
|
|
21
|
+
import kotlin.coroutines.resumeWithException
|
|
22
|
+
import kotlin.coroutines.suspendCoroutine
|
|
23
|
+
import org.json.JSONObject
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ionic Live Update Provider manager backed by the Capawesome live update plugin.
|
|
27
|
+
*
|
|
28
|
+
* Each manager instance is scoped by a `managerKey` so that multiple shells (Portals or
|
|
29
|
+
* Federated Capacitor apps) can persist their own active bundle without colliding with each
|
|
30
|
+
* other or with the standalone plugin's `currentBundleId` / `nextBundleId` state.
|
|
31
|
+
*
|
|
32
|
+
* Provider configuration keys (V1):
|
|
33
|
+
* - `managerKey` (required) — scopes per-manager persisted state
|
|
34
|
+
* - `appId` (optional) — Capawesome Cloud app UUID; falls back to plugin config
|
|
35
|
+
* - `channel` (optional) — channel to sync; falls back to plugin config
|
|
36
|
+
*/
|
|
37
|
+
class LiveUpdateIonicManager
|
|
38
|
+
@Throws(ProviderError.InvalidConfiguration::class)
|
|
39
|
+
constructor(context: Context, configuration: Map<String, *>, private val liveUpdate: LiveUpdate) : ProviderManager {
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a manager without a running Capacitor plugin, e.g. for Ionic Portals hosts
|
|
43
|
+
* that construct the manager directly and attach it to a Portal via
|
|
44
|
+
* `setLiveUpdateProviderManager(...)`. Uses a headless [LiveUpdate] instance with the
|
|
45
|
+
* default configuration; pass `appId` and `channel` via the configuration map.
|
|
46
|
+
*/
|
|
47
|
+
@Throws(ProviderError.InvalidConfiguration::class)
|
|
48
|
+
constructor(context: Context, configuration: Map<String, *>) : this(context, configuration, LiveUpdate(LiveUpdateConfig(), context))
|
|
49
|
+
|
|
50
|
+
companion object {
|
|
51
|
+
const val SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdateIonicProvider"
|
|
52
|
+
const val PREF_PREFIX_LAST_SYNCED_BUNDLE_ID = "lastSyncedBundleId."
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override var latestAppDirectory: File? = null
|
|
56
|
+
private set
|
|
57
|
+
|
|
58
|
+
private val appId: String?
|
|
59
|
+
private val channel: String?
|
|
60
|
+
private val managerKey: String
|
|
61
|
+
private val sharedPreferences: SharedPreferences
|
|
62
|
+
|
|
63
|
+
init {
|
|
64
|
+
val managerKeyValue = configuration["managerKey"]
|
|
65
|
+
if (managerKeyValue !is String || managerKeyValue.isEmpty()) {
|
|
66
|
+
throw ProviderError.InvalidConfiguration(LiveUpdatePlugin.ERROR_MANAGER_KEY_MISSING)
|
|
67
|
+
}
|
|
68
|
+
managerKey = managerKeyValue
|
|
69
|
+
appId = configuration["appId"] as? String
|
|
70
|
+
channel = configuration["channel"] as? String
|
|
71
|
+
sharedPreferences = context.applicationContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
72
|
+
|
|
73
|
+
// Restore the last synced bundle directory, if any.
|
|
74
|
+
val persistedBundleId = sharedPreferences.getString(getLastSyncedBundleIdKey(), null)
|
|
75
|
+
if (persistedBundleId != null) {
|
|
76
|
+
latestAppDirectory = liveUpdate.getBundleDirectory(persistedBundleId)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override suspend fun sync(): ProviderSyncResult? {
|
|
81
|
+
try {
|
|
82
|
+
val result = fetchLatestBundle()
|
|
83
|
+
|
|
84
|
+
// No update available; nothing to report.
|
|
85
|
+
val bundleId = result.bundleId ?: return null
|
|
86
|
+
|
|
87
|
+
// Bundle already on disk — just point latestAppDirectory at it and persist.
|
|
88
|
+
val existingDirectory = liveUpdate.getBundleDirectory(bundleId)
|
|
89
|
+
if (existingDirectory != null) {
|
|
90
|
+
applySyncedBundle(bundleId, existingDirectory)
|
|
91
|
+
return MetadataSyncResult(buildMetadata(bundleId, result.customProperties))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Otherwise, download and then apply.
|
|
95
|
+
downloadBundle(bundleId, result)
|
|
96
|
+
val downloadedDirectory =
|
|
97
|
+
liveUpdate.getBundleDirectory(bundleId) ?: throw Exception(LiveUpdatePlugin.ERROR_BUNDLE_DIRECTORY_NOT_FOUND)
|
|
98
|
+
applySyncedBundle(bundleId, downloadedDirectory)
|
|
99
|
+
return MetadataSyncResult(buildMetadata(bundleId, result.customProperties))
|
|
100
|
+
} catch (exception: Exception) {
|
|
101
|
+
Logger.error(LiveUpdatePlugin.TAG, "Ionic provider sync failed: ${exception.message}", exception)
|
|
102
|
+
throw exception
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun applySyncedBundle(bundleId: String, directory: File) {
|
|
107
|
+
latestAppDirectory = directory
|
|
108
|
+
sharedPreferences.edit().putString(getLastSyncedBundleIdKey(), bundleId).apply()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private fun buildMetadata(bundleId: String, customProperties: JSONObject?): Map<String, Any> {
|
|
112
|
+
val metadata = mutableMapOf<String, Any>("bundleId" to bundleId)
|
|
113
|
+
channel?.let { metadata["channel"] = it }
|
|
114
|
+
customProperties?.let {
|
|
115
|
+
val customPropertiesMap = jsonObjectToMap(it)
|
|
116
|
+
if (customPropertiesMap.isNotEmpty()) {
|
|
117
|
+
metadata["customProperties"] = customPropertiesMap
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return metadata
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private suspend fun downloadBundle(bundleId: String, result: FetchLatestBundleResult) {
|
|
124
|
+
val downloadUrl = result.downloadUrl
|
|
125
|
+
if (downloadUrl.isNullOrEmpty()) {
|
|
126
|
+
throw Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_URL_MISSING)
|
|
127
|
+
}
|
|
128
|
+
val artifactType = if (result.artifactType == ArtifactType.MANIFEST) "manifest" else "zip"
|
|
129
|
+
val options = DownloadBundleOptions(artifactType, bundleId, result.checksum, result.signature, downloadUrl)
|
|
130
|
+
return suspendCoroutine { continuation ->
|
|
131
|
+
liveUpdate.downloadBundle(
|
|
132
|
+
options,
|
|
133
|
+
object : EmptyCallback {
|
|
134
|
+
override fun success() = continuation.resume(Unit)
|
|
135
|
+
|
|
136
|
+
override fun error(exception: Exception) = continuation.resumeWithException(exception)
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private suspend fun fetchLatestBundle(): FetchLatestBundleResult = suspendCoroutine { continuation ->
|
|
143
|
+
liveUpdate.fetchLatestBundle(
|
|
144
|
+
FetchLatestBundleOptions(appId, channel),
|
|
145
|
+
object : NonEmptyCallback<FetchLatestBundleResult> {
|
|
146
|
+
override fun success(result: FetchLatestBundleResult) = continuation.resume(result)
|
|
147
|
+
|
|
148
|
+
override fun error(exception: Exception) = continuation.resumeWithException(exception)
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private fun getLastSyncedBundleIdKey(): String {
|
|
154
|
+
return PREF_PREFIX_LAST_SYNCED_BUNDLE_ID + managerKey
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun jsonObjectToMap(jsonObject: JSONObject): Map<String, Any> {
|
|
158
|
+
val map = mutableMapOf<String, Any>()
|
|
159
|
+
val keys = jsonObject.keys()
|
|
160
|
+
while (keys.hasNext()) {
|
|
161
|
+
val key = keys.next()
|
|
162
|
+
val value = jsonObject.opt(key)
|
|
163
|
+
if (value != null && value != JSONObject.NULL) {
|
|
164
|
+
map[key] = value
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return map
|
|
168
|
+
}
|
|
169
|
+
}
|
package/dist/docs.json
CHANGED
|
@@ -881,6 +881,18 @@
|
|
|
881
881
|
"complexTypes": [],
|
|
882
882
|
"type": "string | null"
|
|
883
883
|
},
|
|
884
|
+
{
|
|
885
|
+
"name": "channel",
|
|
886
|
+
"tags": [
|
|
887
|
+
{
|
|
888
|
+
"text": "8.3.0",
|
|
889
|
+
"name": "since"
|
|
890
|
+
}
|
|
891
|
+
],
|
|
892
|
+
"docs": "The name of the channel that the bundle is actually from.\n\nThis is the resolved channel after applying any forced channel assignment\nand may differ from the channel set by the Live Update SDK.",
|
|
893
|
+
"complexTypes": [],
|
|
894
|
+
"type": "string | undefined"
|
|
895
|
+
},
|
|
884
896
|
{
|
|
885
897
|
"name": "checksum",
|
|
886
898
|
"tags": [
|
|
@@ -955,7 +967,7 @@
|
|
|
955
967
|
"name": "since"
|
|
956
968
|
}
|
|
957
969
|
],
|
|
958
|
-
"docs": "The
|
|
970
|
+
"docs": "The preferred channel name from which the latest bundle should be fetched.\n\nThis is the SDK preference and may be overridden by a forced channel assignment\nconfigured in the Capawesome Cloud Console.",
|
|
959
971
|
"complexTypes": [],
|
|
960
972
|
"type": "string | undefined"
|
|
961
973
|
}
|
|
@@ -1521,7 +1533,7 @@
|
|
|
1521
1533
|
"name": "since"
|
|
1522
1534
|
}
|
|
1523
1535
|
],
|
|
1524
|
-
"docs": "The
|
|
1536
|
+
"docs": "The preferred channel name from which the latest bundle should be fetched.\n\nThis is the SDK preference and may be overridden by a forced channel assignment\nconfigured in the Capawesome Cloud Console.",
|
|
1525
1537
|
"complexTypes": [],
|
|
1526
1538
|
"type": "string | undefined"
|
|
1527
1539
|
}
|