@capawesome/capacitor-live-update 6.8.0 → 6.9.1
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/README.md +429 -20
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +894 -279
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +35 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +56 -11
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +274 -38
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +52 -3
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetChannelsResponseItem.java +28 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +11 -2
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/DownloadBundleProgressEvent.java +32 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/NextBundleSetEvent.java +26 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchChannelsOptions.java +38 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetConfigOptions.java +19 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SyncOptions.java +4 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ChannelResult.java +28 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchChannelsResult.java +28 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +8 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBlockedBundlesResult.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetConfigResult.java +40 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetDownloadedBundlesResult.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/IsSyncingResult.java +26 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ReadyResult.java +32 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/DownloadProgressCallback.java +5 -0
- package/dist/docs.json +853 -50
- package/dist/esm/definitions.d.ts +411 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +10 -2
- package/dist/esm/web.js +24 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +24 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +24 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/Classes/Events/DownloadBundleProgressEvent.swift +25 -0
- package/ios/Plugin/Classes/Events/NextBundleSetEvent.swift +16 -0
- package/ios/Plugin/Classes/Options/FetchChannelsOptions.swift +26 -0
- package/ios/Plugin/Classes/Options/SetConfigOptions.swift +14 -0
- package/ios/Plugin/Classes/Options/SyncOptions.swift +4 -0
- package/ios/Plugin/Classes/Results/ChannelResult.swift +19 -0
- package/ios/Plugin/Classes/Results/FetchChannelsResult.swift +16 -0
- package/ios/Plugin/Classes/Results/{FetchLatestBundle.swift → FetchLatestBundleResult.swift} +6 -1
- package/ios/Plugin/Classes/Results/GetBlockedBundlesResult.swift +16 -0
- package/ios/Plugin/Classes/Results/GetConfigResult.swift +19 -0
- package/ios/Plugin/Classes/Results/GetDownloadedBundlesResult.swift +16 -0
- package/ios/Plugin/Classes/Results/IsSyncingResult.swift +16 -0
- package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
- package/ios/Plugin/LiveUpdate.swift +456 -152
- package/ios/Plugin/LiveUpdateConfig.swift +3 -0
- package/ios/Plugin/LiveUpdateHttpClient.swift +21 -4
- package/ios/Plugin/LiveUpdatePlugin.swift +174 -14
- package/ios/Plugin/LiveUpdatePreferences.swift +54 -12
- package/ios/Plugin/Protocols/Api/GetChannelsResponseItem.swift +4 -0
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +1 -0
- package/package.json +5 -5
- package/ios/Plugin/LiveUpdatePlugin.h +0 -10
- package/ios/Plugin/LiveUpdatePlugin.m +0 -27
|
@@ -10,31 +10,45 @@ import android.os.Handler;
|
|
|
10
10
|
import android.util.Base64;
|
|
11
11
|
import androidx.annotation.NonNull;
|
|
12
12
|
import androidx.annotation.Nullable;
|
|
13
|
+
import com.getcapacitor.Bridge;
|
|
13
14
|
import com.getcapacitor.Logger;
|
|
14
15
|
import com.getcapacitor.plugin.WebView;
|
|
15
16
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.Manifest;
|
|
16
17
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.ManifestItem;
|
|
18
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetChannelsResponseItem;
|
|
17
19
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
|
|
20
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.DownloadBundleProgressEvent;
|
|
21
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.NextBundleSetEvent;
|
|
18
22
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundleOptions;
|
|
19
23
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
|
|
24
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchChannelsOptions;
|
|
20
25
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
|
|
21
26
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetBundleOptions;
|
|
22
27
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
|
|
28
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetConfigOptions;
|
|
23
29
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
|
|
24
30
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetNextBundleOptions;
|
|
25
31
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SyncOptions;
|
|
32
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.ChannelResult;
|
|
33
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchChannelsResult;
|
|
26
34
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
|
|
35
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBlockedBundlesResult;
|
|
27
36
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundleResult;
|
|
28
37
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundlesResult;
|
|
29
38
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetChannelResult;
|
|
39
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetConfigResult;
|
|
30
40
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCurrentBundleResult;
|
|
31
41
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetCustomIdResult;
|
|
32
42
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDeviceIdResult;
|
|
43
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDownloadedBundlesResult;
|
|
33
44
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetNextBundleResult;
|
|
34
45
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionCodeResult;
|
|
35
46
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetVersionNameResult;
|
|
47
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingResult;
|
|
48
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.ReadyResult;
|
|
36
49
|
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
|
|
37
50
|
import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
|
|
51
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.DownloadProgressCallback;
|
|
38
52
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
|
|
39
53
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
40
54
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
@@ -52,8 +66,12 @@ import java.util.ArrayList;
|
|
|
52
66
|
import java.util.Arrays;
|
|
53
67
|
import java.util.List;
|
|
54
68
|
import java.util.UUID;
|
|
69
|
+
import java.util.concurrent.CountDownLatch;
|
|
70
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
71
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
55
72
|
import java.util.concurrent.atomic.AtomicReference;
|
|
56
73
|
import net.lingala.zip4j.ZipFile;
|
|
74
|
+
import okhttp3.Call;
|
|
57
75
|
import okhttp3.HttpUrl;
|
|
58
76
|
import okhttp3.Response;
|
|
59
77
|
import okhttp3.ResponseBody;
|
|
@@ -65,13 +83,12 @@ import org.json.JSONObject;
|
|
|
65
83
|
|
|
66
84
|
public class LiveUpdate {
|
|
67
85
|
|
|
86
|
+
private final long autoUpdateIntervalMs = 15 * 60 * 1000; // 15 minutes
|
|
87
|
+
|
|
68
88
|
@NonNull
|
|
69
89
|
private final LiveUpdateConfig config;
|
|
70
90
|
|
|
71
|
-
private final String defaultWebAssetDir;
|
|
72
|
-
|
|
73
|
-
@NonNull
|
|
74
|
-
private final String host;
|
|
91
|
+
private final String defaultWebAssetDir = Bridge.DEFAULT_WEB_ASSET_DIR;
|
|
75
92
|
|
|
76
93
|
@NonNull
|
|
77
94
|
private final LiveUpdateHttpClient httpClient;
|
|
@@ -86,47 +103,64 @@ public class LiveUpdate {
|
|
|
86
103
|
private final SharedPreferences.Editor webViewSettingsEditor;
|
|
87
104
|
|
|
88
105
|
private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
|
|
106
|
+
private final String downloadsDirectory = "capawesome_capacitor_live_update_downloads";
|
|
89
107
|
private final Handler rollbackHandler = new Handler();
|
|
90
108
|
private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
|
|
91
109
|
|
|
110
|
+
private boolean initialPageLoaded = false;
|
|
111
|
+
private long lastAutoUpdateCheckTimestamp = 0;
|
|
112
|
+
private boolean rollbackPerformed = false;
|
|
113
|
+
private boolean syncInProgress = false;
|
|
114
|
+
|
|
92
115
|
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
|
|
93
116
|
this.config = config;
|
|
94
|
-
this.defaultWebAssetDir = plugin.getBridge().DEFAULT_WEB_ASSET_DIR;
|
|
95
|
-
if (config.getLocation() != null && config.getLocation().equals("eu")) {
|
|
96
|
-
this.host = "api.cloud.capawesome.eu";
|
|
97
|
-
} else {
|
|
98
|
-
this.host = "api.cloud.capawesome.io";
|
|
99
|
-
}
|
|
100
117
|
this.httpClient = new LiveUpdateHttpClient(config);
|
|
101
118
|
this.plugin = plugin;
|
|
102
119
|
this.preferences = new LiveUpdatePreferences(plugin.getContext());
|
|
103
120
|
this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
|
|
104
121
|
|
|
122
|
+
// Set the device ID on the HTTP client
|
|
123
|
+
this.httpClient.setDeviceId(getDeviceId());
|
|
124
|
+
|
|
125
|
+
// Delete the leftovers of downloads that were interrupted by a process kill
|
|
126
|
+
tryDeleteDownloadsDirectory();
|
|
127
|
+
|
|
105
128
|
if (config.getEnabled()) {
|
|
106
|
-
|
|
129
|
+
boolean wasUpdated = wasUpdated();
|
|
130
|
+
if (wasUpdated) {
|
|
131
|
+
// Reset the runtime configuration if the app was updated to a new version
|
|
132
|
+
resetConfig();
|
|
133
|
+
}
|
|
134
|
+
if (wasUpdated && config.getResetOnUpdate()) {
|
|
107
135
|
Logger.debug(LiveUpdatePlugin.TAG, "App was updated. Resetting to default bundle.");
|
|
108
136
|
reset();
|
|
109
137
|
} else {
|
|
138
|
+
// Start the rollback timer to rollback to the default bundle
|
|
139
|
+
// if the app is not ready after a certain time
|
|
110
140
|
startRollbackTimer();
|
|
111
141
|
}
|
|
112
142
|
saveCurrentVersionCode();
|
|
113
143
|
}
|
|
114
144
|
}
|
|
115
145
|
|
|
146
|
+
public void clearBlockedBundles() {
|
|
147
|
+
preferences.setBlockedBundleIds(null);
|
|
148
|
+
}
|
|
149
|
+
|
|
116
150
|
public void deleteBundle(@NonNull DeleteBundleOptions options, @NonNull EmptyCallback callback) {
|
|
117
151
|
String bundleId = options.getBundleId();
|
|
118
152
|
|
|
119
|
-
if (!
|
|
153
|
+
if (!hasBundleById(bundleId)) {
|
|
120
154
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
121
155
|
callback.error(exception);
|
|
122
156
|
return;
|
|
123
157
|
}
|
|
124
|
-
|
|
158
|
+
deleteBundleById(bundleId);
|
|
125
159
|
|
|
126
160
|
callback.success();
|
|
127
161
|
}
|
|
128
162
|
|
|
129
|
-
public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback)
|
|
163
|
+
public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) {
|
|
130
164
|
ArtifactType artifactType = options.getArtifactType();
|
|
131
165
|
String bundleId = options.getBundleId();
|
|
132
166
|
String checksum = options.getChecksum();
|
|
@@ -134,7 +168,7 @@ public class LiveUpdate {
|
|
|
134
168
|
String url = options.getUrl();
|
|
135
169
|
|
|
136
170
|
// Check if the bundle already exists
|
|
137
|
-
if (
|
|
171
|
+
if (hasBundleById(bundleId)) {
|
|
138
172
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_EXISTS);
|
|
139
173
|
callback.error(exception);
|
|
140
174
|
return;
|
|
@@ -142,50 +176,143 @@ public class LiveUpdate {
|
|
|
142
176
|
|
|
143
177
|
// Download the bundle
|
|
144
178
|
if (artifactType == ArtifactType.MANIFEST) {
|
|
145
|
-
downloadBundleOfTypeManifest(bundleId, url);
|
|
179
|
+
downloadBundleOfTypeManifest(bundleId, url, callback);
|
|
146
180
|
} else {
|
|
147
|
-
downloadBundleOfTypeZip(bundleId,
|
|
181
|
+
downloadBundleOfTypeZip(bundleId, checksum, signature, url, callback);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public void fetchChannels(@NonNull FetchChannelsOptions options, @NonNull NonEmptyCallback<FetchChannelsResult> callback) {
|
|
186
|
+
try {
|
|
187
|
+
HttpUrl.Builder urlBuilder = new HttpUrl.Builder()
|
|
188
|
+
.scheme("https")
|
|
189
|
+
.host(config.getServerDomain())
|
|
190
|
+
.addPathSegment("v1")
|
|
191
|
+
.addPathSegment("apps")
|
|
192
|
+
.addPathSegment(getAppId())
|
|
193
|
+
.addPathSegment("channels");
|
|
194
|
+
if (options.getLimit() != null) {
|
|
195
|
+
urlBuilder.addQueryParameter("limit", String.valueOf(options.getLimit()));
|
|
196
|
+
}
|
|
197
|
+
if (options.getOffset() != null) {
|
|
198
|
+
urlBuilder.addQueryParameter("offset", String.valueOf(options.getOffset()));
|
|
199
|
+
}
|
|
200
|
+
if (options.getQuery() != null) {
|
|
201
|
+
urlBuilder.addQueryParameter("query", options.getQuery());
|
|
202
|
+
}
|
|
203
|
+
String url = urlBuilder.build().toString();
|
|
204
|
+
|
|
205
|
+
httpClient.enqueue(
|
|
206
|
+
url,
|
|
207
|
+
new NonEmptyCallback<Response>() {
|
|
208
|
+
@Override
|
|
209
|
+
public void success(@NonNull Response response) {
|
|
210
|
+
try {
|
|
211
|
+
String responseBodyString = response.body().string();
|
|
212
|
+
if (response.isSuccessful()) {
|
|
213
|
+
JSONArray responseJsonArray = new JSONArray(responseBodyString);
|
|
214
|
+
ChannelResult[] channels = new ChannelResult[responseJsonArray.length()];
|
|
215
|
+
for (int i = 0; i < responseJsonArray.length(); i++) {
|
|
216
|
+
GetChannelsResponseItem item = new GetChannelsResponseItem(responseJsonArray.getJSONObject(i));
|
|
217
|
+
channels[i] = new ChannelResult(item.getId(), item.getName());
|
|
218
|
+
}
|
|
219
|
+
FetchChannelsResult result = new FetchChannelsResult(channels);
|
|
220
|
+
callback.success(result);
|
|
221
|
+
} else {
|
|
222
|
+
callback.error(new Exception(responseBodyString));
|
|
223
|
+
}
|
|
224
|
+
} catch (Exception e) {
|
|
225
|
+
callback.error(e);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@Override
|
|
230
|
+
public void error(@NonNull Exception exception) {
|
|
231
|
+
callback.error(exception);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
} catch (Exception e) {
|
|
236
|
+
callback.error(e);
|
|
148
237
|
}
|
|
149
|
-
callback.success();
|
|
150
238
|
}
|
|
151
239
|
|
|
152
|
-
public void fetchLatestBundle(@NonNull FetchLatestBundleOptions options, @NonNull NonEmptyCallback callback)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
240
|
+
public void fetchLatestBundle(@NonNull FetchLatestBundleOptions options, @NonNull NonEmptyCallback<FetchLatestBundleResult> callback) {
|
|
241
|
+
fetchLatestBundleInternal(
|
|
242
|
+
options,
|
|
243
|
+
new NonEmptyCallback<GetLatestBundleResponse>() {
|
|
244
|
+
@Override
|
|
245
|
+
public void success(@Nullable GetLatestBundleResponse response) {
|
|
246
|
+
ArtifactType artifactType = response == null ? null : response.getArtifactType();
|
|
247
|
+
String bundleId = response == null ? null : response.getBundleId();
|
|
248
|
+
String checksum = response == null ? null : response.getChecksum();
|
|
249
|
+
JSONObject customProperties = response == null ? null : response.getCustomProperties();
|
|
250
|
+
String downloadUrl = response == null ? null : response.getUrl();
|
|
251
|
+
String signature = response == null ? null : response.getSignature();
|
|
252
|
+
FetchLatestBundleResult result = new FetchLatestBundleResult(
|
|
253
|
+
artifactType,
|
|
254
|
+
bundleId,
|
|
255
|
+
checksum,
|
|
256
|
+
customProperties,
|
|
257
|
+
downloadUrl,
|
|
258
|
+
signature
|
|
259
|
+
);
|
|
260
|
+
callback.success(result);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@Override
|
|
264
|
+
public void error(@NonNull Exception exception) {
|
|
265
|
+
callback.error(exception);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public void getBlockedBundles(@NonNull NonEmptyCallback<GetBlockedBundlesResult> callback) {
|
|
272
|
+
String blockedIds = preferences.getBlockedBundleIds();
|
|
273
|
+
String[] bundleIds;
|
|
274
|
+
if (blockedIds == null || blockedIds.isEmpty()) {
|
|
275
|
+
bundleIds = new String[0];
|
|
276
|
+
} else {
|
|
277
|
+
bundleIds = blockedIds.split(",");
|
|
278
|
+
}
|
|
279
|
+
GetBlockedBundlesResult result = new GetBlockedBundlesResult(bundleIds);
|
|
160
280
|
callback.success(result);
|
|
161
281
|
}
|
|
162
282
|
|
|
163
|
-
public void getBundle(@NonNull NonEmptyCallback callback) {
|
|
283
|
+
public void getBundle(@NonNull NonEmptyCallback<GetBundleResult> callback) {
|
|
164
284
|
String bundleId = getCurrentBundleId();
|
|
165
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
166
|
-
bundleId = null;
|
|
167
|
-
}
|
|
168
285
|
GetBundleResult result = new GetBundleResult(bundleId);
|
|
169
286
|
callback.success(result);
|
|
170
287
|
}
|
|
171
288
|
|
|
172
289
|
public void getBundles(@NonNull NonEmptyCallback callback) {
|
|
173
|
-
String[] bundleIds =
|
|
290
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
174
291
|
GetBundlesResult result = new GetBundlesResult(bundleIds);
|
|
175
292
|
callback.success(result);
|
|
176
293
|
}
|
|
177
294
|
|
|
295
|
+
public void getDownloadedBundles(@NonNull NonEmptyCallback<GetDownloadedBundlesResult> callback) {
|
|
296
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
297
|
+
GetDownloadedBundlesResult result = new GetDownloadedBundlesResult(bundleIds);
|
|
298
|
+
callback.success(result);
|
|
299
|
+
}
|
|
300
|
+
|
|
178
301
|
public void getChannel(@NonNull NonEmptyCallback callback) {
|
|
179
302
|
String channel = getChannel();
|
|
180
303
|
GetChannelResult result = new GetChannelResult(channel);
|
|
181
304
|
callback.success(result);
|
|
182
305
|
}
|
|
183
306
|
|
|
307
|
+
public void getConfig(@NonNull NonEmptyCallback<GetConfigResult> callback) {
|
|
308
|
+
String appId = getAppId();
|
|
309
|
+
String autoUpdateStrategy = config.getAutoUpdateStrategy();
|
|
310
|
+
GetConfigResult result = new GetConfigResult(appId, autoUpdateStrategy);
|
|
311
|
+
callback.success(result);
|
|
312
|
+
}
|
|
313
|
+
|
|
184
314
|
public void getCurrentBundle(@NonNull NonEmptyCallback<GetCurrentBundleResult> callback) {
|
|
185
315
|
String bundleId = getCurrentBundleId();
|
|
186
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
187
|
-
bundleId = null;
|
|
188
|
-
}
|
|
189
316
|
GetCurrentBundleResult result = new GetCurrentBundleResult(bundleId);
|
|
190
317
|
callback.success(result);
|
|
191
318
|
}
|
|
@@ -204,15 +331,12 @@ public class LiveUpdate {
|
|
|
204
331
|
|
|
205
332
|
public void getNextBundle(@NonNull NonEmptyCallback<GetNextBundleResult> callback) {
|
|
206
333
|
String bundleId = getNextBundleId();
|
|
207
|
-
if (bundleId.equals(defaultWebAssetDir)) {
|
|
208
|
-
bundleId = null;
|
|
209
|
-
}
|
|
210
334
|
GetNextBundleResult result = new GetNextBundleResult(bundleId);
|
|
211
335
|
callback.success(result);
|
|
212
336
|
}
|
|
213
337
|
|
|
214
338
|
public void getVersionCode(@NonNull NonEmptyCallback callback) throws PackageManager.NameNotFoundException {
|
|
215
|
-
String versionCode =
|
|
339
|
+
String versionCode = getVersionCodeAsString();
|
|
216
340
|
GetVersionCodeResult result = new GetVersionCodeResult(versionCode);
|
|
217
341
|
callback.success(result);
|
|
218
342
|
}
|
|
@@ -223,14 +347,51 @@ public class LiveUpdate {
|
|
|
223
347
|
callback.success(result);
|
|
224
348
|
}
|
|
225
349
|
|
|
226
|
-
public void
|
|
350
|
+
public void isSyncing(@NonNull NonEmptyCallback<IsSyncingResult> callback) {
|
|
351
|
+
IsSyncingResult result = new IsSyncingResult(syncInProgress);
|
|
352
|
+
callback.success(result);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
public void handleOnPageLoaded() {
|
|
356
|
+
// Wait for initial page load to perform auto update to make sure the WebViewLocalServer is initialized.
|
|
357
|
+
// Otherwise, a NullPointerException may occur for `com.getcapacitor.WebViewLocalServer.getBasePath()`.
|
|
358
|
+
if ("background".equals(config.getAutoUpdateStrategy()) && !initialPageLoaded) {
|
|
359
|
+
initialPageLoaded = true;
|
|
360
|
+
performAutoUpdate();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
public void handleOnResume() {
|
|
365
|
+
if ("background".equals(config.getAutoUpdateStrategy()) && initialPageLoaded) {
|
|
366
|
+
performAutoUpdate();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public void ready(@NonNull NonEmptyCallback callback) {
|
|
227
371
|
Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
|
|
372
|
+
if (config.getReadyTimeout() <= 0) {
|
|
373
|
+
Logger.warn(LiveUpdatePlugin.TAG, "Ready timeout is set to 0. Automatic rollback is disabled.");
|
|
374
|
+
}
|
|
228
375
|
// Stop the rollback timer
|
|
229
376
|
stopRollbackTimer();
|
|
230
377
|
// Delete unused bundles
|
|
231
378
|
if (config.getAutoDeleteBundles()) {
|
|
232
379
|
deleteUnusedBundles();
|
|
233
380
|
}
|
|
381
|
+
// Get the current and previous bundle IDs
|
|
382
|
+
String currentBundleId = getCurrentBundleId();
|
|
383
|
+
String previousBundleId = getPreviousBundleId();
|
|
384
|
+
// Block the rolled back bundle if enabled
|
|
385
|
+
if (config.getAutoBlockRolledBackBundles() && rollbackPerformed && previousBundleId != null) {
|
|
386
|
+
addBlockedBundleId(previousBundleId);
|
|
387
|
+
}
|
|
388
|
+
// Return the result
|
|
389
|
+
ReadyResult result = new ReadyResult(currentBundleId, previousBundleId, rollbackPerformed);
|
|
390
|
+
callback.success(result);
|
|
391
|
+
// Set the new previous bundle ID
|
|
392
|
+
setPreviousBundleId(currentBundleId);
|
|
393
|
+
// Reset the rollback flag
|
|
394
|
+
rollbackPerformed = false;
|
|
234
395
|
}
|
|
235
396
|
|
|
236
397
|
public void reload() {
|
|
@@ -240,19 +401,23 @@ public class LiveUpdate {
|
|
|
240
401
|
}
|
|
241
402
|
|
|
242
403
|
public void reset() {
|
|
243
|
-
|
|
404
|
+
setNextBundleById(null);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
public void resetConfig() {
|
|
408
|
+
preferences.setAppId(null);
|
|
244
409
|
}
|
|
245
410
|
|
|
246
411
|
public void setBundle(@NonNull SetBundleOptions options, @NonNull EmptyCallback callback) {
|
|
247
412
|
String bundleId = options.getBundleId();
|
|
248
413
|
|
|
249
|
-
if (!
|
|
414
|
+
if (!hasBundleById(bundleId)) {
|
|
250
415
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
251
416
|
callback.error(exception);
|
|
252
417
|
return;
|
|
253
418
|
}
|
|
254
419
|
|
|
255
|
-
|
|
420
|
+
setNextBundleById(bundleId);
|
|
256
421
|
callback.success();
|
|
257
422
|
}
|
|
258
423
|
|
|
@@ -263,6 +428,11 @@ public class LiveUpdate {
|
|
|
263
428
|
callback.success();
|
|
264
429
|
}
|
|
265
430
|
|
|
431
|
+
public void setConfig(@NonNull SetConfigOptions options) {
|
|
432
|
+
String appId = options.getAppId();
|
|
433
|
+
preferences.setAppId(appId);
|
|
434
|
+
}
|
|
435
|
+
|
|
266
436
|
public void setCustomId(@NonNull SetCustomIdOptions options, @NonNull EmptyCallback callback) {
|
|
267
437
|
String customId = options.getCustomId();
|
|
268
438
|
|
|
@@ -276,8 +446,8 @@ public class LiveUpdate {
|
|
|
276
446
|
if (bundleId == null) {
|
|
277
447
|
reset();
|
|
278
448
|
} else {
|
|
279
|
-
if (
|
|
280
|
-
|
|
449
|
+
if (hasBundleById(bundleId)) {
|
|
450
|
+
setNextBundleById(bundleId);
|
|
281
451
|
} else {
|
|
282
452
|
Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
|
|
283
453
|
callback.error(exception);
|
|
@@ -287,45 +457,99 @@ public class LiveUpdate {
|
|
|
287
457
|
callback.success();
|
|
288
458
|
}
|
|
289
459
|
|
|
290
|
-
public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback)
|
|
460
|
+
public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback) {
|
|
461
|
+
if (syncInProgress) {
|
|
462
|
+
Exception exception = new Exception(LiveUpdatePlugin.ERROR_SYNC_IN_PROGRESS);
|
|
463
|
+
callback.error(exception);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
syncInProgress = true;
|
|
467
|
+
|
|
291
468
|
String channel = options.getChannel();
|
|
292
469
|
// Fetch the latest bundle
|
|
293
470
|
FetchLatestBundleOptions fetchLatestBundleOptions = new FetchLatestBundleOptions(channel);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
471
|
+
fetchLatestBundleInternal(
|
|
472
|
+
fetchLatestBundleOptions,
|
|
473
|
+
new NonEmptyCallback<GetLatestBundleResponse>() {
|
|
474
|
+
@Override
|
|
475
|
+
public void success(@Nullable GetLatestBundleResponse response) {
|
|
476
|
+
try {
|
|
477
|
+
if (response == null) {
|
|
478
|
+
Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
|
|
479
|
+
syncInProgress = false;
|
|
480
|
+
SyncResult syncResult = new SyncResult(null);
|
|
481
|
+
callback.success(syncResult);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
ArtifactType artifactType = response.getArtifactType();
|
|
485
|
+
String latestBundleId = response.getBundleId();
|
|
486
|
+
String checksum = response.getChecksum();
|
|
487
|
+
String signature = response.getSignature();
|
|
488
|
+
String url = response.getUrl();
|
|
489
|
+
// Check if the bundle is blocked
|
|
490
|
+
if (isBlockedBundleId(latestBundleId)) {
|
|
491
|
+
Logger.warn(LiveUpdatePlugin.TAG, "Bundle is blocked and will not be downloaded.");
|
|
492
|
+
syncInProgress = false;
|
|
493
|
+
SyncResult syncResult = new SyncResult(null);
|
|
494
|
+
callback.success(syncResult);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Check if the bundle already exists
|
|
498
|
+
if (hasBundleById(latestBundleId)) {
|
|
499
|
+
String nextBundleId = null;
|
|
500
|
+
String currentBundleId = getCurrentBundleId();
|
|
501
|
+
if (!latestBundleId.equals(currentBundleId)) {
|
|
502
|
+
// Set the next bundle
|
|
503
|
+
setNextBundleById(latestBundleId);
|
|
504
|
+
nextBundleId = latestBundleId;
|
|
505
|
+
}
|
|
506
|
+
syncInProgress = false;
|
|
507
|
+
SyncResult syncResult = new SyncResult(nextBundleId);
|
|
508
|
+
callback.success(syncResult);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Download the bundle
|
|
513
|
+
EmptyCallback downloadCallback = new EmptyCallback() {
|
|
514
|
+
@Override
|
|
515
|
+
public void success() {
|
|
516
|
+
try {
|
|
517
|
+
// Set the next bundle
|
|
518
|
+
setNextBundleById(latestBundleId);
|
|
519
|
+
syncInProgress = false;
|
|
520
|
+
SyncResult syncResult = new SyncResult(latestBundleId);
|
|
521
|
+
callback.success(syncResult);
|
|
522
|
+
} catch (Exception e) {
|
|
523
|
+
syncInProgress = false;
|
|
524
|
+
callback.error(e);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
@Override
|
|
529
|
+
public void error(@NonNull Exception exception) {
|
|
530
|
+
syncInProgress = false;
|
|
531
|
+
callback.error(exception);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
if (artifactType == ArtifactType.MANIFEST) {
|
|
536
|
+
downloadBundleOfTypeManifest(latestBundleId, url, downloadCallback);
|
|
537
|
+
} else {
|
|
538
|
+
downloadBundleOfTypeZip(latestBundleId, checksum, signature, url, downloadCallback);
|
|
539
|
+
}
|
|
540
|
+
} catch (Exception e) {
|
|
541
|
+
syncInProgress = false;
|
|
542
|
+
callback.error(e);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
@Override
|
|
547
|
+
public void error(@NonNull Exception exception) {
|
|
548
|
+
syncInProgress = false;
|
|
549
|
+
callback.error(exception);
|
|
550
|
+
}
|
|
314
551
|
}
|
|
315
|
-
|
|
316
|
-
callback.success(syncResult);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
// Download the bundle
|
|
320
|
-
if (artifactType == ArtifactType.MANIFEST) {
|
|
321
|
-
downloadBundleOfTypeManifest(latestBundleId, url);
|
|
322
|
-
} else {
|
|
323
|
-
downloadBundleOfTypeZip(latestBundleId, url, checksum, signature);
|
|
324
|
-
}
|
|
325
|
-
// Set the next bundle
|
|
326
|
-
setNextBundle(latestBundleId);
|
|
327
|
-
SyncResult syncResult = new SyncResult(latestBundleId);
|
|
328
|
-
callback.success(syncResult);
|
|
552
|
+
);
|
|
329
553
|
}
|
|
330
554
|
|
|
331
555
|
private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
|
|
@@ -340,7 +564,10 @@ public class LiveUpdate {
|
|
|
340
564
|
|
|
341
565
|
// Move the bundle directory to the bundles directory
|
|
342
566
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
343
|
-
indexHtmlFile.getParentFile().renameTo(bundleDirectory);
|
|
567
|
+
boolean moved = indexHtmlFile.getParentFile().renameTo(bundleDirectory);
|
|
568
|
+
if (!moved) {
|
|
569
|
+
throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_MOVE_FAILED);
|
|
570
|
+
}
|
|
344
571
|
}
|
|
345
572
|
|
|
346
573
|
private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
|
|
@@ -349,7 +576,7 @@ public class LiveUpdate {
|
|
|
349
576
|
|
|
350
577
|
private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
|
|
351
578
|
// Unzip the file to the bundle directory
|
|
352
|
-
File unzippedDirectory =
|
|
579
|
+
File unzippedDirectory = unzipDownloadedBundle(zipFile);
|
|
353
580
|
// Add the bundle
|
|
354
581
|
addBundle(bundleId, unzippedDirectory);
|
|
355
582
|
}
|
|
@@ -362,19 +589,14 @@ public class LiveUpdate {
|
|
|
362
589
|
return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
|
|
363
590
|
}
|
|
364
591
|
|
|
365
|
-
private File
|
|
366
|
-
|
|
367
|
-
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
592
|
+
private File buildDownloadsDirectory() {
|
|
593
|
+
return new File(plugin.getContext().getCacheDir(), downloadsDirectory);
|
|
368
594
|
}
|
|
369
595
|
|
|
370
|
-
private File
|
|
371
|
-
String
|
|
372
|
-
return new File(plugin.getContext().getCacheDir(), fileName);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
private void copyCurrentBundleFile(@NonNull String href, @NonNull File destinationDirectory) throws IOException {
|
|
596
|
+
private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
|
|
597
|
+
String href = fileToCopy.getHref();
|
|
376
598
|
String currentBundleId = getCurrentBundleId();
|
|
377
|
-
if (currentBundleId
|
|
599
|
+
if (currentBundleId == null) {
|
|
378
600
|
// Create the source input stream
|
|
379
601
|
AssetManager assets = plugin.getContext().getAssets();
|
|
380
602
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
|
|
@@ -396,10 +618,20 @@ public class LiveUpdate {
|
|
|
396
618
|
}
|
|
397
619
|
}
|
|
398
620
|
|
|
399
|
-
private
|
|
400
|
-
|
|
401
|
-
|
|
621
|
+
private List<ManifestItem> copyCurrentBundleFilesAndReturnFailures(
|
|
622
|
+
@NonNull List<ManifestItem> filesToCopy,
|
|
623
|
+
@NonNull File destinationDirectory
|
|
624
|
+
) {
|
|
625
|
+
List<ManifestItem> missingItems = new ArrayList<>();
|
|
626
|
+
for (ManifestItem fileToCopy : filesToCopy) {
|
|
627
|
+
boolean success = tryCopyCurrentBundleFile(fileToCopy, destinationDirectory);
|
|
628
|
+
if (!success) {
|
|
629
|
+
Logger.warn(LiveUpdatePlugin.TAG, "Failed to copy file: " + fileToCopy.getHref());
|
|
630
|
+
// If the file could not be copied, add it to the list of missing items
|
|
631
|
+
missingItems.add(fileToCopy);
|
|
632
|
+
}
|
|
402
633
|
}
|
|
634
|
+
return missingItems;
|
|
403
635
|
}
|
|
404
636
|
|
|
405
637
|
private void copyFile(File input, File output) throws IOException {
|
|
@@ -445,197 +677,407 @@ public class LiveUpdate {
|
|
|
445
677
|
}
|
|
446
678
|
}
|
|
447
679
|
|
|
448
|
-
private File
|
|
449
|
-
File
|
|
450
|
-
|
|
451
|
-
return
|
|
680
|
+
private File createTemporaryDownloadDirectory() {
|
|
681
|
+
File temporaryDirectory = new File(buildDownloadsDirectory(), UUID.randomUUID().toString());
|
|
682
|
+
temporaryDirectory.mkdirs();
|
|
683
|
+
return temporaryDirectory;
|
|
452
684
|
}
|
|
453
685
|
|
|
454
|
-
private void
|
|
686
|
+
private void deleteBundleById(@NonNull String bundleId) {
|
|
455
687
|
// Delete the bundle directory
|
|
456
688
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
457
689
|
deleteFileRecursively(bundleDirectory);
|
|
458
690
|
// Reset the next bundle if it is the deleted bundle
|
|
459
691
|
String nextBundleId = getNextBundleId();
|
|
460
692
|
if (bundleId.equals(nextBundleId)) {
|
|
461
|
-
|
|
693
|
+
setNextBundleById(null);
|
|
462
694
|
}
|
|
463
695
|
}
|
|
464
696
|
|
|
465
697
|
private void deleteFileRecursively(@NonNull File file) {
|
|
466
698
|
if (file.isDirectory()) {
|
|
467
|
-
|
|
468
|
-
|
|
699
|
+
File[] children = file.listFiles();
|
|
700
|
+
if (children != null) {
|
|
701
|
+
for (File child : children) {
|
|
702
|
+
deleteFileRecursively(child);
|
|
703
|
+
}
|
|
469
704
|
}
|
|
470
705
|
}
|
|
471
706
|
file.delete();
|
|
472
707
|
}
|
|
473
708
|
|
|
709
|
+
@NonNull
|
|
710
|
+
private EmptyCallback deleteTemporaryDirectoryOnCompletion(@NonNull File temporaryDirectory, @NonNull EmptyCallback callback) {
|
|
711
|
+
return new EmptyCallback() {
|
|
712
|
+
@Override
|
|
713
|
+
public void success() {
|
|
714
|
+
deleteFileRecursively(temporaryDirectory);
|
|
715
|
+
callback.success();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
@Override
|
|
719
|
+
public void error(@NonNull Exception exception) {
|
|
720
|
+
deleteFileRecursively(temporaryDirectory);
|
|
721
|
+
callback.error(exception);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
474
726
|
private void deleteUnusedBundles() {
|
|
475
|
-
String[] bundleIds =
|
|
727
|
+
String[] bundleIds = getDownloadedBundleIds();
|
|
476
728
|
for (String bundleId : bundleIds) {
|
|
477
729
|
if (!isBundleInUse(bundleId)) {
|
|
478
|
-
|
|
730
|
+
deleteBundleById(bundleId);
|
|
479
731
|
}
|
|
480
732
|
}
|
|
481
733
|
}
|
|
482
734
|
|
|
483
|
-
private
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
735
|
+
private Call downloadAndVerifyFile(
|
|
736
|
+
@NonNull String url,
|
|
737
|
+
@NonNull File file,
|
|
738
|
+
@Nullable String checksum,
|
|
739
|
+
@Nullable String signature,
|
|
740
|
+
@Nullable DownloadProgressCallback progressCallback,
|
|
741
|
+
@NonNull EmptyCallback completionCallback
|
|
742
|
+
) {
|
|
743
|
+
return httpClient.enqueue(
|
|
744
|
+
url,
|
|
745
|
+
new NonEmptyCallback<Response>() {
|
|
746
|
+
@Override
|
|
747
|
+
public void success(@NonNull Response response) {
|
|
748
|
+
try {
|
|
749
|
+
if (response.isSuccessful()) {
|
|
750
|
+
ResponseBody responseBody = response.body();
|
|
751
|
+
LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file, progressCallback);
|
|
752
|
+
|
|
753
|
+
// Extract checksum/signature from headers
|
|
754
|
+
String finalChecksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
|
|
755
|
+
String finalSignature = signature == null ? LiveUpdateHttpClient.getSignatureFromResponse(response) : signature;
|
|
756
|
+
|
|
757
|
+
// Verify file
|
|
758
|
+
verifyFile(file, finalChecksum, finalSignature);
|
|
759
|
+
completionCallback.success();
|
|
760
|
+
} else {
|
|
761
|
+
String errorMessage = response.body().string();
|
|
762
|
+
Exception exception = new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
|
|
763
|
+
Logger.error(LiveUpdatePlugin.TAG, errorMessage, exception);
|
|
764
|
+
completionCallback.error(exception);
|
|
765
|
+
}
|
|
766
|
+
} catch (Exception e) {
|
|
767
|
+
completionCallback.error(e);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
@Override
|
|
772
|
+
public void error(@NonNull Exception exception) {
|
|
773
|
+
completionCallback.error(exception);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
);
|
|
498
777
|
}
|
|
499
778
|
|
|
500
|
-
private
|
|
779
|
+
private Call downloadBundleFile(
|
|
780
|
+
@NonNull String baseUrl,
|
|
781
|
+
@NonNull String href,
|
|
782
|
+
@NonNull File destinationDirectory,
|
|
783
|
+
@Nullable DownloadProgressCallback progressCallback,
|
|
784
|
+
@NonNull EmptyCallback completionCallback
|
|
785
|
+
) {
|
|
501
786
|
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
|
|
502
787
|
urlBuilder.addQueryParameter("href", href);
|
|
503
788
|
String url = urlBuilder.build().toString();
|
|
504
789
|
|
|
505
790
|
File destinationFile = new File(destinationDirectory, href);
|
|
506
791
|
destinationFile.getParentFile().mkdirs();
|
|
507
|
-
downloadAndVerifyFile(url, destinationFile, null, null);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
792
|
+
return downloadAndVerifyFile(url, destinationFile, null, null, progressCallback, completionCallback);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
private void downloadBundleFiles(
|
|
796
|
+
@NonNull String baseUrl,
|
|
797
|
+
@NonNull List<ManifestItem> filesToDownload,
|
|
798
|
+
@NonNull File destinationDirectory,
|
|
799
|
+
@Nullable DownloadProgressCallback progressCallback,
|
|
800
|
+
@NonNull EmptyCallback completionCallback
|
|
801
|
+
) {
|
|
802
|
+
if (filesToDownload.isEmpty()) {
|
|
803
|
+
if (progressCallback != null) {
|
|
804
|
+
progressCallback.onProgress(0, 0);
|
|
805
|
+
}
|
|
806
|
+
completionCallback.success();
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Thread-safe progress tracking
|
|
811
|
+
AtomicLong totalBytesDownloaded = new AtomicLong(0);
|
|
812
|
+
long totalBytesToDownload = 0;
|
|
813
|
+
for (ManifestItem item : filesToDownload) {
|
|
814
|
+
totalBytesToDownload += item.getSizeInBytes();
|
|
815
|
+
}
|
|
816
|
+
final long finalTotalBytesToDownload = totalBytesToDownload;
|
|
817
|
+
|
|
818
|
+
// Coordination primitives
|
|
819
|
+
CountDownLatch latch = new CountDownLatch(filesToDownload.size());
|
|
820
|
+
AtomicReference<Exception> firstError = new AtomicReference<>();
|
|
821
|
+
AtomicBoolean completionHandled = new AtomicBoolean(false);
|
|
822
|
+
List<Call> activeCalls = new ArrayList<>();
|
|
823
|
+
|
|
824
|
+
// Start all downloads asynchronously (OkHttp's dispatcher handles parallelization)
|
|
825
|
+
for (ManifestItem item : filesToDownload) {
|
|
826
|
+
Call call = downloadBundleFile(
|
|
827
|
+
baseUrl,
|
|
828
|
+
item.getHref(),
|
|
829
|
+
destinationDirectory,
|
|
830
|
+
(downloadedBytes, totalBytes) -> {
|
|
831
|
+
// Per-file progress
|
|
832
|
+
if (progressCallback != null) {
|
|
833
|
+
long totalProgress = totalBytesDownloaded.get() + downloadedBytes;
|
|
834
|
+
progressCallback.onProgress(totalProgress, finalTotalBytesToDownload);
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
new EmptyCallback() {
|
|
838
|
+
@Override
|
|
839
|
+
public void success() {
|
|
840
|
+
// Update total progress
|
|
841
|
+
totalBytesDownloaded.addAndGet(item.getSizeInBytes());
|
|
842
|
+
if (progressCallback != null) {
|
|
843
|
+
progressCallback.onProgress(totalBytesDownloaded.get(), finalTotalBytesToDownload);
|
|
844
|
+
}
|
|
845
|
+
latch.countDown();
|
|
846
|
+
|
|
847
|
+
// Check if this was the last download to complete
|
|
848
|
+
if (latch.getCount() == 0 && completionHandled.compareAndSet(false, true)) {
|
|
849
|
+
Exception error = firstError.get();
|
|
850
|
+
if (error != null) {
|
|
851
|
+
completionCallback.error(error);
|
|
852
|
+
} else {
|
|
853
|
+
// Final progress update
|
|
854
|
+
if (progressCallback != null) {
|
|
855
|
+
progressCallback.onProgress(finalTotalBytesToDownload, finalTotalBytesToDownload);
|
|
856
|
+
}
|
|
857
|
+
completionCallback.success();
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
@Override
|
|
863
|
+
public void error(@NonNull Exception e) {
|
|
864
|
+
// Capture first error and cancel all remaining downloads
|
|
865
|
+
if (firstError.compareAndSet(null, e)) {
|
|
866
|
+
Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + item.getHref(), e);
|
|
867
|
+
// Cancel all in-flight downloads (fail-fast)
|
|
868
|
+
synchronized (activeCalls) {
|
|
869
|
+
for (Call activeCall : activeCalls) {
|
|
870
|
+
if (!activeCall.isCanceled() && !activeCall.isExecuted()) {
|
|
871
|
+
activeCall.cancel();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
latch.countDown();
|
|
877
|
+
|
|
878
|
+
// Check if this was the last download to complete
|
|
879
|
+
if (latch.getCount() == 0 && completionHandled.compareAndSet(false, true)) {
|
|
880
|
+
completionCallback.error(firstError.get());
|
|
881
|
+
}
|
|
524
882
|
}
|
|
525
883
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
// Stop if an exception occurred
|
|
530
|
-
if (exceptionReference.get() != null) {
|
|
531
|
-
break;
|
|
884
|
+
);
|
|
885
|
+
synchronized (activeCalls) {
|
|
886
|
+
activeCalls.add(call);
|
|
532
887
|
}
|
|
533
|
-
// Start a new thread
|
|
534
|
-
Thread thread = new Thread(() -> {
|
|
535
|
-
try {
|
|
536
|
-
downloadBundleFile(baseUrl, href, destinationDirectory);
|
|
537
|
-
} catch (Exception exception) {
|
|
538
|
-
exceptionReference.set(exception);
|
|
539
|
-
Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + href, exception);
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
threads.add(thread);
|
|
543
|
-
thread.start();
|
|
544
|
-
}
|
|
545
|
-
// Wait for all threads to finish
|
|
546
|
-
for (Thread thread : threads) {
|
|
547
|
-
thread.join();
|
|
548
|
-
}
|
|
549
|
-
// Throw the exception if one occurred
|
|
550
|
-
Exception exception = exceptionReference.get();
|
|
551
|
-
if (exception != null) {
|
|
552
|
-
throw exception;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
private void downloadBundleOfTypeManifest(@NonNull String bundleId, @NonNull String downloadUrl) throws Exception {
|
|
557
|
-
// Create a temporary directory
|
|
558
|
-
File temporaryDirectory = createTemporaryDirectory();
|
|
559
|
-
// Download the latest manifest
|
|
560
|
-
File latestManifestFile = downloadBundleFile(downloadUrl, manifestFileName, temporaryDirectory);
|
|
561
|
-
Manifest latestManifest = loadManifest(latestManifestFile);
|
|
562
|
-
// Load the current manifest
|
|
563
|
-
Manifest currentManifest = loadCurrentManifest();
|
|
564
|
-
// Compare the manifests
|
|
565
|
-
List<ManifestItem> itemsToCopy = new ArrayList<>();
|
|
566
|
-
List<ManifestItem> itemsToDownload = new ArrayList<>();
|
|
567
|
-
if (currentManifest == null) {
|
|
568
|
-
itemsToDownload.addAll(latestManifest.getItems());
|
|
569
|
-
} else {
|
|
570
|
-
itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
|
|
571
|
-
itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
|
|
572
|
-
}
|
|
573
|
-
// Copy the files
|
|
574
|
-
List<String> hrefsToCopy = new ArrayList<>();
|
|
575
|
-
for (ManifestItem item : itemsToCopy) {
|
|
576
|
-
hrefsToCopy.add(item.getHref());
|
|
577
888
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private void downloadBundleOfTypeManifest(
|
|
892
|
+
@NonNull String bundleId,
|
|
893
|
+
@NonNull String downloadUrl,
|
|
894
|
+
@NonNull EmptyCallback completionCallback
|
|
895
|
+
) {
|
|
896
|
+
// Create a temporary directory that is deleted once the download completed or failed
|
|
897
|
+
File temporaryDirectory = createTemporaryDownloadDirectory();
|
|
898
|
+
EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
|
|
899
|
+
try {
|
|
900
|
+
// Download the latest manifest
|
|
901
|
+
downloadBundleFile(
|
|
902
|
+
downloadUrl,
|
|
903
|
+
manifestFileName,
|
|
904
|
+
temporaryDirectory,
|
|
905
|
+
null,
|
|
906
|
+
new EmptyCallback() {
|
|
907
|
+
@Override
|
|
908
|
+
public void success() {
|
|
909
|
+
try {
|
|
910
|
+
File latestManifestFile = new File(temporaryDirectory, manifestFileName);
|
|
911
|
+
Manifest latestManifest = loadManifest(latestManifestFile);
|
|
912
|
+
// Load the current manifest
|
|
913
|
+
Manifest currentManifest = loadCurrentManifest();
|
|
914
|
+
// Compare the manifests
|
|
915
|
+
List<ManifestItem> itemsToCopy = new ArrayList<>();
|
|
916
|
+
List<ManifestItem> itemsToDownload = new ArrayList<>();
|
|
917
|
+
if (currentManifest == null) {
|
|
918
|
+
itemsToDownload.addAll(latestManifest.getItems());
|
|
919
|
+
} else {
|
|
920
|
+
itemsToCopy.addAll(Manifest.findDuplicateItems(latestManifest, currentManifest));
|
|
921
|
+
itemsToDownload.addAll(Manifest.findMissingItems(latestManifest, currentManifest));
|
|
922
|
+
}
|
|
923
|
+
// Copy the files
|
|
924
|
+
List<ManifestItem> missingItems = copyCurrentBundleFilesAndReturnFailures(itemsToCopy, temporaryDirectory);
|
|
925
|
+
// If items could not be copied, add them to the list of items to download
|
|
926
|
+
if (!missingItems.isEmpty()) {
|
|
927
|
+
itemsToDownload.addAll(missingItems);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Download the files
|
|
931
|
+
downloadBundleFiles(
|
|
932
|
+
downloadUrl,
|
|
933
|
+
itemsToDownload,
|
|
934
|
+
temporaryDirectory,
|
|
935
|
+
(downloadedBytes, totalBytes) -> {
|
|
936
|
+
DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(
|
|
937
|
+
bundleId,
|
|
938
|
+
downloadedBytes,
|
|
939
|
+
totalBytes
|
|
940
|
+
);
|
|
941
|
+
notifyDownloadBundleProgressListeners(event);
|
|
942
|
+
},
|
|
943
|
+
new EmptyCallback() {
|
|
944
|
+
@Override
|
|
945
|
+
public void success() {
|
|
946
|
+
try {
|
|
947
|
+
// Add the bundle
|
|
948
|
+
addBundleOfTypeManifest(bundleId, temporaryDirectory);
|
|
949
|
+
callback.success();
|
|
950
|
+
} catch (Exception e) {
|
|
951
|
+
callback.error(e);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
@Override
|
|
956
|
+
public void error(@NonNull Exception exception) {
|
|
957
|
+
callback.error(exception);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
);
|
|
961
|
+
} catch (Exception e) {
|
|
962
|
+
callback.error(e);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
@Override
|
|
967
|
+
public void error(@NonNull Exception exception) {
|
|
968
|
+
callback.error(exception);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
);
|
|
972
|
+
} catch (Exception e) {
|
|
973
|
+
callback.error(e);
|
|
583
974
|
}
|
|
584
|
-
downloadBundleFiles(downloadUrl, hrefsToDownload, temporaryDirectory);
|
|
585
|
-
// Add the bundle
|
|
586
|
-
addBundleOfTypeManifest(bundleId, temporaryDirectory);
|
|
587
975
|
}
|
|
588
976
|
|
|
589
977
|
private void downloadBundleOfTypeZip(
|
|
590
978
|
@NonNull String bundleId,
|
|
591
|
-
@NonNull String downloadUrl,
|
|
592
979
|
@Nullable String checksum,
|
|
593
|
-
@Nullable String signature
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
980
|
+
@Nullable String signature,
|
|
981
|
+
@NonNull String downloadUrl,
|
|
982
|
+
@NonNull EmptyCallback completionCallback
|
|
983
|
+
) {
|
|
984
|
+
// Create a temporary directory that is deleted once the download completed or failed
|
|
985
|
+
File temporaryDirectory = createTemporaryDownloadDirectory();
|
|
986
|
+
EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
|
|
987
|
+
File zipFile = new File(temporaryDirectory, "bundle.zip");
|
|
988
|
+
try {
|
|
989
|
+
// Download the bundle
|
|
990
|
+
downloadAndVerifyFile(
|
|
991
|
+
downloadUrl,
|
|
992
|
+
zipFile,
|
|
993
|
+
checksum,
|
|
994
|
+
signature,
|
|
995
|
+
(downloadedBytes, totalBytes) -> {
|
|
996
|
+
DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
|
|
997
|
+
notifyDownloadBundleProgressListeners(event);
|
|
998
|
+
},
|
|
999
|
+
new EmptyCallback() {
|
|
1000
|
+
@Override
|
|
1001
|
+
public void success() {
|
|
1002
|
+
try {
|
|
1003
|
+
// Add the bundle
|
|
1004
|
+
addBundleOfTypeZip(bundleId, zipFile);
|
|
1005
|
+
callback.success();
|
|
1006
|
+
} catch (Exception e) {
|
|
1007
|
+
callback.error(e);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
@Override
|
|
1012
|
+
public void error(@NonNull Exception exception) {
|
|
1013
|
+
callback.error(exception);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
);
|
|
1017
|
+
} catch (Exception e) {
|
|
1018
|
+
callback.error(e);
|
|
1019
|
+
}
|
|
602
1020
|
}
|
|
603
1021
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
.
|
|
610
|
-
.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1022
|
+
private void fetchLatestBundleInternal(
|
|
1023
|
+
@NonNull FetchLatestBundleOptions options,
|
|
1024
|
+
@NonNull NonEmptyCallback<GetLatestBundleResponse> callback
|
|
1025
|
+
) {
|
|
1026
|
+
try {
|
|
1027
|
+
String channel = options.getChannel() == null ? getChannel() : options.getChannel();
|
|
1028
|
+
String url = new HttpUrl.Builder()
|
|
1029
|
+
.scheme("https")
|
|
1030
|
+
.host(config.getServerDomain())
|
|
1031
|
+
.addPathSegment("v1")
|
|
1032
|
+
.addPathSegment("apps")
|
|
1033
|
+
.addPathSegment(getAppId())
|
|
1034
|
+
.addPathSegment("bundles")
|
|
1035
|
+
.addPathSegment("latest")
|
|
1036
|
+
.addQueryParameter("appVersionCode", getVersionCodeAsString())
|
|
1037
|
+
.addQueryParameter("appVersionName", getVersionName())
|
|
1038
|
+
.addQueryParameter("bundleId", getCurrentBundleId())
|
|
1039
|
+
.addQueryParameter("channelName", channel)
|
|
1040
|
+
.addQueryParameter("customId", preferences.getCustomId())
|
|
1041
|
+
.addQueryParameter("deviceId", getDeviceId())
|
|
1042
|
+
.addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
|
|
1043
|
+
.addQueryParameter("platform", "0")
|
|
1044
|
+
.addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
|
|
1045
|
+
.build()
|
|
1046
|
+
.toString();
|
|
1047
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
|
|
1048
|
+
|
|
1049
|
+
httpClient.enqueue(
|
|
1050
|
+
url,
|
|
1051
|
+
new NonEmptyCallback<Response>() {
|
|
1052
|
+
@Override
|
|
1053
|
+
public void success(@NonNull Response response) {
|
|
1054
|
+
try {
|
|
1055
|
+
String responseBodyString = response.body().string();
|
|
1056
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
|
|
1057
|
+
if (response.isSuccessful()) {
|
|
1058
|
+
JSONObject responseJson = new JSONObject(responseBodyString);
|
|
1059
|
+
GetLatestBundleResponse result = new GetLatestBundleResponse(responseJson);
|
|
1060
|
+
callback.success(result);
|
|
1061
|
+
} else {
|
|
1062
|
+
callback.success(null);
|
|
1063
|
+
}
|
|
1064
|
+
} catch (Exception e) {
|
|
1065
|
+
callback.error(e);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
@Override
|
|
1070
|
+
public void error(@NonNull Exception exception) {
|
|
1071
|
+
callback.error(exception);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
);
|
|
1075
|
+
} catch (Exception e) {
|
|
1076
|
+
callback.error(e);
|
|
635
1077
|
}
|
|
636
1078
|
}
|
|
637
1079
|
|
|
638
|
-
private String[]
|
|
1080
|
+
private String[] getDownloadedBundleIds() {
|
|
639
1081
|
File bundlesDirectory = buildBundlesDirectory();
|
|
640
1082
|
File[] bundles = bundlesDirectory.listFiles();
|
|
641
1083
|
if (bundles == null) {
|
|
@@ -675,9 +1117,22 @@ public class LiveUpdate {
|
|
|
675
1117
|
return checksum.toString();
|
|
676
1118
|
}
|
|
677
1119
|
|
|
1120
|
+
@Nullable
|
|
1121
|
+
private String getAppId() {
|
|
1122
|
+
String appId = preferences.getAppId();
|
|
1123
|
+
if (appId != null) {
|
|
1124
|
+
return appId;
|
|
1125
|
+
}
|
|
1126
|
+
return config.getAppId();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
678
1129
|
@Nullable
|
|
679
1130
|
private String getChannel() {
|
|
680
1131
|
String channel = null;
|
|
1132
|
+
String nativeChannel = getNativeChannel();
|
|
1133
|
+
if (nativeChannel != null) {
|
|
1134
|
+
channel = nativeChannel;
|
|
1135
|
+
}
|
|
681
1136
|
if (config.getDefaultChannel() != null) {
|
|
682
1137
|
channel = config.getDefaultChannel();
|
|
683
1138
|
}
|
|
@@ -687,13 +1142,26 @@ public class LiveUpdate {
|
|
|
687
1142
|
return channel;
|
|
688
1143
|
}
|
|
689
1144
|
|
|
1145
|
+
@Nullable
|
|
1146
|
+
private String getNativeChannel() {
|
|
1147
|
+
int resId = plugin
|
|
1148
|
+
.getContext()
|
|
1149
|
+
.getResources()
|
|
1150
|
+
.getIdentifier("capawesome_live_update_default_channel", "string", plugin.getContext().getPackageName());
|
|
1151
|
+
if (resId == 0) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
return plugin.getContext().getResources().getString(resId);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
690
1157
|
/**
|
|
691
|
-
* @return The current bundle ID
|
|
1158
|
+
* @return The current bundle ID or `null` if the default bundle is in use.
|
|
692
1159
|
*/
|
|
1160
|
+
@Nullable
|
|
693
1161
|
private String getCurrentBundleId() {
|
|
694
1162
|
String currentPath = getCurrentCapacitorServerPath();
|
|
695
1163
|
if (currentPath.equals(defaultWebAssetDir)) {
|
|
696
|
-
return
|
|
1164
|
+
return null;
|
|
697
1165
|
}
|
|
698
1166
|
return new File(currentPath).getName();
|
|
699
1167
|
}
|
|
@@ -707,22 +1175,22 @@ public class LiveUpdate {
|
|
|
707
1175
|
|
|
708
1176
|
@NonNull
|
|
709
1177
|
private String getDeviceId() {
|
|
710
|
-
String deviceId = preferences.getDeviceIdForApp(
|
|
1178
|
+
String deviceId = preferences.getDeviceIdForApp(getAppId());
|
|
711
1179
|
if (deviceId == null) {
|
|
712
1180
|
deviceId = UUID.randomUUID().toString().toLowerCase();
|
|
713
|
-
preferences.setDeviceIdForApp(
|
|
1181
|
+
preferences.setDeviceIdForApp(getAppId(), deviceId);
|
|
714
1182
|
}
|
|
715
1183
|
return deviceId;
|
|
716
1184
|
}
|
|
717
1185
|
|
|
718
1186
|
/**
|
|
719
|
-
* @return The next bundle ID
|
|
1187
|
+
* @return The next bundle ID or `null` if the default bundle will be used.
|
|
720
1188
|
*/
|
|
721
|
-
@
|
|
1189
|
+
@Nullable
|
|
722
1190
|
private String getNextBundleId() {
|
|
723
1191
|
String nextPath = getNextCapacitorServerPath();
|
|
724
1192
|
if (nextPath.equals(defaultWebAssetDir)) {
|
|
725
|
-
return
|
|
1193
|
+
return null;
|
|
726
1194
|
}
|
|
727
1195
|
return new File(nextPath).getName();
|
|
728
1196
|
}
|
|
@@ -732,31 +1200,54 @@ public class LiveUpdate {
|
|
|
732
1200
|
*/
|
|
733
1201
|
@NonNull
|
|
734
1202
|
private String getNextCapacitorServerPath() {
|
|
735
|
-
|
|
1203
|
+
String path = plugin
|
|
736
1204
|
.getContext()
|
|
737
1205
|
.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
|
|
738
1206
|
.getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
|
|
1207
|
+
// Empty path means default path
|
|
1208
|
+
if (path.isEmpty()) {
|
|
1209
|
+
path = defaultWebAssetDir;
|
|
1210
|
+
}
|
|
1211
|
+
return path;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* @return The previous bundle ID or `null` if the default bundle was used.
|
|
1216
|
+
*/
|
|
1217
|
+
@Nullable
|
|
1218
|
+
private String getPreviousBundleId() {
|
|
1219
|
+
return preferences.getPreviousBundleId();
|
|
739
1220
|
}
|
|
740
1221
|
|
|
741
|
-
private
|
|
742
|
-
return
|
|
1222
|
+
private int getVersionCodeAsInt() throws PackageManager.NameNotFoundException {
|
|
1223
|
+
return getPackageInfo().versionCode;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
|
|
1227
|
+
return String.valueOf(getVersionCodeAsInt());
|
|
743
1228
|
}
|
|
744
1229
|
|
|
745
1230
|
private String getVersionName() throws PackageManager.NameNotFoundException {
|
|
746
1231
|
return getPackageInfo().versionName;
|
|
747
1232
|
}
|
|
748
1233
|
|
|
749
|
-
private boolean
|
|
1234
|
+
private boolean hasBundleById(@NonNull String bundleId) {
|
|
750
1235
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
751
1236
|
return bundleDirectory.exists();
|
|
752
1237
|
}
|
|
753
1238
|
|
|
1239
|
+
private boolean isBundleInUse(@NonNull String bundleId) {
|
|
1240
|
+
String currentBundleId = getCurrentBundleId();
|
|
1241
|
+
String nextBundleId = getNextBundleId();
|
|
1242
|
+
return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
754
1245
|
@Nullable
|
|
755
1246
|
private Manifest loadCurrentManifest() throws Exception {
|
|
756
1247
|
String currentBundleId = getCurrentBundleId();
|
|
757
|
-
if (currentBundleId
|
|
1248
|
+
if (currentBundleId == null) {
|
|
758
1249
|
AssetManager assets = plugin.getContext().getAssets();
|
|
759
|
-
|
|
1250
|
+
boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
760
1251
|
if (manifestFileExists) {
|
|
761
1252
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
762
1253
|
BufferedSource source = Okio.buffer(Okio.source(inputStream));
|
|
@@ -786,25 +1277,53 @@ public class LiveUpdate {
|
|
|
786
1277
|
return loadManifest(source);
|
|
787
1278
|
}
|
|
788
1279
|
|
|
789
|
-
private
|
|
790
|
-
|
|
791
|
-
String nextBundleId = getNextBundleId();
|
|
792
|
-
return bundleId.equals(currentBundleId) || bundleId.equals(nextBundleId);
|
|
1280
|
+
private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
|
|
1281
|
+
plugin.notifyDownloadBundleProgressListeners(event);
|
|
793
1282
|
}
|
|
794
1283
|
|
|
795
|
-
private void
|
|
796
|
-
if
|
|
797
|
-
|
|
1284
|
+
private void performAutoUpdate() {
|
|
1285
|
+
// Check if enough time has passed since the last check
|
|
1286
|
+
long now = System.currentTimeMillis();
|
|
1287
|
+
if (lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs) {
|
|
1288
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Auto-update skipped. Last check was less than 15 minutes ago.");
|
|
798
1289
|
return;
|
|
799
1290
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1291
|
+
|
|
1292
|
+
// Update the timestamp
|
|
1293
|
+
lastAutoUpdateCheckTimestamp = now;
|
|
1294
|
+
|
|
1295
|
+
// Run sync
|
|
1296
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Auto-update started.");
|
|
1297
|
+
SyncOptions options = new SyncOptions((String) null);
|
|
1298
|
+
NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
|
|
1299
|
+
@Override
|
|
1300
|
+
public void success(@NonNull Result result) {
|
|
1301
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Auto-update completed successfully.");
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
@Override
|
|
1305
|
+
public void error(@NonNull Exception exception) {
|
|
1306
|
+
Logger.error(LiveUpdatePlugin.TAG, "Auto-update failed: " + exception.getMessage(), exception);
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
sync(options, callback);
|
|
803
1310
|
}
|
|
804
1311
|
|
|
805
|
-
private void
|
|
806
|
-
|
|
807
|
-
|
|
1312
|
+
private void rollback() {
|
|
1313
|
+
// Set the rollback flag
|
|
1314
|
+
rollbackPerformed = true;
|
|
1315
|
+
// Set the new previous bundle ID
|
|
1316
|
+
String currentBundleId = getCurrentBundleId();
|
|
1317
|
+
setPreviousBundleId(currentBundleId);
|
|
1318
|
+
// Log the rollback result
|
|
1319
|
+
if (currentBundleId == null) {
|
|
1320
|
+
Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Default bundle is already in use.");
|
|
1321
|
+
} else {
|
|
1322
|
+
Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Rolling back to default bundle.");
|
|
1323
|
+
// Rollback to the default bundle
|
|
1324
|
+
setNextBundleById(null);
|
|
1325
|
+
setCurrentBundleById(null);
|
|
1326
|
+
}
|
|
808
1327
|
}
|
|
809
1328
|
|
|
810
1329
|
@Nullable
|
|
@@ -832,6 +1351,18 @@ public class LiveUpdate {
|
|
|
832
1351
|
return null;
|
|
833
1352
|
}
|
|
834
1353
|
|
|
1354
|
+
/**
|
|
1355
|
+
* @param bundleId The bundle ID to set as the current bundle. If `null`, the default bundle will be used.
|
|
1356
|
+
*/
|
|
1357
|
+
private void setCurrentBundleById(@Nullable String bundleId) {
|
|
1358
|
+
if (bundleId == null) {
|
|
1359
|
+
setCurrentCapacitorServerPath(defaultWebAssetDir);
|
|
1360
|
+
} else {
|
|
1361
|
+
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
1362
|
+
setCurrentCapacitorServerPath(bundleDirectory.getPath());
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
835
1366
|
private void setCurrentCapacitorServerPath(@NonNull String path) {
|
|
836
1367
|
if (path.equals(defaultWebAssetDir)) {
|
|
837
1368
|
this.plugin.getBridge().setServerAssetPath(path);
|
|
@@ -839,15 +1370,72 @@ public class LiveUpdate {
|
|
|
839
1370
|
this.plugin.getBridge().setServerBasePath(path);
|
|
840
1371
|
}
|
|
841
1372
|
this.plugin.getBridge().reload();
|
|
1373
|
+
// Notify listeners
|
|
1374
|
+
notifyReloadedListeners();
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* @param bundleId The bundle ID to set as the next bundle. If `null`, the default bundle will be used.
|
|
1379
|
+
*/
|
|
1380
|
+
private void setNextBundleById(@Nullable String bundleId) {
|
|
1381
|
+
if (bundleId == null) {
|
|
1382
|
+
setNextCapacitorServerPath(defaultWebAssetDir);
|
|
1383
|
+
} else {
|
|
1384
|
+
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
1385
|
+
setNextCapacitorServerPath(bundleDirectory.getPath());
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Notify listeners
|
|
1389
|
+
notifyNextBundleSetListeners(bundleId);
|
|
842
1390
|
}
|
|
843
1391
|
|
|
844
|
-
private void
|
|
845
|
-
|
|
1392
|
+
private void notifyNextBundleSetListeners(@Nullable String bundleId) {
|
|
1393
|
+
NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
|
|
1394
|
+
plugin.notifyNextBundleSetListeners(event);
|
|
846
1395
|
}
|
|
847
1396
|
|
|
848
|
-
private void
|
|
849
|
-
|
|
850
|
-
|
|
1397
|
+
private void notifyReloadedListeners() {
|
|
1398
|
+
plugin.notifyReloadedListeners();
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
private void addBlockedBundleId(@NonNull String bundleId) {
|
|
1402
|
+
String blockedIds = preferences.getBlockedBundleIds();
|
|
1403
|
+
List<String> blockedList = new ArrayList<>();
|
|
1404
|
+
|
|
1405
|
+
// Parse existing blocked IDs
|
|
1406
|
+
if (blockedIds != null && !blockedIds.isEmpty()) {
|
|
1407
|
+
String[] ids = blockedIds.split(",");
|
|
1408
|
+
blockedList.addAll(Arrays.asList(ids));
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Skip if already blocked
|
|
1412
|
+
if (blockedList.contains(bundleId)) {
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Remove oldest if limit reached
|
|
1417
|
+
if (blockedList.size() >= 100) {
|
|
1418
|
+
blockedList.remove(0);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Add new bundle
|
|
1422
|
+
blockedList.add(bundleId);
|
|
1423
|
+
|
|
1424
|
+
// Save back to preferences
|
|
1425
|
+
String newBlockedIds = String.join(",", blockedList);
|
|
1426
|
+
preferences.setBlockedBundleIds(newBlockedIds);
|
|
1427
|
+
|
|
1428
|
+
Logger.debug(LiveUpdatePlugin.TAG, "Bundle blocked: " + bundleId);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
private boolean isBlockedBundleId(@NonNull String bundleId) {
|
|
1432
|
+
String blockedIds = preferences.getBlockedBundleIds();
|
|
1433
|
+
if (blockedIds == null || blockedIds.isEmpty()) {
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
String[] ids = blockedIds.split(",");
|
|
1438
|
+
return Arrays.asList(ids).contains(bundleId);
|
|
851
1439
|
}
|
|
852
1440
|
|
|
853
1441
|
private void setNextCapacitorServerPath(@NonNull String path) {
|
|
@@ -855,11 +1443,25 @@ public class LiveUpdate {
|
|
|
855
1443
|
this.webViewSettingsEditor.commit();
|
|
856
1444
|
}
|
|
857
1445
|
|
|
858
|
-
private
|
|
859
|
-
|
|
1446
|
+
private boolean wasUpdated() throws PackageManager.NameNotFoundException {
|
|
1447
|
+
int lastVersionCode = preferences.getLastVersionCode();
|
|
1448
|
+
int currentVersionCode = getVersionCodeAsInt();
|
|
1449
|
+
return lastVersionCode != currentVersionCode;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
private void saveCurrentVersionCode() throws PackageManager.NameNotFoundException {
|
|
1453
|
+
int currentVersionCode = getVersionCodeAsInt();
|
|
1454
|
+
preferences.setLastVersionCode(currentVersionCode);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
private void setPreviousBundleId(@Nullable String bundleId) {
|
|
1458
|
+
preferences.setPreviousBundleId(bundleId);
|
|
860
1459
|
}
|
|
861
1460
|
|
|
862
1461
|
private void startRollbackTimer() {
|
|
1462
|
+
if (config.getReadyTimeout() <= 0) {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
863
1465
|
stopRollbackTimer();
|
|
864
1466
|
rollbackHandler.postDelayed(() -> rollback(), config.getReadyTimeout());
|
|
865
1467
|
}
|
|
@@ -868,10 +1470,29 @@ public class LiveUpdate {
|
|
|
868
1470
|
rollbackHandler.removeCallbacksAndMessages(null);
|
|
869
1471
|
}
|
|
870
1472
|
|
|
871
|
-
private
|
|
872
|
-
|
|
1473
|
+
private boolean tryCopyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) {
|
|
1474
|
+
try {
|
|
1475
|
+
copyCurrentBundleFile(fileToCopy, destinationDirectory);
|
|
1476
|
+
return true;
|
|
1477
|
+
} catch (IOException exception) {
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
private void tryDeleteDownloadsDirectory() {
|
|
1483
|
+
deleteFileRecursively(buildDownloadsDirectory());
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
private File unzipDownloadedBundle(@NonNull File zipFile) throws IOException {
|
|
1487
|
+
File destination = new File(zipFile.getParentFile(), "bundle");
|
|
873
1488
|
String destinationPath = destination.getPath();
|
|
874
|
-
new ZipFile(zipFile)
|
|
1489
|
+
ZipFile zip = new ZipFile(zipFile);
|
|
1490
|
+
// Clear stored Unix permissions to prevent EACCES errors on newer Android versions
|
|
1491
|
+
// where restrictive directory permissions from the zip block file creation.
|
|
1492
|
+
for (net.lingala.zip4j.model.FileHeader fileHeader : zip.getFileHeaders()) {
|
|
1493
|
+
fileHeader.setExternalFileAttributes(null);
|
|
1494
|
+
}
|
|
1495
|
+
zip.extractAll(destinationPath);
|
|
875
1496
|
return destination;
|
|
876
1497
|
}
|
|
877
1498
|
|
|
@@ -926,12 +1547,6 @@ public class LiveUpdate {
|
|
|
926
1547
|
}
|
|
927
1548
|
}
|
|
928
1549
|
|
|
929
|
-
private boolean wasUpdated() throws PackageManager.NameNotFoundException {
|
|
930
|
-
int lastVersionCode = preferences.getLastVersionCode();
|
|
931
|
-
int currentVersionCode = getPackageInfo().versionCode;
|
|
932
|
-
return lastVersionCode != currentVersionCode;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
1550
|
private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
|
936
1551
|
String packageName = this.plugin.getContext().getPackageName();
|
|
937
1552
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|