@capawesome/capacitor-live-update 8.5.0 → 8.7.0

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 (26) hide show
  1. package/CapawesomeCapacitorLiveUpdate.podspec +15 -0
  2. package/Package.swift +21 -5
  3. package/README.md +38 -5
  4. package/android/build.gradle +19 -0
  5. package/android/consumer-rules.pro +5 -0
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +129 -55
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +31 -1
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +12 -14
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +11 -5
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchLatestBundleOptions.java +23 -1
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +30 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicManager.kt +229 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicPreferences.kt +85 -0
  14. package/dist/docs.json +1 -1
  15. package/dist/esm/definitions.d.ts +3 -0
  16. package/dist/esm/definitions.js.map +1 -1
  17. package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +7 -6
  18. package/ios/Plugin/Classes/Options/FetchLatestBundleOptions.swift +19 -3
  19. package/ios/Plugin/Classes/Results/FetchLatestBundleResult.swift +24 -0
  20. package/ios/Plugin/Enums/CustomError.swift +13 -0
  21. package/ios/Plugin/LiveUpdate.swift +66 -39
  22. package/ios/Plugin/LiveUpdatePlugin.swift +2 -2
  23. package/ios/Plugin/Providers/Ionic/LiveUpdateIonicManager.swift +187 -0
  24. package/ios/Plugin/Providers/Ionic/LiveUpdateIonicPreferences.swift +81 -0
  25. package/ios/Plugin/Providers/Ionic/LiveUpdatePlugin+LiveUpdateProvider.swift +15 -0
  26. package/package.json +2 -1
@@ -17,4 +17,19 @@ Pod::Spec.new do |s|
17
17
  s.dependency 'ZIPFoundation', '~> 0.9.0'
18
18
  s.swift_version = '5.1'
19
19
  s.static_framework = true
20
+ s.default_subspec = 'Default'
21
+
22
+ s.subspec 'Default' do |ss|
23
+ # Default subspec
24
+ end
25
+
26
+ # Opt-in subspec that pulls in the Ionic Live Update Provider SDK and
27
+ # compiles in the provider/manager classes guarded by
28
+ # `#if CAPAWESOME_INCLUDE_IONIC_PROVIDER`.
29
+ s.subspec 'IonicProvider' do |ss|
30
+ ss.dependency 'LiveUpdateProvider', '~> 1.0.0'
31
+ ss.pod_target_xcconfig = {
32
+ 'OTHER_SWIFT_FLAGS' => '$(inherited) -DCAPAWESOME_INCLUDE_IONIC_PROVIDER'
33
+ }
34
+ end
20
35
  end
package/Package.swift CHANGED
@@ -1,4 +1,4 @@
1
- // swift-tools-version: 5.9
1
+ // swift-tools-version: 6.1
2
2
  import PackageDescription
3
3
 
4
4
  let package = Package(
@@ -9,10 +9,17 @@ let package = Package(
9
9
  name: "CapawesomeCapacitorLiveUpdate",
10
10
  targets: ["LiveUpdatePlugin"])
11
11
  ],
12
+ traits: [
13
+ .trait(
14
+ name: "IonicProvider",
15
+ description: "Enables the Ionic Live Update Provider SDK integration."
16
+ )
17
+ ],
12
18
  dependencies: [
13
19
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
14
20
  .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.10.2")),
15
- .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.0"))
21
+ .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.0")),
22
+ .package(url: "https://github.com/ionic-team/live-update-provider-sdk.git", from: "1.0.0")
16
23
  ],
17
24
  targets: [
18
25
  .target(
@@ -21,8 +28,17 @@ let package = Package(
21
28
  .product(name: "Capacitor", package: "capacitor-swift-pm"),
22
29
  .product(name: "Cordova", package: "capacitor-swift-pm"),
23
30
  .product(name: "Alamofire", package: "Alamofire"),
24
- .product(name: "ZIPFoundation", package: "ZIPFoundation")
31
+ .product(name: "ZIPFoundation", package: "ZIPFoundation"),
32
+ .product(
33
+ name: "LiveUpdateProvider",
34
+ package: "live-update-provider-sdk",
35
+ condition: .when(traits: ["IonicProvider"])
36
+ )
25
37
  ],
26
- path: "ios/Plugin")
27
- ]
38
+ path: "ios/Plugin",
39
+ swiftSettings: [
40
+ .define("CAPAWESOME_INCLUDE_IONIC_PROVIDER", .when(traits: ["IonicProvider"]))
41
+ ])
42
+ ],
43
+ swiftLanguageModes: [.v5]
28
44
  )
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  Capacitor plugin that allows you to update your app remotely in real-time without requiring users to download a new version from the app store, known as Over-the-Air (OTA) updates.
4
4
 
