@capgo/capacitor-updater 8.46.1 → 8.47.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,8 +7,7 @@
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
+ import android.app.AlertDialog;
12
11
  import android.content.Context;
13
12
  import android.content.Intent;
14
13
  import android.content.SharedPreferences;
@@ -87,6 +86,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
87
86
  private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
88
87
  private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
89
88
  private static final String DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.defaultChannel";
89
+ private static final String PREVIEW_SESSION_PREF_KEY = "CapacitorUpdater.previewSession";
90
+ private static final String PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY = "CapacitorUpdater.previewPreviousShakeMenu";
91
+ private static final String PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY = "CapacitorUpdater.previewPreviousShakeChannelSelector";
92
+ private static final String PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY = "CapacitorUpdater.previewPreviousNextBundle";
93
+ private static final String PREVIEW_PREVIOUS_APP_ID_PREF_KEY = "CapacitorUpdater.previewPreviousAppId";
94
+ private static final String PREVIEW_APP_ID_PREF_KEY = "CapacitorUpdater.previewAppId";
90
95
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
91
96
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
92
97
  private static final String LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY = "CapacitorUpdater.lastReportedAppExitTimestamp";
@@ -95,8 +100,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
95
100
  private static final int SPLASH_SCREEN_RETRY_DELAY_MS = 100;
96
101
  private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
97
102
  private static final long PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS = 30000L;
98
-
99
- private final String pluginVersion = "8.46.1";
103
+ static final int APPLICATION_EXIT_REASON_UNKNOWN = 0;
104
+ static final int APPLICATION_EXIT_REASON_EXIT_SELF = 1;
105
+ static final int APPLICATION_EXIT_REASON_SIGNALED = 2;
106
+ static final int APPLICATION_EXIT_REASON_LOW_MEMORY = 3;
107
+ static final int APPLICATION_EXIT_REASON_CRASH = 4;
108
+ static final int APPLICATION_EXIT_REASON_CRASH_NATIVE = 5;
109
+ static final int APPLICATION_EXIT_REASON_ANR = 6;
110
+ static final int APPLICATION_EXIT_REASON_INITIALIZATION_FAILURE = 7;
111
+ static final int APPLICATION_EXIT_REASON_PERMISSION_CHANGE = 8;
112
+ static final int APPLICATION_EXIT_REASON_EXCESSIVE_RESOURCE_USAGE = 9;
113
+ static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
114
+ static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
115
+
116
+ private final String pluginVersion = "8.47.0";
100
117
  private static final String DELAY_CONDITION_PREFERENCES = "";
101
118
 
102
119
  private SharedPreferences.Editor editor;
@@ -126,7 +143,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
126
143
  private volatile boolean onLaunchDirectUpdateUsed = false;
127
144
  Boolean shakeMenuEnabled = false;
128
145
  Boolean shakeChannelSelectorEnabled = false;
146
+ Boolean previewSessionEnabled = false;
147
+ private Boolean previewSessionAlertPending = false;
129
148
  private Boolean allowManualBundleError = false;
149
+ private Boolean allowPreview = false;
130
150
  Boolean allowSetDefaultChannel = true;
131
151
 
132
152
  String getUpdateUrl() {
@@ -411,6 +431,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
411
431
  "appId is missing in capacitor.config.json or plugin config, and cannot be retrieved from the native app, please add it globally or in the plugin config"
412
432
  );
413
433
  }
434
+ this.allowPreview = this.getConfig().getBoolean("allowPreview", false);
414
435
  logger.info("appId: " + implementation.appId);
415
436
 
416
437
  this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
@@ -499,12 +520,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
499
520
  this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
