@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.
- package/Package.swift +1 -1
- package/README.md +114 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +703 -60
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +85 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +27 -3
- package/dist/docs.json +213 -5
- package/dist/esm/definitions.d.ts +128 -19
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +4 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +4 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +4 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AppHealthTracker.swift +82 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +353 -100
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +681 -300
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +32 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +20 -3
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +12 -2
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1692
|
-
|
|
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
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1988
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2765
|
+
isFailure,
|
|
2130
2766
|
plannedDirectUpdate,
|
|
2131
2767
|
"download_fail",
|
|
2132
2768
|
"downloadFailed",
|
|
2133
|
-
|
|
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.
|
|
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
|
}
|