5
5
  <div class="capawesome-z29o10a">
6
- <a href="https://cloud.capawesome.io/" target="_blank">
7
- <img alt="Deliver Live Updates to your Capacitor app with Capawesome Cloud" src="https://cloud.capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1" />
6
+ <a href="https://capawesome.io/" target="_blank">
7
+ <img alt="Ship a fix in one command with Capawesome Cloud Live Updates, no store review" src="https://capawesome.io/assets/banners/cloud-ship-a-fix-in-one-command.png" />
8
8
  </a>
9
9
  </div>
10
10
 
@@ -25,6 +25,8 @@ The Capacitor Live Update plugin is one of the most feature-rich Over-the-Air (O
25
25
  - 📡 **Update Lifecycle Events**: Track download progress, react to bundle changes, and detect reloads with auto-blocking of rolled back bundles.
26
26
  - 🏠 **Self-Hosted Bundles**: Download bundles from any URL, no Capawesome Cloud dependency required.
27
27
  - 🏷️ **Custom Properties**: Associate custom key-value metadata with bundles via Capawesome Cloud.
28
+ - 🌀 **Ionic Portals Support**: Power live updates for [Ionic Portals](https://ionic.io/docs/portals) via the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk).
29
+ - 🧩 **Federated Capacitor Support**: Power live updates for [Federated Capacitor](https://ionic.io/docs/portals/for-capacitor/overview) shells via the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk).
28
30
  - 🔒 **Security**: Verify the authenticity and integrity of the bundle using a public key.
29
31
  - ⚔️ **Battle-Tested**: Used in more than 1,000 projects to update apps on more than 20,000,000 devices.
30
32
  - 🌐 **Open Source**: Licensed under the MIT License.
@@ -114,6 +116,8 @@ android {
114
116
 
115
117
  If needed, you can define the following project variable in your app’s `variables.gradle` file to change the default version of the dependency:
116
118
 
119
+ - `$ionicLiveUpdateProviderVersion` version of `io.ionic:liveupdateprovider` (default: `1.0.0`)
120
+ - `$kotlinxCoroutinesVersion` version of `org.jetbrains.kotlinx:kotlinx-coroutines-android` (default: `1.11.0`)
117
121
  - `$okhttp3Version` version of `com.squareup.okhttp3:okhttp` and `com.squareup.okhttp3:okhttp-brotli` (default: `5.3.2`)
118
122
  - `$zip4jVersion` version of `net.lingala.zip4j:zip4j` (default: `2.11.5`)
119
123
 
@@ -160,6 +164,31 @@ Add the `NSPrivacyAccessedAPICategoryUserDefaults` dictionary key to your [Priva
160
164
 
161
165
  We recommend to declare [`CA92.1`](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401) as the reason for accessing the [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults) API.
162
166
 
167
+ #### Swift Package Manager
168
+
169
+ ##### Package traits
170
+
171
+ The Ionic Live Update Provider SDK is optional and is not included by default. If you use this plugin as a live update provider for Ionic Portals or Federated Capacitor, enable the `IonicProvider` package trait so that the SDK is linked into your app:
172
+
173
+ ```json
174
+ {
175
+ "experimental": {
176
+ "ios": {
177
+ "spm": {
178
+ "swiftToolsVersion": "6.1",
179
+ "packageTraits": {
180
+ "@capawesome/capacitor-live-update": ["IonicProvider"]
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ This trait is the Swift Package Manager equivalent of the `CapawesomeCapacitorLiveUpdate/IonicProvider` CocoaPods subspec. See [Ionic Live Update Provider SDK Integration](#ionic-live-update-provider-sdk-integration) for details.
189
+
190
+ **Attention**: SPM trait support requires Capacitor CLI **8.3.0+** and Xcode **16.3+** (Swift 6.1+).
191
+
163
192
  ### Electron
164
193
 
165
194
  This plugin supports the [Capacitor Electron platform](https://github.com/capawesome-team/capacitor-electron) (`@capawesome/capacitor-electron` version `0.2.0` or later). No additional configuration is required.
@@ -190,6 +219,10 @@ export default defineConfig({
190
219
  });
191
220
  ```
192
221
 
222
+ ## Ionic Live Update Provider SDK Integration
223
+
224
+ This plugin implements the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk) contract, so that **Ionic Portals** and **Federated Capacitor** apps can use Capawesome Cloud to deliver live updates. Federated Capacitor resolves the provider by the plugin name `LiveUpdate`. The manager configuration accepts the keys `appId`, `channel`, `publicKey`, `serverDomain` and `httpTimeout`; we recommend enabling code signing via `publicKey` in production. See [the full setup guide](https://github.com/capawesome-team/capacitor-plugins/blob/main/packages/live-update/docs/ionic-live-update-provider-sdk-integration.md) for details.
225
+
193
226
  ## Configuration
194
227
 
195
228
  <docgen-config>
@@ -1230,9 +1263,9 @@ Remove all listeners for this plugin.
1230
1263
 
1231
1264
  #### GetDeviceIdResult
1232
1265
 
1233
- | Prop | Type | Description | Since |
1234
- | -------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
1235
- | **`deviceId`** | <code>string</code> | The unique identifier of the device. On iOS, [`identifierForVendor`](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) is used. The value of this property is the same for apps that come from the same vendor running on the same device. On Electron, a random UUID is generated on first use and stored in the app's user data directory. The value changes when the user data is cleared. | 5.0.0 |
1266
+ | Prop | Type | Description | Since |
1267
+ | -------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
1268
+ | **`deviceId`** | <code>string</code> | The unique identifier of the device. On iOS, [`identifierForVendor`](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) is used. The value of this property is the same for apps that come from the same vendor running on the same device. On Android, a random UUID is generated once per installation and stored by the plugin. The value is shared across all app IDs and changes when the app is reinstalled or its data is cleared. On Electron, a random UUID is generated on first use and stored in the app's user data directory. The value changes when the user data is cleared. | 5.0.0 |
1236
1269
 
1237
1270
 
1238
1271
  #### IsSyncingResult
@@ -1,3 +1,5 @@
1
+ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2
+
1
3
  ext {
2
4
  junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
5
  androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
@@ -5,19 +7,24 @@ ext {
5
7
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
6
8
  okhttp3Version = project.hasProperty('okhttp3Version') ? rootProject.ext.okhttp3Version : '5.3.2'
7
9
  zip4jVersion = project.hasProperty('zip4jVersion') ? rootProject.ext.zip4jVersion : '2.11.5'
10
+ ionicLiveUpdateProviderVersion = project.hasProperty('ionicLiveUpdateProviderVersion') ? rootProject.ext.ionicLiveUpdateProviderVersion : '1.0.0'
11
+ kotlinxCoroutinesVersion = project.hasProperty('kotlinxCoroutinesVersion') ? rootProject.ext.kotlinxCoroutinesVersion : '1.11.0'
8
12
  }
9
13
 
10
14
  buildscript {
15
+ ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '2.2.20'
11
16
  repositories {
12
17
  google()
13
18
  mavenCentral()
14
19
  }
15
20
  dependencies {
16
21
  classpath 'com.android.tools.build:gradle:8.13.0'
22
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
17
23
  }
18
24
  }
19
25
 
20
26
  apply plugin: 'com.android.library'
27
+ apply plugin: 'org.jetbrains.kotlin.android'
21
28
 
22
29
  android {
23
30
  namespace = "io.capawesome.capacitorjs.plugins.liveupdate"
@@ -28,6 +35,7 @@ android {
28
35
  versionCode 1
29
36
  versionName "1.0"
30
37
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
38
+ consumerProguardFiles 'consumer-rules.pro'
31
39
  }
32
40
  buildTypes {
33
41
  release {
@@ -44,6 +52,12 @@ android {
44
52
  }
45
53
  }
46
54
 
55
+ kotlin {
56
+ compilerOptions {
57
+ jvmTarget = JvmTarget.JVM_21
58
+ }
59
+ }
60
+
47
61
  repositories {
48
62
  google()
49
63
  mavenCentral()
@@ -57,6 +71,11 @@ dependencies {
57
71
  implementation "net.lingala.zip4j:zip4j:$zip4jVersion"
58
72
  implementation "com.squareup.okhttp3:okhttp:$okhttp3Version"
59
73
  implementation "com.squareup.okhttp3:okhttp-brotli:$okhttp3Version"
74
+ // The Ionic Live Update Provider SDK is compile-time only: Federated Capacitor
75
+ // and Ionic Portals bring it transitively at runtime, and the provider classes
76
+ // are only loaded when a host actually calls `createManager`.
77
+ compileOnly "io.ionic:liveupdateprovider:$ionicLiveUpdateProviderVersion"
78
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion"
60
79
  testImplementation "junit:junit:$junitVersion"
61
80
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
62
81
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -0,0 +1,5 @@
1
+ # The Ionic Live Update Provider SDK is a compileOnly dependency: it is only
2
+ # present at runtime when the app also uses Federated Capacitor or Ionic
3
+ # Portals (which bring it transitively). Suppress R8 missing-class errors for
4
+ # apps that don't.
5
+ -dontwarn io.ionic.liveupdateprovider.**
@@ -1,6 +1,7 @@
1
1
  package io.capawesome.capacitorjs.plugins.liveupdate;
2
2
 
3
3
  import android.app.Activity;
4
+ import android.content.Context;
4
5
  import android.content.SharedPreferences;
5
6
  import android.content.pm.PackageInfo;
6
7
  import android.content.pm.PackageManager;
@@ -85,17 +86,22 @@ import org.json.JSONObject;
85
86
 
86
87
  public class LiveUpdate {
87
88
 
89
+ private static final AtomicBoolean downloadsDirectoryDeleted = new AtomicBoolean(false);
90
+
88
91
  private final long autoUpdateIntervalMs = 15 * 60 * 1000; // 15 minutes
89
92
 
90
93
  @NonNull
91
94
  private final LiveUpdateConfig config;
92
95
 
96
+ @NonNull
97
+ private final Context context;
98
+
93
99
  private final String defaultWebAssetDir = Bridge.DEFAULT_WEB_ASSET_DIR;
94
100
 
95
101
  @NonNull
96
102
  private final LiveUpdateHttpClient httpClient;
97
103
 
98
- @NonNull
104
+ @Nullable
99
105
  private final LiveUpdatePlugin plugin;
100
106
 
101
107
  @NonNull
@@ -115,11 +121,26 @@ public class LiveUpdate {
115
121
  private boolean syncInProgress = false;
116
122
 
117
123
  public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
124
+ this(config, plugin.getContext(), plugin);
125
+ }
126
+
127
+ /**
128
+ * Creates a headless instance without a Capacitor plugin, e.g. for Ionic Portals hosts
129
+ * that construct a provider manager directly. WebView-related operations (server path
130
+ * changes, reload) and plugin events are skipped in this mode.
131
+ */
132
+ public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull Context context) throws PackageManager.NameNotFoundException {
133
+ this(config, context, null);
134
+ }
135
+
136
+ private LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull Context context, @Nullable LiveUpdatePlugin plugin)
137
+ throws PackageManager.NameNotFoundException {
118
138
  this.config = config;
139
+ this.context = context;
119
140
  this.httpClient = new LiveUpdateHttpClient(config);
120
141
  this.plugin = plugin;
121
- this.preferences = new LiveUpdatePreferences(plugin.getContext());
122
- this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
142
+ this.preferences = new LiveUpdatePreferences(context);
143
+ this.webViewSettingsEditor = context.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
123
144
 
124
145
  // Check version and reset config if version changed
125
146
  checkAndResetConfigIfVersionChanged();
@@ -127,8 +148,11 @@ public class LiveUpdate {
127
148
  // Set the device ID on the HTTP client (after any potential config reset)
128
149
  this.httpClient.setDeviceId(getDeviceId());
129
150
 
130
- // Delete the leftovers of downloads that were interrupted by a process kill
131
- tryDeleteDownloadsDirectory();
151
+ // Delete the leftovers of downloads that were interrupted by a process kill.
152
+ // Only once per process, because other instances may be downloading at this moment.
153
+ if (downloadsDirectoryDeleted.compareAndSet(false, true)) {
154
+ tryDeleteDownloadsDirectory();
155
+ }
132
156
 
133
157
  // Start the rollback timer to rollback to the default bundle
134
158
  // if the app is not ready after a certain time
@@ -264,6 +288,15 @@ public class LiveUpdate {
264
288
  );
265
289
  }
266
290
 
291
+ @Nullable
292
+ public String getAppId() {
293
+ String appId = preferences.getAppId();
294
+ if (appId != null) {
295
+ return appId;
296
+ }
297
+ return config.getAppId();
298
+ }
299
+
267
300
  public void getBlockedBundles(@NonNull NonEmptyCallback<GetBlockedBundlesResult> callback) {
268
301
  String blockedIds = preferences.getBlockedBundleIds();
269
302
  String[] bundleIds;
@@ -294,6 +327,22 @@ public class LiveUpdate {
294
327
  callback.success(result);
295
328
  }
296
329
 
330
+ @Nullable
331
+ public String getChannel() {
332
+ String channel = null;
333
+ if (config.getDefaultChannel() != null) {
334
+ channel = config.getDefaultChannel();
335
+ }
336
+ String nativeChannel = getNativeChannel();
337
+ if (nativeChannel != null) {
338
+ channel = nativeChannel;
339
+ }
340
+ if (preferences.getChannel() != null) {
341
+ channel = preferences.getChannel();
342
+ }
343
+ return channel;
344
+ }
345
+
297
346
  public void getConfig(@NonNull NonEmptyCallback<GetConfigResult> callback) {
298
347
  String appId = getAppId();
299
348
  String autoUpdateStrategy = config.getAutoUpdateStrategy();
@@ -307,6 +356,17 @@ public class LiveUpdate {
307
356
  callback.success(result);
308
357
  }
309
358
 
359
+ /**
360
+ * @return The on-disk directory of a downloaded bundle, or `null` if the bundle does not exist.
361
+ */
362
+ @Nullable
363
+ public File getBundleDirectory(@NonNull String bundleId) {
364
+ if (!hasBundleById(bundleId)) {
365
+ return null;
366
+ }
367
+ return buildBundleDirectoryFor(bundleId);
368
+ }
369
+
310
370
  public void getCustomId(@NonNull NonEmptyCallback callback) {
311
371
  String customId = preferences.getCustomId();
312
372
  GetCustomIdResult result = new GetCustomIdResult(customId);
@@ -331,6 +391,10 @@ public class LiveUpdate {
331
391
  callback.success(result);
332
392
  }
333
393
 
394
+ public String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
395
+ return String.valueOf(getVersionCodeAsInt());
396
+ }
397
+
334
398
  public void getVersionName(@NonNull NonEmptyCallback callback) throws PackageManager.NameNotFoundException {
335
399
  String versionName = getVersionName();
336
400
  GetVersionNameResult result = new GetVersionNameResult(versionName);
@@ -529,6 +593,20 @@ public class LiveUpdate {
529
593
  );
530
594
  }
531
595
 
596
+ /**
597
+ * @return `true` if the bundle does not exist or was deleted, `false` if it is the current or next bundle.
598
+ */
599
+ public boolean tryDeleteUnusedBundle(@NonNull String bundleId) {
600
+ if (!hasBundleById(bundleId)) {
601
+ return true;
602
+ }
603
+ if (isBundleInUse(bundleId)) {
604
+ return false;
605
+ }
606
+ deleteBundleById(bundleId);
607
+ return true;
608
+ }
609
+
532
610
  private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
533
611
  // Search folder with index.html file
534
612
  File indexHtmlFile = searchIndexHtmlFile(sourceDirectory);
@@ -542,7 +620,8 @@ public class LiveUpdate {
542
620
  // Move the bundle directory to the bundles directory
543
621
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
544
622
  boolean moved = indexHtmlFile.getParentFile().renameTo(bundleDirectory);
545
- if (!moved) {
623
+ // A concurrent download of the same bundle may have finished first
624
+ if (!moved && !hasBundleById(bundleId)) {
546
625
  throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_MOVE_FAILED);
547
626
  }
548
627
  }
@@ -559,15 +638,15 @@ public class LiveUpdate {
559
638
  }
560
639
 
561
640
  private File buildBundlesDirectory() {
562
- return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
641
+ return new File(context.getFilesDir(), bundlesDirectory);
563
642
  }
564
643
 
565
644
  private File buildBundleDirectoryFor(@NonNull String bundleId) {
566
- return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
645
+ return new File(context.getFilesDir(), bundlesDirectory + "/" + bundleId);
567
646
  }
568
647
 
569
648
  private File buildDownloadsDirectory() {
570
- return new File(plugin.getContext().getCacheDir(), downloadsDirectory);
649
+ return new File(context.getCacheDir(), downloadsDirectory);
571
650
  }
572
651
 
573
652
  private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
@@ -575,7 +654,7 @@ public class LiveUpdate {
575
654
  String currentBundleId = getCurrentBundleId();
576
655
  if (currentBundleId == null) {
577
656
  // Create the source input stream
578
- AssetManager assets = plugin.getContext().getAssets();
657
+ AssetManager assets = context.getAssets();
579
658
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
580
659
  // Create the destination file
581
660
  File destination = new File(destinationDirectory, href);
@@ -1003,18 +1082,20 @@ public class LiveUpdate {
1003
1082
  @NonNull NonEmptyCallback<GetLatestBundleResponse> callback
1004
1083
  ) {
1005
1084
  try {
1085
+ String appId = requireAppId(options.getAppId());
1086
+ String bundleId = options.getBundleId() == null ? getCurrentBundleId() : options.getBundleId();
1006
1087
  String channel = options.getChannel() == null ? getChannel() : options.getChannel();
1007
1088
  String url = new HttpUrl.Builder()
1008
1089
  .scheme("https")
1009
1090
  .host(config.getServerDomain())
1010
1091
  .addPathSegment("v1")
1011
1092
  .addPathSegment("apps")
1012
- .addPathSegment(requireAppId())
1093
+ .addPathSegment(appId)
1013
1094
  .addPathSegment("bundles")
1014
1095
  .addPathSegment("latest")
1015
1096
  .addQueryParameter("appVersionCode", getVersionCodeAsString())
1016
1097
  .addQueryParameter("appVersionName", getVersionName())
1017
- .addQueryParameter("bundleId", getCurrentBundleId())
1098
+ .addQueryParameter("bundleId", bundleId)
1018
1099
  .addQueryParameter("channelName", channel)
1019
1100
  .addQueryParameter("customId", preferences.getCustomId())
1020
1101
  .addQueryParameter("deviceId", getDeviceId())
@@ -1096,41 +1177,13 @@ public class LiveUpdate {
1096
1177
  return checksum.toString();
1097
1178
  }
1098
1179
 
1099
- @Nullable
1100
- private String getAppId() {
1101
- String appId = preferences.getAppId();
1102
- if (appId != null) {
1103
- return appId;
1104
- }
1105
- return config.getAppId();
1106
- }
1107
-
1108
- @Nullable
1109
- private String getChannel() {
1110
- String channel = null;
1111
- if (config.getDefaultChannel() != null) {
1112
- channel = config.getDefaultChannel();
1113
- }
1114
- String nativeChannel = getNativeChannel();
1115
- if (nativeChannel != null) {
1116
- channel = nativeChannel;
1117
- }
1118
- if (preferences.getChannel() != null) {
1119
- channel = preferences.getChannel();
1120
- }
1121
- return channel;
1122
- }
1123
-
1124
1180
  @Nullable
1125
1181
  private String getNativeChannel() {
1126
- int resId = plugin
1127
- .getContext()
1128
- .getResources()
1129
- .getIdentifier("capawesome_live_update_default_channel", "string", plugin.getContext().getPackageName());
1182
+ int resId = context.getResources().getIdentifier("capawesome_live_update_default_channel", "string", context.getPackageName());
1130
1183
  if (resId == 0) {
1131
1184
  return null;
1132
1185
  }
1133
- return plugin.getContext().getResources().getString(resId);
1186
+ return context.getResources().getString(resId);
1134
1187
  }
1135
1188
 
1136
1189
  /**
@@ -1149,16 +1202,24 @@ public class LiveUpdate {
1149
1202
  * @return The absolute path to the current bundle directory (`public` for the built-in bundle).
1150
1203
  */
1151
1204
  private String getCurrentCapacitorServerPath() {
1205
+ if (plugin == null) {
1206
+ // Headless mode: there is no WebView, so the default bundle is always current.
1207
+ return defaultWebAssetDir;
1208
+ }
1152
1209
  return plugin.getBridge().getServerBasePath();
1153
1210
  }
1154
1211
 
1155
1212
  @NonNull
1156
1213
  private String getDeviceId() {
1157
- String deviceId = preferences.getDeviceIdForApp(getAppId());
1214
+ String deviceId = preferences.getDeviceId();
1215
+ if (deviceId != null) {
1216
+ return deviceId;
1217
+ }
1218
+ deviceId = preferences.getLegacyDeviceIdForApp(getAppId());
1158
1219
  if (deviceId == null) {
1159
1220
  deviceId = UUID.randomUUID().toString().toLowerCase();
1160
- preferences.setDeviceIdForApp(getAppId(), deviceId);
1161
1221
  }
1222
+ preferences.setDeviceId(deviceId);
1162
1223
  return deviceId;
1163
1224
  }
1164
1225
 
@@ -1179,8 +1240,7 @@ public class LiveUpdate {
1179
1240
  */
1180
1241
  @NonNull
1181
1242
  private String getNextCapacitorServerPath() {
1182
- String path = plugin
1183
- .getContext()
1243
+ String path = context
1184
1244
  .getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
1185
1245
  .getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
1186
1246
  // Empty path means default path
@@ -1202,10 +1262,6 @@ public class LiveUpdate {
1202
1262
  return (int) PackageInfoCompat.getLongVersionCode(getPackageInfo());
1203
1263
  }
1204
1264
 
1205
- private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
1206
- return String.valueOf(getVersionCodeAsInt());
1207
- }
1208
-
1209
1265
  private String getVersionName() throws PackageManager.NameNotFoundException {
1210
1266
  return getPackageInfo().versionName;
1211
1267
  }
@@ -1234,7 +1290,7 @@ public class LiveUpdate {
1234
1290
  private Manifest loadCurrentManifest() throws Exception {
1235
1291
  String currentBundleId = getCurrentBundleId();
1236
1292
  if (currentBundleId == null) {
1237
- AssetManager assets = plugin.getContext().getAssets();
1293
+ AssetManager assets = context.getAssets();
1238
1294
  boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
1239
1295
  if (manifestFileExists) {
1240
1296
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
@@ -1266,6 +1322,9 @@ public class LiveUpdate {
1266
1322
  }
1267
1323
 
1268
1324
  private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
1325
+ if (plugin == null) {
1326
+ return;
1327
+ }
1269
1328
  plugin.notifyDownloadBundleProgressListeners(event);
1270
1329
  }
1271
1330
 
@@ -1299,7 +1358,12 @@ public class LiveUpdate {
1299
1358
 
1300
1359
  @NonNull
1301
1360
  private String requireAppId() throws Exception {
1302
- String appId = getAppId();
1361
+ return requireAppId(null);
1362
+ }
1363
+
1364
+ @NonNull
1365
+ private String requireAppId(@Nullable String appIdOverride) throws Exception {
1366
+ String appId = appIdOverride == null ? getAppId() : appIdOverride;
1303
1367
  if (appId == null || appId.isEmpty()) {
1304
1368
  throw new Exception(LiveUpdatePlugin.ERROR_APP_ID_MISSING);
1305
1369
  }
@@ -1361,6 +1425,10 @@ public class LiveUpdate {
1361
1425
  }
1362
1426
 
1363
1427
  private void setCurrentCapacitorServerPath(@NonNull String path) {
1428
+ if (plugin == null) {
1429
+ // Headless mode: there is no WebView to point at the new path.
1430
+ return;
1431
+ }
1364
1432
  if (path.equals(defaultWebAssetDir)) {
1365
1433
  this.plugin.getBridge().setServerAssetPath(path);
1366
1434
  } else {
@@ -1387,11 +1455,17 @@ public class LiveUpdate {
1387
1455
  }
1388
1456
 
1389
1457
  private void notifyNextBundleSetListeners(@Nullable String bundleId) {
1458
+ if (plugin == null) {
1459
+ return;
1460
+ }
1390
1461
  NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
1391
1462
  plugin.notifyNextBundleSetListeners(event);
1392
1463
  }
1393
1464
 
1394
1465
  private void notifyReloadedListeners() {
1466
+ if (plugin == null) {
1467
+ return;
1468
+ }
1395
1469
  plugin.notifyReloadedListeners();
1396
1470
  }
1397
1471
 
@@ -1548,11 +1622,11 @@ public class LiveUpdate {
1548
1622
  }
1549
1623
 
1550
1624
  private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
1551
- String packageName = this.plugin.getContext().getPackageName();
1625
+ String packageName = this.context.getPackageName();
1552
1626
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1553
- return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
1627
+ return this.context.getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
1554
1628
  } else {
1555
- return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, 0);
1629
+ return this.context.getPackageManager().getPackageInfo(packageName, 0);
1556
1630
  }
1557
1631
  }
1558
1632
  }