500
521
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
501
522
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
523
+ this.previewSessionEnabled = Boolean.TRUE.equals(this.allowPreview) && this.prefs.getBoolean(PREVIEW_SESSION_PREF_KEY, false);
524
+ if (!Boolean.TRUE.equals(this.allowPreview) && this.prefs.getBoolean(PREVIEW_SESSION_PREF_KEY, false)) {
525
+ this.clearPreviewSessionBecauseDisabled();
526
+ }
527
+ this.implementation.previewSession = Boolean.TRUE.equals(this.previewSessionEnabled);
528
+ if (Boolean.TRUE.equals(this.previewSessionEnabled)) {
529
+ final String previewAppId = this.prefs.getString(PREVIEW_APP_ID_PREF_KEY, "");
530
+ if (previewAppId != null && !previewAppId.isEmpty()) {
531
+ this.setActiveAppId(previewAppId);
532
+ logger.info("Using preview appId " + previewAppId);
533
+ }
534
+ this.shakeMenuEnabled = true;
535
+ this.shakeChannelSelectorEnabled = false;
536
+ }
502
537
  boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
503
538
 
504
539
  // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
505
540
  this.wasRecentlyInstalledOrUpdated = this.checkIfRecentlyInstalledOrUpdated();
541
+ final boolean nativeBuildVersionChanged = this.hasNativeBuildVersionChanged();
506
542
 
507
543
  this.implementation.autoReset(this.currentBuildVersion, resetWhenUpdate);
544
+ if (nativeBuildVersionChanged) {
545
+ this.clearPreviewSessionForNativeBuildChange();
546
+ }
508
547
  this.reportPreviousAppExitReasons();
509
548
  this.reportPreviousWebViewRenderProcessGone();
510
549
  this.installWebViewStatsReporter();
@@ -850,66 +889,38 @@ public class CapacitorUpdaterPlugin extends Plugin {
850
889
  return false;
851
890
  }
852
891
 
892
+ private boolean hasNativeBuildVersionChanged() {
893
+ final String lastKnownVersion = this.getStoredNativeBuildVersion();
894
+ return !lastKnownVersion.isEmpty() && !lastKnownVersion.equals(this.currentBuildVersion);
895
+ }
896
+
853
897
  private void reportPreviousAppExitReasons() {
854
898
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || this.implementation == null || this.implementation.statsUrl.isEmpty()) {
855
899
  return;
856
900
  }
857
901
 
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
- }
902
+ AndroidAppExitReporter.reportPreviousAppExitReasons(
903
+ this.getContext(),
904
+ this.prefs,
905
+ this.implementation,
906
+ this.logger,
907
+ LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY
908
+ );
898
909
  }
899
910
 
