@capgo/capacitor-updater 8.51.24 → 8.52.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/CapgoCapacitorUpdater.podspec +0 -3
- package/Package.swift +3 -11
- package/README.md +4 -1
- package/android/build.gradle +0 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +183 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +70 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +0 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +80 -21
- package/android/src/main/java/ee/forgr/capacitor_updater/Version.java +306 -0
- package/dist/docs.json +16 -0
- package/dist/esm/definitions.d.ts +14 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +113 -41
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoSemanticVersion.swift +162 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +226 -72
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +3 -4
- package/ios/Sources/CapacitorUpdaterPlugin/RedirectPolicyDelegate.swift +53 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ZipArchiveReader.swift +247 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ZipCentralDirectory.swift +280 -0
- package/package.json +1 -1
|
@@ -13,8 +13,5 @@ Pod::Spec.new do |s|
|
|
|
13
13
|
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
14
|
s.ios.deployment_target = '15.0'
|
|
15
15
|
s.dependency 'Capacitor'
|
|
16
|
-
s.dependency 'ZIPFoundation', '~> 0.9'
|
|
17
|
-
s.dependency 'Alamofire', '5.10.2'
|
|
18
|
-
s.dependency 'Version', '0.8.0'
|
|
19
16
|
s.swift_version = '5.1'
|
|
20
17
|
end
|
package/Package.swift
CHANGED
|
@@ -10,28 +10,20 @@ let package = Package(
|
|
|
10
10
|
targets: ["CapacitorUpdaterPlugin"])
|
|
11
11
|
],
|
|
12
12
|
dependencies: [
|
|
13
|
-
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
14
|
-
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.12.0")),
|
|
15
|
-
.package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.20"),
|
|
16
|
-
.package(url: "https://github.com/mrackwitz/Version.git", exact: "0.8.0")
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
17
14
|
],
|
|
18
15
|
targets: [
|
|
19
16
|
.target(
|
|
20
17
|
name: "CapacitorUpdaterPlugin",
|
|
21
18
|
dependencies: [
|
|
22
19
|
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
23
|
-
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
24
|
-
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
|
|
25
|
-
.product(name: "Alamofire", package: "Alamofire"),
|
|
26
|
-
.product(name: "Version", package: "Version")
|
|
20
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
27
21
|
],
|
|
28
22
|
path: "ios/Sources/CapacitorUpdaterPlugin"),
|
|
29
23
|
.testTarget(
|
|
30
24
|
name: "CapacitorUpdaterPluginTests",
|
|
31
25
|
dependencies: [
|
|
32
|
-
"CapacitorUpdaterPlugin"
|
|
33
|
-
.product(name: "Version", package: "Version"),
|
|
34
|
-
.product(name: "ZIPFoundation", package: "ZIPFoundation")
|
|
26
|
+
"CapacitorUpdaterPlugin"
|
|
35
27
|
],
|
|
36
28
|
path: "ios/Tests/CapacitorUpdaterPluginTests")
|
|
37
29
|
],
|
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ First follow the migration guide of Capacitor:
|
|
|
86
86
|
|
|
87
87
|
The minimum iOS version is now **15.0** to match Capacitor 7/8 requirements.
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
The plugin has no third-party iOS dependencies besides Capacitor: ZIP extraction uses a built-in reader on top of Apple's native `Compression` framework, and networking uses `URLSession`.
|
|
90
90
|
|
|
91
91
|
## Compatibility
|
|
92
92
|
|
|
@@ -335,6 +335,7 @@ CapacitorUpdater can be configured with these options:
|
|
|
335
335
|
| **`localApi`** | <code>string</code> | Configure the CLI to use a local api for testing. | <code>undefined</code> | 6.3.3 |
|
|
336
336
|
| **`localApiFiles`** | <code>string</code> | Configure the CLI to use a local file api for testing. | <code>undefined</code> | 6.3.3 |
|
|
337
337
|
| **`allowModifyUrl`** | <code>boolean</code> | Allow the plugin to modify the updateUrl, statsUrl and channelUrl dynamically from the JavaScript side. | <code>false</code> | 5.4.0 |
|
|
338
|
+
| **`allowHttpsToHttpRedirect`** | <code>boolean</code> | Allow the plugin to follow redirects from HTTPS to plain HTTP for its own network requests (update checks, stats, channel calls, and bundle/manifest downloads). Blocked by default so a redirect can never downgrade updater traffic to an unencrypted connection. Only enable this if your self-hosted update server or CDN must redirect to an HTTP URL. Direct HTTP URLs (for example `localApi` during development) are not affected. Only available for Android and iOS. | <code>false</code> | 8.52.0 |
|
|
338
339
|
| **`allowModifyAppId`** | <code>boolean</code> | Allow the plugin to modify the appId dynamically from the JavaScript side. | <code>false</code> | 7.14.0 |
|
|
339
340
|
| **`allowManualBundleError`** | <code>boolean</code> | Allow marking bundles as errored from JavaScript while using manual update flows. When enabled, {@link CapacitorUpdaterPlugin.setBundleError} can change a bundle status to `error`. | <code>false</code> | 7.20.0 |
|
|
340
341
|
| **`allowPreview`** | <code>boolean</code> | Allow JavaScript to start a native preview session and temporarily request updates for another app id. This is intended for trusted container apps that implement Expo Go-style preview flows. Only available for Android and iOS. | <code>false</code> | 8.47.0 |
|
|
@@ -383,6 +384,7 @@ In `capacitor.config.json`:
|
|
|
383
384
|
"localApi": undefined,
|
|
384
385
|
"localApiFiles": undefined,
|
|
385
386
|
"allowModifyUrl": undefined,
|
|
387
|
+
"allowHttpsToHttpRedirect": undefined,
|
|
386
388
|
"allowModifyAppId": undefined,
|
|
387
389
|
"allowManualBundleError": undefined,
|
|
388
390
|
"allowPreview": undefined,
|
|
@@ -437,6 +439,7 @@ const config: CapacitorConfig = {
|
|
|
437
439
|
localApi: undefined,
|
|
438
440
|
localApiFiles: undefined,
|
|
439
441
|
allowModifyUrl: undefined,
|
|
442
|
+
allowHttpsToHttpRedirect: undefined,
|
|
440
443
|
allowModifyAppId: undefined,
|
|
441
444
|
allowManualBundleError: undefined,
|
|
442
445
|
allowPreview: undefined,
|
package/android/build.gradle
CHANGED
|
@@ -58,10 +58,8 @@ dependencies {
|
|
|
58
58
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
59
59
|
implementation project(':capacitor-android')
|
|
60
60
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
61
|
-
implementation 'io.github.g00fy2:versioncompare:1.5.0'
|
|
62
61
|
// Play Core library for in-app updates
|
|
63
62
|
implementation 'com.google.android.play:app-update:2.1.0'
|
|
64
|
-
implementation 'com.google.android.play:app-update-ktx:2.1.0'
|
|
65
63
|
testImplementation "junit:junit:$junitVersion"
|
|
66
64
|
testImplementation 'org.mockito:mockito-core:5.23.0'
|
|
67
65
|
testImplementation 'org.json:json:20250517'
|
|
@@ -48,12 +48,10 @@ import com.google.android.play.core.install.InstallStateUpdatedListener;
|
|
|
48
48
|
import com.google.android.play.core.install.model.AppUpdateType;
|
|
49
49
|
import com.google.android.play.core.install.model.InstallStatus;
|
|
50
50
|
import com.google.android.play.core.install.model.UpdateAvailability;
|
|
51
|
-
import io.github.g00fy2.versioncompare.Version;
|
|
52
51
|
import java.io.ByteArrayOutputStream;
|
|
53
52
|
import java.io.File;
|
|
54
53
|
import java.io.IOException;
|
|
55
54
|
import java.io.InputStream;
|
|
56
|
-
import java.net.HttpURLConnection;
|
|
57
55
|
import java.net.MalformedURLException;
|
|
58
56
|
import java.net.URL;
|
|
59
57
|
import java.nio.charset.StandardCharsets;
|
|
@@ -75,7 +73,11 @@ import java.util.concurrent.Semaphore;
|
|
|
75
73
|
import java.util.concurrent.TimeUnit;
|
|
76
74
|
import java.util.concurrent.TimeoutException;
|
|
77
75
|
import java.util.concurrent.atomic.AtomicReference;
|
|
78
|
-
|
|
76
|
+
import okhttp3.HttpUrl;
|
|
77
|
+
import okhttp3.OkHttpClient;
|
|
78
|
+
import okhttp3.Request;
|
|
79
|
+
import okhttp3.Response;
|
|
80
|
+
import okhttp3.ResponseBody;
|
|
79
81
|
import org.json.JSONArray;
|
|
80
82
|
import org.json.JSONException;
|
|
81
83
|
import org.json.JSONObject;
|
|
@@ -146,7 +148,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
146
148
|
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
147
149
|
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
148
150
|
|
|
149
|
-
private final String pluginVersion = "8.
|
|
151
|
+
private final String pluginVersion = "8.52.0";
|
|
150
152
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
151
153
|
|
|
152
154
|
private SharedPreferences.Editor editor;
|
|
@@ -222,6 +224,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
222
224
|
private volatile Thread appReadyCheck;
|
|
223
225
|
// When true, sendReadyToJs should wait for notifyAppReady before hiding splash.
|
|
224
226
|
private volatile boolean pendingNotifyAppReadyWait = false;
|
|
227
|
+
// Armed only after a reload. The next document stamps this generation into notifyAppReady.
|
|
228
|
+
private final Object readyGuardLock = new Object();
|
|
229
|
+
private volatile int readyGeneration = 0;
|
|
230
|
+
private volatile boolean readyGuardArmed = false;
|
|
225
231
|
private volatile int pendingNotifyAppReadyPhase = -1;
|
|
226
232
|
private volatile long downloadStartTimeMs = 0;
|
|
227
233
|
private static final long DOWNLOAD_TIMEOUT_MS = 600000; // 10 minute timeout
|
|
@@ -710,6 +716,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
710
716
|
CapacitorUpdaterPlugin.this.scheduleDirectUpdateFinish(latest);
|
|
711
717
|
}
|
|
712
718
|
|
|
719
|
+
@Override
|
|
720
|
+
public void backgroundDownloadSettled(final BundleInfo bundle, final String status) {
|
|
721
|
+
CapacitorUpdaterPlugin.this.emitLaunchDownloadReady(bundle, status);
|
|
722
|
+
}
|
|
723
|
+
|
|
713
724
|
@Override
|
|
714
725
|
public void notifyListeners(final String id, final Map<String, Object> res) {
|
|
715
726
|
if (activity != null) {
|
|
@@ -727,7 +738,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
727
738
|
this.implementation.CAP_SERVER_PATH = WebView.CAP_SERVER_PATH;
|
|
728
739
|
this.implementation.pluginVersion = this.pluginVersion;
|
|
729
740
|
this.implementation.versionCode = this.getVersionCode(pInfo);
|
|
730
|
-
// Removed unused OkHttpClient creation - using shared client in DownloadService instead
|
|
731
741
|
this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
|
|
732
742
|
this.currentBuildVersion = this.getVersionCode(pInfo);
|
|
733
743
|
this.delayUpdateUtils = new DelayUpdateUtils(this.prefs, this.editor, this.currentVersionNative, logger);
|
|
@@ -886,6 +896,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
886
896
|
long responseTimeoutMillis = responseTimeoutSeconds > 0 ? (long) responseTimeoutSeconds * 1000L : 20_000L;
|
|
887
897
|
this.implementation.timeout = (int) Math.min(Integer.MAX_VALUE, responseTimeoutMillis);
|
|
888
898
|
DownloadService.applyHttpTimeouts(this.implementation.timeout);
|
|
899
|
+
DownloadService.setAllowHttpsToHttpRedirect(this.getConfig().getBoolean("allowHttpsToHttpRedirect", false));
|
|
889
900
|
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
890
901
|
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
891
902
|
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
@@ -2336,6 +2347,24 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2336
2347
|
this.endBackGroundTaskWithNotif("test", current.getVersionName(), current, false, plannedDirectUpdate);
|
|
2337
2348
|
}
|
|
2338
2349
|
|
|
2350
|
+
void emitLaunchDownloadReady(final BundleInfo bundle, final String status) {
|
|
2351
|
+
final BundleInfo readyBundle = bundle != null ? bundle : this.implementation.getCurrentBundle();
|
|
2352
|
+
if (readyBundle == null) {
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
this.endBackGroundTaskWithNotif(
|
|
2356
|
+
status,
|
|
2357
|
+
readyBundle.getVersionName(),
|
|
2358
|
+
readyBundle,
|
|
2359
|
+
false,
|
|
2360
|
+
false,
|
|
2361
|
+
"download_fail",
|
|
2362
|
+
"downloadFailed",
|
|
2363
|
+
false,
|
|
2364
|
+
false
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2339
2368
|
void scheduleDirectUpdateFinish(final BundleInfo latest) {
|
|
2340
2369
|
startNewThread(() -> {
|
|
2341
2370
|
try {
|
|
@@ -2663,6 +2692,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2663
2692
|
CapacitorUpdaterPlugin.this.editor,
|
|
2664
2693
|
DEFAULT_CHANNEL_PREF_KEY,
|
|
2665
2694
|
configDefaultChannel,
|
|
2695
|
+
CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
|
|
2666
2696
|
(res) -> {
|
|
2667
2697
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
2668
2698
|
if (jsRes.has("error")) {
|
|
@@ -2924,6 +2954,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2924
2954
|
}
|
|
2925
2955
|
|
|
2926
2956
|
private void applyCurrentBundleToBridge() {
|
|
2957
|
+
this.stampReadyGenerationBeforeReload();
|
|
2927
2958
|
final String path = this.implementation.getCurrentBundlePath();
|
|
2928
2959
|
final boolean usingBuiltin = this.implementation.isUsingBuiltin();
|
|
2929
2960
|
if (this.keepUrlPathAfterReload) {
|
|
@@ -3861,17 +3892,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
3861
3892
|
}
|
|
3862
3893
|
|
|
3863
3894
|
private JSONObject fetchPreviewPayload(final String payloadUrl) throws IOException, JSONException {
|
|
3864
|
-
final
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3895
|
+
final HttpUrl url = payloadUrl != null ? HttpUrl.parse(payloadUrl) : null;
|
|
3896
|
+
if (url == null) {
|
|
3897
|
+
throw new MalformedURLException("Expected an http or https preview payload URL");
|
|
3898
|
+
}
|
|
3899
|
+
// Shared client: no cookies, Capgo User-Agent.
|
|
3900
|
+
final OkHttpClient client = DownloadService.sharedClient
|
|
3901
|
+
.newBuilder()
|
|
3902
|
+
.connectTimeout(30, TimeUnit.SECONDS)
|
|
3903
|
+
.readTimeout(60, TimeUnit.SECONDS)
|
|
3904
|
+
.build();
|
|
3905
|
+
final Request request = new Request.Builder().url(url).get().header("Accept", "application/json").build();
|
|
3906
|
+
|
|
3907
|
+
try (Response response = client.newCall(request).execute()) {
|
|
3908
|
+
final int statusCode = response.code();
|
|
3909
|
+
final ResponseBody responseBody = response.body();
|
|
3910
|
+
final String body = this.readResponseBody(responseBody != null ? responseBody.byteStream() : null);
|
|
3875
3911
|
final JSONObject payload = new JSONObject(body);
|
|
3876
3912
|
if (statusCode < 200 || statusCode >= 300) {
|
|
3877
3913
|
throw new IOException(
|
|
@@ -3879,8 +3915,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
3879
3915
|
);
|
|
3880
3916
|
}
|
|
3881
3917
|
return payload;
|
|
3882
|
-
} finally {
|
|
3883
|
-
connection.disconnect();
|
|
3884
3918
|
}
|
|
3885
3919
|
}
|
|
3886
3920
|
|
|
@@ -4489,10 +4523,120 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4489
4523
|
);
|
|
4490
4524
|
}
|
|
4491
4525
|
|
|
4526
|
+
static boolean shouldAcceptReadyCall(
|
|
4527
|
+
final boolean guardArmed,
|
|
4528
|
+
final int expectedGeneration,
|
|
4529
|
+
final boolean hasGeneration,
|
|
4530
|
+
final int reportedGeneration
|
|
4531
|
+
) {
|
|
4532
|
+
if (!guardArmed) {
|
|
4533
|
+
return true;
|
|
4534
|
+
}
|
|
4535
|
+
return hasGeneration && reportedGeneration == expectedGeneration;
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
static String readyGenerationScript(final int generation) {
|
|
4539
|
+
// Wrap Capacitor.nativePromise, not the plugin proxy. registerPlugin's get trap
|
|
4540
|
+
// ignores assignments to notifyAppReady. Each document keeps its own generation.
|
|
4541
|
+
return (
|
|
4542
|
+
"(function(){window.__CAPGO_READY_GEN=" +
|
|
4543
|
+
generation +
|
|
4544
|
+
";if(window.__capgoReadyBridge)return;" +
|
|
4545
|
+
"function arm(){var cap=window.Capacitor;if(!cap||typeof cap.nativePromise!=='function'||cap.__capgoNativePromise)return false;" +
|
|
4546
|
+
"var orig=cap.nativePromise.bind(cap);" +
|
|
4547
|
+
"cap.nativePromise=function(pluginName,methodName,options){if(pluginName==='CapacitorUpdater'&&methodName==='notifyAppReady'){" +
|
|
4548
|
+
"var next={};if(options&&typeof options==='object'){for(var k in options){if(Object.prototype.hasOwnProperty.call(options,k))next[k]=options[k];}}" +
|
|
4549
|
+
"next.loadGeneration=window.__CAPGO_READY_GEN;options=next;}return orig(pluginName,methodName,options);};" +
|
|
4550
|
+
"cap.__capgoNativePromise=true;window.__capgoReadyBridge=true;return true;}" +
|
|
4551
|
+
"if(!arm()){var n=0;var t=setInterval(function(){if(arm()||++n>100)clearInterval(t);},20);}" +
|
|
4552
|
+
"})();"
|
|
4553
|
+
);
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
private int armReadyGuard() {
|
|
4557
|
+
synchronized (this.readyGuardLock) {
|
|
4558
|
+
this.readyGeneration = this.readyGeneration + 1;
|
|
4559
|
+
this.readyGuardArmed = true;
|
|
4560
|
+
return this.readyGeneration;
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
|
|
4564
|
+
private void disarmReadyGuard(final int generation) {
|
|
4565
|
+
synchronized (this.readyGuardLock) {
|
|
4566
|
+
if (this.readyGeneration == generation) {
|
|
4567
|
+
this.readyGuardArmed = false;
|
|
4568
|
+
logger.warn("Could not stamp notifyAppReady for the next page. Readiness guard disabled for this reload.");
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
private void stampReadyGenerationBeforeReload() {
|
|
4574
|
+
final int generation = this.armReadyGuard();
|
|
4575
|
+
final android.webkit.WebView webView = this.bridge != null ? this.bridge.getWebView() : null;
|
|
4576
|
+
if (webView == null) {
|
|
4577
|
+
this.disarmReadyGuard(generation);
|
|
4578
|
+
return;
|
|
4579
|
+
}
|
|
4580
|
+
webView.post(() -> {
|
|
4581
|
+
if (!this.installReadyGenerationScript(webView, generation)) {
|
|
4582
|
+
this.disarmReadyGuard(generation);
|
|
4583
|
+
}
|
|
4584
|
+
});
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4587
|
+
private boolean installReadyGenerationScript(final android.webkit.WebView webView, final int generation) {
|
|
4588
|
+
try {
|
|
4589
|
+
final Class<?> webViewFeature = Class.forName("androidx.webkit.WebViewFeature");
|
|
4590
|
+
final String feature = (String) webViewFeature.getField("DOCUMENT_START_SCRIPT").get(null);
|
|
4591
|
+
final Boolean supported = (Boolean) webViewFeature.getMethod("isFeatureSupported", String.class).invoke(null, feature);
|
|
4592
|
+
if (!Boolean.TRUE.equals(supported)) {
|
|
4593
|
+
return false;
|
|
4594
|
+
}
|
|
4595
|
+
if (this.bridge == null || this.bridge.getAppUrl() == null) {
|
|
4596
|
+
return false;
|
|
4597
|
+
}
|
|
4598
|
+
final String allowedOrigin = Uri.parse(this.bridge.getAppUrl())
|
|
4599
|
+
.buildUpon()
|
|
4600
|
+
.path(null)
|
|
4601
|
+
.fragment(null)
|
|
4602
|
+
.clearQuery()
|
|
4603
|
+
.build()
|
|
4604
|
+
.toString();
|
|
4605
|
+
final Class<?> webViewCompat = Class.forName("androidx.webkit.WebViewCompat");
|
|
4606
|
+
webViewCompat
|
|
4607
|
+
.getMethod("addDocumentStartJavaScript", android.webkit.WebView.class, String.class, Set.class)
|
|
4608
|
+
.invoke(null, webView, readyGenerationScript(generation), java.util.Collections.singleton(allowedOrigin));
|
|
4609
|
+
return true;
|
|
4610
|
+
} catch (final Exception e) {
|
|
4611
|
+
logger.warn("Unable to stamp notifyAppReady generation: " + e.getMessage());
|
|
4612
|
+
return false;
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
private boolean acceptsReadyCall(final PluginCall call) {
|
|
4617
|
+
if (!this.readyGuardArmed) {
|
|
4618
|
+
return true;
|
|
4619
|
+
}
|
|
4620
|
+
final JSObject data = call.getData();
|
|
4621
|
+
final boolean hasGeneration = data != null && data.has("loadGeneration");
|
|
4622
|
+
final int reported = hasGeneration ? data.optInt("loadGeneration", -1) : -1;
|
|
4623
|
+
return shouldAcceptReadyCall(this.readyGuardArmed, this.readyGeneration, hasGeneration, reported);
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4492
4626
|
@PluginMethod
|
|
4493
4627
|
public void notifyAppReady(final PluginCall call) {
|
|
4494
4628
|
ensureBridgeSet();
|
|
4495
4629
|
try {
|
|
4630
|
+
if (!this.acceptsReadyCall(call)) {
|
|
4631
|
+
logger.info("Ignoring notifyAppReady from a page that is no longer current");
|
|
4632
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
4633
|
+
final JSObject ignored = new JSObject();
|
|
4634
|
+
if (current != null) {
|
|
4635
|
+
ignored.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
4636
|
+
}
|
|
4637
|
+
call.resolve(ignored);
|
|
4638
|
+
return;
|
|
4639
|
+
}
|
|
4496
4640
|
final BundleInfo bundle = this.implementation.getCurrentBundle();
|
|
4497
4641
|
this.implementation.setSuccess(bundle, this.autoDeletePrevious);
|
|
4498
4642
|
this.reportAppLaunchReady(bundle);
|
|
@@ -5194,20 +5338,28 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
5194
5338
|
this.performReset(true, false, true);
|
|
5195
5339
|
if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
|
|
5196
5340
|
final String failedId = current.getId();
|
|
5197
|
-
final
|
|
5198
|
-
|
|
5199
|
-
//
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5341
|
+
final BundleInfo latest = this.implementation.getBundleInfo(failedId);
|
|
5342
|
+
final boolean stillCurrent = failedId.equals(this.implementation.getCurrentBundle().getId());
|
|
5343
|
+
// performReset waits for notifyAppReady. Resetting onto this same bundle writes
|
|
5344
|
+
// SUCCESS before we get here, so the pre-reset snapshot must not become DELETING.
|
|
5345
|
+
if (latest == null || BundleStatus.ERROR != latest.getStatus() || stillCurrent) {
|
|
5346
|
+
logger.info("Skip deleting bundle " + failedId + " after reset");
|
|
5347
|
+
} else {
|
|
5348
|
+
final String failedVersion = latest.getVersionName();
|
|
5349
|
+
logger.info("Deleting failing bundle: " + failedVersion);
|
|
5350
|
+
// Mark before async work so kill/OOM still resumes via drainPendingDeletes.
|
|
5351
|
+
CapacitorUpdaterPlugin.this.implementation.saveBundleInfo(failedId, latest.setStatus(BundleStatus.DELETING));
|
|
5352
|
+
startNewThread(() -> {
|
|
5353
|
+
try {
|
|
5354
|
+
final Boolean res = CapacitorUpdaterPlugin.this.implementation.delete(failedId, false, false);
|
|
5355
|
+
if (Boolean.TRUE.equals(res)) {
|
|
5356
|
+
logger.info("Failed bundle deleted: " + failedVersion);
|
|
5357
|
+
}
|
|
5358
|
+
} catch (final IOException e) {
|
|
5359
|
+
logger.error("Failed to delete failed bundle: " + failedVersion + " " + e.getMessage());
|
|
5206
5360
|
}
|
|
5207
|
-
}
|
|
5208
|
-
|
|
5209
|
-
}
|
|
5210
|
-
});
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5211
5363
|
}
|
|
5212
5364
|
} else {
|
|
5213
5365
|
logger.info("notifyAppReady was called. This is fine: " + current.getId());
|
|
@@ -255,8 +255,36 @@ public class CapgoUpdater {
|
|
|
255
255
|
|
|
256
256
|
void directUpdateFinish(final BundleInfo latest) {}
|
|
257
257
|
|
|
258
|
+
/** Launch downloads have no waiter. The plugin emits appReady from here when WorkManager settles. */
|
|
259
|
+
void backgroundDownloadSettled(final BundleInfo bundle, final String status) {}
|
|
260
|
+
|
|
258
261
|
void notifyListeners(final String id, final Map<String, Object> res) {}
|
|
259
262
|
|
|
263
|
+
static boolean shouldNotifyLaunchDownloadReady(
|
|
264
|
+
final boolean awaitedByCaller,
|
|
265
|
+
final boolean success,
|
|
266
|
+
final boolean directInstall,
|
|
267
|
+
final boolean previewSession
|
|
268
|
+
) {
|
|
269
|
+
if (awaitedByCaller) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
if (!success) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
return !directInstall && !previewSession;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
static String launchDownloadReadyStatus(final boolean success, final boolean setNext) {
|
|
279
|
+
if (!success) {
|
|
280
|
+
return "Error downloading file";
|
|
281
|
+
}
|
|
282
|
+
if (setNext) {
|
|
283
|
+
return "update downloaded, will install next background";
|
|
284
|
+
}
|
|
285
|
+
return "update downloaded, autoUpdate onlyDownload";
|
|
286
|
+
}
|
|
287
|
+
|
|
260
288
|
public String randomString() {
|
|
261
289
|
final StringBuilder sb = new StringBuilder(10);
|
|
262
290
|
for (int i = 0; i < 10; i++) sb.append(AB.charAt(rnd.nextInt(AB.length())));
|
|
@@ -703,12 +731,28 @@ public class CapgoUpdater {
|
|
|
703
731
|
}
|
|
704
732
|
}
|
|
705
733
|
|
|
734
|
+
private void notifyLaunchDownloadReady(
|
|
735
|
+
final boolean awaitedByCaller,
|
|
736
|
+
final boolean success,
|
|
737
|
+
final boolean directInstall,
|
|
738
|
+
final boolean previewSession,
|
|
739
|
+
final boolean setNext,
|
|
740
|
+
final BundleInfo bundle
|
|
741
|
+
) {
|
|
742
|
+
if (!shouldNotifyLaunchDownloadReady(awaitedByCaller, success, directInstall, previewSession)) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
final BundleInfo readyBundle = bundle != null ? bundle : this.getCurrentBundle();
|
|
746
|
+
this.backgroundDownloadSettled(readyBundle, launchDownloadReadyStatus(success, setNext));
|
|
747
|
+
}
|
|
748
|
+
|
|
706
749
|
private void observeWorkProgress(Context context, String id, boolean setNext) {
|
|
707
750
|
if (!(context instanceof LifecycleOwner)) {
|
|
708
751
|
logger.error("Context is not a LifecycleOwner, cannot observe work progress");
|
|
709
752
|
return;
|
|
710
753
|
}
|
|
711
754
|
|
|
755
|
+
final AtomicBoolean terminalHandled = new AtomicBoolean(false);
|
|
712
756
|
activity.runOnUiThread(() -> {
|
|
713
757
|
WorkManager.getInstance(context)
|
|
714
758
|
.getWorkInfosByTagLiveData(id)
|
|
@@ -724,6 +768,7 @@ public class CapgoUpdater {
|
|
|
724
768
|
notifyDownload(id, percent);
|
|
725
769
|
break;
|
|
726
770
|
case SUCCEEDED:
|
|
771
|
+
if (!terminalHandled.compareAndSet(false, true)) break;
|
|
727
772
|
logger.info("Download succeeded: " + workInfo.getState());
|
|
728
773
|
Data outputData = workInfo.getOutputData();
|
|
729
774
|
String dest = outputData.getString(DownloadService.FILEDEST);
|
|
@@ -733,6 +778,10 @@ public class CapgoUpdater {
|
|
|
733
778
|
boolean isManifest = outputData.getBoolean(DownloadService.IS_MANIFEST, false);
|
|
734
779
|
|
|
735
780
|
io.execute(() -> {
|
|
781
|
+
// finishDownload clears directUpdate, so read the install plan first.
|
|
782
|
+
final boolean directInstall =
|
|
783
|
+
setNext && Boolean.TRUE.equals(CapgoUpdater.this.directUpdate) && !CapgoUpdater.this.previewSession;
|
|
784
|
+
final boolean previewSession = CapgoUpdater.this.previewSession;
|
|
736
785
|
boolean success = finishDownload(id, dest, version, sessionKey, checksum, setNext, isManifest);
|
|
737
786
|
BundleInfo resultBundle;
|
|
738
787
|
if (!success) {
|
|
@@ -759,14 +808,18 @@ public class CapgoUpdater {
|
|
|
759
808
|
resultBundle = getBundleInfo(id);
|
|
760
809
|
}
|
|
761
810
|
|
|
762
|
-
// Complete the future if it exists
|
|
811
|
+
// Complete the future if it exists. download() waits on it.
|
|
812
|
+
// downloadBackground does not, so the launch check must emit appReady here.
|
|
763
813
|
CompletableFuture<BundleInfo> future = downloadFutures.remove(id);
|
|
764
814
|
if (future != null) {
|
|
765
815
|
future.complete(resultBundle);
|
|
766
816
|
}
|
|
817
|
+
final BundleInfo readyBundle = success && setNext ? resultBundle : null;
|
|
818
|
+
notifyLaunchDownloadReady(future != null, success, directInstall, previewSession, setNext, readyBundle);
|
|
767
819
|
});
|
|
768
820
|
break;
|
|
769
821
|
case FAILED:
|
|
822
|
+
if (!terminalHandled.compareAndSet(false, true)) break;
|
|
770
823
|
Data failedData = workInfo.getOutputData();
|
|
771
824
|
String error = failedData.getString(DownloadService.ERROR);
|
|
772
825
|
logger.error("Download failed");
|
|
@@ -801,9 +854,11 @@ public class CapgoUpdater {
|
|
|
801
854
|
if (failedFuture != null) {
|
|
802
855
|
failedFuture.complete(failedBundle);
|
|
803
856
|
}
|
|
857
|
+
notifyLaunchDownloadReady(failedFuture != null, false, false, false, false, null);
|
|
804
858
|
});
|
|
805
859
|
break;
|
|
806
860
|
case CANCELLED:
|
|
861
|
+
if (!terminalHandled.compareAndSet(false, true)) break;
|
|
807
862
|
DataManager.getInstance().clearManifest(id);
|
|
808
863
|
CompletableFuture<BundleInfo> cancelledFuture = downloadFutures.remove(id);
|
|
809
864
|
if (cancelledFuture != null) {
|
|
@@ -2507,8 +2562,18 @@ public class CapgoUpdater {
|
|
|
2507
2562
|
final SharedPreferences.Editor editor,
|
|
2508
2563
|
final String defaultChannelKey,
|
|
2509
2564
|
final String configDefaultChannel,
|
|
2565
|
+
final boolean allowSetDefaultChannel,
|
|
2510
2566
|
final Callback callback
|
|
2511
2567
|
) {
|
|
2568
|
+
if (!allowSetDefaultChannel) {
|
|
2569
|
+
logger.error("unsetChannel is disabled by allowSetDefaultChannel config");
|
|
2570
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
2571
|
+
retError.put("message", "unsetChannel is disabled by configuration");
|
|
2572
|
+
retError.put("error", "disabled_by_config");
|
|
2573
|
+
callback.callback(retError);
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2512
2577
|
// Clear persisted defaultChannel and revert to config value
|
|
2513
2578
|
editor.remove(defaultChannelKey);
|
|
2514
2579
|
editor.apply();
|
|
@@ -3459,6 +3524,10 @@ public class CapgoUpdater {
|
|
|
3459
3524
|
if (!newBundle.isBuiltin() && !this.bundleExists(next)) {
|
|
3460
3525
|
return false;
|
|
3461
3526
|
}
|
|
3527
|
+
if (next.equals(this.getCurrentBundleId()) && BundleStatus.SUCCESS == newBundle.getStatus()) {
|
|
3528
|
+
logger.info("Bundle " + next + " is already the current successful bundle. Skip next().");
|
|
3529
|
+
return true;
|
|
3530
|
+
}
|
|
3462
3531
|
this.editor.putString(NEXT_VERSION, next);
|
|
3463
3532
|
this.setBundleStatus(next, BundleStatus.PENDING);
|
|
3464
3533
|
bundleToNotify = newBundle;
|