@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.
Files changed (56) hide show
  1. package/CapawesomeCapacitorLiveUpdate.podspec +2 -2
  2. package/README.md +429 -20
  3. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +894 -279
  4. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +35 -0
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +56 -11
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +274 -38
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +52 -3
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetChannelsResponseItem.java +28 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +11 -2
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/DownloadBundleProgressEvent.java +32 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/NextBundleSetEvent.java +26 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchChannelsOptions.java +38 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetConfigOptions.java +19 -0
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SyncOptions.java +4 -0
  15. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ChannelResult.java +28 -0
  16. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchChannelsResult.java +28 -0
  17. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +8 -0
  18. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBlockedBundlesResult.java +27 -0
  19. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetConfigResult.java +40 -0
  20. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetDownloadedBundlesResult.java +27 -0
  21. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/IsSyncingResult.java +26 -0
  22. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ReadyResult.java +32 -0
  23. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/DownloadProgressCallback.java +5 -0
  24. package/dist/docs.json +853 -50
  25. package/dist/esm/definitions.d.ts +411 -1
  26. package/dist/esm/definitions.js.map +1 -1
  27. package/dist/esm/web.d.ts +10 -2
  28. package/dist/esm/web.js +24 -0
  29. package/dist/esm/web.js.map +1 -1
  30. package/dist/plugin.cjs.js +24 -0
  31. package/dist/plugin.cjs.js.map +1 -1
  32. package/dist/plugin.js +24 -0
  33. package/dist/plugin.js.map +1 -1
  34. package/ios/Plugin/Classes/Events/DownloadBundleProgressEvent.swift +25 -0
  35. package/ios/Plugin/Classes/Events/NextBundleSetEvent.swift +16 -0
  36. package/ios/Plugin/Classes/Options/FetchChannelsOptions.swift +26 -0
  37. package/ios/Plugin/Classes/Options/SetConfigOptions.swift +14 -0
  38. package/ios/Plugin/Classes/Options/SyncOptions.swift +4 -0
  39. package/ios/Plugin/Classes/Results/ChannelResult.swift +19 -0
  40. package/ios/Plugin/Classes/Results/FetchChannelsResult.swift +16 -0
  41. package/ios/Plugin/Classes/Results/{FetchLatestBundle.swift → FetchLatestBundleResult.swift} +6 -1
  42. package/ios/Plugin/Classes/Results/GetBlockedBundlesResult.swift +16 -0
  43. package/ios/Plugin/Classes/Results/GetConfigResult.swift +19 -0
  44. package/ios/Plugin/Classes/Results/GetDownloadedBundlesResult.swift +16 -0
  45. package/ios/Plugin/Classes/Results/IsSyncingResult.swift +16 -0
  46. package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
  47. package/ios/Plugin/LiveUpdate.swift +456 -152
  48. package/ios/Plugin/LiveUpdateConfig.swift +3 -0
  49. package/ios/Plugin/LiveUpdateHttpClient.swift +21 -4
  50. package/ios/Plugin/LiveUpdatePlugin.swift +174 -14
  51. package/ios/Plugin/LiveUpdatePreferences.swift +54 -12
  52. package/ios/Plugin/Protocols/Api/GetChannelsResponseItem.swift +4 -0
  53. package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +1 -0
  54. package/package.json +5 -5
  55. package/ios/Plugin/LiveUpdatePlugin.h +0 -10
  56. 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
