@capgo/capacitor-updater 8.45.10 → 8.46.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.
@@ -7,6 +7,8 @@
7
7
  package ee.forgr.capacitor_updater;
8
8
 
9
9
  import android.app.Activity;
10
+ import android.app.ActivityManager;
11
+ import android.app.ApplicationExitInfo;
10
12
  import android.content.Context;
11
13
  import android.content.Intent;
12
14
  import android.content.SharedPreferences;
@@ -20,6 +22,7 @@ import android.os.Looper;
20
22
  import android.view.Gravity;
21
23
  import android.view.View;
22
24
  import android.view.ViewGroup;
25
+ import android.webkit.RenderProcessGoneDetail;
23
26
  import android.widget.FrameLayout;
24
27
  import android.widget.ProgressBar;
25
28
  import androidx.core.content.pm.PackageInfoCompat;
@@ -32,6 +35,7 @@ import com.getcapacitor.PluginCall;
32
35
  import com.getcapacitor.PluginHandle;
33
36
  import com.getcapacitor.PluginMethod;
34
37
  import com.getcapacitor.PluginResult;
38
+ import com.getcapacitor.WebViewListener;
35
39
  import com.getcapacitor.annotation.CapacitorPlugin;
36
40
  import com.getcapacitor.plugin.WebView;
37
41
  import com.google.android.gms.tasks.Task;
@@ -50,8 +54,8 @@ import java.io.IOException;
50
54
  import java.net.MalformedURLException;
51
55
  import java.net.URL;
52
56
  import java.util.ArrayList;
53
- import java.util.Arrays;
54
57
  import java.util.Date;
58
+ import java.util.HashMap;
55
59
  import java.util.HashSet;
56
60
  import java.util.List;
57
61
  import java.util.Map;
@@ -85,12 +89,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
85
89
  private static final String DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.defaultChannel";
86
90
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
87
91
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
92
+ private static final String LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY = "CapacitorUpdater.lastReportedAppExitTimestamp";
93
+ private static final String LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY = "CapacitorUpdater.lastWebViewRenderProcessGone";
88
94
  private static final String SPLASH_SCREEN_PLUGIN_ID = "SplashScreen";
89
95
  private static final int SPLASH_SCREEN_RETRY_DELAY_MS = 100;
90
96
  private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
91
97
  private static final long PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS = 30000L;
92
98
 
93
- private final String pluginVersion = "8.45.10";
99
+ private final String pluginVersion = "8.46.0";
94
100
  private static final String DELAY_CONDITION_PREFERENCES = "";
95
101
 
96
102
  private SharedPreferences.Editor editor;
@@ -155,6 +161,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
155
161
  private final Handler mainHandler = new Handler(Looper.getMainLooper());
156
162
  private FrameLayout splashscreenLoaderOverlay;
157
163
  private Runnable splashscreenTimeoutRunnable;
164
+ private WebViewListener webViewStatsListener;
158
165
 
159
166
  private static final class FireAndForgetPluginCall extends PluginCall {
160
167
 
@@ -211,6 +218,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
211
218
  }
212
219
  }
213
220
 
