@capawesome/capacitor-live-update 8.6.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.
- package/CapawesomeCapacitorLiveUpdate.podspec +15 -0
- package/Package.swift +21 -5
- package/README.md +35 -2
- package/android/build.gradle +19 -0
- package/android/consumer-rules.pro +5 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +123 -53
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +31 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +11 -5
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchLatestBundleOptions.java +23 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +30 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicManager.kt +229 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicPreferences.kt +85 -0
- package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +7 -6
- package/ios/Plugin/Classes/Options/FetchLatestBundleOptions.swift +19 -3
- package/ios/Plugin/Classes/Results/FetchLatestBundleResult.swift +24 -0
- package/ios/Plugin/Enums/CustomError.swift +13 -0
- package/ios/Plugin/LiveUpdate.swift +66 -39
- package/ios/Plugin/LiveUpdatePlugin.swift +2 -2
- package/ios/Plugin/Providers/Ionic/LiveUpdateIonicManager.swift +187 -0
- package/ios/Plugin/Providers/Ionic/LiveUpdateIonicPreferences.swift +81 -0
- package/ios/Plugin/Providers/Ionic/LiveUpdatePlugin+LiveUpdateProvider.swift +15 -0
- 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:
|
|
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://
|
|
7
|
-
<img alt="
|
|
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>
|
package/android/build.gradle
CHANGED
|
@@ -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
|
-
@
|
|
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(
|
|
122
|
-
this.webViewSettingsEditor =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
641
|
+
return new File(context.getFilesDir(), bundlesDirectory);
|
|
563
642
|
}
|
|
564
643
|
|
|
565
644
|
private File buildBundleDirectoryFor(@NonNull String bundleId) {
|
|
566
|
-
return new File(
|
|
645
|
+
return new File(context.getFilesDir(), bundlesDirectory + "/" + bundleId);
|
|
567
646
|
}
|
|
568
647
|
|
|
569
648
|
private File buildDownloadsDirectory() {
|
|
570
|
-
return new File(
|
|
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 =
|
|
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(
|
|
1093
|
+
.addPathSegment(appId)
|
|
1013
1094
|
.addPathSegment("bundles")
|
|
1014
1095
|
.addPathSegment("latest")
|
|
1015
1096
|
.addQueryParameter("appVersionCode", getVersionCodeAsString())
|
|
1016
1097
|
.addQueryParameter("appVersionName", getVersionName())
|
|
1017
|
-
.addQueryParameter("bundleId",
|
|
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 =
|
|
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
|
|
1186
|
+
return context.getResources().getString(resId);
|
|
1134
1187
|
}
|
|
1135
1188
|
|
|
1136
1189
|
/**
|
|
@@ -1149,6 +1202,10 @@ 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
|
|
|
@@ -1183,8 +1240,7 @@ public class LiveUpdate {
|
|
|
1183
1240
|
*/
|
|
1184
1241
|
@NonNull
|
|
1185
1242
|
private String getNextCapacitorServerPath() {
|
|
1186
|
-
String path =
|
|
1187
|
-
.getContext()
|
|
1243
|
+
String path = context
|
|
1188
1244
|
.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
|
|
1189
1245
|
.getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
|
|
1190
1246
|
// Empty path means default path
|
|
@@ -1206,10 +1262,6 @@ public class LiveUpdate {
|
|
|
1206
1262
|
return (int) PackageInfoCompat.getLongVersionCode(getPackageInfo());
|
|
1207
1263
|
}
|
|
1208
1264
|
|
|
1209
|
-
private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
|
|
1210
|
-
return String.valueOf(getVersionCodeAsInt());
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
1265
|
private String getVersionName() throws PackageManager.NameNotFoundException {
|
|
1214
1266
|
return getPackageInfo().versionName;
|
|
1215
1267
|
}
|
|
@@ -1238,7 +1290,7 @@ public class LiveUpdate {
|
|
|
1238
1290
|
private Manifest loadCurrentManifest() throws Exception {
|
|
1239
1291
|
String currentBundleId = getCurrentBundleId();
|
|
1240
1292
|
if (currentBundleId == null) {
|
|
1241
|
-
AssetManager assets =
|
|
1293
|
+
AssetManager assets = context.getAssets();
|
|
1242
1294
|
boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
1243
1295
|
if (manifestFileExists) {
|
|
1244
1296
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
@@ -1270,6 +1322,9 @@ public class LiveUpdate {
|
|
|
1270
1322
|
}
|
|
1271
1323
|
|
|
1272
1324
|
private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
|
|
1325
|
+
if (plugin == null) {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1273
1328
|
plugin.notifyDownloadBundleProgressListeners(event);
|
|
1274
1329
|
}
|
|
1275
1330
|
|
|
@@ -1303,7 +1358,12 @@ public class LiveUpdate {
|
|
|
1303
1358
|
|
|
1304
1359
|
@NonNull
|
|
1305
1360
|
private String requireAppId() throws Exception {
|
|
1306
|
-
|
|
1361
|
+
return requireAppId(null);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
@NonNull
|
|
1365
|
+
private String requireAppId(@Nullable String appIdOverride) throws Exception {
|
|
1366
|
+
String appId = appIdOverride == null ? getAppId() : appIdOverride;
|
|
1307
1367
|
if (appId == null || appId.isEmpty()) {
|
|
1308
1368
|
throw new Exception(LiveUpdatePlugin.ERROR_APP_ID_MISSING);
|
|
1309
1369
|
}
|
|
@@ -1365,6 +1425,10 @@ public class LiveUpdate {
|
|
|
1365
1425
|
}
|
|
1366
1426
|
|
|
1367
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
|
+
}
|
|
1368
1432
|
if (path.equals(defaultWebAssetDir)) {
|
|
1369
1433
|
this.plugin.getBridge().setServerAssetPath(path);
|
|
1370
1434
|
} else {
|
|
@@ -1391,11 +1455,17 @@ public class LiveUpdate {
|
|
|
1391
1455
|
}
|
|
1392
1456
|
|
|
1393
1457
|
private void notifyNextBundleSetListeners(@Nullable String bundleId) {
|
|
1458
|
+
if (plugin == null) {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1394
1461
|
NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
|
|
1395
1462
|
plugin.notifyNextBundleSetListeners(event);
|
|
1396
1463
|
}
|
|
1397
1464
|
|
|
1398
1465
|
private void notifyReloadedListeners() {
|
|
1466
|
+
if (plugin == null) {
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1399
1469
|
plugin.notifyReloadedListeners();
|
|
1400
1470
|
}
|
|
1401
1471
|
|
|
@@ -1552,11 +1622,11 @@ public class LiveUpdate {
|
|
|
1552
1622
|
}
|
|
1553
1623
|
|
|
1554
1624
|
private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
|
1555
|
-
String packageName = this.
|
|
1625
|
+
String packageName = this.context.getPackageName();
|
|
1556
1626
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1557
|
-
return this.
|
|
1627
|
+
return this.context.getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
|
|
1558
1628
|
} else {
|
|
1559
|
-
return this.
|
|
1629
|
+
return this.context.getPackageManager().getPackageInfo(packageName, 0);
|
|
1560
1630
|
}
|
|
1561
1631
|
}
|
|
1562
1632
|
}
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package io.capawesome.capacitorjs.plugins.liveupdate;
|
|
2
2
|
|
|
3
|
+
import android.content.Context;
|
|
3
4
|
import android.webkit.WebView;
|
|
4
5
|
import androidx.annotation.NonNull;
|
|
5
6
|
import androidx.annotation.Nullable;
|
|
@@ -32,14 +33,18 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingRes
|
|
|
32
33
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
|
|
33
34
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
|
|
34
35
|
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
36
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic.LiveUpdateIonicManager;
|
|
37
|
+
import io.ionic.liveupdateprovider.ProviderError;
|
|
38
|
+
import java.util.Map;
|
|
35
39
|
|
|
36
40
|
@CapacitorPlugin(name = "LiveUpdate")
|
|
37
41
|
public class LiveUpdatePlugin extends Plugin {
|
|
38
42
|
|
|
39
43
|
public static final String TAG = "LiveUpdate";
|
|
40
|
-
public static final String VERSION = "8.
|
|
44
|
+
public static final String VERSION = "8.7.0";
|
|
41
45
|
public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
|
|
42
46
|
public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
|
|
47
|
+
public static final String ERROR_BUNDLE_DIRECTORY_NOT_FOUND = "Bundle directory could not be resolved.";
|
|
43
48
|
public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
|
|
44
49
|
public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
|
|
45
50
|
public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
|
|
@@ -49,7 +54,9 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
49
54
|
public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
|
|
50
55
|
public static final String ERROR_CUSTOM_ID_MISSING = "customId must be provided.";
|
|
51
56
|
public static final String ERROR_DOWNLOAD_FAILED = "Bundle could not be downloaded.";
|
|
57
|
+
public static final String ERROR_DOWNLOAD_URL_MISSING = "Bundle does not have a valid download URL.";
|
|
52
58
|
public static final String ERROR_HTTP_TIMEOUT = "Request timed out.";
|
|
59
|
+
public static final String ERROR_PLUGIN_NOT_INITIALIZED = "Plugin is not initialized.";
|
|
53
60
|
public static final String ERROR_URL_MISSING = "url must be provided.";
|
|
54
61
|
public static final String ERROR_SIGNATURE_VERIFICATION_FAILED = "Signature verification failed.";
|
|
55
62
|
public static final String ERROR_PUBLIC_KEY_INVALID = "Invalid public key.";
|
|
@@ -73,6 +80,29 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Creates a manager for the Ionic Live Update Provider SDK. Invoked natively by
|
|
85
|
+
* Federated Capacitor after resolving this plugin by its Capacitor plugin name.
|
|
86
|
+
*
|
|
87
|
+
* The SDK is a compileOnly dependency, so this class must load without it:
|
|
88
|
+
* - It does NOT implement the SDK's `LiveUpdateProvider` interface, because superinterfaces
|
|
89
|
+
* are resolved when the class is loaded.
|
|
90
|
+
* - The return type is `Object` instead of `ProviderManager`, because Capacitor calls
|
|
91
|
+
* `Class.getDeclaredMethods()` on every plugin class at load, which resolves the return
|
|
92
|
+
* and parameter types of all declared methods.
|
|
93
|
+
*
|
|
94
|
+
* Federated Capacitor falls back to resolving `createManager(Context, Map)` reflectively by
|
|
95
|
+
* name and parameter types and casts the result, so neither must change. SDK types are only
|
|
96
|
+
* referenced inside the method body, which is resolved lazily.
|
|
97
|
+
*/
|
|
98
|
+
@NonNull
|
|
99
|
+
public Object createManager(@NonNull Context context, @NonNull Map<String, ?> configuration) throws ProviderError.InvalidConfiguration {
|
|
100
|
+
if (implementation == null) {
|
|
101
|
+
throw new ProviderError.InvalidConfiguration(ERROR_PLUGIN_NOT_INITIALIZED, null);
|
|
102
|
+
}
|
|
103
|
+
return new LiveUpdateIonicManager(context, configuration, implementation);
|
|
104
|
+
}
|
|
105
|
+
|
|
76
106
|
@Override
|
|
77
107
|
protected void handleOnResume() {
|
|
78
108
|
super.handleOnResume();
|
|
@@ -28,11 +28,17 @@ public class DownloadBundleOptions {
|
|
|
28
28
|
@Nullable String signature,
|
|
29
29
|
@NonNull String url
|
|
30
30
|
) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
this(artifactType.equals("manifest") ? ArtifactType.MANIFEST : ArtifactType.ZIP, bundleId, checksum, signature, url);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public DownloadBundleOptions(
|
|
35
|
+
@NonNull ArtifactType artifactType,
|
|
36
|
+
@NonNull String bundleId,
|
|
37
|
+
@Nullable String checksum,
|
|
38
|
+
@Nullable String signature,
|
|
39
|
+
@NonNull String url
|
|
40
|
+
) {
|
|
41
|
+
this.artifactType = artifactType;
|
|
36
42
|
this.bundleId = bundleId;
|
|
37
43
|
this.checksum = checksum;
|
|
38
44
|
this.signature = signature;
|