900
911
  static String statsActionForApplicationExitReason(final int reason) {
901
912
  switch (reason) {
902
- case ApplicationExitInfo.REASON_CRASH:
913
+ case APPLICATION_EXIT_REASON_CRASH:
903
914
  return "app_crash";
904
- case ApplicationExitInfo.REASON_CRASH_NATIVE:
915
+ case APPLICATION_EXIT_REASON_CRASH_NATIVE:
905
916
  return "app_crash_native";
906
- case ApplicationExitInfo.REASON_ANR:
917
+ case APPLICATION_EXIT_REASON_ANR:
907
918
  return "app_anr";
908
- case ApplicationExitInfo.REASON_LOW_MEMORY:
919
+ case APPLICATION_EXIT_REASON_LOW_MEMORY:
909
920
  return "app_killed_low_memory";
910
- case ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE:
921
+ case APPLICATION_EXIT_REASON_EXCESSIVE_RESOURCE_USAGE:
911
922
  return "app_killed_excessive_resource_usage";
912
- case ApplicationExitInfo.REASON_INITIALIZATION_FAILURE:
923
+ case APPLICATION_EXIT_REASON_INITIALIZATION_FAILURE:
913
924
  return "app_initialization_failure";
914
925
  default:
915
926
  return null;
@@ -918,58 +929,34 @@ public class CapacitorUpdaterPlugin extends Plugin {
918
929
 
919
930
  static String applicationExitReasonName(final int reason) {
920
931
  switch (reason) {
921
- case ApplicationExitInfo.REASON_EXIT_SELF:
932
+ case APPLICATION_EXIT_REASON_EXIT_SELF:
922
933
  return "exit_self";
923
- case ApplicationExitInfo.REASON_SIGNALED:
934
+ case APPLICATION_EXIT_REASON_SIGNALED:
924
935
  return "signaled";
925
- case ApplicationExitInfo.REASON_LOW_MEMORY:
936
+ case APPLICATION_EXIT_REASON_LOW_MEMORY:
926
937
  return "low_memory";
927
- case ApplicationExitInfo.REASON_CRASH:
938
+ case APPLICATION_EXIT_REASON_CRASH:
928
939
  return "crash";
929
- case ApplicationExitInfo.REASON_CRASH_NATIVE:
940
+ case APPLICATION_EXIT_REASON_CRASH_NATIVE:
930
941
  return "crash_native";
931
- case ApplicationExitInfo.REASON_ANR:
942
+ case APPLICATION_EXIT_REASON_ANR:
932
943
  return "anr";
933
- case ApplicationExitInfo.REASON_INITIALIZATION_FAILURE:
944
+ case APPLICATION_EXIT_REASON_INITIALIZATION_FAILURE:
934
945
  return "initialization_failure";
935
- case ApplicationExitInfo.REASON_PERMISSION_CHANGE:
946
+ case APPLICATION_EXIT_REASON_PERMISSION_CHANGE:
936
947
  return "permission_change";
937
- case ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE:
948
+ case APPLICATION_EXIT_REASON_EXCESSIVE_RESOURCE_USAGE:
938
949
  return "excessive_resource_usage";
939
- case ApplicationExitInfo.REASON_USER_REQUESTED:
950
+ case APPLICATION_EXIT_REASON_USER_REQUESTED:
940
951
  return "user_requested";
941
- case ApplicationExitInfo.REASON_DEPENDENCY_DIED:
952
+ case APPLICATION_EXIT_REASON_DEPENDENCY_DIED:
942
953
  return "dependency_died";
943
954
  default:
944
955
  return "unknown";
945
956
  }
946
957
  }
947
958
 
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) {
959
+ static String truncateStatsMetadataValue(final String value, final int maxLength) {
973
960
  return value.length() <= maxLength ? value : value.substring(0, maxLength);
974
961
  }
975
962
 
@@ -1104,6 +1091,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
1104
1091
  return map;
1105
1092
  }
1106
1093
 
1094
+ private static JSObject jsonObjectToJSObject(final JSONObject json) throws JSONException {
1095
+ final JSObject ret = new JSObject();
1096
+ final JSONArray names = json.names();
1097
+ if (names == null) {
1098
+ return ret;
1099
+ }
1100
+ for (int i = 0; i < names.length(); i++) {
1101
+ final String key = names.getString(i);
1102
+ ret.put(key, json.get(key));
1103
+ }
1104
+ return ret;
1105
+ }
1106
+
1107
1107
  @PluginMethod
1108
1108
  public void reportWebViewError(final PluginCall call) {
1109
1109
  final JSObject data = call.getData();
@@ -2051,6 +2051,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2051
2051
  }
2052
2052
  this.notifyBundleSet(next);
2053
2053
  this.implementation.setNextBundle(null);
2054
+ this.showPreviewSessionNoticeIfNeeded();
2054
2055
  call.resolve();
2055
2056
  return;
2056
2057
  }
@@ -2062,6 +2063,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2062
2063
  }
2063
2064
 
2064
2065
  if (this._reload()) {
2066
+ this.showPreviewSessionNoticeIfNeeded();
2065
2067
  call.resolve();
2066
2068
  } else {
2067
2069
  logger.error("Reload failed");
@@ -2116,6 +2118,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2116
2118
  } else {
2117
2119
  logger.info("Bundle successfully set to " + id);
2118
2120
  this.notifyBundleSet(this.implementation.getBundleInfo(id));
2121
+ this.showPreviewSessionNoticeIfNeeded();
2119
2122
  call.resolve();
2120
2123
  }
