@capgo/capacitor-updater 7.50.1 → 7.51.15
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/Package.swift +2 -1
- package/README.md +53 -48
- package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +29 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +451 -128
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1108 -180
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +102 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +513 -201
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +115 -133
- package/dist/docs.json +32 -8
- package/dist/esm/definitions.d.ts +41 -17
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +124 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +9 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +789 -92
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1014 -268
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +49 -31
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +44 -20
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +28 -0
- package/package.json +12 -7
|
@@ -50,6 +50,7 @@ import com.google.android.play.core.install.model.InstallStatus;
|
|
|
50
50
|
import com.google.android.play.core.install.model.UpdateAvailability;
|
|
51
51
|
import io.github.g00fy2.versioncompare.Version;
|
|
52
52
|
import java.io.ByteArrayOutputStream;
|
|
53
|
+
import java.io.File;
|
|
53
54
|
import java.io.IOException;
|
|
54
55
|
import java.io.InputStream;
|
|
55
56
|
import java.net.HttpURLConnection;
|
|
@@ -68,6 +69,7 @@ import java.util.Set;
|
|
|
68
69
|
import java.util.TimeZone;
|
|
69
70
|
import java.util.Timer;
|
|
70
71
|
import java.util.TimerTask;
|
|
72
|
+
import java.util.concurrent.CountDownLatch;
|
|
71
73
|
import java.util.concurrent.Phaser;
|
|
72
74
|
import java.util.concurrent.Semaphore;
|
|
73
75
|
import java.util.concurrent.TimeUnit;
|
|
@@ -103,6 +105,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
103
105
|
private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
|
|
104
106
|
private static final String DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.defaultChannel";
|
|
105
107
|
private static final String PREVIEW_SESSION_PREF_KEY = "CapacitorUpdater.previewSession";
|
|
108
|
+
private static final String DEFAULT_CHANNEL_INSTALL_MARKER_PREF_KEY = "CapacitorUpdater.defaultChannelInstallMarkerCreated";
|
|
109
|
+
private static final String DEFAULT_CHANNEL_INSTALL_MARKER_FILE = "CapacitorUpdater.defaultChannelInstallMarker";
|
|
106
110
|
private static final String PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY = "CapacitorUpdater.previewPreviousShakeMenu";
|
|
107
111
|
private static final String PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY = "CapacitorUpdater.previewPreviousShakeChannelSelector";
|
|
108
112
|
private static final String PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY = "CapacitorUpdater.previewPreviousNextBundle";
|
|
@@ -142,7 +146,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
142
146
|
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
143
147
|
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
144
148
|
|
|
145
|
-
private final String pluginVersion = "7.
|
|
149
|
+
private final String pluginVersion = "7.51.15";
|
|
146
150
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
147
151
|
|
|
148
152
|
private SharedPreferences.Editor editor;
|
|
@@ -151,6 +155,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
151
155
|
protected CapgoUpdater implementation;
|
|
152
156
|
private Boolean persistCustomId = false;
|
|
153
157
|
private Boolean persistModifyUrl = false;
|
|
158
|
+
private Boolean persistDefaultChannelOnReinstall = true;
|
|
154
159
|
|
|
155
160
|
private Integer appReadyTimeout = 10000;
|
|
156
161
|
private Integer periodCheckDelay = 0;
|
|
@@ -215,6 +220,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
215
220
|
|
|
216
221
|
private volatile Thread backgroundDownloadTask;
|
|
217
222
|
private volatile Thread appReadyCheck;
|
|
223
|
+
// When true, sendReadyToJs should wait for notifyAppReady before hiding splash.
|
|
224
|
+
private volatile boolean pendingNotifyAppReadyWait = false;
|
|
225
|
+
private volatile int pendingNotifyAppReadyPhase = -1;
|
|
218
226
|
private volatile long downloadStartTimeMs = 0;
|
|
219
227
|
private static final long DOWNLOAD_TIMEOUT_MS = 600000; // 10 minute timeout
|
|
220
228
|
|
|
@@ -227,8 +235,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
227
235
|
|
|
228
236
|
// Lock to ensure cleanup completes before downloads start
|
|
229
237
|
private final Object cleanupLock = new Object();
|
|
238
|
+
private volatile CountDownLatch cleanupLatch = new CountDownLatch(0);
|
|
230
239
|
private volatile boolean cleanupComplete = false;
|
|
231
240
|
private volatile Thread cleanupThread = null;
|
|
241
|
+
private volatile boolean defaultChannelCleanupMustRetry = false;
|
|
232
242
|
|
|
233
243
|
private int lastNotifiedStatPercent = 0;
|
|
234
244
|
|
|
@@ -236,6 +246,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
236
246
|
|
|
237
247
|
private ShakeMenu shakeMenu;
|
|
238
248
|
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
249
|
+
private final long launchStartedAtMs = System.currentTimeMillis();
|
|
250
|
+
private volatile long webViewPageStartedAtMs = 0;
|
|
251
|
+
private volatile boolean launchStartReported = false;
|
|
252
|
+
private volatile boolean launchReadyReported = false;
|
|
253
|
+
private volatile boolean launchTimeoutReported = false;
|
|
254
|
+
private final Object launchReportLock = new Object();
|
|
239
255
|
private FrameLayout splashscreenLoaderOverlay;
|
|
240
256
|
private Runnable splashscreenTimeoutRunnable;
|
|
241
257
|
private FrameLayout previewTransitionLoaderOverlay;
|
|
@@ -268,6 +284,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
268
284
|
// App lifecycle observer using ProcessLifecycleOwner for reliable foreground/background detection
|
|
269
285
|
private AppLifecycleObserver appLifecycleObserver;
|
|
270
286
|
|
|
287
|
+
private boolean isProcessLifecycleObserverActive() {
|
|
288
|
+
return this.appLifecycleObserver != null && this.appLifecycleObserver.isRegistered();
|
|
289
|
+
}
|
|
290
|
+
|
|
271
291
|
// Play Store In-App Updates
|
|
272
292
|
private AppUpdateManager appUpdateManager;
|
|
273
293
|
private AppUpdateInfo cachedAppUpdateInfo;
|
|
@@ -747,6 +767,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
747
767
|
|
|
748
768
|
this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
|
|
749
769
|
this.persistModifyUrl = this.getConfig().getBoolean("persistModifyUrl", false);
|
|
770
|
+
this.persistDefaultChannelOnReinstall = this.getConfig().getBoolean("persistDefaultChannelOnReinstall", true);
|
|
750
771
|
this.allowSetDefaultChannel = this.getConfig().getBoolean("allowSetDefaultChannel", true);
|
|
751
772
|
this.implementation.setPublicKey(this.getConfig().getString("publicKey", ""));
|
|
752
773
|
// Log public key prefix if encryption is enabled
|
|
@@ -773,6 +794,35 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
773
794
|
}
|
|
774
795
|
}
|
|
775
796
|
|
|
797
|
+
final boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
|
|
798
|
+
final boolean nativeBuildVersionChanged = this.hasNativeBuildVersionChanged();
|
|
799
|
+
final boolean defaultChannelPersistenceDisabled = !Boolean.TRUE.equals(this.persistDefaultChannelOnReinstall);
|
|
800
|
+
final boolean restoredReinstall = defaultChannelPersistenceDisabled && this.isRestoredReinstall();
|
|
801
|
+
boolean installMarkerCanBePrepared = true;
|
|
802
|
+
if (
|
|
803
|
+
shouldClearPersistedDefaultChannel(
|
|
804
|
+
Boolean.TRUE.equals(this.persistDefaultChannelOnReinstall),
|
|
805
|
+
resetWhenUpdate,
|
|
806
|
+
nativeBuildVersionChanged,
|
|
807
|
+
restoredReinstall
|
|
808
|
+
)
|
|
809
|
+
) {
|
|
810
|
+
installMarkerCanBePrepared = clearPersistedDefaultChannel(this.editor);
|
|
811
|
+
if (installMarkerCanBePrepared) {
|
|
812
|
+
logger.info("Cleared persisted defaultChannel because reinstall persistence is disabled");
|
|
813
|
+
} else {
|
|
814
|
+
logger.warn("Cannot durably clear persisted defaultChannel");
|
|
815
|
+
this.defaultChannelCleanupMustRetry = true;
|
|
816
|
+
if (!invalidateDefaultChannelInstallMarker(this.defaultChannelInstallMarker())) {
|
|
817
|
+
logger.warn("Cannot invalidate default channel install marker for cleanup retry");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (defaultChannelPersistenceDisabled && installMarkerCanBePrepared) {
|
|
822
|
+
this.prepareDefaultChannelInstallMarker();
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
final String configDefaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
776
826
|
// Load defaultChannel: first try from persistent storage (set via setChannel), then fall back to config
|
|
777
827
|
if (this.prefs.contains(DEFAULT_CHANNEL_PREF_KEY)) {
|
|
778
828
|
final String storedDefaultChannel = this.prefs.getString(DEFAULT_CHANNEL_PREF_KEY, "");
|
|
@@ -780,20 +830,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
780
830
|
this.implementation.defaultChannel = storedDefaultChannel;
|
|
781
831
|
logger.info("Loaded persisted defaultChannel from setChannel()");
|
|
782
832
|
} else {
|
|
783
|
-
this.implementation.defaultChannel =
|
|
833
|
+
this.implementation.defaultChannel = configDefaultChannel;
|
|
784
834
|
}
|
|
785
835
|
} else {
|
|
786
|
-
this.implementation.defaultChannel =
|
|
836
|
+
this.implementation.defaultChannel = configDefaultChannel;
|
|
787
837
|
}
|
|
788
838
|
|
|
789
839
|
this.periodCheckDelay = normalizedPeriodCheckDelayMs(this.getConfig().getInt("periodCheckDelay", 0));
|
|
790
840
|
|
|
791
841
|
this.implementation.documentsDir = this.getContext().getFilesDir();
|
|
842
|
+
this.implementation.noBackupDir = this.getContext().getNoBackupFilesDir();
|
|
792
843
|
this.implementation.prefs = this.prefs;
|
|
793
844
|
this.implementation.editor = this.editor;
|
|
794
845
|
this.implementation.versionOs = Build.VERSION.RELEASE;
|
|
795
846
|
// Use DeviceIdHelper to get or create device ID that persists across reinstalls
|
|
796
847
|
this.implementation.deviceID = DeviceIdHelper.getOrCreateDeviceId(this.getContext(), this.prefs);
|
|
848
|
+
this.implementation.restorePendingStats();
|
|
797
849
|
|
|
798
850
|
// Update User-Agent for shared OkHttpClient with OS version
|
|
799
851
|
DownloadService.updateUserAgent(this.implementation.appId, this.pluginVersion, this.implementation.versionOs);
|
|
@@ -807,6 +859,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
807
859
|
}
|
|
808
860
|
logger.info("init for device " + this.implementation.deviceID);
|
|
809
861
|
logger.info("version native " + this.currentVersionNative.getOriginalString());
|
|
862
|
+
this.reportAppLaunchStart();
|
|
810
863
|
this.autoDeleteFailed = this.getConfig().getBoolean("autoDeleteFailed", true);
|
|
811
864
|
this.autoDeletePrevious = this.getConfig().getBoolean("autoDeletePrevious", true);
|
|
812
865
|
this.updateUrl = this.getConfig().getString("updateUrl", updateUrlDefault);
|
|
@@ -828,7 +881,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
828
881
|
this.autoSplashscreenLoader = this.getConfig().getBoolean("autoSplashscreenLoader", false);
|
|
829
882
|
int splashscreenTimeoutValue = this.getConfig().getInt("autoSplashscreenTimeout", 10000);
|
|
830
883
|
this.autoSplashscreenTimeout = Math.max(0, splashscreenTimeoutValue);
|
|
831
|
-
|
|
884
|
+
int responseTimeoutSeconds = this.getConfig().getInt("responseTimeout", 20);
|
|
885
|
+
long responseTimeoutMillis = responseTimeoutSeconds > 0 ? (long) responseTimeoutSeconds * 1000L : 20_000L;
|
|
886
|
+
this.implementation.timeout = (int) Math.min(Integer.MAX_VALUE, responseTimeoutMillis);
|
|
887
|
+
DownloadService.applyHttpTimeouts(this.implementation.timeout);
|
|
832
888
|
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
833
889
|
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
834
890
|
this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
|
|
@@ -851,11 +907,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
851
907
|
? this.prefs.getBoolean(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY, false)
|
|
852
908
|
: this.shakeChannelSelectorEnabled;
|
|
853
909
|
}
|
|
854
|
-
boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
|
|
855
910
|
|
|
856
911
|
// Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
|
|
857
912
|
this.wasRecentlyInstalledOrUpdated = this.checkIfRecentlyInstalledOrUpdated();
|
|
858
|
-
final boolean nativeBuildVersionChanged = this.hasNativeBuildVersionChanged();
|
|
859
913
|
|
|
860
914
|
this.implementation.autoReset(this.currentBuildVersion, resetWhenUpdate);
|
|
861
915
|
if (nativeBuildVersionChanged) {
|
|
@@ -866,11 +920,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
866
920
|
this.reportPreviousAppExitReasons();
|
|
867
921
|
this.reportPreviousWebViewRenderProcessGone();
|
|
868
922
|
this.installWebViewStatsReporter();
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
923
|
+
// Downloads (including shake-menu / CapgoUpdater entry points) wait on this gate.
|
|
924
|
+
this.implementation.downloadGate = this::waitForCleanupIfNeeded;
|
|
925
|
+
// Always run async cleanup: delete obsolete bundles on native update (when enabled)
|
|
926
|
+
// and sweep orphan folders every launch. Must not block app startup.
|
|
927
|
+
if (!resetWhenUpdate) {
|
|
872
928
|
this.persistCurrentNativeBuildVersion();
|
|
873
929
|
}
|
|
930
|
+
this.cleanupObsoleteVersions(resetWhenUpdate);
|
|
874
931
|
|
|
875
932
|
// Check for 'kill' delay condition on app launch
|
|
876
933
|
// This handles cases where the app was killed by the system (onDestroy is not reliable)
|
|
@@ -899,11 +956,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
899
956
|
},
|
|
900
957
|
logger
|
|
901
958
|
);
|
|
902
|
-
this.appLifecycleObserver.register();
|
|
903
|
-
|
|
959
|
+
this.appLifecycleObserver.register(this.getContext());
|
|
960
|
+
if (!this.appLifecycleObserver.isRegistered()) {
|
|
961
|
+
logger.warn("ProcessLifecycleOwner unavailable; using activity lifecycle callbacks");
|
|
962
|
+
} else {
|
|
963
|
+
logger.info("Using ProcessLifecycleOwner for foreground/background detection (Android 14+)");
|
|
964
|
+
}
|
|
904
965
|
} else {
|
|
905
966
|
logger.info("Using activity lifecycle callbacks for foreground/background detection (Android <14)");
|
|
906
967
|
}
|
|
968
|
+
|
|
969
|
+
// Expect notifyAppReady before the first appReady/splash hide (same idea as iOS).
|
|
970
|
+
this.armPendingNotifyAppReadyWait();
|
|
907
971
|
}
|
|
908
972
|
|
|
909
973
|
private boolean semaphoreWait(final int phase, Number waitTime) {
|
|
@@ -946,6 +1010,32 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
946
1010
|
semaphoreReady.arriveAndDeregister();
|
|
947
1011
|
}
|
|
948
1012
|
|
|
1013
|
+
private void armPendingNotifyAppReadyWait() {
|
|
1014
|
+
this.clearPendingNotifyAppReadyWait();
|
|
1015
|
+
this.pendingNotifyAppReadyPhase = this.semaphoreUp();
|
|
1016
|
+
this.pendingNotifyAppReadyWait = true;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private void clearPendingNotifyAppReadyWait() {
|
|
1020
|
+
if (!this.pendingNotifyAppReadyWait) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
final int phase = this.pendingNotifyAppReadyPhase;
|
|
1024
|
+
this.pendingNotifyAppReadyWait = false;
|
|
1025
|
+
this.pendingNotifyAppReadyPhase = -1;
|
|
1026
|
+
this.cleanupTimedOutSemaphoreWait(phase);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
private boolean consumePendingNotifyAppReadyWait(final int[] phaseOut) {
|
|
1030
|
+
if (!this.pendingNotifyAppReadyWait) {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
phaseOut[0] = this.pendingNotifyAppReadyPhase;
|
|
1034
|
+
this.pendingNotifyAppReadyWait = false;
|
|
1035
|
+
this.pendingNotifyAppReadyPhase = -1;
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
949
1039
|
protected long getMinimumPendingBundleAppReadyTimeoutMs() {
|
|
950
1040
|
return PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS;
|
|
951
1041
|
}
|
|
@@ -984,19 +1074,40 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
984
1074
|
|
|
985
1075
|
private void sendReadyToJs(final BundleInfo current, final String msg, final boolean isDirectUpdate) {
|
|
986
1076
|
logger.info("sendReadyToJs: " + msg);
|
|
987
|
-
final
|
|
988
|
-
|
|
989
|
-
ret.put("status", msg);
|
|
1077
|
+
final int[] pendingPhase = new int[] { -1 };
|
|
1078
|
+
final boolean shouldWait = this.consumePendingNotifyAppReadyWait(pendingPhase);
|
|
990
1079
|
|
|
991
|
-
|
|
992
|
-
|
|
1080
|
+
final Runnable emitReady = () -> {
|
|
1081
|
+
final JSObject ret = new JSObject();
|
|
1082
|
+
ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
1083
|
+
ret.put("status", msg);
|
|
993
1084
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1085
|
+
this.notifyListeners("appReady", ret, true);
|
|
1086
|
+
|
|
1087
|
+
// Auto hide splashscreen if enabled
|
|
1088
|
+
// We show it on background when conditions are met, so we should hide it on foreground regardless of update outcome
|
|
1089
|
+
if (this.autoSplashscreen) {
|
|
1090
|
+
this.hideSplashscreen();
|
|
1091
|
+
}
|
|
1092
|
+
this.hidePreviewTransitionLoader("app-ready");
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
if (!shouldWait) {
|
|
1096
|
+
emitReady.run();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Never block the UI thread waiting for notifyAppReady (JS needs it).
|
|
1101
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
1102
|
+
startNewThread(() -> {
|
|
1103
|
+
this.semaphoreWait(pendingPhase[0], this.appReadyTimeout);
|
|
1104
|
+
emitReady.run();
|
|
1105
|
+
});
|
|
1106
|
+
return;
|
|
998
1107
|
}
|
|
999
|
-
|
|
1108
|
+
|
|
1109
|
+
this.semaphoreWait(pendingPhase[0], this.appReadyTimeout);
|
|
1110
|
+
emitReady.run();
|
|
1000
1111
|
}
|
|
1001
1112
|
|
|
1002
1113
|
private void hideSplashscreen() {
|
|
@@ -1300,6 +1411,74 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1300
1411
|
return false;
|
|
1301
1412
|
}
|
|
1302
1413
|
|
|
1414
|
+
static boolean shouldClearPersistedDefaultChannel(
|
|
1415
|
+
final boolean persistDefaultChannelOnReinstall,
|
|
1416
|
+
final boolean resetWhenUpdate,
|
|
1417
|
+
final boolean nativeBuildVersionChanged,
|
|
1418
|
+
final boolean restoredReinstall
|
|
1419
|
+
) {
|
|
1420
|
+
return !persistDefaultChannelOnReinstall && (restoredReinstall || (resetWhenUpdate && nativeBuildVersionChanged));
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
static boolean clearPersistedDefaultChannel(final SharedPreferences.Editor editor) {
|
|
1424
|
+
editor.remove(DEFAULT_CHANNEL_PREF_KEY);
|
|
1425
|
+
editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
|
|
1426
|
+
editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY);
|
|
1427
|
+
return editor.commit();
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
private File defaultChannelInstallMarker() {
|
|
1431
|
+
return new File(this.getContext().getNoBackupFilesDir(), DEFAULT_CHANNEL_INSTALL_MARKER_FILE);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
static boolean invalidateDefaultChannelInstallMarker(final File marker) {
|
|
1435
|
+
return !marker.exists() || marker.delete();
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
private boolean isRestoredReinstall() {
|
|
1439
|
+
return isRestoredReinstall(
|
|
1440
|
+
this.defaultChannelInstallMarker(),
|
|
1441
|
+
this.prefs.getBoolean(DEFAULT_CHANNEL_INSTALL_MARKER_PREF_KEY, false)
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
static boolean isRestoredReinstall(final File marker, final boolean markerWasCreated) {
|
|
1446
|
+
return markerWasCreated && !marker.exists();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
private void prepareDefaultChannelInstallMarker() {
|
|
1450
|
+
prepareDefaultChannelInstallMarker(
|
|
1451
|
+
this.defaultChannelInstallMarker(),
|
|
1452
|
+
this.prefs.getBoolean(DEFAULT_CHANNEL_INSTALL_MARKER_PREF_KEY, false),
|
|
1453
|
+
this.editor,
|
|
1454
|
+
this.logger
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
static void prepareDefaultChannelInstallMarker(
|
|
1459
|
+
final File marker,
|
|
1460
|
+
final boolean markerWasCreated,
|
|
1461
|
+
final SharedPreferences.Editor editor,
|
|
1462
|
+
final Logger logger
|
|
1463
|
+
) {
|
|
1464
|
+
if (!marker.exists()) {
|
|
1465
|
+
try {
|
|
1466
|
+
if (!marker.createNewFile() && !marker.exists()) {
|
|
1467
|
+
throw new IOException("Marker file was not created");
|
|
1468
|
+
}
|
|
1469
|
+
} catch (final IOException e) {
|
|
1470
|
+
logger.warn("Cannot create default channel install marker: " + e.getMessage());
|
|
1471
|
+
editor.remove(DEFAULT_CHANNEL_INSTALL_MARKER_PREF_KEY);
|
|
1472
|
+
editor.commit();
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (!markerWasCreated) {
|
|
1477
|
+
editor.putBoolean(DEFAULT_CHANNEL_INSTALL_MARKER_PREF_KEY, true);
|
|
1478
|
+
editor.apply();
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1303
1482
|
private boolean hasNativeBuildVersionChanged() {
|
|
1304
1483
|
final String lastKnownVersion = this.getStoredNativeBuildVersion();
|
|
1305
1484
|
return !lastKnownVersion.isEmpty() && !lastKnownVersion.equals(this.currentBuildVersion);
|
|
@@ -1486,26 +1665,39 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1486
1665
|
this.webViewStatsListener = new WebViewListener() {
|
|
1487
1666
|
@Override
|
|
1488
1667
|
public void onPageStarted(final android.webkit.WebView view) {
|
|
1668
|
+
CapacitorUpdaterPlugin.this.webViewPageStartedAtMs = System.currentTimeMillis();
|
|
1489
1669
|
CapacitorUpdaterPlugin.this.evaluateWebViewStatsReporterScript(view, script);
|
|
1490
1670
|
}
|
|
1491
1671
|
|
|
1492
1672
|
@Override
|
|
1493
1673
|
public void onPageLoaded(final android.webkit.WebView view) {
|
|
1674
|
+
CapacitorUpdaterPlugin.this.reportWebViewPageLoaded(view);
|
|
1494
1675
|
CapacitorUpdaterPlugin.this.evaluateWebViewStatsReporterScript(view, script);
|
|
1495
1676
|
}
|
|
1496
|
-
|
|
1497
|
-
@Override
|
|
1498
|
-
public boolean onRenderProcessGone(final android.webkit.WebView view, final RenderProcessGoneDetail detail) {
|
|
1499
|
-
final Map<String, String> metadata = CapacitorUpdaterPlugin.this.buildWebViewRenderProcessGoneMetadata(detail);
|
|
1500
|
-
CapacitorUpdaterPlugin.this.persistPendingWebViewRenderProcessGone(metadata);
|
|
1501
|
-
return false;
|
|
1502
|
-
}
|
|
1503
1677
|
};
|
|
1504
1678
|
|
|
1505
1679
|
this.bridge.addWebViewListener(this.webViewStatsListener);
|
|
1680
|
+
// Keep RenderProcessGoneDetail off the Plugin class method table and off this
|
|
1681
|
+
// listener so Android < 8 (API 26) does not crash during plugin reflection.
|
|
1682
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1683
|
+
this.installWebViewRenderProcessGoneReporter();
|
|
1684
|
+
}
|
|
1506
1685
|
this.evaluateWebViewStatsReporterScript(webView, script);
|
|
1507
1686
|
}
|
|
1508
1687
|
|
|
1688
|
+
private void installWebViewRenderProcessGoneReporter() {
|
|
1689
|
+
this.bridge.addWebViewListener(
|
|
1690
|
+
new WebViewListener() {
|
|
1691
|
+
@Override
|
|
1692
|
+
public boolean onRenderProcessGone(final android.webkit.WebView view, final RenderProcessGoneDetail detail) {
|
|
1693
|
+
final Map<String, String> metadata = CapacitorUpdaterPlugin.this.buildWebViewRenderProcessGoneMetadata(detail);
|
|
1694
|
+
CapacitorUpdaterPlugin.this.persistPendingWebViewRenderProcessGone(metadata);
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1509
1701
|
private void installDocumentStartWebViewStatsReporter(final android.webkit.WebView webView, final String script) {
|
|
1510
1702
|
try {
|
|
1511
1703
|
final Class<?> webViewFeature = Class.forName("androidx.webkit.WebViewFeature");
|
|
@@ -1545,16 +1737,94 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1545
1737
|
});
|
|
1546
1738
|
}
|
|
1547
1739
|
|
|
1548
|
-
private
|
|
1740
|
+
private void reportAppLaunchStart() {
|
|
1741
|
+
if (
|
|
1742
|
+
this.implementation == null ||
|
|
1743
|
+
this.implementation.statsUrl == null ||
|
|
1744
|
+
this.implementation.statsUrl.isEmpty() ||
|
|
1745
|
+
this.launchStartReported
|
|
1746
|
+
) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
this.launchStartReported = true;
|
|
1751
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
1752
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1753
|
+
metadata.put("launch_started_at", Long.toString(this.launchStartedAtMs));
|
|
1754
|
+
metadata.put("source", "plugin_load");
|
|
1755
|
+
this.implementation.sendStats("app_launch_start", current == null ? "" : current.getVersionName(), "", metadata);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
private void reportAppLaunchReady(final BundleInfo bundle) {
|
|
1759
|
+
synchronized (this.launchReportLock) {
|
|
1760
|
+
if (
|
|
1761
|
+
this.implementation == null ||
|
|
1762
|
+
this.implementation.statsUrl == null ||
|
|
1763
|
+
this.implementation.statsUrl.isEmpty() ||
|
|
1764
|
+
this.launchReadyReported ||
|
|
1765
|
+
this.launchTimeoutReported
|
|
1766
|
+
) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
this.launchReadyReported = true;
|
|
1770
|
+
}
|
|
1771
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1772
|
+
metadata.put("duration_ms", Long.toString(Math.max(0, System.currentTimeMillis() - this.launchStartedAtMs)));
|
|
1773
|
+
metadata.put("launch_started_at", Long.toString(this.launchStartedAtMs));
|
|
1774
|
+
metadata.put("source", "notify_app_ready");
|
|
1775
|
+
this.implementation.sendStats("app_launch_ready", bundle == null ? "" : bundle.getVersionName(), "", metadata);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
private void reportAppLaunchTimeout(final BundleInfo bundle) {
|
|
1779
|
+
synchronized (this.launchReportLock) {
|
|
1780
|
+
if (
|
|
1781
|
+
this.implementation == null ||
|
|
1782
|
+
this.implementation.statsUrl == null ||
|
|
1783
|
+
this.implementation.statsUrl.isEmpty() ||
|
|
1784
|
+
this.launchReadyReported ||
|
|
1785
|
+
this.launchTimeoutReported
|
|
1786
|
+
) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
this.launchTimeoutReported = true;
|
|
1790
|
+
}
|
|
1791
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1792
|
+
metadata.put("duration_ms", Long.toString(Math.max(0, System.currentTimeMillis() - this.launchStartedAtMs)));
|
|
1793
|
+
metadata.put("launch_started_at", Long.toString(this.launchStartedAtMs));
|
|
1794
|
+
metadata.put("timeout_ms", Long.toString(this.resolveAppReadyCheckTimeoutMs()));
|
|
1795
|
+
metadata.put("source", "app_ready_timeout");
|
|
1796
|
+
this.implementation.sendStats("app_launch_timeout", bundle == null ? "" : bundle.getVersionName(), "", metadata);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
private void reportWebViewPageLoaded(final android.webkit.WebView view) {
|
|
1800
|
+
if (this.implementation == null || this.implementation.statsUrl == null || this.implementation.statsUrl.isEmpty()) {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
final Map<String, String> metadata = new HashMap<>();
|
|
1805
|
+
metadata.put("source", "android_webview_listener");
|
|
1806
|
+
final long pageStartedAt = this.webViewPageStartedAtMs;
|
|
1807
|
+
if (pageStartedAt > 0) {
|
|
1808
|
+
metadata.put("duration_ms", Long.toString(Math.max(0, System.currentTimeMillis() - pageStartedAt)));
|
|
1809
|
+
metadata.put("page_started_at", Long.toString(pageStartedAt));
|
|
1810
|
+
}
|
|
1811
|
+
if (view != null && view.getUrl() != null && !view.getUrl().isEmpty()) {
|
|
1812
|
+
metadata.put("href", truncateStatsMetadataValue(sanitizeStatsMetadataUrl(view.getUrl()), 512));
|
|
1813
|
+
}
|
|
1814
|
+
this.reportWebViewStats("webview_page_loaded", metadata);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
private Map<String, String> buildWebViewRenderProcessGoneMetadata(final Object detailObj) {
|
|
1549
1818
|
final Map<String, String> metadata = new HashMap<>();
|
|
1550
1819
|
metadata.put("error_type", "render_process_gone");
|
|
1551
1820
|
metadata.put("source", "android_on_render_process_gone");
|
|
1552
1821
|
metadata.put("timestamp", Long.toString(System.currentTimeMillis()));
|
|
1553
|
-
|
|
1822
|
+
// Parameter typed as Object so Android < 8 ART does not resolve
|
|
1823
|
+
// RenderProcessGoneDetail while reflecting CapacitorUpdaterPlugin methods.
|
|
1824
|
+
if (detailObj != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1825
|
+
final RenderProcessGoneDetail detail = (RenderProcessGoneDetail) detailObj;
|
|
1554
1826
|
metadata.put("did_crash", Boolean.toString(detail.didCrash()));
|
|
1555
|
-
|
|
1556
|
-
metadata.put("renderer_priority_at_exit", Integer.toString(detail.rendererPriorityAtExit()));
|
|
1557
|
-
}
|
|
1827
|
+
metadata.put("renderer_priority_at_exit", Integer.toString(detail.rendererPriorityAtExit()));
|
|
1558
1828
|
}
|
|
1559
1829
|
return metadata;
|
|
1560
1830
|
}
|
|
@@ -1652,6 +1922,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1652
1922
|
return "webview_render_process_gone";
|
|
1653
1923
|
case "web_content_process_terminated":
|
|
1654
1924
|
return "webview_content_process_terminated";
|
|
1925
|
+
case "webview_dom_content_loaded":
|
|
1926
|
+
return "webview_dom_content_loaded";
|
|
1927
|
+
case "webview_page_loaded":
|
|
1928
|
+
return "webview_page_loaded";
|
|
1655
1929
|
case "javascript_error":
|
|
1656
1930
|
default:
|
|
1657
1931
|
return "webview_javascript_error";
|
|
@@ -1670,6 +1944,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1670
1944
|
putStatsMetadataValue(metadata, "href", sanitizeStatsMetadataUrl(data.optString("href", "")), 512);
|
|
1671
1945
|
putStatsMetadataValue(metadata, "user_agent", data.optString("user_agent", ""), 256);
|
|
1672
1946
|
putStatsMetadataValue(metadata, "session_id", data.optString("session_id", ""), 128);
|
|
1947
|
+
putStatsMetadataValue(metadata, "duration_ms", data.optString("duration_ms", ""), 32);
|
|
1948
|
+
putStatsMetadataValue(metadata, "page_started_at", data.optString("page_started_at", ""), 64);
|
|
1673
1949
|
putStatsMetadataValue(metadata, "previous_session_id", data.optString("previous_session_id", ""), 128);
|
|
1674
1950
|
putStatsMetadataValue(metadata, "previous_href", sanitizeStatsMetadataUrl(data.optString("previous_href", "")), 512);
|
|
1675
1951
|
putStatsMetadataValue(metadata, "previous_started_at", data.optString("previous_started_at", ""), 64);
|
|
@@ -1802,12 +2078,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1802
2078
|
"if(previous&&previous.active){send({type:'webview_unclean_restart',message:'WebView restarted without a clean page unload',previous_session_id:s(previous.id),previous_href:s(previous.href),previous_started_at:s(previous.started_at),previous_updated_at:s(previous.updated_at)});}" +
|
|
1803
2079
|
"writeSession(true);" +
|
|
1804
2080
|
"setInterval(function(){writeSession(true);},15000);" +
|
|
2081
|
+
"function pageDuration(){var started=Number(window.__capgoWebViewSessionStartedAt||Date.now());return String(Math.max(0,Date.now()-started));}" +
|
|
1805
2082
|
"function markClean(){writeSession(false);}" +
|
|
1806
2083
|
"window.addEventListener('pagehide',markClean,true);" +
|
|
1807
2084
|
"window.addEventListener('beforeunload',markClean,true);" +
|
|
1808
2085
|
"window.addEventListener('error',function(event){var target=event&&event.target;if(target&&target!==window&&(target.src||target.href)){send({type:'resource_error',message:'Resource failed to load',source:s(target.src||target.href),tag_name:s(target.tagName)});return;}send({type:'javascript_error',message:s((event&&event.message)||(event&&event.error)),source:s(event&&event.filename),line:s(event&&event.lineno),column:s(event&&event.colno),stack:stack(event&&event.error)});},true);" +
|
|
1809
2086
|
"window.addEventListener('unhandledrejection',function(event){var reason=event&&event.reason;send({type:'unhandled_rejection',message:s(reason),stack:stack(reason)});},true);" +
|
|
1810
2087
|
"document.addEventListener('securitypolicyviolation',function(event){send({type:'security_policy_violation',message:s(event&&event.violatedDirective),source:s(event&&event.blockedURI)});},true);" +
|
|
2088
|
+
"document.addEventListener('DOMContentLoaded',function(){send({type:'webview_dom_content_loaded',message:'WebView DOM content loaded',duration_ms:pageDuration(),page_started_at:String(window.__capgoWebViewSessionStartedAt)});},true);" +
|
|
1811
2089
|
"document.addEventListener('deviceready',scheduleFlush,false);" +
|
|
1812
2090
|
"setTimeout(scheduleFlush,0);" +
|
|
1813
2091
|
"})();"
|
|
@@ -2128,67 +2406,57 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2128
2406
|
return false;
|
|
2129
2407
|
}
|
|
2130
2408
|
|
|
2131
|
-
private void cleanupObsoleteVersions() {
|
|
2409
|
+
private void cleanupObsoleteVersions(final boolean resetWhenUpdate) {
|
|
2410
|
+
// Latch created before start so waiters never race past an unstarted cleanup thread.
|
|
2411
|
+
final CountDownLatch latch = new CountDownLatch(1);
|
|
2412
|
+
this.cleanupComplete = false;
|
|
2413
|
+
this.cleanupLatch = latch;
|
|
2132
2414
|
cleanupThread = startNewThread(() -> {
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
for (final BundleInfo info : storedBundles) {
|
|
2156
|
-
if (info != null && info.getId() != null && !info.getId().isEmpty()) {
|
|
2157
|
-
allowedIds.add(info.getId());
|
|
2415
|
+
try {
|
|
2416
|
+
synchronized (cleanupLock) {
|
|
2417
|
+
try {
|
|
2418
|
+
final String previous = this.getStoredNativeBuildVersion();
|
|
2419
|
+
final boolean nativeVersionChanged = !"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous);
|
|
2420
|
+
if (resetWhenUpdate && nativeVersionChanged) {
|
|
2421
|
+
logger.info("New native build version detected: " + this.currentBuildVersion);
|
|
2422
|
+
this.implementation.reset(true);
|
|
2423
|
+
final List<BundleInfo> installed = this.implementation.list(false);
|
|
2424
|
+
for (final BundleInfo bundle : installed) {
|
|
2425
|
+
try {
|
|
2426
|
+
logger.info("Deleting obsolete bundle: " + bundle.getId());
|
|
2427
|
+
this.implementation.delete(bundle.getId());
|
|
2428
|
+
} catch (final Exception e) {
|
|
2429
|
+
logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
|
|
2430
|
+
}
|
|
2431
|
+
try {
|
|
2432
|
+
Thread.sleep(75L);
|
|
2433
|
+
} catch (final InterruptedException ie) {
|
|
2434
|
+
Thread.currentThread().interrupt();
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2158
2437
|
}
|
|
2438
|
+
this.implementation.cleanupDeltaCache();
|
|
2159
2439
|
}
|
|
2160
|
-
this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
|
|
2161
|
-
this.implementation.cleanupOrphanedTempFolders(Thread.currentThread());
|
|
2162
2440
|
|
|
2163
|
-
//
|
|
2164
|
-
|
|
2165
|
-
logger.warn("Cleanup was cancelled before delta cache cleanup");
|
|
2166
|
-
return;
|
|
2167
|
-
}
|
|
2168
|
-
this.implementation.cleanupDeltaCache(Thread.currentThread());
|
|
2169
|
-
}
|
|
2170
|
-
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
2171
|
-
this.editor.apply();
|
|
2172
|
-
} catch (Exception e) {
|
|
2173
|
-
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
2174
|
-
} finally {
|
|
2175
|
-
cleanupComplete = true;
|
|
2176
|
-
logger.info("Cleanup complete");
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
});
|
|
2441
|
+
// Resume any DELETING leftovers from prior kills, one-by-one.
|
|
2442
|
+
this.implementation.drainPendingDeletes();
|
|
2180
2443
|
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2444
|
+
// Always sweep orphan folders so incomplete prior cleanups (or failed deletes)
|
|
2445
|
+
// cannot leave hundreds of MB behind across launches.
|
|
2446
|
+
final Set<String> allowedIds = this.implementation.allowedBundleIdsForCleanup();
|
|
2447
|
+
this.implementation.cleanupDownloadDirectories(allowedIds);
|
|
2448
|
+
this.implementation.cleanupOrphanedTempFolders(null);
|
|
2449
|
+
|
|
2450
|
+
this.persistCurrentNativeBuildVersion();
|
|
2451
|
+
} catch (Exception e) {
|
|
2452
|
+
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
2453
|
+
} finally {
|
|
2454
|
+
cleanupComplete = true;
|
|
2455
|
+
logger.info("Cleanup complete");
|
|
2456
|
+
}
|
|
2189
2457
|
}
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2458
|
+
} finally {
|
|
2459
|
+
latch.countDown();
|
|
2192
2460
|
}
|
|
2193
2461
|
});
|
|
2194
2462
|
}
|
|
@@ -2202,6 +2470,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2202
2470
|
}
|
|
2203
2471
|
|
|
2204
2472
|
void persistCurrentNativeBuildVersion() {
|
|
2473
|
+
if (this.defaultChannelCleanupMustRetry) {
|
|
2474
|
+
logger.warn("Keeping the previous native build version so default channel cleanup retries");
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2205
2477
|
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
2206
2478
|
this.editor.apply();
|
|
2207
2479
|
}
|
|
@@ -2212,11 +2484,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2212
2484
|
}
|
|
2213
2485
|
|
|
2214
2486
|
logger.info("Waiting for cleanup to complete before starting download...");
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2487
|
+
try {
|
|
2488
|
+
this.cleanupLatch.await();
|
|
2489
|
+
} catch (final InterruptedException e) {
|
|
2490
|
+
Thread.currentThread().interrupt();
|
|
2491
|
+
logger.warn("Interrupted while waiting for cleanup");
|
|
2492
|
+
throw new IllegalStateException("Interrupted while waiting for cleanup");
|
|
2219
2493
|
}
|
|
2494
|
+
logger.info("Cleanup finished, proceeding with download");
|
|
2220
2495
|
}
|
|
2221
2496
|
|
|
2222
2497
|
public void notifyDownload(final String id, final int percent) {
|
|
@@ -2257,11 +2532,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2257
2532
|
call.reject("setUpdateUrl called without url");
|
|
2258
2533
|
return;
|
|
2259
2534
|
}
|
|
2260
|
-
this.updateUrl = url;
|
|
2261
2535
|
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2262
2536
|
this.editor.putString(UPDATE_URL_PREF_KEY, url);
|
|
2263
|
-
this.editor.
|
|
2537
|
+
if (!this.editor.commit()) {
|
|
2538
|
+
logger.error("Failed to persist updateUrl");
|
|
2539
|
+
call.reject("Failed to persist updateUrl");
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2264
2542
|
}
|
|
2543
|
+
this.updateUrl = url;
|
|
2265
2544
|
call.resolve();
|
|
2266
2545
|
}
|
|
2267
2546
|
|
|
@@ -2278,11 +2557,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2278
2557
|
call.reject("setStatsUrl called without url");
|
|
2279
2558
|
return;
|
|
2280
2559
|
}
|
|
2281
|
-
this.implementation.statsUrl = url;
|
|
2282
2560
|
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2283
2561
|
this.editor.putString(STATS_URL_PREF_KEY, url);
|
|
2284
|
-
this.editor.
|
|
2562
|
+
if (!this.editor.commit()) {
|
|
2563
|
+
logger.error("Failed to persist statsUrl");
|
|
2564
|
+
call.reject("Failed to persist statsUrl");
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2285
2567
|
}
|
|
2568
|
+
this.implementation.statsUrl = url;
|
|
2286
2569
|
call.resolve();
|
|
2287
2570
|
}
|
|
2288
2571
|
|
|
@@ -2299,11 +2582,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2299
2582
|
call.reject("setChannelUrl called without url");
|
|
2300
2583
|
return;
|
|
2301
2584
|
}
|
|
2302
|
-
this.implementation.channelUrl = url;
|
|
2303
2585
|
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
2304
2586
|
this.editor.putString(CHANNEL_URL_PREF_KEY, url);
|
|
2305
|
-
this.editor.
|
|
2587
|
+
if (!this.editor.commit()) {
|
|
2588
|
+
logger.error("Failed to persist channelUrl");
|
|
2589
|
+
call.reject("Failed to persist channelUrl");
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2306
2592
|
}
|
|
2593
|
+
this.implementation.channelUrl = url;
|
|
2307
2594
|
call.resolve();
|
|
2308
2595
|
}
|
|
2309
2596
|
|
|
@@ -2554,6 +2841,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2554
2841
|
final String checksum,
|
|
2555
2842
|
final JSONArray manifest
|
|
2556
2843
|
) throws IOException {
|
|
2844
|
+
// Manual/preview downloads must wait too — launch orphan sweep can delete their temps.
|
|
2845
|
+
waitForCleanupIfNeeded();
|
|
2557
2846
|
if (manifest != null) {
|
|
2558
2847
|
return this.implementation.downloadManifest(url, version, sessionKey, checksum, manifest);
|
|
2559
2848
|
}
|
|
@@ -2727,6 +3016,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2727
3016
|
}
|
|
2728
3017
|
|
|
2729
3018
|
protected boolean _reload() {
|
|
3019
|
+
// Drop any launch pending wait; this reload owns notifyAppReady synchronization.
|
|
3020
|
+
this.clearPendingNotifyAppReadyWait();
|
|
2730
3021
|
final int phase = this.semaphoreUp();
|
|
2731
3022
|
this.applyCurrentBundleToBridge();
|
|
2732
3023
|
|
|
@@ -3736,7 +4027,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
3736
4027
|
|
|
3737
4028
|
new AlertDialog.Builder(getActivity())
|
|
3738
4029
|
.setTitle("Preview started")
|
|
3739
|
-
.setMessage("Shake
|
|
4030
|
+
.setMessage("shake".equals(this.shakeMenuGesture) ? "Shake to open menu." : "Three-finger pinch to open menu.")
|
|
3740
4031
|
.setPositiveButton("Got it", (dialog, which) -> dialog.dismiss())
|
|
3741
4032
|
.show();
|
|
3742
4033
|
} catch (final Exception e) {
|
|
@@ -3954,12 +4245,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
3954
4245
|
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
3955
4246
|
return "preview_session";
|
|
3956
4247
|
}
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
4248
|
+
synchronized (this) {
|
|
4249
|
+
final Thread previousTask = this.backgroundDownloadTask;
|
|
4250
|
+
final Thread task = this.backgroundDownload();
|
|
4251
|
+
if (task == null) {
|
|
4252
|
+
return "unavailable";
|
|
4253
|
+
}
|
|
4254
|
+
if (previousTask != null && previousTask == task) {
|
|
4255
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
4256
|
+
return "already_running";
|
|
4257
|
+
}
|
|
4258
|
+
return "queued";
|
|
3960
4259
|
}
|
|
3961
|
-
this.backgroundDownload();
|
|
3962
|
-
return "queued";
|
|
3963
4260
|
}
|
|
3964
4261
|
|
|
3965
4262
|
@PluginMethod
|
|
@@ -4187,6 +4484,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4187
4484
|
try {
|
|
4188
4485
|
final BundleInfo bundle = this.implementation.getCurrentBundle();
|
|
4189
4486
|
this.implementation.setSuccess(bundle, this.autoDeletePrevious);
|
|
4487
|
+
this.reportAppLaunchReady(bundle);
|
|
4190
4488
|
logger.info("Current bundle loaded successfully. ['notifyAppReady()' was called] " + bundle);
|
|
4191
4489
|
logger.info("semaphoreReady countDown");
|
|
4192
4490
|
this.semaphoreDown();
|
|
@@ -4471,10 +4769,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4471
4769
|
return true;
|
|
4472
4770
|
}
|
|
4473
4771
|
|
|
4474
|
-
private Thread backgroundDownload() {
|
|
4772
|
+
private synchronized Thread backgroundDownload() {
|
|
4475
4773
|
if (this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4476
4774
|
return null;
|
|
4477
4775
|
}
|
|
4776
|
+
if (this.isDownloadStuckOrTimedOut()) {
|
|
4777
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
4778
|
+
return this.backgroundDownloadTask;
|
|
4779
|
+
}
|
|
4478
4780
|
final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
|
|
4479
4781
|
final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
|
|
4480
4782
|
final String messageUpdate = initialDirectUpdateAllowed
|
|
@@ -4483,8 +4785,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4483
4785
|
? "Update will occur next time app moves to background."
|
|
4484
4786
|
: "Update will be downloaded and made available.";
|
|
4485
4787
|
Thread newTask = startNewThread(() -> {
|
|
4486
|
-
// Wait for cleanup to complete before starting download
|
|
4487
|
-
waitForCleanupIfNeeded();
|
|
4488
4788
|
if (CapacitorUpdaterPlugin.this.shouldBlockAutoUpdateForPreviewSession()) {
|
|
4489
4789
|
CapacitorUpdaterPlugin.this.clearBackgroundDownloadState();
|
|
4490
4790
|
return;
|
|
@@ -4497,7 +4797,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4497
4797
|
return;
|
|
4498
4798
|
}
|
|
4499
4799
|
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
4500
|
-
final BundleInfo
|
|
4800
|
+
final BundleInfo currentBeforeCleanup = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
4501
4801
|
|
|
4502
4802
|
// Handle network errors and other failures first
|
|
4503
4803
|
if (jsRes.has("error") || jsRes.has("kind")) {
|
|
@@ -4505,8 +4805,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4505
4805
|
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
|
|
4506
4806
|
int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
|
|
4507
4807
|
String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
|
|
4508
|
-
String latestVersion = jsRes.has("version") ? jsRes.getString("version") :
|
|
4509
|
-
CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
|
|
4808
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : currentBeforeCleanup.getVersionName();
|
|
4809
|
+
CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
|
|
4810
|
+
kind,
|
|
4811
|
+
error,
|
|
4812
|
+
errorMessage,
|
|
4813
|
+
statusCode,
|
|
4814
|
+
latestVersion,
|
|
4815
|
+
currentBeforeCleanup
|
|
4816
|
+
);
|
|
4510
4817
|
CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(
|
|
4511
4818
|
jsRes,
|
|
4512
4819
|
jsRes.has("version") ? jsRes.getString("version") : ""
|
|
@@ -4526,7 +4833,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4526
4833
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4527
4834
|
errorMessage,
|
|
4528
4835
|
latestVersion,
|
|
4529
|
-
|
|
4836
|
+
currentBeforeCleanup,
|
|
4530
4837
|
isFailure,
|
|
4531
4838
|
plannedDirectUpdate,
|
|
4532
4839
|
"download_fail",
|
|
@@ -4536,6 +4843,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4536
4843
|
return;
|
|
4537
4844
|
}
|
|
4538
4845
|
try {
|
|
4846
|
+
// File mutations wait here. getLatest already ran in parallel with cleanup.
|
|
4847
|
+
waitForCleanupIfNeeded();
|
|
4848
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
4539
4849
|
final String latestVersionName = jsRes.getString("version");
|
|
4540
4850
|
|
|
4541
4851
|
if ("builtin".equals(latestVersionName)) {
|
|
@@ -4784,8 +5094,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4784
5094
|
logger.error("error in update check " + e.getMessage());
|
|
4785
5095
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
4786
5096
|
"Error in update check",
|
|
4787
|
-
|
|
4788
|
-
|
|
5097
|
+
currentBeforeCleanup.getVersionName(),
|
|
5098
|
+
currentBeforeCleanup,
|
|
4789
5099
|
true,
|
|
4790
5100
|
plannedDirectUpdate
|
|
4791
5101
|
);
|
|
@@ -4861,19 +5171,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4861
5171
|
ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
4862
5172
|
this.persistLastFailedBundle(current);
|
|
4863
5173
|
this.notifyListeners("updateFailed", ret);
|
|
5174
|
+
this.reportAppLaunchTimeout(current);
|
|
4864
5175
|
this.implementation.sendStats("update_fail", current.getVersionName());
|
|
4865
5176
|
this.implementation.setError(current);
|
|
4866
5177
|
this.performReset(true, false, true);
|
|
4867
5178
|
if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
5179
|
+
final String failedId = current.getId();
|
|
5180
|
+
final String failedVersion = current.getVersionName();
|
|
5181
|
+
logger.info("Deleting failing bundle: " + failedVersion);
|
|
5182
|
+
// Mark before async work so kill/OOM still resumes via drainPendingDeletes.
|
|
5183
|
+
CapacitorUpdaterPlugin.this.implementation.saveBundleInfo(failedId, current.setStatus(BundleStatus.DELETING));
|
|
5184
|
+
startNewThread(() -> {
|
|
5185
|
+
try {
|
|
5186
|
+
final Boolean res = CapacitorUpdaterPlugin.this.implementation.delete(failedId, false, false);
|
|
5187
|
+
if (Boolean.TRUE.equals(res)) {
|
|
5188
|
+
logger.info("Failed bundle deleted: " + failedVersion);
|
|
5189
|
+
}
|
|
5190
|
+
} catch (final IOException e) {
|
|
5191
|
+
logger.error("Failed to delete failed bundle: " + failedVersion + " " + e.getMessage());
|
|
4873
5192
|
}
|
|
4874
|
-
}
|
|
4875
|
-
logger.error("Failed to delete failed bundle: " + current.getVersionName() + " " + e.getMessage());
|
|
4876
|
-
}
|
|
5193
|
+
});
|
|
4877
5194
|
}
|
|
4878
5195
|
} else {
|
|
4879
5196
|
logger.info("notifyAppReady was called. This is fine: " + current.getId());
|
|
@@ -4988,6 +5305,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
4988
5305
|
|
|
4989
5306
|
// Do other background work after splashscreen is shown
|
|
4990
5307
|
CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_background", current.getVersionName());
|
|
5308
|
+
CapacitorUpdaterPlugin.this.implementation.persistPendingStats();
|
|
4991
5309
|
logger.info("Checking for pending update");
|
|
4992
5310
|
|
|
4993
5311
|
try {
|
|
@@ -5063,9 +5381,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
5063
5381
|
|
|
5064
5382
|
// On Android < 14, use activity lifecycle for foreground detection
|
|
5065
5383
|
// On Android 14+, ProcessLifecycleOwner handles this via AppLifecycleObserver
|
|
5066
|
-
if (
|
|
5067
|
-
if (isPreviousMainActivity) {
|
|
5068
|
-
logger.info("handleOnStart: appMovedToForeground
|
|
5384
|
+
if (!this.isProcessLifecycleObserverActive()) {
|
|
5385
|
+
if (isPreviousMainActivity || Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
5386
|
+
logger.info("handleOnStart: appMovedToForeground");
|
|
5069
5387
|
this.appMovedToForeground();
|
|
5070
5388
|
}
|
|
5071
5389
|
isPreviousMainActivity = true;
|
|
@@ -5084,11 +5402,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
5084
5402
|
|
|
5085
5403
|
// On Android < 14, use activity lifecycle for background detection
|
|
5086
5404
|
// On Android 14+, ProcessLifecycleOwner handles this via AppLifecycleObserver
|
|
5087
|
-
if (
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
logger.info("handleOnStop: appMovedToBackground (Android <14 path)");
|
|
5405
|
+
if (!this.isProcessLifecycleObserverActive()) {
|
|
5406
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
5407
|
+
logger.info("handleOnStop: appMovedToBackground");
|
|
5091
5408
|
this.appMovedToBackground();
|
|
5409
|
+
} else {
|
|
5410
|
+
isPreviousMainActivity = isMainActivity();
|
|
5411
|
+
if (isPreviousMainActivity) {
|
|
5412
|
+
logger.info("handleOnStop: appMovedToBackground (Android <14 path)");
|
|
5413
|
+
this.appMovedToBackground();
|
|
5414
|
+
}
|
|
5092
5415
|
}
|
|
5093
5416
|
}
|
|
5094
5417
|
} catch (Exception e) {
|