@capgo/capacitor-updater 7.42.9 → 7.45.10

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.
@@ -22,6 +22,8 @@ import android.view.View;
22
22
  import android.view.ViewGroup;
23
23
  import android.widget.FrameLayout;
24
24
  import android.widget.ProgressBar;
25
+ import androidx.core.content.pm.PackageInfoCompat;
26
+ import com.getcapacitor.Bridge;
25
27
  import com.getcapacitor.CapConfig;
26
28
  import com.getcapacitor.JSArray;
27
29
  import com.getcapacitor.JSObject;
@@ -29,6 +31,7 @@ import com.getcapacitor.Plugin;
29
31
  import com.getcapacitor.PluginCall;
30
32
  import com.getcapacitor.PluginHandle;
31
33
  import com.getcapacitor.PluginMethod;
34
+ import com.getcapacitor.PluginResult;
32
35
  import com.getcapacitor.annotation.CapacitorPlugin;
33
36
  import com.getcapacitor.plugin.WebView;
34
37
  import com.google.android.gms.tasks.Task;
@@ -47,7 +50,6 @@ import java.io.IOException;
47
50
  import java.net.MalformedURLException;
48
51
  import java.net.URL;
49
52
  import java.util.ArrayList;
50
- import java.util.Arrays;
51
53
  import java.util.Date;
52
54
  import java.util.HashSet;
53
55
  import java.util.List;
@@ -56,7 +58,6 @@ import java.util.Objects;
56
58
  import java.util.Set;
57
59
  import java.util.Timer;
58
60
  import java.util.TimerTask;
59
- import java.util.UUID;
60
61
  import java.util.concurrent.Phaser;
61
62
  import java.util.concurrent.Semaphore;
62
63
  import java.util.concurrent.TimeUnit;
@@ -83,8 +84,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
83
84
  private static final String DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.defaultChannel";
84
85
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
85
86
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
87
+ private static final String SPLASH_SCREEN_PLUGIN_ID = "SplashScreen";
88
+ private static final int SPLASH_SCREEN_RETRY_DELAY_MS = 100;
89
+ private static final int SPLASH_SCREEN_MAX_RETRIES = 20;
90
+ private static final long PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS = 30000L;
86
91
 
87
- private final String pluginVersion = "7.42.9";
92
+ private final String pluginVersion = "7.45.10";
88
93
  private static final String DELAY_CONDITION_PREFERENCES = "";
89
94
 
90
95
  private SharedPreferences.Editor editor;