2121
2124
  } catch (final Exception e) {
@@ -2125,6 +2128,308 @@ public class CapacitorUpdaterPlugin extends Plugin {
2125
2128
  });
2126
2129
  }
2127
2130
 
2131
+ @PluginMethod
2132
+ public void startPreviewSession(final PluginCall call) {
2133
+ if (!Boolean.TRUE.equals(this.allowPreview)) {
2134
+ logger.error("startPreviewSession not allowed set allowPreview in your config to true to enable it");
2135
+ call.reject("startPreviewSession not allowed");
2136
+ return;
2137
+ }
2138
+ final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
2139
+ startNewThread(() -> {
2140
+ try {
2141
+ if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
2142
+ final BundleInfo current = this.implementation.getCurrentBundle();
2143
+ if (!this.implementation.setPreviewFallbackBundle(current.getId())) {
2144
+ logger.error("Could not save current bundle as preview fallback");
2145
+ call.reject("Could not save current bundle as preview fallback");
2146
+ return;
2147
+ }
2148
+
2149
+ final BundleInfo previousNext = this.implementation.getNextBundle();
2150
+ if (previousNext == null || previousNext.isDeleted() || previousNext.isErrorStatus()) {
2151
+ this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
2152
+ } else {
2153
+ this.editor.putString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, previousNext.getId());
2154
+ }
2155
+
2156
+ this.editor.putString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, this.implementation.appId);
2157
+ this.editor.putBoolean(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY, Boolean.TRUE.equals(this.shakeMenuEnabled));
2158
+ this.editor.putBoolean(
2159
+ PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY,
2160
+ Boolean.TRUE.equals(this.shakeChannelSelectorEnabled)
2161
+ );
2162
+ logger.info("Preview session started with fallback bundle: " + current);
2163
+ }
2164
+
2165
+ if (previewAppId != null) {
2166
+ this.setActiveAppId(previewAppId);
2167
+ this.editor.putString(PREVIEW_APP_ID_PREF_KEY, previewAppId);
2168
+ logger.info("Preview session using appId: " + previewAppId);
2169
+ }
2170
+
2171
+ this.previewSessionEnabled = true;
2172
+ this.previewSessionAlertPending = true;
2173
+ this.implementation.previewSession = true;
2174
+ this.shakeMenuEnabled = true;
2175
+ this.shakeChannelSelectorEnabled = false;
2176
+ this.editor.putBoolean(PREVIEW_SESSION_PREF_KEY, true);
2177
+ this.editor.apply();
2178
+ this.ensureShakeMenuStarted();
2179
+ call.resolve();
2180
+ } catch (final Exception e) {
2181
+ logger.error("Could not start preview session " + e.getMessage());
2182
+ call.reject("Could not start preview session", e);
2183
+ }
2184
+ });
2185
+ }
2186
+
2187
+ public boolean leavePreviewSessionFromShakeMenu() {
2188
+ final BundleInfo previewBundle = this.implementation.getCurrentBundle();
2189
+
2190
+ final boolean didReset = this.resetToPreviewFallbackBundle();
2191
+ if (!didReset) {
2192
+ return false;
2193
+ }
2194
+
2195
+ if (!this.clearPreviewChannelOverride()) {
2196
+ return false;
2197
+ }
2198
+ final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2199
+ this.endPreviewSession();
2200
+ final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
2201
+ if (
2202
+ !previewBundle.isBuiltin() &&
2203
+ (previewFallbackBundle == null || !previewBundle.getId().equals(previewFallbackBundle.getId())) &&
2204
+ (restoredNextBundle == null || !previewBundle.getId().equals(restoredNextBundle.getId()))
2205
+ ) {
2206
+ try {
2207
+ this.implementation.delete(previewBundle.getId(), false);
2208
+ } catch (final Exception err) {
2209
+ logger.warn("Cannot delete preview bundle " + previewBundle.getId() + ": " + err.getMessage());
2210
+ }
2211
+ }
2212
+ return true;
2213
+ }
2214
+
2215
+ public boolean reloadPreviewSessionFromShakeMenu() {
2216
+ return this._reload();
2217
+ }
2218
+
2219
+ public boolean hasActivePreviewSession() {
2220
+ return Boolean.TRUE.equals(this.previewSessionEnabled);
2221
+ }
2222
+
2223
+ private boolean resetToPreviewFallbackBundle() {
2224
+ final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
2225
+ if (fallback == null || fallback.isErrorStatus()) {
2226
+ logger.error("No preview fallback bundle available");
2227
+ return false;
2228
+ }
2229
+ if (!this.implementation.canSet(fallback)) {
2230
+ logger.error("Preview fallback bundle is not installable");
2231
+ return false;
2232
+ }
2233
+
2234
+ final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
2235
+ final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
2236
+ logger.info("Resetting to preview fallback bundle: " + fallback.getVersionName());
2237
+ if (this.implementation.stagePreviewFallbackReload(fallback) && this._reload()) {
2238
+ this.implementation.finalizeResetTransition(previousBundleName, false);
2239
+ this.notifyBundleSet(fallback);
2240
+ return true;
2241
+ }
2242
+ this.implementation.restoreResetState(previousState);
2243
+ this.restoreLiveBundleStateAfterFailedReload();
2244
+ return false;
2245
+ }
2246
+
2247
+ private void endPreviewSession() {
2248
+ final boolean previousShakeMenuEnabled = this.prefs.getBoolean(
2249
+ PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY,
2250
+ this.getConfig().getBoolean("shakeMenu", false)
2251
+ );
2252
+ final boolean previousShakeChannelSelectorEnabled = this.prefs.getBoolean(
2253
+ PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY,
2254
+ this.getConfig().getBoolean("allowShakeChannelSelector", false)
2255
+ );
2256
+ this.restorePreviewPreviousNextBundle();
2257
+ this.restorePreviewPreviousAppId();
2258
+
2259
+ this.previewSessionEnabled = false;
2260
+ this.previewSessionAlertPending = false;
2261
+ this.implementation.previewSession = false;
2262
+ this.shakeMenuEnabled = previousShakeMenuEnabled;
2263
+ this.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled;
2264
+ this.implementation.setPreviewFallbackBundle(null);
2265
+ this.clearPreviewSessionPreferences();
2266
+ logger.info("Preview session ended");
2267
+ }
2268
+
2269
+ private void clearPreviewSessionBecauseDisabled() {
2270
+ logger.info("Preview session disabled by config; restoring preview fallback");
2271
+ final BundleInfo fallback = this.implementation.getPreviewFallbackBundle();
2272
+ final BundleInfo bundleToRestore = fallback == null || fallback.isErrorStatus()
2273
+ ? this.implementation.getBundleInfo(BundleInfo.ID_BUILTIN)
2274
+ : fallback;
2275
+
2276
+ if (this.implementation.canSet(bundleToRestore)) {
2277
+ this.implementation.stagePreviewFallbackReload(bundleToRestore);
2278
+ } else {
2279
+ logger.warn("Could not restore preview fallback while disabling preview");
2280
+ }
2281
+
2282
+ this.restorePreviewPreviousNextBundle();
2283
+ this.restorePreviewPreviousAppId();
2284
+ this.previewSessionEnabled = false;
2285
+ this.previewSessionAlertPending = false;
2286
+ this.implementation.previewSession = false;
2287
+ this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2288
+ this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
2289
+ this.clearPreviewSessionPreferences();
2290
+ }
2291
+
2292
+ private void clearPreviewSessionPreferences() {
2293
+ if (this.implementation != null) {
2294
+ this.implementation.setPreviewFallbackBundle(null);
2295
+ }
2296
+ this.editor.remove(PREVIEW_SESSION_PREF_KEY);
2297
+ this.editor.remove(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY);
2298
+ this.editor.remove(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY);
2299
+ this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
2300
+ this.editor.remove(PREVIEW_PREVIOUS_APP_ID_PREF_KEY);
2301
+ this.editor.remove(PREVIEW_APP_ID_PREF_KEY);
2302
+ this.editor.apply();
2303
+ }
2304
+
2305
+ private void setActiveAppId(final String appId) {
2306
+ this.implementation.appId = appId;
2307
+ if (this.implementation.versionOs != null) {
2308
+ DownloadService.updateUserAgent(this.implementation.appId, this.pluginVersion, this.implementation.versionOs);
2309
+ }
2310
+ }
2311
+
2312
+ private void restorePreviewPreviousAppId() {
2313
+ final String previousAppId = this.prefs.getString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, "");
2314
+ if (previousAppId == null || previousAppId.isEmpty()) {
2315
+ return;
2316
+ }
2317
+ this.setActiveAppId(previousAppId);
2318
+ logger.info("Restored appId after preview: " + previousAppId);
2319
+ }
2320
+
2321
+ private String normalizePreviewAppId(final String rawAppId) {
2322
+ if (rawAppId == null) {
2323
+ return null;
2324
+ }
2325
+
2326
+ final String appId = rawAppId.trim();
2327
+ if (appId.isEmpty()) {
2328
+ return null;
2329
+ }
2330
+
2331
+ final String lowercasedAppId = appId.toLowerCase(java.util.Locale.ROOT);
2332
+ if ("undefined".equals(lowercasedAppId) || "null".equals(lowercasedAppId)) {
2333
+ return null;
2334
+ }
2335
+
2336
+ return appId;
2337
+ }
2338
+
2339
+ private void clearPreviewSessionForNativeBuildChange() {
2340
+ if (!Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getPreviewFallbackBundle() == null) {
2341
+ return;
2342
+ }
2343
+ logger.info("Native build changed; clearing preview session state");
2344
+ this.previewSessionEnabled = false;
2345
+ this.previewSessionAlertPending = false;
2346
+ this.implementation.previewSession = false;
2347
+ this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2348
+ this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
2349
+ this.restorePreviewPreviousAppId();
2350
+ this.implementation.setPreviewFallbackBundle(null);
2351
+ this.implementation.setNextBundle(null);
2352
+ this.clearPreviewChannelOverride();
2353
+ this.clearPreviewSessionPreferences();
2354
+ }
2355
+
2356
+ private boolean clearPreviewChannelOverride() {
2357
+ final String configDefaultChannel = this.getConfig().getString("defaultChannel", "");
2358
+ final AtomicReference<Map<String, Object>> unsetChannelResult = new AtomicReference<>();
2359
+ try {
2360
+ this.implementation.unsetChannel(this.editor, DEFAULT_CHANNEL_PREF_KEY, configDefaultChannel, unsetChannelResult::set);
2361
+ } catch (final Exception err) {
2362
+ logger.error("Could not clear preview channel override: " + err.getMessage());
2363
+ return false;
2364
+ }
2365
+
2366
+ final Map<String, Object> result = unsetChannelResult.get();
2367
+ if (result == null) {
2368
+ logger.error("Could not clear preview channel override: no result");
2369
+ return false;
2370
+ }
2371
+ if (result.containsKey("error")) {
2372
+ final Object message = result.getOrDefault("message", result.get("error"));
2373
+ logger.error("Could not clear preview channel override: " + message);
2374
+ return false;
2375
+ }
2376
+ return true;
2377
+ }
2378
+
2379
+ private void restorePreviewPreviousNextBundle() {
2380
+ final String previousNextBundleId = this.prefs.getString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, null);
2381
+ if (previousNextBundleId == null || previousNextBundleId.isEmpty()) {
2382
+ this.implementation.setNextBundle(null);
2383
+ return;
2384
+ }
2385
+ if (!this.implementation.setNextBundle(previousNextBundleId)) {
2386
+ logger.warn("Could not restore pre-preview next bundle: " + previousNextBundleId);
2387
+ this.implementation.setNextBundle(null);
2388
+ }
2389
+ }
2390
+
2391
+ private void ensureShakeMenuStarted() {
2392
+ if (getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
2393
+ try {
2394
+ shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
2395
+ logger.info("Shake menu initialized");
2396
+ } catch (Exception e) {
2397
+ logger.error("Failed to initialize shake menu: " + e.getMessage());
2398
+ }
2399
+ }
2400
+ }
2401
+
2402
+ private void showPreviewSessionNoticeIfNeeded() {
2403
+ if (!Boolean.TRUE.equals(this.previewSessionEnabled) || !Boolean.TRUE.equals(this.previewSessionAlertPending)) {
2404
+ return;
2405
+ }
2406
+ this.previewSessionAlertPending = false;
2407
+
2408
+ new Handler(Looper.getMainLooper()).postDelayed(
2409
+ () -> {
2410
+ try {
2411
+ if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
2412
+ return;
2413
+ }
2414
+ if (getActivity() == null || getActivity().isFinishing()) {
2415
+ this.previewSessionAlertPending = true;
2416
+ return;
2417
+ }
2418
+
2419
+ new AlertDialog.Builder(getActivity())
2420
+ .setTitle("Preview started")
2421
+ .setMessage("Shake your device anytime to reload or leave the test app.")
2422
+ .setPositiveButton("Got it", (dialog, which) -> dialog.dismiss())
2423
+ .show();
2424
+ } catch (final Exception e) {
2425
+ this.previewSessionAlertPending = true;
2426
+ logger.warn("Could not show preview session notice: " + e.getMessage());
2427
+ }
2428
+ },
2429
+ 600
2430
+ );
2431
+ }
2432
+
2128
2433
  @PluginMethod