- if (wasUpdated() && config.getResetOnUpdate()) {
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 (!hasBundle(bundleId)) {
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
- deleteBundle(bundleId);
158
+ deleteBundleById(bundleId);
125
159
 
126
160
  callback.success();
127
161
  }
128
162
 
129
- public void downloadBundle(@NonNull DownloadBundleOptions options, @NonNull EmptyCallback callback) throws Exception {
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 (hasBundle(bundleId)) {
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, url, checksum, signature);
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) throws Exception {
153
- GetLatestBundleResponse response = fetchLatestBundle(options);
154
- ArtifactType artifactType = response == null ? null : response.getArtifactType();
155
- String bundleId = response == null ? null : response.getBundleId();
156
- String checksum = response == null ? null : response.getChecksum();
157
- String downloadUrl = response == null ? null : response.getUrl();
158
- String signature = response == null ? null : response.getSignature();
159
- FetchLatestBundleResult result = new FetchLatestBundleResult(artifactType, bundleId, checksum, downloadUrl, signature);
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 = getBundleIds();
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 = getVersionCode();
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 ready() {
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
- setNextCapacitorServerPathToDefaultWebAssetDir();
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 (!hasBundle(bundleId)) {
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
- setNextBundle(bundleId);
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 (hasBundle(bundleId)) {
280
- setNextBundle(bundleId);
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) throws Exception {
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
- GetLatestBundleResponse response = fetchLatestBundle(fetchLatestBundleOptions);
295
- if (response == null) {
296
- Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
297
- SyncResult syncResult = new SyncResult(null);
298
- callback.success(syncResult);
299
- return;
300
- }
301
- ArtifactType artifactType = response.getArtifactType();
302
- String checksum = response.getChecksum();
303
- String signature = response.getSignature();
304
- String latestBundleId = response.getBundleId();
305
- String url = response.getUrl();
306
- // Check if the bundle already exists
307
- if (hasBundle(latestBundleId)) {
308
- String nextBundleId = null;
309
- String currentBundleId = getCurrentBundleId();
310
- if (!latestBundleId.equals(currentBundleId)) {
311
- // Set the next bundle
312
- setNextBundle(latestBundleId);
313
- nextBundleId = latestBundleId;
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
- SyncResult syncResult = new SyncResult(nextBundleId);
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 = unzipFile(zipFile);
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 buildTemporaryDirectory() {
366
- String fileName = UUID.randomUUID().toString();
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 buildTemporaryZipFile() {
371
- String fileName = UUID.randomUUID().toString() + ".zip";
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.equals(defaultWebAssetDir)) {
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 void copyCurrentBundleFiles(@NonNull List<String> hrefs, @NonNull File destinationDirectory) throws IOException {
400
- for (String href : hrefs) {
401
- copyCurrentBundleFile(href, destinationDirectory);
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 createTemporaryDirectory() {
449
- File file = buildTemporaryDirectory();
450
- file.mkdir();
451
- return file;
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 deleteBundle(@NonNull String bundleId) {
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
- setNextCapacitorServerPathToDefaultWebAssetDir();
693
+ setNextBundleById(null);
462
694
  }
463
695
  }
464
696
 
465
697
  private void deleteFileRecursively(@NonNull File file) {
466
698
  if (file.isDirectory()) {
467
- for (File child : file.listFiles()) {
468
- deleteFileRecursively(child);
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 = getBundleIds();
727
+ String[] bundleIds = getDownloadedBundleIds();
476
728
  for (String bundleId : bundleIds) {
477
729
  if (!isBundleInUse(bundleId)) {
478
- deleteBundle(bundleId);
730
+ deleteBundleById(bundleId);
479
731
  }
480
732
  }
481
733
  }
482
734
 
483
- private void downloadAndVerifyFile(@NonNull String url, @NonNull File file, @Nullable String checksum, @Nullable String signature)
484
- throws Exception {
485
- Response response = httpClient.execute(url);
486
- ResponseBody responseBody = response.body();
487
- if (response.isSuccessful()) {
488
- LiveUpdateHttpClient.writeResponseBodyToFile(responseBody, file);
489
- // Verify the file
490
- checksum = checksum == null ? LiveUpdateHttpClient.getChecksumFromResponse(response) : checksum;
491
- signature = signature == null ? LiveUpdateHttpClient.getSignatureFromResponse(response) : signature;
492
- verifyFile(file, checksum, signature);
493
- } else {
494
- Exception exception = new Exception(responseBody.string());
495
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
496
- throw new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
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 File downloadBundleFile(@NonNull String baseUrl, @NonNull String href, @NonNull File destinationDirectory) throws Exception {
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
- return destinationFile;
509
- }
510
-
511
- private void downloadBundleFiles(@NonNull String baseUrl, @NonNull List<String> hrefs, @NonNull File destinationDirectory)
512
- throws Exception {
513
- // Limit the number of threads to twice the number of processors
514
- int maxThreads = Runtime.getRuntime().availableProcessors() * 2;
515
- List<Thread> threads = new ArrayList<>();
516
- AtomicReference<Exception> exceptionReference = new AtomicReference<>();
517
- for (String href : hrefs) {
518
- // Wait until a thread is available
519
- while (threads.size() >= maxThreads) {
520
- for (Thread thread : threads) {
521
- if (!thread.isAlive()) {
522
- threads.remove(thread);
523
- break;
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
- // Wait a bit before checking again
527
- Thread.sleep(10);
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
- copyCurrentBundleFiles(hrefsToCopy, temporaryDirectory);
579
- // Download the files
580
- List<String> hrefsToDownload = new ArrayList<>();
581
- for (ManifestItem item : itemsToDownload) {
582
- hrefsToDownload.add(item.getHref());
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
- ) throws Exception {
595
- File file = buildTemporaryZipFile();
596
- // Download the bundle
597
- downloadAndVerifyFile(downloadUrl, file, checksum, signature);
598
- // Add the bundle
599
- addBundleOfTypeZip(bundleId, file);
600
- // Delete the temporary file
601
- file.delete();
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
- @Nullable
605
- private GetLatestBundleResponse fetchLatestBundle(@NonNull FetchLatestBundleOptions options) throws Exception {
606
- String channel = options.getChannel() == null ? getChannel() : options.getChannel();
607
- String url = new HttpUrl.Builder()
608
- .scheme("https")
609
- .host(host)
610
- .addPathSegment("v1")
611
- .addPathSegment("apps")
612
- .addPathSegment(config.getAppId())
613
- .addPathSegment("bundles")
614
- .addPathSegment("latest")
615
- .addQueryParameter("appVersionCode", getVersionCode())
616
- .addQueryParameter("appVersionName", getVersionName())
617
- .addQueryParameter("bundleId", getCurrentBundleId())
618
- .addQueryParameter("channelName", channel)
619
- .addQueryParameter("customId", preferences.getCustomId())
620
- .addQueryParameter("deviceId", getDeviceId())
621
- .addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
622
- .addQueryParameter("platform", "0")
623
- .addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
624
- .build()
625
- .toString();
626
- Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
627
- Response response = httpClient.execute(url);
628
- String responseBodyString = response.body().string();
629
- Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
630
- if (response.isSuccessful()) {
631
- JSONObject responseJson = new JSONObject(responseBodyString);
632
- return new GetLatestBundleResponse(responseJson);
633
- } else {
634
- return null;
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[] getBundleIds() {
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 (`public` for the built-in bundle).
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 defaultWebAssetDir;
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(config.getAppId());
1178
+ String deviceId = preferences.getDeviceIdForApp(getAppId());
711
1179
  if (deviceId == null) {
712
1180
  deviceId = UUID.randomUUID().toString().toLowerCase();
713
- preferences.setDeviceIdForApp(config.getAppId(), deviceId);
1181
+ preferences.setDeviceIdForApp(getAppId(), deviceId);
714
1182
  }
715
1183
  return deviceId;
716
1184
  }
717
1185
 
718
1186
  /**
719
- * @return The next bundle ID (`public` for the built-in bundle).
1187
+ * @return The next bundle ID or `null` if the default bundle will be used.
720
1188
  */
721
- @NonNull
1189
+ @Nullable
722
1190
  private String getNextBundleId() {
723
1191
  String nextPath = getNextCapacitorServerPath();
724
1192
  if (nextPath.equals(defaultWebAssetDir)) {
725
- return defaultWebAssetDir;
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
- return plugin
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 String getVersionCode() throws PackageManager.NameNotFoundException {
742
- return String.valueOf(getPackageInfo().versionCode);
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 hasBundle(@NonNull String bundleId) {
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.equals(defaultWebAssetDir)) {
1248
+ if (currentBundleId == null) {
758
1249
  AssetManager assets = plugin.getContext().getAssets();
759
- Boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
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 boolean isBundleInUse(@NonNull String bundleId) {
790
- String currentBundleId = getCurrentBundleId();
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 rollback() {
796
- if (getCurrentBundleId() == defaultWebAssetDir) {
797
- Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Default bundle is already in use.");
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
- Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Rolling back to default bundle.");
801
- setNextCapacitorServerPathToDefaultWebAssetDir();
802
- setCurrentCapacitorServerPathToDefaultWebAssetDir();
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 saveCurrentVersionCode() throws PackageManager.NameNotFoundException {
806
- int currentVersionCode = getPackageInfo().versionCode;
807
- preferences.setLastVersionCode(currentVersionCode);
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 setCurrentCapacitorServerPathToDefaultWebAssetDir() {
845
- setCurrentCapacitorServerPath(defaultWebAssetDir);
1392
+ private void notifyNextBundleSetListeners(@Nullable String bundleId) {
1393
+ NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
1394
+ plugin.notifyNextBundleSetListeners(event);
846
1395
  }
847
1396
 
848
- private void setNextBundle(@NonNull String bundleId) {
849
- File bundleDirectory = buildBundleDirectoryFor(bundleId);
850
- setNextCapacitorServerPath(bundleDirectory.getPath());
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 void setNextCapacitorServerPathToDefaultWebAssetDir() {
859
- setNextCapacitorServerPath(defaultWebAssetDir);
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 File unzipFile(@NonNull File zipFile) throws IOException {
872
- File destination = buildTemporaryDirectory();
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).extractAll(destinationPath);
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) {