221
+ private boolean shouldNotifyBreakingEvents(final JSObject response) {
222
+ if (response == null) {
223
+ return false;
224
+ }
225
+
226
+ if (response.optBoolean("breaking", false)) {
227
+ return true;
228
+ }
229
+
230
+ final String error = response.optString("error", "");
231
+ final String message = response.optString("message", "");
232
+ return "disable_auto_update_to_major".equals(error) || "store_update_required".equals(message);
233
+ }
234
+
235
+ private void notifyBreakingEventsIfNeeded(final JSObject response, final String version) {
236
+ if (shouldNotifyBreakingEvents(response)) {
237
+ notifyBreakingEvents(version);
238
+ }
239
+ }
240
+
214
241
  private void persistLastFailedBundle(BundleInfo bundle) {
215
242
  if (this.prefs == null) {
216
243
  return;
@@ -427,13 +454,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
427
454
  this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
428
455
  }
429
456
 
430
- int userValue = this.getConfig().getInt("periodCheckDelay", 0);
431
-
432
- if (userValue >= 0 && userValue <= 600) {
433
- this.periodCheckDelay = 600 * 1000;
434
- } else if (userValue > 600) {
435
- this.periodCheckDelay = userValue * 1000;
436
- }
457
+ this.periodCheckDelay = normalizedPeriodCheckDelayMs(this.getConfig().getInt("periodCheckDelay", 0));
437
458
 
438
459
  this.implementation.documentsDir = this.getContext().getFilesDir();
439
460
  this.implementation.prefs = this.prefs;
@@ -483,9 +504,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
483
504
  // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
484
505
  this.wasRecentlyInstalledOrUpdated = this.checkIfRecentlyInstalledOrUpdated();
485
506
 
486
- this.implementation.autoReset();
507
+ this.implementation.autoReset(this.currentBuildVersion, resetWhenUpdate);
508
+ this.reportPreviousAppExitReasons();
509
+ this.reportPreviousWebViewRenderProcessGone();
510
+ this.installWebViewStatsReporter();
487
511
  if (resetWhenUpdate) {
488
512
  this.cleanupObsoleteVersions();
513
+ } else {
514
+ this.persistCurrentNativeBuildVersion();
489
515
  }
490
516
 
491
517
  // Check for 'kill' delay condition on app launch
@@ -811,7 +837,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
811
837
 
812
838
  private boolean checkIfRecentlyInstalledOrUpdated() {
813
839
  String currentVersion = this.currentBuildVersion;
814
- String lastKnownVersion = this.prefs.getString("LatestNativeBuildVersion", "");
840
+ String lastKnownVersion = this.getStoredNativeBuildVersion();
815
841
 
816
842
  if (lastKnownVersion.isEmpty()) {
817
843
  // First time running, consider it as recently installed
@@ -824,6 +850,456 @@ public class CapacitorUpdaterPlugin extends Plugin {
824
850
  return false;
825
851
  }
826
852
 
853
+ private void reportPreviousAppExitReasons() {
854
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || this.implementation == null || this.implementation.statsUrl.isEmpty()) {
855
+ return;
856
+ }
857
+
858
+ try {
859
+ final ActivityManager activityManager = (ActivityManager) this.getContext().getSystemService(Context.ACTIVITY_SERVICE);
860
+ if (activityManager == null) {
861
+ return;
862
+ }
863
+
864
+ final List<ApplicationExitInfo> exitReasons = activityManager.getHistoricalProcessExitReasons(
865
+ this.getContext().getPackageName(),
866
+ 0,
867
+ 8
868
+ );
869
+ if (exitReasons == null || exitReasons.isEmpty()) {
870
+ return;
871
+ }
872
+
873
+ final long lastReportedTimestamp = this.prefs.getLong(LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY, 0L);
874
+ long newestReportedTimestamp = lastReportedTimestamp;
875
+ final BundleInfo current = this.implementation.getCurrentBundle();
876
+ final String versionName = current == null ? "" : current.getVersionName();
877
+
878
+ for (final ApplicationExitInfo exitInfo : exitReasons) {
879
+ if (exitInfo == null || exitInfo.getTimestamp() <= lastReportedTimestamp) {
880
+ continue;
881
+ }
882
+
883
+ final String action = statsActionForApplicationExitReason(exitInfo.getReason());
884
+ if (action == null) {
885
+ continue;
886
+ }
887
+
888
+ this.implementation.sendStats(action, versionName, "", buildApplicationExitMetadata(exitInfo));
889
+ newestReportedTimestamp = Math.max(newestReportedTimestamp, exitInfo.getTimestamp());
890
+ }
891
+
892
+ if (newestReportedTimestamp > lastReportedTimestamp) {
893
+ this.prefs.edit().putLong(LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY, newestReportedTimestamp).apply();
894
+ }
895
+ } catch (final Exception e) {
896
+ logger.warn("Unable to report previous app exit reason: " + e.getMessage());
897
+ }
898
+ }
899
+
900
+ static String statsActionForApplicationExitReason(final int reason) {
901
+ switch (reason) {
902
+ case ApplicationExitInfo.REASON_CRASH:
903
+ return "app_crash";
904
+ case ApplicationExitInfo.REASON_CRASH_NATIVE:
905
+ return "app_crash_native";
906
+ case ApplicationExitInfo.REASON_ANR:
907
+ return "app_anr";
908
+ case ApplicationExitInfo.REASON_LOW_MEMORY:
909
+ return "app_killed_low_memory";
910
+ case ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE:
911
+ return "app_killed_excessive_resource_usage";
912
+ case ApplicationExitInfo.REASON_INITIALIZATION_FAILURE:
913
+ return "app_initialization_failure";
914
+ default:
915
+ return null;
916
+ }
917
+ }
918
+
919
+ static String applicationExitReasonName(final int reason) {
920
+ switch (reason) {
921
+ case ApplicationExitInfo.REASON_EXIT_SELF:
922
+ return "exit_self";
923
+ case ApplicationExitInfo.REASON_SIGNALED:
924
+ return "signaled";
925
+ case ApplicationExitInfo.REASON_LOW_MEMORY:
926
+ return "low_memory";
927
+ case ApplicationExitInfo.REASON_CRASH:
928
+ return "crash";
929
+ case ApplicationExitInfo.REASON_CRASH_NATIVE:
930
+ return "crash_native";
931
+ case ApplicationExitInfo.REASON_ANR:
932
+ return "anr";
933
+ case ApplicationExitInfo.REASON_INITIALIZATION_FAILURE:
934
+ return "initialization_failure";
935
+ case ApplicationExitInfo.REASON_PERMISSION_CHANGE:
936
+ return "permission_change";
937
+ case ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE:
938
+ return "excessive_resource_usage";
939
+ case ApplicationExitInfo.REASON_USER_REQUESTED:
940
+ return "user_requested";
941
+ case ApplicationExitInfo.REASON_DEPENDENCY_DIED:
942
+ return "dependency_died";
943
+ default:
944
+ return "unknown";
945
+ }
946
+ }
947
+
948
+ private static Map<String, String> buildApplicationExitMetadata(final ApplicationExitInfo exitInfo) {
949
+ final Map<String, String> metadata = new HashMap<>();
950
+ metadata.put("exit_reason", applicationExitReasonName(exitInfo.getReason()));
951
+ metadata.put("exit_reason_code", Integer.toString(exitInfo.getReason()));
952
+ metadata.put("exit_status", Integer.toString(exitInfo.getStatus()));
953
+ metadata.put("exit_importance", Integer.toString(exitInfo.getImportance()));
954
+ metadata.put("exit_timestamp", Long.toString(exitInfo.getTimestamp()));
955
+ metadata.put("pid", Integer.toString(exitInfo.getPid()));
956
+ metadata.put("pss_kb", Long.toString(exitInfo.getPss()));
957
+ metadata.put("rss_kb", Long.toString(exitInfo.getRss()));
958
+
959
+ final String processName = exitInfo.getProcessName();
960
+ if (processName != null && !processName.isEmpty()) {
961
+ metadata.put("process_name", truncateStatsMetadataValue(processName, 128));
962
+ }
963
+
964
+ final String description = exitInfo.getDescription();
965
+ if (description != null && !description.isEmpty()) {
966
+ metadata.put("exit_description", truncateStatsMetadataValue(description, 512));
967
+ }
968
+
969
+ return metadata;
970
+ }
971
+
972
+ private static String truncateStatsMetadataValue(final String value, final int maxLength) {
973
+ return value.length() <= maxLength ? value : value.substring(0, maxLength);
974
+ }
975
+
976
+ private void installWebViewStatsReporter() {
977
+ if (this.bridge == null || this.bridge.getWebView() == null || this.webViewStatsListener != null) {
978
+ return;
979
+ }
980
+
981
+ final android.webkit.WebView webView = this.bridge.getWebView();
982
+ final String script = buildWebViewStatsReporterScript();
983
+ this.installDocumentStartWebViewStatsReporter(webView, script);
984
+
985
+ this.webViewStatsListener = new WebViewListener() {
986
+ @Override
987
+ public void onPageStarted(final android.webkit.WebView view) {
988
+ CapacitorUpdaterPlugin.this.evaluateWebViewStatsReporterScript(view, script);
989
+ }
990
+
991
+ @Override
992
+ public void onPageLoaded(final android.webkit.WebView view) {
993
+ CapacitorUpdaterPlugin.this.evaluateWebViewStatsReporterScript(view, script);
994
+ }
995
+
996
+ @Override
997
+ public boolean onRenderProcessGone(final android.webkit.WebView view, final RenderProcessGoneDetail detail) {
998
+ final Map<String, String> metadata = CapacitorUpdaterPlugin.this.buildWebViewRenderProcessGoneMetadata(detail);
999
+ CapacitorUpdaterPlugin.this.persistPendingWebViewRenderProcessGone(metadata);
1000
+ return false;
1001
+ }
1002
+ };
1003
+
1004
+ this.bridge.addWebViewListener(this.webViewStatsListener);
1005
+ this.evaluateWebViewStatsReporterScript(webView, script);
1006
+ }
1007
+
1008
+ private void installDocumentStartWebViewStatsReporter(final android.webkit.WebView webView, final String script) {
1009
+ try {
1010
+ final Class<?> webViewFeature = Class.forName("androidx.webkit.WebViewFeature");
1011
+ final String feature = (String) webViewFeature.getField("DOCUMENT_START_SCRIPT").get(null);
1012
+ final Boolean supported = (Boolean) webViewFeature.getMethod("isFeatureSupported", String.class).invoke(null, feature);
1013
+ if (!Boolean.TRUE.equals(supported)) {
1014
+ return;
1015
+ }
1016
+
1017
+ final String allowedOrigin = Uri.parse(this.bridge.getAppUrl())
1018
+ .buildUpon()
1019
+ .path(null)
1020
+ .fragment(null)
1021
+ .clearQuery()
1022
+ .build()
1023
+ .toString();
1024
+ final Class<?> webViewCompat = Class.forName("androidx.webkit.WebViewCompat");
1025
+ webViewCompat
1026
+ .getMethod("addDocumentStartJavaScript", android.webkit.WebView.class, String.class, Set.class)
1027
+ .invoke(null, webView, script, java.util.Collections.singleton(allowedOrigin));
1028
+ } catch (final Exception e) {
1029
+ logger.debug("Unable to install document-start WebView stats reporter: " + e.getMessage());
1030
+ }
1031
+ }
1032
+
1033
+ private void evaluateWebViewStatsReporterScript(final android.webkit.WebView webView, final String script) {
1034
+ if (webView == null) {
1035
+ return;
1036
+ }
1037
+
1038
+ this.mainHandler.post(() -> {
1039
+ try {
1040
+ webView.evaluateJavascript(script, null);
1041
+ } catch (final Exception e) {
1042
+ logger.debug("Unable to evaluate WebView stats reporter: " + e.getMessage());
1043
+ }
1044
+ });
1045
+ }
1046
+
1047
+ private Map<String, String> buildWebViewRenderProcessGoneMetadata(final RenderProcessGoneDetail detail) {
1048
+ final Map<String, String> metadata = new HashMap<>();
1049
+ metadata.put("error_type", "render_process_gone");
1050
+ metadata.put("source", "android_on_render_process_gone");
1051
+ metadata.put("timestamp", Long.toString(System.currentTimeMillis()));
1052
+ if (detail != null) {
1053
+ metadata.put("did_crash", Boolean.toString(detail.didCrash()));
1054
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1055
+ metadata.put("renderer_priority_at_exit", Integer.toString(detail.rendererPriorityAtExit()));
1056
+ }
1057
+ }
1058
+ return metadata;
1059
+ }
1060
+
1061
+ private void persistPendingWebViewRenderProcessGone(final Map<String, String> metadata) {
1062
+ try {
1063
+ final JSONObject json = new JSONObject(metadata);
1064
+ this.prefs.edit().putString(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY, json.toString()).commit();
1065
+ } catch (final Exception e) {
1066
+ logger.debug("Unable to persist WebView render process crash metadata: " + e.getMessage());
1067
+ }
1068
+ }
1069
+
1070
+ private void reportPreviousWebViewRenderProcessGone() {
1071
+ if (this.implementation == null || this.implementation.statsUrl.isEmpty()) {
1072
+ return;
1073
+ }
1074
+
1075
+ final String rawMetadata = this.prefs.getString(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY, "");
1076
+ if (rawMetadata == null || rawMetadata.isEmpty()) {
1077
+ return;
1078
+ }
1079
+
1080
+ try {
1081
+ final Map<String, String> metadata = jsonObjectToStringMap(new JSONObject(rawMetadata));
1082
+ metadata.put("reported_after_restart", "true");
1083
+ this.reportWebViewStats("webview_render_process_gone", metadata);
1084
+ this.prefs.edit().remove(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY).apply();
1085
+ } catch (final JSONException e) {
1086
+ this.prefs.edit().remove(LAST_WEBVIEW_RENDER_PROCESS_GONE_PREF_KEY).apply();
1087
+ }
1088
+ }
1089
+
1090
+ private static Map<String, String> jsonObjectToStringMap(final JSONObject json) throws JSONException {
1091
+ final Map<String, String> map = new HashMap<>();
1092
+ final JSONArray names = json.names();
1093
+ if (names == null) {
1094
+ return map;
1095
+ }
1096
+
1097
+ for (int i = 0; i < names.length(); i++) {
1098
+ final String key = names.getString(i);
1099
+ final String value = json.optString(key, "");
1100
+ if (!value.isEmpty()) {
1101
+ map.put(key, value);
1102
+ }
1103
+ }
1104
+ return map;
1105
+ }
1106
+
1107
+ @PluginMethod
1108
+ public void reportWebViewError(final PluginCall call) {
1109
+ final JSObject data = call.getData();
1110
+ this.reportWebViewStats(
1111
+ statsActionForWebViewErrorType(data.optString("type", "javascript_error")),
1112
+ buildWebViewErrorMetadata(data)
1113
+ );
1114
+ call.resolve();
1115
+ }
1116
+
1117
+ private void reportWebViewStats(final String action, final Map<String, String> metadata) {
1118
+ if (this.implementation == null) {
1119
+ return;
1120
+ }
1121
+
1122
+ final BundleInfo current = this.implementation.getCurrentBundle();
1123
+ final String versionName = current == null ? "" : current.getVersionName();
1124
+ this.implementation.sendStats(action, versionName, "", metadata);
1125
+ }
1126
+
1127
+ static String statsActionForWebViewErrorType(final String type) {
1128
+ switch (type) {
1129
+ case "unhandled_rejection":
1130
+ return "webview_unhandled_rejection";
1131
+ case "resource_error":
1132
+ return "webview_resource_error";
1133
+ case "security_policy_violation":
1134
+ return "webview_security_policy_violation";
1135
+ case "webview_unclean_restart":
1136
+ return "webview_unclean_restart";
1137
+ case "render_process_gone":
1138
+ return "webview_render_process_gone";
1139
+ case "web_content_process_terminated":
1140
+ return "webview_content_process_terminated";
1141
+ case "javascript_error":
1142
+ default:
1143
+ return "webview_javascript_error";
1144
+ }
1145
+ }
1146
+
1147
+ static Map<String, String> buildWebViewErrorMetadata(final JSObject data) {
1148
+ final Map<String, String> metadata = new HashMap<>();
1149
+ putStatsMetadataValue(metadata, "error_type", data.optString("type", "javascript_error"), 64);
1150
+ putStatsMetadataValue(metadata, "message", data.optString("message", ""), 1024);
1151
+ putStatsMetadataValue(metadata, "source", sanitizeStatsMetadataUrl(data.optString("source", "")), 512);
1152
+ putStatsMetadataValue(metadata, "line", data.optString("line", data.optString("lineno", "")), 32);
1153
+ putStatsMetadataValue(metadata, "column", data.optString("column", data.optString("colno", "")), 32);
1154
+ putStatsMetadataValue(metadata, "stack", data.optString("stack", ""), 2048);
1155
+ putStatsMetadataValue(metadata, "tag_name", data.optString("tag_name", ""), 64);
1156
+ putStatsMetadataValue(metadata, "href", sanitizeStatsMetadataUrl(data.optString("href", "")), 512);
1157
+ putStatsMetadataValue(metadata, "user_agent", data.optString("user_agent", ""), 256);
1158
+ putStatsMetadataValue(metadata, "session_id", data.optString("session_id", ""), 128);
1159
+ putStatsMetadataValue(metadata, "previous_session_id", data.optString("previous_session_id", ""), 128);
1160
+ putStatsMetadataValue(metadata, "previous_href", sanitizeStatsMetadataUrl(data.optString("previous_href", "")), 512);
1161
+ putStatsMetadataValue(metadata, "previous_started_at", data.optString("previous_started_at", ""), 64);
1162
+ putStatsMetadataValue(metadata, "previous_updated_at", data.optString("previous_updated_at", ""), 64);
1163
+ return metadata;
1164
+ }
1165
+
1166
+ private static void putStatsMetadataValue(
1167
+ final Map<String, String> metadata,
1168
+ final String key,
1169
+ final String value,
1170
+ final int maxLength
1171
+ ) {
1172
+ if (value == null || value.isEmpty()) {
1173
+ return;
1174
+ }
1175
+
1176
+ metadata.put(key, truncateStatsMetadataValue(value, maxLength));
1177
+ }
1178
+
1179
+ static String sanitizeStatsMetadataUrl(final String value) {
1180
+ if (value == null || value.isEmpty()) {
1181
+ return "";
1182
+ }
1183
+
1184
+ try {
1185
+ final java.net.URI uri = new java.net.URI(value);
1186
+ if (uri.getScheme() != null && uri.getHost() != null) {
1187
+ final String path = sanitizeStatsMetadataUrlPath(uri.getPath());
1188
+ return new java.net.URI(
1189
+ uri.getScheme(),
1190
+ null,
1191
+ uri.getHost(),
1192
+ uri.getPort(),
1193
+ path.isEmpty() ? null : path,
1194
+ null,
1195
+ null
1196
+ ).toString();
1197
+ }
1198
+ } catch (Exception ignored) {}
1199
+
1200
+ try {
1201
+ final Uri uri = Uri.parse(value);
1202
+ if (uri.getScheme() != null && uri.getHost() != null) {
1203
+ final String host = stripUrlUserInfo(uri.getHost());
1204
+ if (host.isEmpty()) {
1205
+ return stripUrlQueryAndFragment(value);
1206
+ }
1207
+ final StringBuilder authority = new StringBuilder(host);
1208
+ if (uri.getPort() != -1) {
1209
+ authority.append(':').append(uri.getPort());
1210
+ }
1211
+ final Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(authority.toString());
1212
+ final String path = sanitizeStatsMetadataUrlPath(uri.getPath());
1213
+ if (!path.isEmpty()) {
1214
+ builder.path(path);
1215
+ }
1216
+ return builder.build().toString();
1217
+ }
1218
+ } catch (Exception ignored) {}
1219
+
1220
+ return stripUrlQueryAndFragment(value);
1221
+ }
1222
+
1223
+ private static String sanitizeStatsMetadataUrlPath(final String path) {
1224
+ if (path == null || path.isEmpty()) {
1225
+ return "";
1226
+ }
1227
+
1228
+ final String[] segments = path.split("/", -1);
1229
+ for (int index = 0; index < segments.length; index++) {
1230
+ if (isSensitiveUrlPathSegment(segments[index])) {
1231
+ segments[index] = "redacted";
1232
+ }
1233
+ }
1234
+ return String.join("/", segments);
1235
+ }
1236
+
1237
+ private static String stripUrlUserInfo(final String host) {
1238
+ if (host == null || host.isEmpty()) {
1239
+ return "";
1240
+ }
1241
+
1242
+ final int userInfoIndex = host.lastIndexOf('@');
1243
+ if (userInfoIndex < 0) {
1244
+ return host;
1245
+ }
1246
+ return host.substring(userInfoIndex + 1);
1247
+ }
1248
+
1249
+ private static boolean isSensitiveUrlPathSegment(final String segment) {
1250
+ return (
1251
+ segment.matches("[0-9]{6,}") ||
1252
+ segment.matches("[0-9a-fA-F]{16,}") ||
1253
+ segment.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
1254
+ );
1255
+ }
1256
+
1257
+ private static String stripUrlQueryAndFragment(final String value) {
1258
+ int end = value.length();
1259
+ final int queryIndex = value.indexOf('?');
1260
+ final int fragmentIndex = value.indexOf('#');
1261
+ if (queryIndex >= 0) {
1262
+ end = Math.min(end, queryIndex);
1263
+ }
1264
+ if (fragmentIndex >= 0) {
1265
+ end = Math.min(end, fragmentIndex);
1266
+ }
1267
+ return value.substring(0, end);
1268
+ }
1269
+
1270
+ static String buildWebViewStatsReporterScript() {
1271
+ return (
1272
+ "(function(){" +
1273
+ "if(window.__capgoWebViewErrorReporterInstalled){return;}" +
1274
+ "window.__capgoWebViewErrorReporterInstalled=true;" +
1275
+ "var maxReports=20,sentReports=0,queue=[],seen={};" +
1276
+ "var sessionKey='CapacitorUpdater.webViewSession';" +
1277
+ "var sessionId=String(Date.now())+'-'+Math.random().toString(36).slice(2);" +
1278
+ "function s(value){try{if(value===undefined){return '';}if(value===null){return 'null';}if(typeof value==='string'){return value;}if(value&&typeof value.message==='string'){return value.message;}return String(value);}catch(_){return '';}}" +
1279
+ "function stack(value){try{return value&&value.stack?String(value.stack):'';}catch(_){return '';}}" +
1280
+ "function updater(){var cap=window.Capacitor;if(!cap||!cap.Plugins){return null;}return cap.Plugins.CapacitorUpdater||null;}" +
1281
+ "function flush(){var plugin=updater();if(!plugin||typeof plugin.reportWebViewError!=='function'){return false;}while(queue.length){var payload=queue.shift();try{var result=plugin.reportWebViewError(payload);if(result&&typeof result.catch==='function'){result.catch(function(){});}}catch(_){}}return true;}" +
1282
+ "var retries=0;function scheduleFlush(){if(flush()){return;}if(retries++<40){setTimeout(scheduleFlush,250);}}" +
1283
+ "function send(payload){try{if(sentReports>=maxReports){return;}payload.href=payload.href||location.href||'';payload.user_agent=navigator.userAgent||'';payload.session_id=sessionId;var key=[payload.type,payload.message,payload.source,payload.line,payload.column,payload.tag_name].join('|');if(seen[key]){return;}seen[key]=true;sentReports+=1;queue.push(payload);scheduleFlush();}catch(_){}}" +
1284
+ "function readSession(){try{return JSON.parse(localStorage.getItem(sessionKey)||'null')||null;}catch(_){return null;}}" +
1285
+ "function writeSession(active){try{localStorage.setItem(sessionKey,JSON.stringify({id:sessionId,active:active,href:location.href||'',started_at:window.__capgoWebViewSessionStartedAt,updated_at:String(Date.now())}));}catch(_){}}" +
1286
+ "window.__capgoWebViewSessionStartedAt=String(Date.now());" +
1287
+ "var previous=readSession();" +
1288
+ "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)});}" +
1289
+ "writeSession(true);" +
1290
+ "setInterval(function(){writeSession(true);},15000);" +
1291
+ "function markClean(){writeSession(false);}" +
1292
+ "window.addEventListener('pagehide',markClean,true);" +
1293
+ "window.addEventListener('beforeunload',markClean,true);" +
1294
+ "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);" +
1295
+ "window.addEventListener('unhandledrejection',function(event){var reason=event&&event.reason;send({type:'unhandled_rejection',message:s(reason),stack:stack(reason)});},true);" +
1296
+ "document.addEventListener('securitypolicyviolation',function(event){send({type:'security_policy_violation',message:s(event&&event.violatedDirective),source:s(event&&event.blockedURI)});},true);" +
1297
+ "document.addEventListener('deviceready',scheduleFlush,false);" +
1298
+ "setTimeout(scheduleFlush,0);" +
1299
+ "})();"
1300
+ );
1301
+ }
1302
+
827
1303
  private boolean shouldUseDirectUpdate() {
828
1304
  if (Boolean.TRUE.equals(this.autoSplashscreenTimedOut)) {
829
1305
  return false;
@@ -863,6 +1339,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
863
1339
  return plannedDirectUpdate && "onLaunch".equals(directUpdateMode);
864
1340
  }
865
1341
 
1342
+ static int normalizedPeriodCheckDelayMs(final int valueSeconds) {
1343
+ final int normalizedSeconds = normalizedPeriodCheckDelaySeconds(valueSeconds);
1344
+ if (normalizedSeconds <= 0) {
1345
+ return 0;
1346
+ }
1347
+ final long delayMs = (long) normalizedSeconds * 1000L;
1348
+ return delayMs > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) delayMs;
1349
+ }
1350
+
1351
+ static int normalizedPeriodCheckDelaySeconds(final int valueSeconds) {
1352
+ if (valueSeconds <= 0) {
1353
+ return 0;
1354
+ }
1355
+ return Math.max(600, valueSeconds);
1356
+ }
1357
+
866
1358
  private void consumeOnLaunchDirectUpdateAttempt(final boolean plannedDirectUpdate) {
867
1359
  if (!shouldConsumeOnLaunchDirectUpdate(this.directUpdateMode, plannedDirectUpdate)) {
868
1360
  return;
@@ -933,7 +1425,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
933
1425
  cleanupThread = startNewThread(() -> {
934
1426
  synchronized (cleanupLock) {
935
1427
  try {
936
- final String previous = this.prefs.getString("LatestNativeBuildVersion", "");
1428
+ final String previous = this.getStoredNativeBuildVersion();
937
1429
  if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
938
1430
  logger.info("New native build version detected: " + this.currentBuildVersion);
939
1431
  this.implementation.reset(true);
@@ -994,6 +1486,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
994
1486
  });
995
1487
  }
996
1488
 
1489
+ String getStoredNativeBuildVersion() {
1490
+ String previous = this.prefs.getString("LatestNativeBuildVersion", "");
1491
+ if (previous == null || previous.isEmpty()) {
1492
+ previous = this.prefs.getString("LatestVersionNative", "");
1493
+ }
1494
+ return previous == null ? "" : previous;
1495
+ }
1496
+
1497
+ void persistCurrentNativeBuildVersion() {
1498
+ this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
1499
+ this.editor.apply();
1500
+ }
1501
+
997
1502
  private void waitForCleanupIfNeeded() {
998
1503
  if (cleanupComplete) {
999
1504
  return; // Already done, no need to wait
@@ -1293,6 +1798,23 @@ public class CapacitorUpdaterPlugin extends Plugin {
1293
1798
  startNewThread(() ->
1294
1799
  CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
1295
1800
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1801
+ Object channels = res.get("channels");
1802
+ if (channels instanceof List<?> channelsList) {
1803
+ JSArray channelsArray = new JSArray();
1804
+ for (Object channel : channelsList) {
1805
+ if (channel instanceof Map<?, ?> channelMap) {
1806
+ JSObject channelObject = new JSObject();
1807
+ for (Map.Entry<?, ?> entry : channelMap.entrySet()) {
1808
+ Object key = entry.getKey();
1809
+ if (key != null) {
1810
+ channelObject.put(key.toString(), entry.getValue());
1811
+ }
1812
+ }
1813
+ channelsArray.put(channelObject);
1814
+ }
1815
+ }
1816
+ jsRes.put("channels", channelsArray);
1817
+ }
1296
1818
  if (jsRes.has("error")) {
1297
1819
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1298
1820
  String errorCode = jsRes.getString("error");
@@ -1582,23 +2104,25 @@ public class CapacitorUpdaterPlugin extends Plugin {
1582
2104
  call.reject("Set called without id");
1583
2105
  return;
1584
2106
  }
1585
- try {
1586
- logger.info("Setting active bundle " + id);
1587
- if (!this.implementation.set(id)) {
1588
- logger.info("No such bundle " + id);
1589
- call.reject("Update failed, id " + id + " does not exist.");
1590
- } else if (!this._reload()) {
1591
- logger.error("Reload failed after setting bundle " + id);
1592
- call.reject("Reload failed after setting bundle " + id);
1593
- } else {
1594
- logger.info("Bundle successfully set to " + id);
1595
- this.notifyBundleSet(this.implementation.getBundleInfo(id));
1596
- call.resolve();
2107
+ startNewThread(() -> {
2108
+ try {
2109
+ logger.info("Setting active bundle " + id);
2110
+ if (!this.implementation.set(id)) {
2111
+ logger.info("No such bundle " + id);
2112
+ call.reject("Update failed, id " + id + " does not exist.");
2113
+ } else if (!this._reload()) {
2114
+ logger.error("Reload failed after setting bundle " + id);
2115
+ call.reject("Reload failed after setting bundle " + id);
2116
+ } else {
2117
+ logger.info("Bundle successfully set to " + id);
2118
+ this.notifyBundleSet(this.implementation.getBundleInfo(id));
2119
+ call.resolve();
2120
+ }
2121
+ } catch (final Exception e) {
2122
+ logger.error("Could not set id " + id + " " + e.getMessage());
2123
+ call.reject("Could not set id " + id, e);
1597
2124
  }
1598
- } catch (final Exception e) {
1599
- logger.error("Could not set id " + id + " " + e.getMessage());
1600
- call.reject("Could not set id " + id, e);
1601
- }
2125
+ });
1602
2126
  }
1603
2127
 
1604
2128
  @PluginMethod
@@ -1685,13 +2209,27 @@ public class CapacitorUpdaterPlugin extends Plugin {
1685
2209
  startNewThread(() ->
1686
2210
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
1687
2211
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1688
- if (jsRes.has("error")) {
1689
- String error = jsRes.getString("error");
2212
+ if (jsRes.has("error") || jsRes.has("kind")) {
2213
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1690
2214
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1691
- logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1692
- call.reject(jsRes.getString("error"));
2215
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
2216
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
2217
+ jsRes.put("kind", kind);
2218
+ CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
2219
+ if ("failed".equals(kind)) {
2220
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
2221
+ call.reject(error.isEmpty() ? errorMessage : error);
2222
+ } else {
2223
+ if (!jsRes.has("version") || jsRes.getString("version").isEmpty()) {
2224
+ jsRes.put("version", CapacitorUpdaterPlugin.this.implementation.getCurrentBundle().getVersionName());
2225
+ }
2226
+ logger.info("getLatest returned " + kind + ": " + errorMessage);
2227
+ call.resolve(jsRes);
2228
+ }
1693
2229
  return;
1694
2230
  } else if (jsRes.has("message")) {
2231
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
2232
+ CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
1695
2233
  call.reject(jsRes.getString("message"));
1696
2234
  return;
1697
2235
  } else {
@@ -1701,6 +2239,28 @@ public class CapacitorUpdaterPlugin extends Plugin {
1701
2239
  );
1702
2240
  }
1703
2241
 
2242
+ public String triggerBackgroundUpdateCheck() {
2243
+ if (this.updateUrl == null || this.updateUrl.isEmpty() || !this.isValidURL(this.updateUrl)) {
2244
+ logger.error("Error no url or wrong format");
2245
+ return "unavailable";
2246
+ }
2247
+ if (this.isDownloadStuckOrTimedOut()) {
2248
+ logger.info("Download already in progress, skipping duplicate download request");
2249
+ return "already_running";
2250
+ }
2251
+ this.backgroundDownload();
2252
+ return "queued";
2253
+ }
2254
+
2255
+ @PluginMethod
2256
+ public void triggerUpdateCheck(final PluginCall call) {
2257
+ final String status = this.triggerBackgroundUpdateCheck();
2258
+ final JSObject ret = new JSObject();
2259
+ ret.put("status", status);
2260
+ ret.put("queued", "queued".equals(status));
2261
+ call.resolve(ret);
2262
+ }
2263
+
1704
2264
  private boolean _reset(final Boolean toLastSuccessful, final Boolean usePendingBundle) {
1705
2265
  return this.performReset(toLastSuccessful, usePendingBundle, false);
1706
2266
  }
@@ -1774,19 +2334,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
1774
2334
 
1775
2335
  @PluginMethod
1776
2336
  public void reset(final PluginCall call) {
1777
- try {
1778
- final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
1779
- final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
1780
- if (this._reset(toLastSuccessful, usePendingBundle)) {
1781
- call.resolve();
1782
- return;
2337
+ startNewThread(() -> {
2338
+ try {
2339
+ final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
2340
+ final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
2341
+ if (this._reset(toLastSuccessful, usePendingBundle)) {
2342
+ call.resolve();
2343
+ return;
2344
+ }
2345
+ logger.error("Reset failed");
2346
+ call.reject("Reset failed");
2347
+ } catch (final Exception e) {
2348
+ logger.error("Reset failed " + e.getMessage());
2349
+ call.reject("Reset failed", e);
1783
2350
  }
1784
- logger.error("Reset failed");
1785
- call.reject("Reset failed");
1786
- } catch (final Exception e) {
1787
- logger.error("Reset failed " + e.getMessage());
1788
- call.reject("Reset failed", e);
1789
- }
2351
+ });
1790
2352
  }
1791
2353
 
1792
2354
  @PluginMethod
@@ -1852,12 +2414,33 @@ public class CapacitorUpdaterPlugin extends Plugin {
1852
2414
  try {
1853
2415
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1854
2416
  JSObject jsRes = InternalUtils.mapToJSObject(res);
1855
- if (jsRes.has("error")) {
1856
- String error = jsRes.getString("error");
2417
+ if (jsRes.has("error") || jsRes.has("kind")) {
2418
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2419
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1857
2420
  String errorMessage = jsRes.has("message")
1858
2421
  ? jsRes.getString("message")
1859
2422
  : "server did not provide a message";
1860
- logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
2423
+ int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
2424
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(
2425
+ jsRes.has("kind") ? jsRes.getString("kind") : null
2426
+ );
2427
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
2428
+ CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
2429
+ kind,
2430
+ error,
2431
+ errorMessage,
2432
+ statusCode,
2433
+ latestVersion,
2434
+ current
2435
+ );
2436
+
2437
+ if ("failed".equals(kind)) {
2438
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
2439
+ } else if ("blocked".equals(kind)) {
2440
+ logger.info("Update check blocked with error: " + error);
2441
+ } else {
2442
+ logger.info("No new version available");
2443
+ }
1861
2444
  } else if (jsRes.has("version")) {
1862
2445
  String newVersion = jsRes.getString("version");
1863
2446
  String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
@@ -1982,10 +2565,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
1982
2565
  this.checkAppReady(this.resolveAppReadyCheckTimeoutMs());
1983
2566
  }
1984
2567
 
2568
+ synchronized boolean shouldInterruptAppReadyCheck(final Thread existingCheck, final Thread currentThread) {
2569
+ return existingCheck != null && existingCheck != currentThread;
2570
+ }
2571
+
2572
+ synchronized void clearAppReadyCheckIfCurrent(final Thread expectedThread) {
2573
+ if (this.appReadyCheck == expectedThread) {
2574
+ this.appReadyCheck = null;
2575
+ }
2576
+ }
2577
+
1985
2578
  private void checkAppReady(final long waitTimeMs) {
1986
2579
  try {
1987
- if (this.appReadyCheck != null) {
1988
- this.appReadyCheck.interrupt();
2580
+ final Thread currentThread = Thread.currentThread();
2581
+ final Thread existingCheck = this.appReadyCheck;
2582
+ if (this.shouldInterruptAppReadyCheck(existingCheck, currentThread)) {
2583
+ existingCheck.interrupt();
1989
2584
  }
1990
2585
  this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck(waitTimeMs));
1991
2586
  } catch (final Exception e) {
@@ -2002,6 +2597,35 @@ public class CapacitorUpdaterPlugin extends Plugin {
2002
2597
  }
2003
2598
  }
2004
2599
 
2600
+ static String normalizedUpdateResponseKind(final String kind) {
2601
+ if ("up_to_date".equals(kind) || "blocked".equals(kind) || "failed".equals(kind)) {
2602
+ return kind;
2603
+ }
2604
+ return "failed";
2605
+ }
2606
+
2607
+ private String getUpdateResponseKind(final String kind) {
2608
+ return normalizedUpdateResponseKind(kind);
2609
+ }
2610
+
2611
+ private void notifyUpdateCheckResult(
2612
+ final String kind,
2613
+ final String error,
2614
+ final String message,
2615
+ final int statusCode,
2616
+ final String version,
2617
+ final BundleInfo current
2618
+ ) {
2619
+ JSObject ret = new JSObject();
2620
+ ret.put("kind", kind);
2621
+ ret.put("error", error);
2622
+ ret.put("message", message);
2623
+ ret.put("statusCode", statusCode);
2624
+ ret.put("version", version);
2625
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
2626
+ this.notifyListeners("updateCheckResult", ret);
2627
+ }
2628
+
2005
2629
  private void ensureBridgeSet() {
2006
2630
  if (this.bridge != null && this.bridge.getWebView() != null) {
2007
2631
  logger.setBridge(this.bridge);
@@ -2111,30 +2735,41 @@ public class CapacitorUpdaterPlugin extends Plugin {
2111
2735
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2112
2736
 
2113
2737
  // Handle network errors and other failures first
2114
- if (jsRes.has("error")) {
2115
- String error = jsRes.getString("error");
2738
+ if (jsRes.has("error") || jsRes.has("kind")) {
2739
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
2116
2740
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
2117
2741
  int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
2118
- boolean responseIsOk = statusCode >= 200 && statusCode < 300;
2119
-
2120
- logger.error(
2121
- "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
2122
- );
2742
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
2123
2743
  String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
2744
+ CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(kind, error, errorMessage, statusCode, latestVersion, current);
2745
+ CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(
2746
+ jsRes,
2747
+ jsRes.has("version") ? jsRes.getString("version") : ""
2748
+ );
2124
2749
 
2750
+ if ("up_to_date".equals(kind)) {
2751
+ logger.info("No new version available");
2752
+ } else if ("blocked".equals(kind)) {
2753
+ logger.info("Update check blocked with error: " + error);
2754
+ } else {
2755
+ logger.error(
2756
+ "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
2757
+ );
2758
+ }
2759
+
2760
+ boolean isFailure = "failed".equals(kind);
2125
2761
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2126
2762
  errorMessage,
2127
2763
  latestVersion,
2128
2764
  current,
2129
- true,
2765
+ isFailure,
2130
2766
  plannedDirectUpdate,
2131
2767
  "download_fail",
2132
2768
  "downloadFailed",
2133
- !responseIsOk
2769
+ isFailure
2134
2770
  );
2135
2771
  return;
2136
2772
  }
2137
-
2138
2773
  try {
2139
2774
  final String latestVersionName = jsRes.getString("version");
2140
2775
 
@@ -2173,6 +2808,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2173
2808
  }
2174
2809
 
2175
2810
  if (!jsRes.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(jsRes.getString("url"))) {
2811
+ CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersionName);
2176
2812
  logger.error("Error no url or wrong format");
2177
2813
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2178
2814
  "Error no url or wrong format",
@@ -2440,12 +3076,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
2440
3076
 
2441
3077
  @Override
2442
3078
  public void run() {
3079
+ final Thread currentThread = Thread.currentThread();
2443
3080
  try {
2444
3081
  logger.info("Wait for " + this.waitTimeMs + "ms, then check for notifyAppReady");
2445
3082
  Thread.sleep(this.waitTimeMs);
2446
3083
  CapacitorUpdaterPlugin.this.checkRevert();
2447
- CapacitorUpdaterPlugin.this.appReadyCheck = null;
3084
+ CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
2448
3085
  } catch (final InterruptedException e) {
3086
+ CapacitorUpdaterPlugin.this.clearAppReadyCheckIfCurrent(currentThread);
2449
3087
  logger.info(DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
2450
3088
  }
2451
3089
  }
@@ -3077,6 +3715,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
3077
3715
  logger.error("Failed to clean up AppLifecycleObserver: " + e.getMessage());
3078
3716
  }
3079
3717
  }
3718
+
3719
+ if (webViewStatsListener != null && bridge != null) {
3720
+ bridge.removeWebViewListener(webViewStatsListener);
3721
+ webViewStatsListener = null;
3722
+ }
3080
3723
  } catch (Exception e) {
3081
3724
  logger.error("Failed to run handleOnDestroy: " + e.getMessage());
3082
3725
  }