2129
2434
  public void delete(final PluginCall call) {
2130
2435
  final String id = call.getString("id");
@@ -2206,37 +2511,121 @@ public class CapacitorUpdaterPlugin extends Plugin {
2206
2511
  @PluginMethod
2207
2512
  public void getLatest(final PluginCall call) {
2208
2513
  final String channel = call.getString("channel");
2209
- startNewThread(() ->
2210
- CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
2211
- JSObject jsRes = InternalUtils.mapToJSObject(res);
2212
- if (jsRes.has("error") || jsRes.has("kind")) {
2213
- String error = jsRes.has("error") ? jsRes.getString("error") : "";
2214
- String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
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
- }
2229
- return;
2230
- } else if (jsRes.has("message")) {
2231
- String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
2232
- CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
2233
- call.reject(jsRes.getString("message"));
2234
- return;
2514
+ final boolean includeBundleSize = call.getBoolean("includeBundleSize", false);
2515
+ final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
2516
+ final boolean hasPreviewAppId = previewAppId != null;
2517
+ if (hasPreviewAppId && !Boolean.TRUE.equals(this.allowPreview)) {
2518
+ logger.error("getLatest preview override not allowed set allowPreview in your config to true to enable it");
2519
+ call.reject("getLatest preview override not allowed");
2520
+ return;
2521
+ }
2522
+
2523
+ final Callback latestCallback = (res) -> {
2524
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
2525
+ if (jsRes.has("error") || jsRes.has("kind")) {
2526
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
2527
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
2528
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
2529
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
2530
+ jsRes.put("kind", kind);
2531
+ CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
2532
+ if ("failed".equals(kind)) {
2533
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
2534
+ call.reject(error.isEmpty() ? errorMessage : error);
2235
2535
  } else {
2536
+ if (!jsRes.has("version") || jsRes.getString("version").isEmpty()) {
2537
+ jsRes.put("version", CapacitorUpdaterPlugin.this.implementation.getCurrentBundle().getVersionName());
2538
+ }
2539
+ logger.info("getLatest returned " + kind + ": " + errorMessage);
2236
2540
  call.resolve(jsRes);
2237
2541
  }
2238
- })
2239
- );
2542
+ return;
2543
+ } else if (jsRes.has("message")) {
2544
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : "";
2545
+ CapacitorUpdaterPlugin.this.notifyBreakingEventsIfNeeded(jsRes, latestVersion);
2546
+ call.reject(jsRes.getString("message"));
2547
+ return;
2548
+ } else {
2549
+ if (includeBundleSize) {
2550
+ CapacitorUpdaterPlugin.this.attachBundleSize(jsRes);
2551
+ }
2552
+ call.resolve(jsRes);
2553
+ }
2554
+ };
2555
+
2556
+ startNewThread(() -> {
2557
+ if (hasPreviewAppId) {
2558
+ CapacitorUpdaterPlugin.this.implementation.getLatest(
2559
+ CapacitorUpdaterPlugin.this.updateUrl,
2560
+ channel,
2561
+ previewAppId,
2562
+ latestCallback
2563
+ );
2564
+ return;
2565
+ }
2566
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, latestCallback);
2567
+ });
2568
+ }
2569
+
2570
+ private void attachBundleSize(final JSObject latest) {
2571
+ try {
2572
+ final JSONArray manifest = latest.optJSONArray("manifest");
2573
+ if (manifest == null || manifest.length() == 0) {
2574
+ return;
2575
+ }
2576
+ final String sessionKey = latest.optString("sessionKey", "");
2577
+ final JSONObject missing = this.implementation.missingBundleFilesResult(manifest, sessionKey);
2578
+ final JSONArray missingManifest = missing.getJSONArray("missing");
2579
+ final JSONObject size = this.implementation.getBundleDownloadSize(
2580
+ this.updateUrl,
2581
+ latest.optString("version", ""),
2582
+ missingManifest
2583
+ );
2584
+ latest.put("missing", missing);
2585
+ latest.put("downloadSize", size);
2586
+ } catch (Exception e) {
2587
+ logger.error("Failed to attach bundle size to getLatest result");
2588
+ logger.debug("Error: " + e.getMessage());
2589
+ }
2590
+ }
2591
+
2592
+ @PluginMethod
2593
+ public void getMissingBundleFiles(final PluginCall call) {
2594
+ final JSONArray manifest = call.getData().optJSONArray("manifest");
2595
+ if (manifest == null) {
2596
+ call.reject("getMissingBundleFiles called without manifest");
2597
+ return;
2598
+ }
2599
+ String sessionKey = call.getString("sessionKey");
2600
+ if (sessionKey == null) {
2601
+ sessionKey = "";
2602
+ }
2603
+ final String finalSessionKey = sessionKey;
2604
+ startNewThread(() -> {
2605
+ try {
2606
+ call.resolve(jsonObjectToJSObject(this.implementation.missingBundleFilesResult(manifest, finalSessionKey)));
2607
+ } catch (Exception e) {
2608
+ call.reject("Could not get missing bundle files", e);
2609
+ }
2610
+ });
2611
+ }
2612
+
2613
+ @PluginMethod
2614
+ public void getBundleDownloadSize(final PluginCall call) {
2615
+ final JSONArray manifest = call.getData().optJSONArray("manifest");
2616
+ if (manifest == null) {
2617
+ call.reject("getBundleDownloadSize called without manifest");
2618
+ return;
2619
+ }
2620
+ final String version = call.getData().optString("version", "");
2621
+ startNewThread(() -> {
2622
+ try {
2623
+ final JSONObject size = this.implementation.getBundleDownloadSize(this.updateUrl, version, manifest);
2624
+ call.resolve(jsonObjectToJSObject(size));
2625
+ } catch (Exception e) {
2626
+ call.reject("Could not get bundle download size", e);
2627
+ }
2628
+ });
2240
2629
  }
2241
2630
 
2242
2631
  public String triggerBackgroundUpdateCheck() {
@@ -3377,7 +3766,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
3377
3766
  call.reject("setAppId called without appId");
3378
3767
  return;
3379
3768
  }
3380
- this.implementation.appId = appId;
3769
+ this.setActiveAppId(appId);
3381
3770
  call.resolve();
3382
3771
  }
3383
3772