@capawesome/capacitor-live-update 8.6.0 → 8.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapawesomeCapacitorLiveUpdate.podspec +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 +181 -56
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +47 -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 +140 -45
- package/ios/Plugin/LiveUpdatePlugin.swift +16 -3
- 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;
|
|
@@ -8,6 +9,7 @@ import android.content.res.AssetManager;
|
|
|
8
9
|
import android.os.Build;
|
|
9
10
|
import android.os.Handler;
|
|
10
11
|
import android.os.Looper;
|
|
12
|
+
import android.os.SystemClock;
|
|
11
13
|
import android.util.Base64;
|
|
12
14
|
import androidx.annotation.NonNull;
|
|
13
15
|
import androidx.annotation.Nullable;
|
|
@@ -85,17 +87,22 @@ import org.json.JSONObject;
|
|
|
85
87
|
|
|
86
88
|
public class LiveUpdate {
|
|
87
89
|
|
|
90
|
+
private static final AtomicBoolean downloadsDirectoryDeleted = new AtomicBoolean(false);
|
|
91
|
+
|
|
88
92
|
private final long autoUpdateIntervalMs = 15 * 60 * 1000; // 15 minutes
|
|
89
93
|
|
|
90
94
|
@NonNull
|
|
91
95
|
private final LiveUpdateConfig config;
|
|
92
96
|
|
|
97
|
+
@NonNull
|
|
98
|
+
private final Context context;
|
|
99
|
+
|
|
93
100
|
private final String defaultWebAssetDir = Bridge.DEFAULT_WEB_ASSET_DIR;
|
|
94
101
|
|
|
95
102
|
@NonNull
|
|
96
103
|
private final LiveUpdateHttpClient httpClient;
|
|
97
104
|
|
|
98
|
-
@
|
|
105
|
+
@Nullable
|
|
99
106
|
private final LiveUpdatePlugin plugin;
|
|
100
107
|
|
|
101
108
|
@NonNull
|
|
@@ -109,17 +116,42 @@ public class LiveUpdate {
|
|
|
109
116
|
private final Handler rollbackHandler = new Handler(Looper.getMainLooper());
|
|
110
117
|
private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
|
|
111
118
|
|
|
119
|
+
private boolean activityStopped = false;
|
|
112
120
|
private boolean initialPageLoaded = false;
|
|
113
121
|
private long lastAutoUpdateCheckTimestamp = 0;
|
|
122
|
+
|
|
123
|
+
/** Milliseconds left on the rollback timer while it is paused in the background. */
|
|
124
|
+
@Nullable
|
|
125
|
+
private Long remainingRollbackTimeout = null;
|
|
126
|
+
|
|
127
|
+
/** Uptime at which the scheduled rollback fires, or {@code null} if none is scheduled. */
|
|
128
|
+
@Nullable
|
|
129
|
+
private Long rollbackDeadline = null;
|
|
130
|
+
|
|
114
131
|
private boolean rollbackPerformed = false;
|
|
115
132
|
private boolean syncInProgress = false;
|
|
116
133
|
|
|
117
134
|
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
|
|
135
|
+
this(config, plugin.getContext(), plugin);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Creates a headless instance without a Capacitor plugin, e.g. for Ionic Portals hosts
|
|
140
|
+
* that construct a provider manager directly. WebView-related operations (server path
|
|
141
|
+
* changes, reload) and plugin events are skipped in this mode.
|
|
142
|
+
*/
|
|
143
|
+
public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull Context context) throws PackageManager.NameNotFoundException {
|
|
144
|
+
this(config, context, null);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull Context context, @Nullable LiveUpdatePlugin plugin)
|
|
148
|
+
throws PackageManager.NameNotFoundException {
|
|
118
149
|
this.config = config;
|
|
150
|
+
this.context = context;
|
|
119
151
|
this.httpClient = new LiveUpdateHttpClient(config);
|
|
120
152
|
this.plugin = plugin;
|
|
121
|
-
this.preferences = new LiveUpdatePreferences(
|
|
122
|
-
this.webViewSettingsEditor =
|
|
153
|
+
this.preferences = new LiveUpdatePreferences(context);
|
|
154
|
+
this.webViewSettingsEditor = context.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
|
|
123
155
|
|
|
124
156
|
// Check version and reset config if version changed
|
|
125
157
|
checkAndResetConfigIfVersionChanged();
|
|
@@ -127,8 +159,11 @@ public class LiveUpdate {
|
|
|
127
159
|
// Set the device ID on the HTTP client (after any potential config reset)
|
|
128
160
|
this.httpClient.setDeviceId(getDeviceId());
|
|
129
161
|
|
|
130
|
-
// Delete the leftovers of downloads that were interrupted by a process kill
|
|
131
|
-
|
|
162
|
+
// Delete the leftovers of downloads that were interrupted by a process kill.
|
|
163
|
+
// Only once per process, because other instances may be downloading at this moment.
|
|
164
|
+
if (downloadsDirectoryDeleted.compareAndSet(false, true)) {
|
|
165
|
+
tryDeleteDownloadsDirectory();
|
|
166
|
+
}
|
|
132
167
|
|
|
133
168
|
// Start the rollback timer to rollback to the default bundle
|
|
134
169
|
// if the app is not ready after a certain time
|
|
@@ -264,6 +299,15 @@ public class LiveUpdate {
|
|
|
264
299
|
);
|
|
265
300
|
}
|
|
266
301
|
|
|
302
|
+
@Nullable
|
|
303
|
+
public String getAppId() {
|
|
304
|
+
String appId = preferences.getAppId();
|
|
305
|
+
if (appId != null) {
|
|
306
|
+
return appId;
|
|
307
|
+
}
|
|
308
|
+
return config.getAppId();
|
|
309
|
+
}
|
|
310
|
+
|
|
267
311
|
public void getBlockedBundles(@NonNull NonEmptyCallback<GetBlockedBundlesResult> callback) {
|
|
268
312
|
String blockedIds = preferences.getBlockedBundleIds();
|
|
269
313
|
String[] bundleIds;
|
|
@@ -294,6 +338,22 @@ public class LiveUpdate {
|
|
|
294
338
|
callback.success(result);
|
|
295
339
|
}
|
|
296
340
|
|
|
341
|
+
@Nullable
|
|
342
|
+
public String getChannel() {
|
|
343
|
+
String channel = null;
|
|
344
|
+
if (config.getDefaultChannel() != null) {
|
|
345
|
+
channel = config.getDefaultChannel();
|
|
346
|
+
}
|
|
347
|
+
String nativeChannel = getNativeChannel();
|
|
348
|
+
if (nativeChannel != null) {
|
|
349
|
+
channel = nativeChannel;
|
|
350
|
+
}
|
|
351
|
+
if (preferences.getChannel() != null) {
|
|
352
|
+
channel = preferences.getChannel();
|
|
353
|
+
}
|
|
354
|
+
return channel;
|
|
355
|
+
}
|
|
356
|
+
|
|
297
357
|
public void getConfig(@NonNull NonEmptyCallback<GetConfigResult> callback) {
|
|
298
358
|
String appId = getAppId();
|
|
299
359
|
String autoUpdateStrategy = config.getAutoUpdateStrategy();
|
|
@@ -307,6 +367,17 @@ public class LiveUpdate {
|
|
|
307
367
|
callback.success(result);
|
|
308
368
|
}
|
|
309
369
|
|
|
370
|
+
/**
|
|
371
|
+
* @return The on-disk directory of a downloaded bundle, or `null` if the bundle does not exist.
|
|
372
|
+
*/
|
|
373
|
+
@Nullable
|
|
374
|
+
public File getBundleDirectory(@NonNull String bundleId) {
|
|
375
|
+
if (!hasBundleById(bundleId)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
return buildBundleDirectoryFor(bundleId);
|
|
379
|
+
}
|
|
380
|
+
|
|
310
381
|
public void getCustomId(@NonNull NonEmptyCallback callback) {
|
|
311
382
|
String customId = preferences.getCustomId();
|
|
312
383
|
GetCustomIdResult result = new GetCustomIdResult(customId);
|
|
@@ -331,6 +402,10 @@ public class LiveUpdate {
|
|
|
331
402
|
callback.success(result);
|
|
332
403
|
}
|
|
333
404
|
|
|
405
|
+
public String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
|
|
406
|
+
return String.valueOf(getVersionCodeAsInt());
|
|
407
|
+
}
|
|
408
|
+
|
|
334
409
|
public void getVersionName(@NonNull NonEmptyCallback callback) throws PackageManager.NameNotFoundException {
|
|
335
410
|
String versionName = getVersionName();
|
|
336
411
|
GetVersionNameResult result = new GetVersionNameResult(versionName);
|
|
@@ -357,6 +432,16 @@ public class LiveUpdate {
|
|
|
357
432
|
}
|
|
358
433
|
}
|
|
359
434
|
|
|
435
|
+
public synchronized void handleOnStart() {
|
|
436
|
+
activityStopped = false;
|
|
437
|
+
resumeRollbackTimer();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
public synchronized void handleOnStop() {
|
|
441
|
+
activityStopped = true;
|
|
442
|
+
pauseRollbackTimer();
|
|
443
|
+
}
|
|
444
|
+
|
|
360
445
|
public void ready(@NonNull NonEmptyCallback callback) {
|
|
361
446
|
Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
|
|
362
447
|
if (config.getReadyTimeout() <= 0) {
|
|
@@ -529,6 +614,20 @@ public class LiveUpdate {
|
|
|
529
614
|
);
|
|
530
615
|
}
|
|
531
616
|
|
|
617
|
+
/**
|
|
618
|
+
* @return `true` if the bundle does not exist or was deleted, `false` if it is the current or next bundle.
|
|
619
|
+
*/
|
|
620
|
+
public boolean tryDeleteUnusedBundle(@NonNull String bundleId) {
|
|
621
|
+
if (!hasBundleById(bundleId)) {
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
if (isBundleInUse(bundleId)) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
deleteBundleById(bundleId);
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
|
|
532
631
|
private void addBundle(@NonNull String bundleId, @NonNull File sourceDirectory) throws Exception {
|
|
533
632
|
// Search folder with index.html file
|
|
534
633
|
File indexHtmlFile = searchIndexHtmlFile(sourceDirectory);
|
|
@@ -542,7 +641,8 @@ public class LiveUpdate {
|
|
|
542
641
|
// Move the bundle directory to the bundles directory
|
|
543
642
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
544
643
|
boolean moved = indexHtmlFile.getParentFile().renameTo(bundleDirectory);
|
|
545
|
-
|
|
644
|
+
// A concurrent download of the same bundle may have finished first
|
|
645
|
+
if (!moved && !hasBundleById(bundleId)) {
|
|
546
646
|
throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_MOVE_FAILED);
|
|
547
647
|
}
|
|
548
648
|
}
|
|
@@ -559,15 +659,15 @@ public class LiveUpdate {
|
|
|
559
659
|
}
|
|
560
660
|
|
|
561
661
|
private File buildBundlesDirectory() {
|
|
562
|
-
return new File(
|
|
662
|
+
return new File(context.getFilesDir(), bundlesDirectory);
|
|
563
663
|
}
|
|
564
664
|
|
|
565
665
|
private File buildBundleDirectoryFor(@NonNull String bundleId) {
|
|
566
|
-
return new File(
|
|
666
|
+
return new File(context.getFilesDir(), bundlesDirectory + "/" + bundleId);
|
|
567
667
|
}
|
|
568
668
|
|
|
569
669
|
private File buildDownloadsDirectory() {
|
|
570
|
-
return new File(
|
|
670
|
+
return new File(context.getCacheDir(), downloadsDirectory);
|
|
571
671
|
}
|
|
572
672
|
|
|
573
673
|
private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
|
|
@@ -575,7 +675,7 @@ public class LiveUpdate {
|
|
|
575
675
|
String currentBundleId = getCurrentBundleId();
|
|
576
676
|
if (currentBundleId == null) {
|
|
577
677
|
// Create the source input stream
|
|
578
|
-
AssetManager assets =
|
|
678
|
+
AssetManager assets = context.getAssets();
|
|
579
679
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
|
|
580
680
|
// Create the destination file
|
|
581
681
|
File destination = new File(destinationDirectory, href);
|
|
@@ -1003,18 +1103,20 @@ public class LiveUpdate {
|
|
|
1003
1103
|
@NonNull NonEmptyCallback<GetLatestBundleResponse> callback
|
|
1004
1104
|
) {
|
|
1005
1105
|
try {
|
|
1106
|
+
String appId = requireAppId(options.getAppId());
|
|
1107
|
+
String bundleId = options.getBundleId() == null ? getCurrentBundleId() : options.getBundleId();
|
|
1006
1108
|
String channel = options.getChannel() == null ? getChannel() : options.getChannel();
|
|
1007
1109
|
String url = new HttpUrl.Builder()
|
|
1008
1110
|
.scheme("https")
|
|
1009
1111
|
.host(config.getServerDomain())
|
|
1010
1112
|
.addPathSegment("v1")
|
|
1011
1113
|
.addPathSegment("apps")
|
|
1012
|
-
.addPathSegment(
|
|
1114
|
+
.addPathSegment(appId)
|
|
1013
1115
|
.addPathSegment("bundles")
|
|
1014
1116
|
.addPathSegment("latest")
|
|
1015
1117
|
.addQueryParameter("appVersionCode", getVersionCodeAsString())
|
|
1016
1118
|
.addQueryParameter("appVersionName", getVersionName())
|
|
1017
|
-
.addQueryParameter("bundleId",
|
|
1119
|
+
.addQueryParameter("bundleId", bundleId)
|
|
1018
1120
|
.addQueryParameter("channelName", channel)
|
|
1019
1121
|
.addQueryParameter("customId", preferences.getCustomId())
|
|
1020
1122
|
.addQueryParameter("deviceId", getDeviceId())
|
|
@@ -1096,41 +1198,13 @@ public class LiveUpdate {
|
|
|
1096
1198
|
return checksum.toString();
|
|
1097
1199
|
}
|
|
1098
1200
|
|
|
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
1201
|
@Nullable
|
|
1125
1202
|
private String getNativeChannel() {
|
|
1126
|
-
int resId =
|
|
1127
|
-
.getContext()
|
|
1128
|
-
.getResources()
|
|
1129
|
-
.getIdentifier("capawesome_live_update_default_channel", "string", plugin.getContext().getPackageName());
|
|
1203
|
+
int resId = context.getResources().getIdentifier("capawesome_live_update_default_channel", "string", context.getPackageName());
|
|
1130
1204
|
if (resId == 0) {
|
|
1131
1205
|
return null;
|
|
1132
1206
|
}
|
|
1133
|
-
return
|
|
1207
|
+
return context.getResources().getString(resId);
|
|
1134
1208
|
}
|
|
1135
1209
|
|
|
1136
1210
|
/**
|
|
@@ -1149,6 +1223,10 @@ public class LiveUpdate {
|
|
|
1149
1223
|
* @return The absolute path to the current bundle directory (`public` for the built-in bundle).
|
|
1150
1224
|
*/
|
|
1151
1225
|
private String getCurrentCapacitorServerPath() {
|
|
1226
|
+
if (plugin == null) {
|
|
1227
|
+
// Headless mode: there is no WebView, so the default bundle is always current.
|
|
1228
|
+
return defaultWebAssetDir;
|
|
1229
|
+
}
|
|
1152
1230
|
return plugin.getBridge().getServerBasePath();
|
|
1153
1231
|
}
|
|
1154
1232
|
|
|
@@ -1183,8 +1261,7 @@ public class LiveUpdate {
|
|
|
1183
1261
|
*/
|
|
1184
1262
|
@NonNull
|
|
1185
1263
|
private String getNextCapacitorServerPath() {
|
|
1186
|
-
String path =
|
|
1187
|
-
.getContext()
|
|
1264
|
+
String path = context
|
|
1188
1265
|
.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
|
|
1189
1266
|
.getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
|
|
1190
1267
|
// Empty path means default path
|
|
@@ -1206,10 +1283,6 @@ public class LiveUpdate {
|
|
|
1206
1283
|
return (int) PackageInfoCompat.getLongVersionCode(getPackageInfo());
|
|
1207
1284
|
}
|
|
1208
1285
|
|
|
1209
|
-
private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
|
|
1210
|
-
return String.valueOf(getVersionCodeAsInt());
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
1286
|
private String getVersionName() throws PackageManager.NameNotFoundException {
|
|
1214
1287
|
return getPackageInfo().versionName;
|
|
1215
1288
|
}
|
|
@@ -1238,7 +1311,7 @@ public class LiveUpdate {
|
|
|
1238
1311
|
private Manifest loadCurrentManifest() throws Exception {
|
|
1239
1312
|
String currentBundleId = getCurrentBundleId();
|
|
1240
1313
|
if (currentBundleId == null) {
|
|
1241
|
-
AssetManager assets =
|
|
1314
|
+
AssetManager assets = context.getAssets();
|
|
1242
1315
|
boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
|
|
1243
1316
|
if (manifestFileExists) {
|
|
1244
1317
|
InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
|
|
@@ -1270,6 +1343,9 @@ public class LiveUpdate {
|
|
|
1270
1343
|
}
|
|
1271
1344
|
|
|
1272
1345
|
private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
|
|
1346
|
+
if (plugin == null) {
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1273
1349
|
plugin.notifyDownloadBundleProgressListeners(event);
|
|
1274
1350
|
}
|
|
1275
1351
|
|
|
@@ -1301,9 +1377,31 @@ public class LiveUpdate {
|
|
|
1301
1377
|
sync(options, callback);
|
|
1302
1378
|
}
|
|
1303
1379
|
|
|
1380
|
+
private void pauseRollbackTimer() {
|
|
1381
|
+
if (rollbackDeadline == null) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
rollbackHandler.removeCallbacksAndMessages(null);
|
|
1385
|
+
remainingRollbackTimeout = Math.max(0, rollbackDeadline - SystemClock.uptimeMillis());
|
|
1386
|
+
rollbackDeadline = null;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
private void resumeRollbackTimer() {
|
|
1390
|
+
if (remainingRollbackTimeout == null) {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
scheduleRollback(remainingRollbackTimeout);
|
|
1394
|
+
remainingRollbackTimeout = null;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1304
1397
|
@NonNull
|
|
1305
1398
|
private String requireAppId() throws Exception {
|
|
1306
|
-
|
|
1399
|
+
return requireAppId(null);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
@NonNull
|
|
1403
|
+
private String requireAppId(@Nullable String appIdOverride) throws Exception {
|
|
1404
|
+
String appId = appIdOverride == null ? getAppId() : appIdOverride;
|
|
1307
1405
|
if (appId == null || appId.isEmpty()) {
|
|
1308
1406
|
throw new Exception(LiveUpdatePlugin.ERROR_APP_ID_MISSING);
|
|
1309
1407
|
}
|
|
@@ -1365,6 +1463,10 @@ public class LiveUpdate {
|
|
|
1365
1463
|
}
|
|
1366
1464
|
|
|
1367
1465
|
private void setCurrentCapacitorServerPath(@NonNull String path) {
|
|
1466
|
+
if (plugin == null) {
|
|
1467
|
+
// Headless mode: there is no WebView to point at the new path.
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1368
1470
|
if (path.equals(defaultWebAssetDir)) {
|
|
1369
1471
|
this.plugin.getBridge().setServerAssetPath(path);
|
|
1370
1472
|
} else {
|
|
@@ -1391,11 +1493,17 @@ public class LiveUpdate {
|
|
|
1391
1493
|
}
|
|
1392
1494
|
|
|
1393
1495
|
private void notifyNextBundleSetListeners(@Nullable String bundleId) {
|
|
1496
|
+
if (plugin == null) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1394
1499
|
NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
|
|
1395
1500
|
plugin.notifyNextBundleSetListeners(event);
|
|
1396
1501
|
}
|
|
1397
1502
|
|
|
1398
1503
|
private void notifyReloadedListeners() {
|
|
1504
|
+
if (plugin == null) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1399
1507
|
plugin.notifyReloadedListeners();
|
|
1400
1508
|
}
|
|
1401
1509
|
|
|
@@ -1462,16 +1570,33 @@ public class LiveUpdate {
|
|
|
1462
1570
|
preferences.setPreviousBundleId(bundleId);
|
|
1463
1571
|
}
|
|
1464
1572
|
|
|
1465
|
-
private void
|
|
1573
|
+
private void scheduleRollback(long delayMillis) {
|
|
1574
|
+
rollbackDeadline = SystemClock.uptimeMillis() + delayMillis;
|
|
1575
|
+
rollbackHandler.postDelayed(() -> {
|
|
1576
|
+
synchronized (this) {
|
|
1577
|
+
rollbackDeadline = null;
|
|
1578
|
+
}
|
|
1579
|
+
rollback();
|
|
1580
|
+
}, delayMillis);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/** The timer only counts time in the foreground, because a stopped activity may not run JS. */
|
|
1584
|
+
private synchronized void startRollbackTimer() {
|
|
1466
1585
|
if (config.getReadyTimeout() <= 0) {
|
|
1467
1586
|
return;
|
|
1468
1587
|
}
|
|
1469
1588
|
stopRollbackTimer();
|
|
1470
|
-
|
|
1589
|
+
if (activityStopped) {
|
|
1590
|
+
remainingRollbackTimeout = (long) config.getReadyTimeout();
|
|
1591
|
+
} else {
|
|
1592
|
+
scheduleRollback(config.getReadyTimeout());
|
|
1593
|
+
}
|
|
1471
1594
|
}
|
|
1472
1595
|
|
|
1473
|
-
private void stopRollbackTimer() {
|
|
1596
|
+
private synchronized void stopRollbackTimer() {
|
|
1474
1597
|
rollbackHandler.removeCallbacksAndMessages(null);
|
|
1598
|
+
rollbackDeadline = null;
|
|
1599
|
+
remainingRollbackTimeout = null;
|
|
1475
1600
|
}
|
|
1476
1601
|
|
|
1477
1602
|
private boolean tryCopyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) {
|
|
@@ -1552,11 +1677,11 @@ public class LiveUpdate {
|
|
|
1552
1677
|
}
|
|
1553
1678
|
|
|
1554
1679
|
private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
|
1555
|
-
String packageName = this.
|
|
1680
|
+
String packageName = this.context.getPackageName();
|
|
1556
1681
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1557
|
-
return this.
|
|
1682
|
+
return this.context.getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
|
|
1558
1683
|
} else {
|
|
1559
|
-
return this.
|
|
1684
|
+
return this.context.getPackageManager().getPackageInfo(packageName, 0);
|
|
1560
1685
|
}
|
|
1561
1686
|
}
|
|
1562
1687
|
}
|