@@ -108,21 +113,33 @@ public class CapacitorUpdaterPlugin extends Plugin {
108
113
  private Boolean autoSplashscreenLoader = false;
109
114
  private Integer autoSplashscreenTimeout = 10000;
110
115
  private Boolean autoSplashscreenTimedOut = false;
116
+ private int splashscreenInvocationToken = 0;
111
117
  private String directUpdateMode = "false";
112
118
  private Boolean wasRecentlyInstalledOrUpdated = false;
113
- private Boolean onLaunchDirectUpdateUsed = false;
119
+ private volatile boolean onLaunchDirectUpdateUsed = false;
114
120
  Boolean shakeMenuEnabled = false;
121
+ Boolean shakeChannelSelectorEnabled = false;
115
122
  private Boolean allowManualBundleError = false;
116
- private Boolean allowSetDefaultChannel = true;
123
+ Boolean allowSetDefaultChannel = true;
124
+
125
+ String getUpdateUrl() {
126
+ return this.updateUrl;
127
+ }
117
128
 
118
129
  // Used for activity-based foreground/background detection on Android < 14
119
130
  private Boolean isPreviousMainActivity = true;
120
131
 
121
132
  private volatile Thread backgroundDownloadTask;
122
133
  private volatile Thread appReadyCheck;
134
+ private volatile long downloadStartTimeMs = 0;
135
+ private static final long DOWNLOAD_TIMEOUT_MS = 3600000; // 1 hour timeout
123
136
 
124
- // private static final CountDownLatch semaphoreReady = new CountDownLatch(1);
125
- private static final Phaser semaphoreReady = new Phaser(1);
137
+ private final Phaser semaphoreReady = new Phaser(0) {
138
+ @Override
139
+ protected boolean onAdvance(final int phase, final int registeredParties) {
140
+ return false;
141
+ }
142
+ };
126
143
 
127
144
  // Lock to ensure cleanup completes before downloads start
128
145
  private final Object cleanupLock = new Object();
@@ -138,6 +155,28 @@ public class CapacitorUpdaterPlugin extends Plugin {
138
155
  private FrameLayout splashscreenLoaderOverlay;
139
156
  private Runnable splashscreenTimeoutRunnable;
140
157
 
158
+ private static final class FireAndForgetPluginCall extends PluginCall {
159
+
160
+ FireAndForgetPluginCall(final String methodName, final JSObject data) {
161
+ super(null, SPLASH_SCREEN_PLUGIN_ID, PluginCall.CALLBACK_ID_DANGLING, methodName, data);
162
+ }
163
+
164
+ @Override
165
+ public void successCallback(final PluginResult successResult) {}
166
+
167
+ @Override
168
+ public void resolve(final JSObject data) {}
169
+
170
+ @Override
171
+ public void resolve() {}
172
+
173
+ @Override
174
+ public void errorCallback(final String msg) {}
175
+
176
+ @Override
177
+ public void reject(final String msg, final String code, final Exception ex, final JSObject data) {}
178
+ }
179
+
141
180
  // App lifecycle observer using ProcessLifecycleOwner for reliable foreground/background detection
142
181
  private AppLifecycleObserver appLifecycleObserver;
143
182
 
@@ -147,6 +186,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
147
186
  private static final int APP_UPDATE_REQUEST_CODE = 9001;
148
187
  private InstallStateUpdatedListener installStateUpdatedListener;
149
188
 
189
+ private PackageInfo getCurrentPackageInfo() throws PackageManager.NameNotFoundException {
190
+ final PackageManager packageManager = this.getContext().getPackageManager();
191
+ final String packageName = this.getContext().getPackageName();
192
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
193
+ return packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
194
+ }
195
+ return packageManager.getPackageInfo(packageName, 0);
196
+ }
197
+
198
+ private String getVersionCode(final PackageInfo packageInfo) {
199
+ return Long.toString(PackageInfoCompat.getLongVersionCode(packageInfo));
200
+ }
201
+
150
202
  private void notifyBreakingEvents(final String version) {
151
203
  if (version == null || version.isEmpty()) {
152
204
  return;
@@ -158,14 +210,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
158
210
  }
159
211
  }
160
212
 
161
- private JSObject mapToJSObject(Map<String, Object> map) {
162
- JSObject jsObject = new JSObject();
163
- for (Map.Entry<String, Object> entry : map.entrySet()) {
164
- jsObject.put(entry.getKey(), entry.getValue());
165
- }
166
- return jsObject;
167
- }
168
-
169
213
  private void persistLastFailedBundle(BundleInfo bundle) {
170
214
  if (this.prefs == null) {
171
215
  return;
@@ -233,31 +277,37 @@ public class CapacitorUpdaterPlugin extends Plugin {
233
277
  this.implementation = new CapgoUpdater(logger) {
234
278
  @Override
235
279
  public void notifyDownload(final String id, final int percent) {
236
- activity.runOnUiThread(() -> {
237
- CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
238
- });
280
+ if (activity != null) {
281
+ activity.runOnUiThread(() -> {
282
+ CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
283
+ });
284
+ } else {
285
+ logger.warn("notifyDownload: Activity is null, skipping notification");
286
+ }
239
287
  }
240
288
 
241
289
  @Override
242
290
  public void directUpdateFinish(final BundleInfo latest) {
243
- activity.runOnUiThread(() -> {
244
- CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
245
- });
291
+ CapacitorUpdaterPlugin.this.scheduleDirectUpdateFinish(latest);
246
292
  }
247
293
 
248
294
  @Override
249
295
  public void notifyListeners(final String id, final Map<String, Object> res) {
250
- activity.runOnUiThread(() -> {
251
- CapacitorUpdaterPlugin.this.notifyListeners(id, CapacitorUpdaterPlugin.this.mapToJSObject(res));
252
- });
296
+ if (activity != null) {
297
+ activity.runOnUiThread(() -> {
298
+ CapacitorUpdaterPlugin.this.notifyListeners(id, InternalUtils.mapToJSObject(res));
299
+ });
300
+ } else {
301
+ logger.warn("notifyListeners: Activity is null, skipping notification for event: " + id);
302
+ }
253
303
  }
254
304
  };
255
- final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
305
+ final PackageInfo pInfo = this.getCurrentPackageInfo();
256
306
  this.implementation.activity = this.getActivity();
257
307
  this.implementation.versionBuild = this.getConfig().getString("version", pInfo.versionName);
258
308
  this.implementation.CAP_SERVER_PATH = WebView.CAP_SERVER_PATH;
259
309
  this.implementation.pluginVersion = this.pluginVersion;
260
- this.implementation.versionCode = Integer.toString(pInfo.versionCode);
310
+ this.implementation.versionCode = this.getVersionCode(pInfo);
261
311
  // Removed unused OkHttpClient creation - using shared client in DownloadService instead
262
312
  // Handle directUpdate configuration - support string values and backward compatibility
263
313
  String directUpdateConfig = this.getConfig().getString("directUpdate", null);
@@ -299,7 +349,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
299
349
  }
300
350
  }
301
351
  this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
302
- this.currentBuildVersion = Integer.toString(pInfo.versionCode);
352
+ this.currentBuildVersion = this.getVersionCode(pInfo);
303
353
  this.delayUpdateUtils = new DelayUpdateUtils(this.prefs, this.editor, this.currentVersionNative, logger);
304
354
  } catch (final PackageManager.NameNotFoundException e) {
305
355
  logger.error("Error instantiating implementation " + e.getMessage());
@@ -426,6 +476,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
426
476
  this.autoSplashscreenTimeout = Math.max(0, splashscreenTimeoutValue);
427
477
  this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
428
478
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
479
+ this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
429
480
  boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
430
481
 
431
482
  // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
@@ -468,38 +519,86 @@ public class CapacitorUpdaterPlugin extends Plugin {
468
519
  }
469
520
  }
470
521
 
471
- private void semaphoreWait(Number waitTime) {
522
+ private boolean semaphoreWait(final int phase, Number waitTime) {
472
523
  try {
473
- semaphoreReady.awaitAdvanceInterruptibly(semaphoreReady.getPhase(), waitTime.longValue(), TimeUnit.SECONDS);
524
+ semaphoreReady.awaitAdvanceInterruptibly(phase, waitTime.longValue(), TimeUnit.MILLISECONDS);
474
525
  logger.info("semaphoreReady count " + semaphoreReady.getPhase());
526
+ return true;
475
527
  } catch (InterruptedException e) {
476
528
  logger.info("semaphoreWait InterruptedException");
529
+ cleanupTimedOutSemaphoreWait(phase);
477
530
  Thread.currentThread().interrupt(); // Restore interrupted status
531
+ return false;
478
532
  } catch (TimeoutException e) {
479
533
  logger.error("Semaphore timeout: " + e.getMessage());
480
- // Don't throw runtime exception, just log and continue
534
+ cleanupTimedOutSemaphoreWait(phase);
535
+ return false;
481
536
  }
482
537
  }
483
538
 
484
- private void semaphoreUp() {
539
+ private int semaphoreUp() {
485
540
  logger.info("semaphoreUp");
486
- semaphoreReady.register();
541
+ return semaphoreReady.register();
487
542
  }
488
543
 
489
544
  private void semaphoreDown() {
545
+ if (semaphoreReady.getRegisteredParties() == 0) {
546
+ logger.info("semaphoreDown skipped, no pending app ready wait");
547
+ return;
548
+ }
490
549
  logger.info("semaphoreDown");
491
550
  logger.info("semaphoreDown count " + semaphoreReady.getPhase());
492
551
  semaphoreReady.arriveAndDeregister();
493
552
  }
494
553
 
554
+ private void cleanupTimedOutSemaphoreWait(final int phase) {
555
+ if (semaphoreReady.getPhase() != phase || semaphoreReady.getRegisteredParties() == 0) {
556
+ return;
557
+ }
558
+ logger.info("Cleaning up stale app ready wait for phase " + phase);
559
+ semaphoreReady.arriveAndDeregister();
560
+ }
561
+
562
+ protected long getMinimumPendingBundleAppReadyTimeoutMs() {
563
+ return PENDING_BUNDLE_APP_READY_MIN_TIMEOUT_MS;
564
+ }
565
+
566
+ private long resolveAppReadyCheckTimeoutMs() {
567
+ long configuredTimeoutMs = this.appReadyTimeout.longValue();
568
+ try {
569
+ if (this.implementation == null) {
570
+ return configuredTimeoutMs;
571
+ }
572
+
573
+ final BundleInfo current = this.implementation.getCurrentBundle();
574
+ if (current == null || BundleStatus.SUCCESS == current.getStatus()) {
575
+ return configuredTimeoutMs;
576
+ }
577
+
578
+ return Math.max(configuredTimeoutMs, this.getMinimumPendingBundleAppReadyTimeoutMs());
579
+ } catch (final Exception e) {
580
+ logger.warn("Falling back to configured appReadyTimeout: " + e.getMessage());
581
+ return configuredTimeoutMs;
582
+ }
583
+ }
584
+
495
585
  private void sendReadyToJs(final BundleInfo current, final String msg) {
496
586
  sendReadyToJs(current, msg, false);
497
587
  }
498
588
 
589
+ private void notifyBundleSet(final BundleInfo bundle) {
590
+ if (bundle == null) {
591
+ return;
592
+ }
593
+ final JSObject ret = new JSObject();
594
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
595
+ this.notifyListeners("set", ret, true);
596
+ }
597
+
499
598
  private void sendReadyToJs(final BundleInfo current, final String msg, final boolean isDirectUpdate) {
500
599
  logger.info("sendReadyToJs: " + msg);
501
600
  final JSObject ret = new JSObject();
502
- ret.put("bundle", mapToJSObject(current.toJSONMap()));
601
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
503
602
  ret.put("status", msg);
504
603
 
505
604
  // No need to wait for semaphore anymore since _reload() has already waited
@@ -523,47 +622,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
523
622
  private void hideSplashscreenInternal() {
524
623
  cancelSplashscreenTimeout();
525
624
  removeSplashscreenLoader();
526
-
527
- try {
528
- if (getBridge() == null) {
529
- logger.warn("Bridge not ready for hiding splashscreen with autoSplashscreen");
530
- return;
531
- }
532
-
533
- // Try to call the SplashScreen plugin directly through the bridge
534
- PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
535
- if (splashScreenPlugin != null) {
536
- try {
537
- // Create a plugin call for the hide method using reflection to access private msgHandler
538
- JSObject options = new JSObject();
539
- java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
540
- msgHandlerField.setAccessible(true);
541
- Object msgHandler = msgHandlerField.get(getBridge());
542
-
543
- PluginCall call = new PluginCall(
544
- (com.getcapacitor.MessageHandler) msgHandler,
545
- "SplashScreen",
546
- "FAKE_CALLBACK_ID_HIDE",
547
- "hide",
548
- options
549
- );
550
-
551
- // Call the hide method directly
552
- splashScreenPlugin.invoke("hide", call);
553
- logger.info("Splashscreen hidden automatically via direct plugin call");
554
- } catch (Exception e) {
555
- logger.error("Failed to call SplashScreen hide method: " + e.getMessage());
556
- }
557
- } else {
558
- logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.");
559
- }
560
- } catch (Exception e) {
561
- logger.error(
562
- "Error hiding splashscreen with autoSplashscreen: " +
563
- e.getMessage() +
564
- ". Make sure @capacitor/splash-screen plugin is installed and configured."
565
- );
566
- }
625
+ invokeSplashScreenPluginMethod("hide", new JSObject(), SPLASH_SCREEN_MAX_RETRIES, ++this.splashscreenInvocationToken);
567
626
  }
568
627
 
569
628
  private void showSplashscreen() {
@@ -578,37 +637,87 @@ public class CapacitorUpdaterPlugin extends Plugin {
578
637
  cancelSplashscreenTimeout();
579
638
  this.autoSplashscreenTimedOut = false;
580
639
 
640
+ final JSObject options = new JSObject();
641
+ options.put("autoHide", false);
642
+ invokeSplashScreenPluginMethod("show", options, SPLASH_SCREEN_MAX_RETRIES, ++this.splashscreenInvocationToken);
643
+
644
+ addSplashscreenLoaderIfNeeded();
645
+ scheduleSplashscreenTimeout();
646
+ }
647
+
648
+ private void invokeSplashScreenPluginMethod(
649
+ final String methodName,
650
+ final JSObject options,
651
+ final int retriesRemaining,
652
+ final int requestToken
653
+ ) {
654
+ if (requestToken != this.splashscreenInvocationToken) {
655
+ return;
656
+ }
657
+
581
658
  try {
582
- if (getBridge() == null) {
583
- logger.warn("Bridge not ready for showing splashscreen with autoSplashscreen");
584
- } else {
585
- PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
586
- if (splashScreenPlugin != null) {
587
- JSObject options = new JSObject();
588
- java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
589
- msgHandlerField.setAccessible(true);
590
- Object msgHandler = msgHandlerField.get(getBridge());
591
-
592
- PluginCall call = new PluginCall(
593
- (com.getcapacitor.MessageHandler) msgHandler,
594
- "SplashScreen",
595
- "FAKE_CALLBACK_ID_SHOW",
596
- "show",
597
- options
598
- );
659
+ final Bridge bridge = getBridge();
660
+ if (bridge == null) {
661
+ retrySplashScreenInvocation(
662
+ methodName,
663
+ options,
664
+ retriesRemaining,
665
+ requestToken,
666
+ "Bridge not ready for " + ("show".equals(methodName) ? "showing" : "hiding") + " splashscreen"
667
+ );
668
+ return;
669
+ }
599
670
 
600
- splashScreenPlugin.invoke("show", call);
601
- logger.info("Splashscreen shown synchronously to prevent flash");
602
- } else {
603
- logger.warn("autoSplashscreen: SplashScreen plugin not found");
604
- }
671
+ final PluginHandle splashScreenPlugin = bridge.getPlugin(SPLASH_SCREEN_PLUGIN_ID);
672
+ if (splashScreenPlugin == null) {
673
+ retrySplashScreenInvocation(
674
+ methodName,
675
+ options,
676
+ retriesRemaining,
677
+ requestToken,
678
+ "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
679
+ );
680
+ return;
605
681
  }
606
- } catch (Exception e) {
607
- logger.error("Failed to show splashscreen synchronously: " + e.getMessage());
682
+
683
+ splashScreenPlugin.invoke(methodName, new FireAndForgetPluginCall(methodName, options));
684
+ logger.info("Splashscreen " + methodName + " invoked automatically");
685
+ } catch (final Exception e) {
686
+ retrySplashScreenInvocation(
687
+ methodName,
688
+ options,
689
+ retriesRemaining,
690
+ requestToken,
691
+ "Failed to call SplashScreen " + methodName + " method: " + e.getMessage()
692
+ );
608
693
  }
694
+ }
609
695
 
610
- addSplashscreenLoaderIfNeeded();
611
- scheduleSplashscreenTimeout();
696
+ private void retrySplashScreenInvocation(
697
+ final String methodName,
698
+ final JSObject options,
699
+ final int retriesRemaining,
700
+ final int requestToken,
701
+ final String message
702
+ ) {
703
+ if (retriesRemaining > 0) {
704
+ logger.info(message + ". Retrying.");
705
+ this.mainHandler.postDelayed(
706
+ () -> invokeSplashScreenPluginMethod(methodName, options, retriesRemaining - 1, requestToken),
707
+ SPLASH_SCREEN_RETRY_DELAY_MS
708
+ );
709
+ return;
710
+ }
711
+
712
+ if ("show".equals(methodName)) {
713
+ logger.warn(message);
714
+ } else {
715
+ logger.error(message);
716
+ }
717
+ }
718
+
719
+ boolean isCurrentSplashscreenInvocationTokenForTesting(final int requestToken) {
720
+ return requestToken == this.splashscreenInvocationToken;
612
721
  }
613
722
 
614
723
  private void addSplashscreenLoaderIfNeeded() {
@@ -749,14 +858,74 @@ public class CapacitorUpdaterPlugin extends Plugin {
749
858
  return plannedDirectUpdate && !Boolean.TRUE.equals(this.autoSplashscreenTimedOut);
750
859
  }
751
860
 
861
+ static boolean shouldConsumeOnLaunchDirectUpdate(final String directUpdateMode, final boolean plannedDirectUpdate) {
862
+ return plannedDirectUpdate && "onLaunch".equals(directUpdateMode);
863
+ }
864
+
865
+ private void consumeOnLaunchDirectUpdateAttempt(final boolean plannedDirectUpdate) {
866
+ if (!shouldConsumeOnLaunchDirectUpdate(this.directUpdateMode, plannedDirectUpdate)) {
867
+ return;
868
+ }
869
+
870
+ this.onLaunchDirectUpdateUsed = true;
871
+ }
872
+
873
+ void configureDirectUpdateModeForTesting(final String directUpdateMode, final boolean onLaunchDirectUpdateUsed) {
874
+ this.directUpdateMode = directUpdateMode;
875
+ this.onLaunchDirectUpdateUsed = onLaunchDirectUpdateUsed;
876
+ }
877
+
878
+ boolean shouldUseDirectUpdateForTesting() {
879
+ return this.shouldUseDirectUpdate();
880
+ }
881
+
882
+ boolean hasConsumedOnLaunchDirectUpdateForTesting() {
883
+ return this.onLaunchDirectUpdateUsed;
884
+ }
885
+
886
+ boolean isVersionDownloadInProgress(final String version) {
887
+ return (
888
+ version != null &&
889
+ !version.isEmpty() &&
890
+ this.implementation != null &&
891
+ this.implementation.activity != null &&
892
+ DownloadWorkerManager.isVersionDownloading(this.implementation.activity, version)
893
+ );
894
+ }
895
+
896
+ void setLoggerForTesting(final Logger logger) {
897
+ this.logger = logger;
898
+ }
899
+
900
+ void completeBackgroundTaskForTesting(final BundleInfo current, final boolean plannedDirectUpdate) {
901
+ this.endBackGroundTaskWithNotif("test", current.getVersionName(), current, false, plannedDirectUpdate);
902
+ }
903
+
904
+ void scheduleDirectUpdateFinish(final BundleInfo latest) {
905
+ startNewThread(() -> {
906
+ try {
907
+ Activity currentActivity = this.getActivity();
908
+ if (currentActivity != null) {
909
+ this.implementation.activity = currentActivity;
910
+ } else {
911
+ logger.warn("directUpdateFinish: Activity is null, proceeding without refreshing the activity reference");
912
+ }
913
+ this.directUpdateFinish(latest);
914
+ } catch (final Exception e) {
915
+ logger.error("directUpdateFinish failed: " + e.getMessage());
916
+ }
917
+ });
918
+ }
919
+
752
920
  private void directUpdateFinish(final BundleInfo latest) {
753
921
  if ("onLaunch".equals(this.directUpdateMode)) {
754
922
  this.onLaunchDirectUpdateUsed = true;
755
923
  this.implementation.directUpdate = false;
756
924
  }
757
- CapacitorUpdaterPlugin.this.implementation.set(latest);
758
- CapacitorUpdaterPlugin.this._reload();
759
- sendReadyToJs(latest, "update installed", true);
925
+ if (CapacitorUpdaterPlugin.this.implementation.set(latest) && CapacitorUpdaterPlugin.this._reload()) {
926
+ this.notifyBundleSet(latest);
927
+ sendReadyToJs(latest, "update installed", true);
928
+ }
760
929
  }
761
930
 
762
931
  private void cleanupObsoleteVersions() {
@@ -842,7 +1011,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
842
1011
  final JSObject ret = new JSObject();
843
1012
  ret.put("percent", percent);
844
1013
  final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
845
- ret.put("bundle", mapToJSObject(bundleInfo.toJSONMap()));
1014
+ ret.put("bundle", InternalUtils.mapToJSObject(bundleInfo.toJSONMap()));
846
1015
  this.notifyListeners("download", ret);
847
1016
 
848
1017
  if (percent == 100) {
@@ -994,7 +1163,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
994
1163
  DEFAULT_CHANNEL_PREF_KEY,
995
1164
  configDefaultChannel,
996
1165
  (res) -> {
997
- JSObject jsRes = mapToJSObject(res);
1166
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
998
1167
  if (jsRes.has("error")) {
999
1168
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1000
1169
  String errorCode = jsRes.getString("error");
@@ -1007,7 +1176,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1007
1176
  } else {
1008
1177
  if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
1009
1178
  logger.info("Calling autoupdater after channel change!");
1010
- backgroundDownload();
1179
+ // Check if download is already in progress (with timeout protection)
1180
+ if (!this.isDownloadStuckOrTimedOut()) {
1181
+ backgroundDownload();
1182
+ } else {
1183
+ logger.info("Download already in progress, skipping duplicate download request");
1184
+ }
1011
1185
  }
1012
1186
  call.resolve(jsRes);
1013
1187
  }
@@ -1042,7 +1216,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1042
1216
  DEFAULT_CHANNEL_PREF_KEY,
1043
1217
  CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
1044
1218
  (res) -> {
1045
- JSObject jsRes = mapToJSObject(res);
1219
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1046
1220
  if (jsRes.has("error")) {
1047
1221
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1048
1222
  String errorCode = jsRes.getString("error");
@@ -1066,7 +1240,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1066
1240
  } else {
1067
1241
  if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
1068
1242
  logger.info("Calling autoupdater after channel change!");
1069
- backgroundDownload();
1243
+ // Check if download is already in progress (with timeout protection)
1244
+ if (!this.isDownloadStuckOrTimedOut()) {
1245
+ backgroundDownload();
1246
+ } else {
1247
+ logger.info("Download already in progress, skipping duplicate download request");
1248
+ }
1070
1249
  }
1071
1250
  call.resolve(jsRes);
1072
1251
  }
@@ -1085,7 +1264,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1085
1264
  logger.info("getChannel");
1086
1265
  startNewThread(() ->
1087
1266
  CapacitorUpdaterPlugin.this.implementation.getChannel((res) -> {
1088
- JSObject jsRes = mapToJSObject(res);
1267
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1089
1268
  if (jsRes.has("error")) {
1090
1269
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1091
1270
  String errorCode = jsRes.getString("error");
@@ -1112,7 +1291,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1112
1291
  logger.info("listChannels");
1113
1292
  startNewThread(() ->
1114
1293
  CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
1115
- JSObject jsRes = mapToJSObject(res);
1294
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1116
1295
  if (jsRes.has("error")) {
1117
1296
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1118
1297
  String errorCode = jsRes.getString("error");
@@ -1157,12 +1336,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
1157
1336
  final BundleInfo downloaded;
1158
1337
  if (manifest != null) {
1159
1338
  // For manifest downloads, we need to handle this asynchronously
1160
- // since there's no synchronous downloadManifest method in Java
1161
- CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest);
1339
+ // to avoid automatically scheduling/applying the downloaded bundle.
1340
+ // Manual download must not schedule/apply the bundle automatically.
1341
+ CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest, false);
1162
1342
  // Return immediately with a pending status - the actual result will come via listeners
1163
1343
  final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
1164
1344
  downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
1165
- call.resolve(mapToJSObject(downloaded.toJSONMap()));
1345
+ call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
1166
1346
  return;
1167
1347
  } else {
1168
1348
  downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
@@ -1170,7 +1350,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1170
1350
  if (downloaded.isErrorStatus()) {
1171
1351
  throw new RuntimeException("Download failed: " + downloaded.getStatus());
1172
1352
  } else {
1173
- call.resolve(mapToJSObject(downloaded.toJSONMap()));
1353
+ call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
1174
1354
  }
1175
1355
  } catch (final Exception e) {
1176
1356
  logger.error("Failed to download from: " + url + " " + e.getMessage());
@@ -1207,12 +1387,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1207
1387
  this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
1208
1388
  }
1209
1389
 
1210
- protected boolean _reload() {
1390
+ private void applyCurrentBundleToBridge() {
1211
1391
  final String path = this.implementation.getCurrentBundlePath();
1392
+ final boolean usingBuiltin = this.implementation.isUsingBuiltin();
1212
1393
  if (this.keepUrlPathAfterReload) {
1213
1394
  this.syncKeepUrlPathFlag(true);
1214
1395
  }
1215
- this.semaphoreUp();
1216
1396
  logger.info("Reloading: " + path);
1217
1397
 
1218
1398
  AtomicReference<URL> url = new AtomicReference<>();
@@ -1257,7 +1437,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1257
1437
  }
1258
1438
 
1259
1439
  if (url.get() != null) {
1260
- if (this.implementation.isUsingBuiltin()) {
1440
+ if (usingBuiltin) {
1261
1441
  this.bridge.getLocalServer().hostAssets(path);
1262
1442
  } else {
1263
1443
  this.bridge.getLocalServer().hostFiles(path);
@@ -1277,14 +1457,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
1277
1457
  } catch (MalformedURLException e) {
1278
1458
  logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
1279
1459
 
1280
- if (this.implementation.isUsingBuiltin()) {
1460
+ if (usingBuiltin) {
1281
1461
  this.bridge.setServerAssetPath(path);
1282
1462
  } else {
1283
1463
  this.bridge.setServerBasePath(path);
1284
1464
  }
1285
1465
  }
1286
1466
  } else {
1287
- if (this.implementation.isUsingBuiltin()) {
1467
+ if (usingBuiltin) {
1288
1468
  this.bridge.setServerAssetPath(path);
1289
1469
  } else {
1290
1470
  this.bridge.setServerBasePath(path);
@@ -1300,34 +1480,75 @@ public class CapacitorUpdaterPlugin extends Plugin {
1300
1480
  });
1301
1481
  }
1302
1482
  }
1483
+ }
1303
1484
 
1304
- this.checkAppReady();
1305
- this.notifyListeners("appReloaded", new JSObject());
1306
-
1307
- // Wait for the reload to complete (until notifyAppReady is called)
1485
+ protected void restoreLiveBundleStateAfterFailedReload() {
1308
1486
  try {
1309
- this.semaphoreWait(this.appReadyTimeout);
1310
- } catch (Exception e) {
1311
- logger.error("Error waiting for app ready: " + e.getMessage());
1312
- return false;
1487
+ this.applyCurrentBundleToBridge();
1488
+ } catch (final Exception e) {
1489
+ logger.warn("Failed to restore live bundle after rejected reload: " + e.getMessage());
1313
1490
  }
1491
+ }
1314
1492
 
1315
- return true;
1493
+ protected boolean _reload() {
1494
+ final int phase = this.semaphoreUp();
1495
+ this.applyCurrentBundleToBridge();
1496
+
1497
+ final long waitTimeMs = this.resolveAppReadyCheckTimeoutMs();
1498
+ this.checkAppReady(waitTimeMs);
1499
+ this.notifyListeners("appReloaded", new JSObject());
1500
+
1501
+ // Wait for the reload to complete (until notifyAppReady is called)
1502
+ return this.semaphoreWait(phase, waitTimeMs);
1316
1503
  }
1317
1504
 
1318
1505
  @PluginMethod
1319
1506
  public void reload(final PluginCall call) {
1320
- try {
1321
- if (this._reload()) {
1322
- call.resolve();
1323
- } else {
1324
- logger.error("Reload failed");
1325
- call.reject("Reload failed");
1507
+ startNewThread(() -> {
1508
+ try {
1509
+ final BundleInfo current = this.implementation.getCurrentBundle();
1510
+ final BundleInfo next = this.implementation.getNextBundle();
1511
+
1512
+ if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
1513
+ final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
1514
+ final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
1515
+ logger.info("Applying pending bundle before reload: " + next.getVersionName());
1516
+ final boolean didApplyPendingBundle;
1517
+ if (next.isBuiltin()) {
1518
+ this.implementation.prepareResetStateForTransition();
1519
+ didApplyPendingBundle = true;
1520
+ } else {
1521
+ didApplyPendingBundle = this.implementation.stagePendingReload(next);
1522
+ }
1523
+ if (didApplyPendingBundle && this._reload()) {
1524
+ if (next.isBuiltin()) {
1525
+ this.implementation.finalizeResetTransition(previousBundleName, false);
1526
+ } else {
1527
+ this.implementation.finalizePendingReload(next, previousBundleName);
1528
+ }
1529
+ this.notifyBundleSet(next);
1530
+ this.implementation.setNextBundle(null);
1531
+ call.resolve();
1532
+ return;
1533
+ }
1534
+ this.implementation.restoreResetState(previousState);
1535
+ this.restoreLiveBundleStateAfterFailedReload();
1536
+ logger.error("Reload failed after applying pending bundle: " + next.getVersionName());
1537
+ call.reject("Reload failed after applying pending bundle: " + next.getVersionName());
1538
+ return;
1539
+ }
1540
+
1541
+ if (this._reload()) {
1542
+ call.resolve();
1543
+ } else {
1544
+ logger.error("Reload failed");
1545
+ call.reject("Reload failed");
1546
+ }
1547
+ } catch (final Exception e) {
1548
+ logger.error("Could not reload " + e.getMessage());
1549
+ call.reject("Could not reload", e);
1326
1550
  }
1327
- } catch (final Exception e) {
1328
- logger.error("Could not reload " + e.getMessage());
1329
- call.reject("Could not reload", e);
1330
- }
1551
+ });
1331
1552
  }
1332
1553
 
1333
1554
  @PluginMethod
@@ -1344,7 +1565,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1344
1565
  logger.error("Set next id failed. Bundle " + id + " does not exist.");
1345
1566
  call.reject("Set next id failed. Bundle " + id + " does not exist.");
1346
1567
  } else {
1347
- call.resolve(mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1568
+ call.resolve(InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1348
1569
  }
1349
1570
  } catch (final Exception e) {
1350
1571
  logger.error("Could not set next id " + id + " " + e.getMessage());
@@ -1365,9 +1586,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
1365
1586
  if (!this.implementation.set(id)) {
1366
1587
  logger.info("No such bundle " + id);
1367
1588
  call.reject("Update failed, id " + id + " does not exist.");
1589
+ } else if (!this._reload()) {
1590
+ logger.error("Reload failed after setting bundle " + id);
1591
+ call.reject("Reload failed after setting bundle " + id);
1368
1592
  } else {
1369
1593
  logger.info("Bundle successfully set to " + id);
1370
- this.reload(call);
1594
+ this.notifyBundleSet(this.implementation.getBundleInfo(id));
1595
+ call.resolve();
1371
1596
  }
1372
1597
  } catch (final Exception e) {
1373
1598
  logger.error("Could not set id " + id + " " + e.getMessage());
@@ -1428,7 +1653,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1428
1653
  }
1429
1654
  this.implementation.setError(bundle);
1430
1655
  final JSObject ret = new JSObject();
1431
- ret.put("bundle", mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1656
+ ret.put("bundle", InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1432
1657
  call.resolve(ret);
1433
1658
  } catch (final Exception e) {
1434
1659
  logger.error("Could not set bundle error for id " + id + " " + e.getMessage());
@@ -1443,7 +1668,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1443
1668
  final JSObject ret = new JSObject();
1444
1669
  final JSArray values = new JSArray();
1445
1670
  for (final BundleInfo bundle : res) {
1446
- values.put(mapToJSObject(bundle.toJSONMap()));
1671
+ values.put(InternalUtils.mapToJSObject(bundle.toJSONMap()));
1447
1672
  }
1448
1673
  ret.put("bundles", values);
1449
1674
  call.resolve(ret);
@@ -1458,12 +1683,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
1458
1683
  final String channel = call.getString("channel");
1459
1684
  startNewThread(() ->
1460
1685
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
1461
- JSObject jsRes = mapToJSObject(res);
1462
- if (jsRes.has("error")) {
1463
- String error = jsRes.getString("error");
1686
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1687
+ if (jsRes.has("error") || jsRes.has("kind")) {
1688
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1464
1689
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1465
- logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1466
- call.reject(jsRes.getString("error"));
1690
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
1691
+ jsRes.put("kind", kind);
1692
+ if ("failed".equals(kind)) {
1693
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1694
+ call.reject(error.isEmpty() ? errorMessage : error);
1695
+ } else {
1696
+ if (!jsRes.has("version") || jsRes.getString("version").isEmpty()) {
1697
+ jsRes.put("version", CapacitorUpdaterPlugin.this.implementation.getCurrentBundle().getVersionName());
1698
+ }
1699
+ logger.info("getLatest returned " + kind + ": " + errorMessage);
1700
+ call.resolve(jsRes);
1701
+ }
1467
1702
  return;
1468
1703
  } else if (jsRes.has("message")) {
1469
1704
  call.reject(jsRes.getString("message"));
@@ -1475,24 +1710,83 @@ public class CapacitorUpdaterPlugin extends Plugin {
1475
1710
  );
1476
1711
  }
1477
1712
 
1478
- private boolean _reset(final Boolean toLastSuccessful) {
1713
+ private boolean _reset(final Boolean toLastSuccessful, final Boolean usePendingBundle) {
1714
+ return this.performReset(toLastSuccessful, usePendingBundle, false);
1715
+ }
1716
+
1717
+ private boolean performReset(final Boolean toLastSuccessful, final Boolean usePendingBundle, final boolean internal) {
1479
1718
  final BundleInfo fallback = this.implementation.getFallbackBundle();
1480
- this.implementation.reset();
1719
+ final BundleInfo pending = this.implementation.getNextBundle();
1720
+ final CapgoUpdater.ResetState previousState = this.implementation.captureResetState();
1721
+ final String previousBundleName = this.implementation.getCurrentBundle().getVersionName();
1722
+
1723
+ if (Boolean.TRUE.equals(usePendingBundle)) {
1724
+ if (pending == null || pending.isErrorStatus()) {
1725
+ logger.error("No pending bundle available to reset to");
1726
+ return false;
1727
+ }
1728
+ if (!this.implementation.canSet(pending)) {
1729
+ logger.error("Pending bundle is not installable");
1730
+ return false;
1731
+ }
1732
+ this.implementation.prepareResetStateForTransition();
1733
+ logger.info("Resetting to pending bundle: " + pending.getVersionName());
1734
+ final boolean didApplyPendingBundle;
1735
+ if (pending.isBuiltin()) {
1736
+ didApplyPendingBundle = true;
1737
+ } else {
1738
+ didApplyPendingBundle = this.implementation.set(pending);
1739
+ }
1740
+ if (didApplyPendingBundle && this._reload()) {
1741
+ this.implementation.finalizeResetTransition(previousBundleName, internal);
1742
+ this.notifyBundleSet(pending);
1743
+ this.implementation.setNextBundle(null);
1744
+ return true;
1745
+ }
1746
+ this.implementation.restoreResetState(previousState);
1747
+ this.restoreLiveBundleStateAfterFailedReload();
1748
+ return false;
1749
+ }
1481
1750
 
1482
- if (toLastSuccessful && !fallback.isBuiltin()) {
1483
- logger.info("Resetting to: " + fallback);
1484
- return this.implementation.set(fallback) && this._reload();
1751
+ if (Boolean.TRUE.equals(toLastSuccessful) && !fallback.isBuiltin()) {
1752
+ if (this.implementation.canSet(fallback)) {
1753
+ this.implementation.prepareResetStateForTransition();
1754
+ logger.info("Resetting to: " + fallback);
1755
+ if (this.implementation.set(fallback) && this._reload()) {
1756
+ this.implementation.finalizeResetTransition(previousBundleName, internal);
1757
+ this.notifyBundleSet(fallback);
1758
+ return true;
1759
+ }
1760
+ if (!internal) {
1761
+ this.implementation.restoreResetState(previousState);
1762
+ this.restoreLiveBundleStateAfterFailedReload();
1763
+ return false;
1764
+ }
1765
+ logger.warn("Fallback reload failed during internal reset, resetting to native instead");
1766
+ } else {
1767
+ logger.warn("Fallback bundle is not installable, resetting to native instead");
1768
+ }
1485
1769
  }
1486
1770
 
1771
+ this.implementation.prepareResetStateForTransition();
1487
1772
  logger.info("Resetting to native.");
1488
- return this._reload();
1773
+ if (this._reload()) {
1774
+ this.implementation.finalizeResetTransition(previousBundleName, internal);
1775
+ return true;
1776
+ }
1777
+ if (!internal) {
1778
+ this.implementation.restoreResetState(previousState);
1779
+ this.restoreLiveBundleStateAfterFailedReload();
1780
+ }
1781
+ return false;
1489
1782
  }
1490
1783
 
1491
1784
  @PluginMethod
1492
1785
  public void reset(final PluginCall call) {
1493
1786
  try {
1494
1787
  final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
1495
- if (this._reset(toLastSuccessful)) {
1788
+ final Boolean usePendingBundle = call.getBoolean("usePendingBundle", false);
1789
+ if (this._reset(toLastSuccessful, usePendingBundle)) {
1496
1790
  call.resolve();
1497
1791
  return;
1498
1792
  }
@@ -1510,7 +1804,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1510
1804
  try {
1511
1805
  final JSObject ret = new JSObject();
1512
1806
  final BundleInfo bundle = this.implementation.getCurrentBundle();
1513
- ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
1807
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
1514
1808
  ret.put("native", this.currentVersionNative);
1515
1809
  call.resolve(ret);
1516
1810
  } catch (final Exception e) {
@@ -1528,7 +1822,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1528
1822
  return;
1529
1823
  }
1530
1824
 
1531
- call.resolve(mapToJSObject(bundle.toJSONMap()));
1825
+ call.resolve(InternalUtils.mapToJSObject(bundle.toJSONMap()));
1532
1826
  } catch (final Exception e) {
1533
1827
  logger.error("Could not get next bundle " + e.getMessage());
1534
1828
  call.reject("Could not get next bundle", e);
@@ -1547,7 +1841,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1547
1841
  this.persistLastFailedBundle(null);
1548
1842
 
1549
1843
  final JSObject ret = new JSObject();
1550
- ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
1844
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
1551
1845
  call.resolve(ret);
1552
1846
  } catch (final Exception e) {
1553
1847
  logger.error("Could not get failed update " + e.getMessage());
@@ -1566,19 +1860,45 @@ public class CapacitorUpdaterPlugin extends Plugin {
1566
1860
  public void run() {
1567
1861
  try {
1568
1862
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1569
- JSObject jsRes = mapToJSObject(res);
1570
- if (jsRes.has("error")) {
1571
- String error = jsRes.getString("error");
1863
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1864
+ if (jsRes.has("error") || jsRes.has("kind")) {
1865
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1866
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1572
1867
  String errorMessage = jsRes.has("message")
1573
1868
  ? jsRes.getString("message")
1574
1869
  : "server did not provide a message";
1575
- logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1870
+ int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
1871
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(
1872
+ jsRes.has("kind") ? jsRes.getString("kind") : null
1873
+ );
1874
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
1875
+ CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(
1876
+ kind,
1877
+ error,
1878
+ errorMessage,
1879
+ statusCode,
1880
+ latestVersion,
1881
+ current
1882
+ );
1883
+
1884
+ if ("failed".equals(kind)) {
1885
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1886
+ } else if ("blocked".equals(kind)) {
1887
+ logger.info("Update check blocked with error: " + error);
1888
+ } else {
1889
+ logger.info("No new version available");
1890
+ }
1576
1891
  } else if (jsRes.has("version")) {
1577
1892
  String newVersion = jsRes.getString("version");
1578
1893
  String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
1579
1894
  if (!Objects.equals(newVersion, currentVersion)) {
1580
1895
  logger.info("New version found: " + newVersion);
1581
- CapacitorUpdaterPlugin.this.backgroundDownload();
1896
+ // Check if download is already in progress (with timeout protection)
1897
+ if (!CapacitorUpdaterPlugin.this.isDownloadStuckOrTimedOut()) {
1898
+ CapacitorUpdaterPlugin.this.backgroundDownload();
1899
+ } else {
1900
+ logger.info("Download already in progress, skipping duplicate download request");
1901
+ }
1582
1902
  }
1583
1903
  }
1584
1904
  });
@@ -1603,7 +1923,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1603
1923
  this.semaphoreDown();
1604
1924
  logger.info("semaphoreReady countDown done");
1605
1925
  final JSObject ret = new JSObject();
1606
- ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
1926
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
1607
1927
  call.resolve(ret);
1608
1928
  } catch (final Exception e) {
1609
1929
  logger.error("Failed to notify app ready state. [Error calling 'notifyAppReady()'] " + e.getMessage());
@@ -1689,11 +2009,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
1689
2009
  }
1690
2010
 
1691
2011
  private void checkAppReady() {
2012
+ this.checkAppReady(this.resolveAppReadyCheckTimeoutMs());
2013
+ }
2014
+
2015
+ private void checkAppReady(final long waitTimeMs) {
1692
2016
  try {
1693
2017
  if (this.appReadyCheck != null) {
1694
2018
  this.appReadyCheck.interrupt();
1695
2019
  }
1696
- this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck());
2020
+ this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck(waitTimeMs));
1697
2021
  } catch (final Exception e) {
1698
2022
  logger.error("Failed to start " + DeferredNotifyAppReadyCheck.class.getName() + " " + e.getMessage());
1699
2023
  }
@@ -1708,6 +2032,31 @@ public class CapacitorUpdaterPlugin extends Plugin {
1708
2032
  }
1709
2033
  }
1710
2034
 
2035
+ private String getUpdateResponseKind(final String kind) {
2036
+ if ("up_to_date".equals(kind) || "blocked".equals(kind) || "failed".equals(kind)) {
2037
+ return kind;
2038
+ }
2039
+ return "failed";
2040
+ }
2041
+
2042
+ private void notifyUpdateCheckResult(
2043
+ final String kind,
2044
+ final String error,
2045
+ final String message,
2046
+ final int statusCode,
2047
+ final String version,
2048
+ final BundleInfo current
2049
+ ) {
2050
+ JSObject ret = new JSObject();
2051
+ ret.put("kind", kind);
2052
+ ret.put("error", error);
2053
+ ret.put("message", message);
2054
+ ret.put("statusCode", statusCode);
2055
+ ret.put("version", version);
2056
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
2057
+ this.notifyListeners("updateCheckResult", ret);
2058
+ }
2059
+
1711
2060
  private void ensureBridgeSet() {
1712
2061
  if (this.bridge != null && this.bridge.getWebView() != null) {
1713
2062
  logger.setBridge(this.bridge);
@@ -1745,11 +2094,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1745
2094
  String latestVersionName,
1746
2095
  BundleInfo current,
1747
2096
  Boolean error,
1748
- Boolean isDirectUpdate,
2097
+ Boolean plannedDirectUpdate,
1749
2098
  String failureAction,
1750
2099
  String failureEvent,
1751
2100
  boolean shouldSendStats
1752
2101
  ) {
2102
+ this.consumeOnLaunchDirectUpdateAttempt(Boolean.TRUE.equals(plannedDirectUpdate));
1753
2103
  if (error) {
1754
2104
  logger.info(
1755
2105
  "endBackGroundTaskWithNotif error: " +
@@ -1767,54 +2117,86 @@ public class CapacitorUpdaterPlugin extends Plugin {
1767
2117
  this.notifyListeners(failureEvent, ret);
1768
2118
  }
1769
2119
  final JSObject ret = new JSObject();
1770
- ret.put("bundle", mapToJSObject(current.toJSONMap()));
2120
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
1771
2121
  this.notifyListeners("noNeedUpdate", ret);
1772
- this.sendReadyToJs(current, msg, isDirectUpdate);
2122
+ this.sendReadyToJs(current, msg, plannedDirectUpdate);
1773
2123
  this.backgroundDownloadTask = null;
2124
+ this.downloadStartTimeMs = 0;
1774
2125
  logger.info("endBackGroundTaskWithNotif " + msg);
1775
2126
  }
1776
2127
 
2128
+ private boolean isDownloadStuckOrTimedOut() {
2129
+ if (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive()) {
2130
+ return false;
2131
+ }
2132
+
2133
+ // Check if download has timed out
2134
+ if (this.downloadStartTimeMs > 0) {
2135
+ long elapsed = System.currentTimeMillis() - this.downloadStartTimeMs;
2136
+ if (elapsed > DOWNLOAD_TIMEOUT_MS) {
2137
+ logger.warn(
2138
+ "Download has been in progress for " +
2139
+ elapsed +
2140
+ " ms, exceeding timeout of " +
2141
+ DOWNLOAD_TIMEOUT_MS +
2142
+ " ms. Clearing stuck state."
2143
+ );
2144
+ this.backgroundDownloadTask = null;
2145
+ this.downloadStartTimeMs = 0;
2146
+ return false; // Now it's not stuck anymore, caller can proceed
2147
+ }
2148
+ }
2149
+
2150
+ return true;
2151
+ }
2152
+
1777
2153
  private Thread backgroundDownload() {
1778
2154
  final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
1779
2155
  final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
1780
- this.implementation.directUpdate = initialDirectUpdateAllowed;
1781
2156
  final String messageUpdate = initialDirectUpdateAllowed
1782
2157
  ? "Update will occur now."
1783
2158
  : "Update will occur next time app moves to background.";
1784
- return startNewThread(() -> {
2159
+ Thread newTask = startNewThread(() -> {
1785
2160
  // Wait for cleanup to complete before starting download
1786
2161
  waitForCleanupIfNeeded();
1787
2162
  logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
1788
2163
  try {
1789
2164
  CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1790
- JSObject jsRes = mapToJSObject(res);
2165
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1791
2166
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1792
2167
 
1793
2168
  // Handle network errors and other failures first
1794
- if (jsRes.has("error")) {
1795
- String error = jsRes.getString("error");
2169
+ if (jsRes.has("error") || jsRes.has("kind")) {
2170
+ String error = jsRes.has("error") ? jsRes.getString("error") : "";
1796
2171
  String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1797
2172
  int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
1798
- boolean responseIsOk = statusCode >= 200 && statusCode < 300;
1799
-
1800
- logger.error(
1801
- "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
1802
- );
2173
+ String kind = CapacitorUpdaterPlugin.this.getUpdateResponseKind(jsRes.has("kind") ? jsRes.getString("kind") : null);
1803
2174
  String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
2175
+ CapacitorUpdaterPlugin.this.notifyUpdateCheckResult(kind, error, errorMessage, statusCode, latestVersion, current);
2176
+
2177
+ if ("up_to_date".equals(kind)) {
2178
+ logger.info("No new version available");
2179
+ } else if ("blocked".equals(kind)) {
2180
+ logger.info("Update check blocked with error: " + error);
2181
+ } else {
2182
+ logger.error(
2183
+ "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
2184
+ );
2185
+ }
1804
2186
 
2187
+ boolean isFailure = "failed".equals(kind);
1805
2188
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1806
2189
  errorMessage,
1807
2190
  latestVersion,
1808
2191
  current,
1809
- true,
2192
+ isFailure,
1810
2193
  plannedDirectUpdate,
1811
2194
  "download_fail",
1812
2195
  "downloadFailed",
1813
- !responseIsOk
2196
+ isFailure
1814
2197
  );
1815
2198
  return;
1816
2199
  }
1817
-
1818
2200
  try {
1819
2201
  final String latestVersionName = jsRes.getString("version");
1820
2202
 
@@ -1825,7 +2207,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1825
2207
  );
1826
2208
  if (directUpdateAllowedNow) {
1827
2209
  logger.info("Direct update to builtin version");
1828
- this._reset(false);
2210
+ this._reset(false, false);
1829
2211
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1830
2212
  "Updated to builtin version",
1831
2213
  latestVersionName,
@@ -1845,7 +2227,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1845
2227
  "Next update will be to builtin version",
1846
2228
  latestVersionName,
1847
2229
  current,
1848
- false
2230
+ false,
2231
+ plannedDirectUpdate
1849
2232
  );
1850
2233
  }
1851
2234
  return;
@@ -1869,7 +2252,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1869
2252
  final BundleInfo latest = CapacitorUpdaterPlugin.this.implementation.getBundleInfoByName(latestVersionName);
1870
2253
  if (latest != null) {
1871
2254
  final JSObject ret = new JSObject();
1872
- ret.put("bundle", mapToJSObject(latest.toJSONMap()));
2255
+ ret.put("bundle", InternalUtils.mapToJSObject(latest.toJSONMap()));
1873
2256
  if (latest.isErrorStatus()) {
1874
2257
  logger.error("Latest bundle already exists, and is in error state. Aborting update.");
1875
2258
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
@@ -1881,7 +2264,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1881
2264
  );
1882
2265
  return;
1883
2266
  }
1884
- if (latest.isDownloaded()) {
2267
+ if (latest.isDownloaded() && BundleStatus.DOWNLOADING != latest.getStatus()) {
1885
2268
  logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
1886
2269
  final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
1887
2270
  plannedDirectUpdate
@@ -1902,15 +2285,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
1902
2285
  );
1903
2286
  return;
1904
2287
  }
1905
- CapacitorUpdaterPlugin.this.implementation.set(latest);
1906
- CapacitorUpdaterPlugin.this._reload();
1907
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1908
- "Update installed",
1909
- latestVersionName,
1910
- latest,
1911
- false,
1912
- true
1913
- );
2288
+ if (
2289
+ CapacitorUpdaterPlugin.this.implementation.set(latest) && CapacitorUpdaterPlugin.this._reload()
2290
+ ) {
2291
+ CapacitorUpdaterPlugin.this.notifyBundleSet(latest);
2292
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2293
+ "Update installed",
2294
+ latestVersionName,
2295
+ latest,
2296
+ false,
2297
+ true
2298
+ );
2299
+ } else {
2300
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2301
+ "Update install failed",
2302
+ latestVersionName,
2303
+ latest,
2304
+ true,
2305
+ true
2306
+ );
2307
+ }
1914
2308
  } else {
1915
2309
  if (plannedDirectUpdate && !directUpdateAllowedNow) {
1916
2310
  logger.info(
@@ -1923,7 +2317,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1923
2317
  "update downloaded, will install next background",
1924
2318
  latestVersionName,
1925
2319
  latest,
1926
- false
2320
+ false,
2321
+ plannedDirectUpdate
1927
2322
  );
1928
2323
  }
1929
2324
  return;
@@ -1940,6 +2335,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
1940
2335
  }
1941
2336
  }
1942
2337
  }
2338
+ final boolean retryingInFlightDownload =
2339
+ latest != null &&
2340
+ BundleStatus.DOWNLOADING == latest.getStatus() &&
2341
+ CapacitorUpdaterPlugin.this.isVersionDownloadInProgress(latest.getVersionName());
2342
+ CapacitorUpdaterPlugin.this.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate);
2343
+ CapacitorUpdaterPlugin.this.implementation.directUpdate = retryingInFlightDownload
2344
+ ? Boolean.TRUE.equals(CapacitorUpdaterPlugin.this.implementation.directUpdate) || initialDirectUpdateAllowed
2345
+ : initialDirectUpdateAllowed;
1943
2346
  startNewThread(() -> {
1944
2347
  try {
1945
2348
  logger.info(
@@ -1988,7 +2391,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
1988
2391
  });
1989
2392
  } else {
1990
2393
  logger.info("No need to update, " + current.getId() + " is the latest bundle.");
1991
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif("No need to update", latestVersionName, current, false);
2394
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2395
+ "No need to update",
2396
+ latestVersionName,
2397
+ current,
2398
+ false,
2399
+ plannedDirectUpdate
2400
+ );
1992
2401
  }
1993
2402
  } catch (final Exception e) {
1994
2403
  logger.error("error in update check " + e.getMessage());
@@ -2013,6 +2422,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
2013
2422
  );
2014
2423
  }
2015
2424
  });
2425
+ this.backgroundDownloadTask = newTask;
2426
+ this.downloadStartTimeMs = System.currentTimeMillis();
2427
+ return newTask;
2016
2428
  }
2017
2429
 
2018
2430
  private void installNext() {
@@ -2029,15 +2441,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
2029
2441
  if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
2030
2442
  // There is a next bundle waiting for activation
2031
2443
  logger.debug("Next bundle is: " + next.getVersionName());
2032
- if (this.implementation.set(next) && this._reload()) {
2033
- logger.info("Updated to bundle: " + next.getVersionName());
2034
- this.implementation.setNextBundle(null);
2035
- } else {
2036
- logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
2037
- }
2444
+ startNewThread(() -> {
2445
+ if (this.implementation.set(next) && this._reload()) {
2446
+ logger.info("Updated to bundle: " + next.getVersionName());
2447
+ this.notifyBundleSet(next);
2448
+ this.implementation.setNextBundle(null);
2449
+ } else {
2450
+ logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
2451
+ }
2452
+ });
2038
2453
  }
2039
2454
  } catch (final Exception e) {
2040
- logger.error("Error during onActivityStopped " + e.getMessage());
2455
+ logger.error("Error during installNext " + e);
2041
2456
  }
2042
2457
  }
2043
2458
 
@@ -2055,12 +2470,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
2055
2470
  logger.error("notifyAppReady was not called, roll back current bundle: " + current.getId());
2056
2471
  logger.info("Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
2057
2472
  final JSObject ret = new JSObject();
2058
- ret.put("bundle", mapToJSObject(current.toJSONMap()));
2473
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
2059
2474
  this.persistLastFailedBundle(current);
2060
2475
  this.notifyListeners("updateFailed", ret);
2061
2476
  this.implementation.sendStats("update_fail", current.getVersionName());
2062
2477
  this.implementation.setError(current);
2063
- this._reset(true);
2478
+ this.performReset(true, false, true);
2064
2479
  if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
2065
2480
  logger.info("Deleting failing bundle: " + current.getVersionName());
2066
2481
  try {
@@ -2079,11 +2494,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
2079
2494
 
2080
2495
  private class DeferredNotifyAppReadyCheck implements Runnable {
2081
2496
 
2497
+ private final long waitTimeMs;
2498
+
2499
+ DeferredNotifyAppReadyCheck(final long waitTimeMs) {
2500
+ this.waitTimeMs = waitTimeMs;
2501
+ }
2502
+
2082
2503
  @Override
2083
2504
  public void run() {
2084
2505
  try {
2085
- logger.info("Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady");
2086
- Thread.sleep(CapacitorUpdaterPlugin.this.appReadyTimeout);
2506
+ logger.info("Wait for " + this.waitTimeMs + "ms, then check for notifyAppReady");
2507
+ Thread.sleep(this.waitTimeMs);
2087
2508
  CapacitorUpdaterPlugin.this.checkRevert();
2088
2509
  CapacitorUpdaterPlugin.this.appReadyCheck = null;
2089
2510
  } catch (final InterruptedException e) {
@@ -2093,16 +2514,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
2093
2514
  }
2094
2515
 
2095
2516
  public void appMovedToForeground() {
2517
+ // Ensure activity reference is up-to-date before proceeding
2518
+ // This is critical for callbacks that may be invoked during background operations
2519
+ try {
2520
+ Activity currentActivity = this.getActivity();
2521
+ if (currentActivity != null) {
2522
+ CapacitorUpdaterPlugin.this.implementation.activity = currentActivity;
2523
+ } else {
2524
+ logger.warn("appMovedToForeground: Activity is null, operations may be limited");
2525
+ }
2526
+ } catch (Exception e) {
2527
+ logger.error("appMovedToForeground: Failed to update activity reference: " + e.getMessage());
2528
+ }
2529
+
2096
2530
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2097
2531
  CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_foreground", current.getVersionName());
2098
2532
  this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
2099
2533
  this.delayUpdateUtils.unsetBackgroundTimestamp();
2100
2534
 
2101
- if (
2102
- CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() &&
2103
- (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive())
2104
- ) {
2105
- this.backgroundDownloadTask = this.backgroundDownload();
2535
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && !this.isDownloadStuckOrTimedOut()) {
2536
+ this.backgroundDownload();
2106
2537
  } else {
2107
2538
  final CapConfig config = CapConfig.loadDefault(this.getActivity());
2108
2539
  String serverUrl = config.getServerUrl();
@@ -2119,6 +2550,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
2119
2550
  // Reset timeout flag at start of each background cycle
2120
2551
  this.autoSplashscreenTimedOut = false;
2121
2552
 
2553
+ // Ensure activity reference is up-to-date before proceeding
2554
+ try {
2555
+ Activity currentActivity = this.getActivity();
2556
+ if (currentActivity != null) {
2557
+ CapacitorUpdaterPlugin.this.implementation.activity = currentActivity;
2558
+ } else {
2559
+ logger.warn("appMovedToBackground: Activity is null, operations may be limited");
2560
+ }
2561
+ } catch (Exception e) {
2562
+ logger.error("appMovedToBackground: Failed to update activity reference: " + e.getMessage());
2563
+ }
2564
+
2122
2565
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2123
2566
 
2124
2567
  // Show splashscreen FIRST, before any other background work to ensure launcher shows it
@@ -2307,6 +2750,32 @@ public class CapacitorUpdaterPlugin extends Plugin {
2307
2750
  }
2308
2751
  }
2309
2752
 
2753
+ @PluginMethod
2754
+ public void setShakeChannelSelector(final PluginCall call) {
2755
+ final Boolean enabled = call.getBoolean("enabled");
2756
+ if (enabled == null) {
2757
+ logger.error("setShakeChannelSelector called without enabled parameter");
2758
+ call.reject("setShakeChannelSelector called without enabled parameter");
2759
+ return;
2760
+ }
2761
+
2762
+ this.shakeChannelSelectorEnabled = enabled;
2763
+ logger.info("Shake channel selector " + (enabled ? "enabled" : "disabled"));
2764
+ call.resolve();
2765
+ }
2766
+
2767
+ @PluginMethod
2768
+ public void isShakeChannelSelectorEnabled(final PluginCall call) {
2769
+ try {
2770
+ final JSObject ret = new JSObject();
2771
+ ret.put("enabled", this.shakeChannelSelectorEnabled);
2772
+ call.resolve(ret);
2773
+ } catch (final Exception e) {
2774
+ logger.error("Could not get shake channel selector status " + e.getMessage());
2775
+ call.reject("Could not get shake channel selector status", e);
2776
+ }
2777
+ }
2778
+
2310
2779
  @PluginMethod
2311
2780
  public void getAppId(final PluginCall call) {
2312
2781
  try {
@@ -2388,9 +2857,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
2388
2857
 
2389
2858
  JSObject result = new JSObject();
2390
2859
  try {
2391
- PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0);
2860
+ PackageInfo pInfo = getCurrentPackageInfo();
2392
2861
  result.put("currentVersionName", pInfo.versionName);
2393
- result.put("currentVersionCode", String.valueOf(pInfo.versionCode));
2862
+ result.put("currentVersionCode", getVersionCode(pInfo));
2394
2863
  } catch (PackageManager.NameNotFoundException e) {
2395
2864
  result.put("currentVersionName", "0.0.0");
2396
2865
  result.put("currentVersionCode", "0");