@capgo/capacitor-updater 8.51.25 → 8.52.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/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 +182 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +114 -3
- 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 +90 -34
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoSemanticVersion.swift +162 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +218 -71
- 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.1";
|
|
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 {
|
|
@@ -2925,6 +2954,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2925
2954
|
}
|
|
2926
2955
|
|
|
2927
2956
|
private void applyCurrentBundleToBridge() {
|
|
2957
|
+
this.stampReadyGenerationBeforeReload();
|
|
2928
2958
|
final String path = this.implementation.getCurrentBundlePath();
|
|
2929
2959
|
final boolean usingBuiltin = this.implementation.isUsingBuiltin();
|
|
2930
2960
|
if (this.keepUrlPathAfterReload) {
|
|
@@ -3862,17 +3892,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
3862
3892
|
}
|
|
3863
3893
|
|
|
3864
3894
|
private JSONObject fetchPreviewPayload(final String payloadUrl) throws IOException, JSONException {
|
|
3865
|
-
final
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
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);
|
|
3876
3911
|
final JSONObject payload = new JSONObject(body);
|
|
3877
3912
|
if (statusCode < 200 || statusCode >= 300) {
|
|
3878
3913
|
throw new IOException(
|
|
@@ -3880,8 +3915,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
3880
3915
|
);
|
|
3881
3916
|
}
|
|
3882
3917
|
return payload;
|
|
3883
|
-
} finally {
|
|
3884
|
-
connection.disconnect();
|
|
3885
3918
|
}
|
|
3886
3919
|
}
|
|
3887
3920
|
|
|
@@ -4490,10 +4523,120 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4490
4523
|
);
|
|
4491
4524
|
}
|
|
4492
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
|
+
|
|
4493
4626
|
@PluginMethod
|
|
4494
4627
|
public void notifyAppReady(final PluginCall call) {
|
|
4495
4628
|
ensureBridgeSet();
|
|
4496
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
|
+
}
|
|
4497
4640
|
final BundleInfo bundle = this.implementation.getCurrentBundle();
|
|
4498
4641
|
this.implementation.setSuccess(bundle, this.autoDeletePrevious);
|
|
4499
4642
|
this.reportAppLaunchReady(bundle);
|
|
@@ -5195,20 +5338,28 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
5195
5338
|
this.performReset(true, false, true);
|
|
5196
5339
|
if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
|
|
5197
5340
|
final String failedId = current.getId();
|
|
5198
|
-
final
|
|
5199
|
-
|
|
5200
|
-
//
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
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());
|
|
5207
5360
|
}
|
|
5208
|
-
}
|
|
5209
|
-
|
|
5210
|
-
}
|
|
5211
|
-
});
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5212
5363
|
}
|
|
5213
5364
|
} else {
|
|
5214
5365
|
logger.info("notifyAppReady was called. This is fine: " + current.getId());
|
|
@@ -150,6 +150,9 @@ public class CapgoUpdater {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
private final Map<String, CompletableFuture<BundleInfo>> downloadFutures = new ConcurrentHashMap<>();
|
|
153
|
+
// Versions whose WorkManager download is observed by this process. WorkManager persists work across a
|
|
154
|
+
// process kill, but the observer that finishes the download does not survive it.
|
|
155
|
+
private final Set<String> observedDownloadVersions = ConcurrentHashMap.newKeySet();
|
|
153
156
|
private final ExecutorService io = Executors.newSingleThreadExecutor();
|
|
154
157
|
|
|
155
158
|
public CapgoUpdater(Logger logger) {
|
|
@@ -255,8 +258,49 @@ public class CapgoUpdater {
|
|
|
255
258
|
|
|
256
259
|
void directUpdateFinish(final BundleInfo latest) {}
|
|
257
260
|
|
|
261
|
+
/** Launch downloads have no waiter. The plugin emits appReady from here when WorkManager settles. */
|
|
262
|
+
void backgroundDownloadSettled(final BundleInfo bundle, final String status) {}
|
|
263
|
+
|
|
258
264
|
void notifyListeners(final String id, final Map<String, Object> res) {}
|
|
259
265
|
|
|
266
|
+
static boolean shouldNotifyLaunchDownloadReady(
|
|
267
|
+
final boolean awaitedByCaller,
|
|
268
|
+
final boolean success,
|
|
269
|
+
final boolean directInstall,
|
|
270
|
+
final boolean previewSession
|
|
271
|
+
) {
|
|
272
|
+
if (awaitedByCaller) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (!success) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
return !directInstall && !previewSession;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static boolean shouldRestartOrphanedDownload(final boolean observedByThisProcess) {
|
|
282
|
+
return !observedByThisProcess;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
static boolean shouldReleaseLaunchWhileRetrying(
|
|
286
|
+
final boolean awaitedByCaller,
|
|
287
|
+
final boolean setNext,
|
|
288
|
+
final boolean directUpdate,
|
|
289
|
+
final boolean previewSession
|
|
290
|
+
) {
|
|
291
|
+
return !awaitedByCaller && setNext && directUpdate && !previewSession;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
static String launchDownloadReadyStatus(final boolean success, final boolean setNext) {
|
|
295
|
+
if (!success) {
|
|
296
|
+
return "Error downloading file";
|
|
297
|
+
}
|
|
298
|
+
if (setNext) {
|
|
299
|
+
return "update downloaded, will install next background";
|
|
300
|
+
}
|
|
301
|
+
return "update downloaded, autoUpdate onlyDownload";
|
|
302
|
+
}
|
|
303
|
+
|
|
260
304
|
public String randomString() {
|
|
261
305
|
final StringBuilder sb = new StringBuilder(10);
|
|
262
306
|
for (int i = 0; i < 10; i++) sb.append(AB.charAt(rnd.nextInt(AB.length())));
|
|
@@ -703,12 +747,29 @@ public class CapgoUpdater {
|
|
|
703
747
|
}
|
|
704
748
|
}
|
|
705
749
|
|
|
706
|
-
private void
|
|
750
|
+
private void notifyLaunchDownloadReady(
|
|
751
|
+
final boolean awaitedByCaller,
|
|
752
|
+
final boolean success,
|
|
753
|
+
final boolean directInstall,
|
|
754
|
+
final boolean previewSession,
|
|
755
|
+
final boolean setNext,
|
|
756
|
+
final BundleInfo bundle
|
|
757
|
+
) {
|
|
758
|
+
if (!shouldNotifyLaunchDownloadReady(awaitedByCaller, success, directInstall, previewSession)) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
final BundleInfo readyBundle = bundle != null ? bundle : this.getCurrentBundle();
|
|
762
|
+
this.backgroundDownloadSettled(readyBundle, launchDownloadReadyStatus(success, setNext));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private void observeWorkProgress(Context context, String id, String observedVersion, boolean setNext) {
|
|
707
766
|
if (!(context instanceof LifecycleOwner)) {
|
|
708
767
|
logger.error("Context is not a LifecycleOwner, cannot observe work progress");
|
|
709
768
|
return;
|
|
710
769
|
}
|
|
711
770
|
|
|
771
|
+
final AtomicBoolean terminalHandled = new AtomicBoolean(false);
|
|
772
|
+
final AtomicBoolean launchReleasedWhileRetrying = new AtomicBoolean(false);
|
|
712
773
|
activity.runOnUiThread(() -> {
|
|
713
774
|
WorkManager.getInstance(context)
|
|
714
775
|
.getWorkInfosByTagLiveData(id)
|
|
@@ -719,11 +780,33 @@ public class CapgoUpdater {
|
|
|
719
780
|
Data progress = workInfo.getProgress();
|
|
720
781
|
|
|
721
782
|
switch (workInfo.getState()) {
|
|
783
|
+
case ENQUEUED:
|
|
784
|
+
// A run attempt already happened, so WorkManager scheduled a retry (network lost midway,
|
|
785
|
+
// 5xx, ...). Retries back off without limit, so a direct install must not keep the launch
|
|
786
|
+
// (and its splashscreen) blocked until the network comes back.
|
|
787
|
+
if (
|
|
788
|
+
workInfo.getRunAttemptCount() > 0 &&
|
|
789
|
+
shouldReleaseLaunchWhileRetrying(
|
|
790
|
+
downloadFutures.containsKey(id),
|
|
791
|
+
setNext,
|
|
792
|
+
Boolean.TRUE.equals(CapgoUpdater.this.directUpdate),
|
|
793
|
+
CapgoUpdater.this.previewSession
|
|
794
|
+
) &&
|
|
795
|
+
launchReleasedWhileRetrying.compareAndSet(false, true)
|
|
796
|
+
) {
|
|
797
|
+
logger.warn("Direct update download is retrying, continuing launch on the current bundle");
|
|
798
|
+
// Same fallback as the autoSplashscreen timeout: a later success installs on next background.
|
|
799
|
+
CapgoUpdater.this.directUpdate = false;
|
|
800
|
+
io.execute(() -> backgroundDownloadSettled(getCurrentBundle(), launchDownloadReadyStatus(false, false)));
|
|
801
|
+
}
|
|
802
|
+
break;
|
|
722
803
|
case RUNNING:
|
|
723
804
|
int percent = progress.getInt(DownloadService.PERCENT, 0);
|
|
724
805
|
notifyDownload(id, percent);
|
|
725
806
|
break;
|
|
726
807
|
case SUCCEEDED:
|
|
808
|
+
if (!terminalHandled.compareAndSet(false, true)) break;
|
|
809
|
+
observedDownloadVersions.remove(observedVersion);
|
|
727
810
|
logger.info("Download succeeded: " + workInfo.getState());
|
|
728
811
|
Data outputData = workInfo.getOutputData();
|
|
729
812
|
String dest = outputData.getString(DownloadService.FILEDEST);
|
|
@@ -733,6 +816,10 @@ public class CapgoUpdater {
|
|
|
733
816
|
boolean isManifest = outputData.getBoolean(DownloadService.IS_MANIFEST, false);
|
|
734
817
|
|
|
735
818
|
io.execute(() -> {
|
|
819
|
+
// finishDownload clears directUpdate, so read the install plan first.
|
|
820
|
+
final boolean directInstall =
|
|
821
|
+
setNext && Boolean.TRUE.equals(CapgoUpdater.this.directUpdate) && !CapgoUpdater.this.previewSession;
|
|
822
|
+
final boolean previewSession = CapgoUpdater.this.previewSession;
|
|
736
823
|
boolean success = finishDownload(id, dest, version, sessionKey, checksum, setNext, isManifest);
|
|
737
824
|
BundleInfo resultBundle;
|
|
738
825
|
if (!success) {
|
|
@@ -759,14 +846,21 @@ public class CapgoUpdater {
|
|
|
759
846
|
resultBundle = getBundleInfo(id);
|
|
760
847
|
}
|
|
761
848
|
|
|
762
|
-
// Complete the future if it exists
|
|
849
|
+
// Complete the future if it exists. download() waits on it.
|
|
850
|
+
// downloadBackground does not, so the launch check must emit appReady here.
|
|
763
851
|
CompletableFuture<BundleInfo> future = downloadFutures.remove(id);
|
|
764
852
|
if (future != null) {
|
|
765
853
|
future.complete(resultBundle);
|
|
766
854
|
}
|
|
855
|
+
final BundleInfo readyBundle = success && setNext ? resultBundle : null;
|
|
856
|
+
if (!launchReleasedWhileRetrying.get()) {
|
|
857
|
+
notifyLaunchDownloadReady(future != null, success, directInstall, previewSession, setNext, readyBundle);
|
|
858
|
+
}
|
|
767
859
|
});
|
|
768
860
|
break;
|
|
769
861
|
case FAILED:
|
|
862
|
+
if (!terminalHandled.compareAndSet(false, true)) break;
|
|
863
|
+
observedDownloadVersions.remove(observedVersion);
|
|
770
864
|
Data failedData = workInfo.getOutputData();
|
|
771
865
|
String error = failedData.getString(DownloadService.ERROR);
|
|
772
866
|
logger.error("Download failed");
|
|
@@ -801,9 +895,14 @@ public class CapgoUpdater {
|
|
|
801
895
|
if (failedFuture != null) {
|
|
802
896
|
failedFuture.complete(failedBundle);
|
|
803
897
|
}
|
|
898
|
+
if (!launchReleasedWhileRetrying.get()) {
|
|
899
|
+
notifyLaunchDownloadReady(failedFuture != null, false, false, false, false, null);
|
|
900
|
+
}
|
|
804
901
|
});
|
|
805
902
|
break;
|
|
806
903
|
case CANCELLED:
|
|
904
|
+
if (!terminalHandled.compareAndSet(false, true)) break;
|
|
905
|
+
observedDownloadVersions.remove(observedVersion);
|
|
807
906
|
DataManager.getInstance().clearManifest(id);
|
|
808
907
|
CompletableFuture<BundleInfo> cancelledFuture = downloadFutures.remove(id);
|
|
809
908
|
if (cancelledFuture != null) {
|
|
@@ -829,7 +928,8 @@ public class CapgoUpdater {
|
|
|
829
928
|
logger.error("Activity is null, cannot observe work progress");
|
|
830
929
|
return;
|
|
831
930
|
}
|
|
832
|
-
|
|
931
|
+
observedDownloadVersions.add(version);
|
|
932
|
+
observeWorkProgress(this.activity, id, version, setNext);
|
|
833
933
|
|
|
834
934
|
if (manifest != null) {
|
|
835
935
|
DataManager.getInstance().setManifest(id, manifest);
|
|
@@ -1479,6 +1579,13 @@ public class CapgoUpdater {
|
|
|
1479
1579
|
return;
|
|
1480
1580
|
}
|
|
1481
1581
|
logger.info("Retrying failed download for version: " + version);
|
|
1582
|
+
} else if (shouldRestartOrphanedDownload(observedDownloadVersions.contains(version))) {
|
|
1583
|
+
// Left over from a killed process: nothing would finish it or release the launch, so start over.
|
|
1584
|
+
if (!DownloadWorkerManager.cancelVersionDownloadAndAwait(this.activity, version)) {
|
|
1585
|
+
logger.error("Failed to cancel orphaned download before restarting it");
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
logger.info("Restarting download orphaned by a previous process for version: " + version);
|
|
1482
1589
|
} else {
|
|
1483
1590
|
logger.info("Version already downloading: " + version);
|
|
1484
1591
|
return;
|
|
@@ -3469,6 +3576,10 @@ public class CapgoUpdater {
|
|
|
3469
3576
|
if (!newBundle.isBuiltin() && !this.bundleExists(next)) {
|
|
3470
3577
|
return false;
|
|
3471
3578
|
}
|
|
3579
|
+
if (next.equals(this.getCurrentBundleId()) && BundleStatus.SUCCESS == newBundle.getStatus()) {
|
|
3580
|
+
logger.info("Bundle " + next + " is already the current successful bundle. Skip next().");
|
|
3581
|
+
return true;
|
|
3582
|
+
}
|
|
3472
3583
|
this.editor.putString(NEXT_VERSION, next);
|
|
3473
3584
|
this.setBundleStatus(next, BundleStatus.PENDING);
|
|
3474
3585
|
bundleToNotify = newBundle;
|