@capgo/capacitor-updater 8.41.3 → 8.41.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -252,6 +252,10 @@ Capacitor Updater works by unzipping a compiled app bundle to the native device
252
252
  - Do not password encrypt the bundle zip file, or it will fail to unpack.
253
253
  - Make sure the bundle does not contain any extra hidden files or folders, or it may fail to unpack.
254
254
 
255
+ ### Downgrading to a previous version of the updater plugin
256
+
257
+ Downgrading to a previous version of the updater plugin is not supported.
258
+
255
259
  ## Updater Plugin Config
256
260
 
257
261
  <docgen-config>
@@ -50,7 +50,9 @@ repositories {
50
50
 
51
51
  dependencies {
52
52
  def work_version = "2.10.5"
53
+ def lifecycle_version = "2.8.7"
53
54
  implementation "androidx.work:work-runtime:$work_version"
55
+ implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
54
56
  implementation "com.google.android.gms:play-services-tasks:18.4.0"
55
57
  implementation "com.google.guava:guava:33.5.0-android"
56
58
  implementation fileTree(dir: 'libs', include: ['*.jar'])
@@ -0,0 +1,88 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ package ee.forgr.capacitor_updater;
8
+
9
+ import androidx.annotation.NonNull;
10
+ import androidx.lifecycle.DefaultLifecycleObserver;
11
+ import androidx.lifecycle.LifecycleOwner;
12
+ import androidx.lifecycle.ProcessLifecycleOwner;
13
+
14
+ /**
15
+ * Observes app-level lifecycle events using ProcessLifecycleOwner.
16
+ * This provides reliable detection of when the entire app (not just an activity)
17
+ * moves to foreground or background.
18
+ *
19
+ * Unlike activity lifecycle callbacks, this won't trigger false positives when:
20
+ * - Opening a native camera view
21
+ * - Opening share sheets
22
+ * - Any other native activity overlays within the app
23
+ *
24
+ * The ON_STOP event is dispatched with a ~700ms delay after the last activity
25
+ * passes through onStop(), which helps avoid false triggers during configuration
26
+ * changes or quick activity transitions.
27
+ */
28
+ public class AppLifecycleObserver implements DefaultLifecycleObserver {
29
+
30
+ public interface AppLifecycleListener {
31
+ void onAppMovedToForeground();
32
+ void onAppMovedToBackground();
33
+ }
34
+
35
+ private final AppLifecycleListener listener;
36
+ private final Logger logger;
37
+ private boolean isRegistered = false;
38
+
39
+ public AppLifecycleObserver(AppLifecycleListener listener, Logger logger) {
40
+ this.listener = listener;
41
+ this.logger = logger;
42
+ }
43
+
44
+ public void register() {
45
+ if (isRegistered) {
46
+ return;
47
+ }
48
+ try {
49
+ ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
50
+ isRegistered = true;
51
+ logger.info("AppLifecycleObserver registered with ProcessLifecycleOwner");
52
+ } catch (Exception e) {
53
+ logger.error("Failed to register AppLifecycleObserver: " + e.getMessage());
54
+ }
55
+ }
56
+
57
+ public void unregister() {
58
+ if (!isRegistered) {
59
+ return;
60
+ }
61
+ try {
62
+ ProcessLifecycleOwner.get().getLifecycle().removeObserver(this);
63
+ isRegistered = false;
64
+ logger.info("AppLifecycleObserver unregistered from ProcessLifecycleOwner");
65
+ } catch (Exception e) {
66
+ logger.error("Failed to unregister AppLifecycleObserver: " + e.getMessage());
67
+ }
68
+ }
69
+
70
+ @Override
71
+ public void onStart(@NonNull LifecycleOwner owner) {
72
+ // App moved to foreground (at least one activity is visible)
73
+ logger.info("ProcessLifecycleOwner: App moved to foreground");
74
+ if (listener != null) {
75
+ listener.onAppMovedToForeground();
76
+ }
77
+ }
78
+
79
+ @Override
80
+ public void onStop(@NonNull LifecycleOwner owner) {
81
+ // App moved to background (no activities are visible)
82
+ // Note: This is called with a ~700ms delay to avoid false triggers
83
+ logger.info("ProcessLifecycleOwner: App moved to background");
84
+ if (listener != null) {
85
+ listener.onAppMovedToBackground();
86
+ }
87
+ }
88
+ }
@@ -7,7 +7,6 @@
7
7
  package ee.forgr.capacitor_updater;
8
8
 
9
9
  import android.app.Activity;
10
- import android.app.ActivityManager;
11
10
  import android.content.Context;
12
11
  import android.content.Intent;
13
12
  import android.content.SharedPreferences;
@@ -85,7 +84,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
85
84
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
86
85
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
87
86
 
88
- private final String pluginVersion = "8.41.3";
87
+ private final String pluginVersion = "8.41.5";
89
88
  private static final String DELAY_CONDITION_PREFERENCES = "";
90
89
 
91
90
  private SharedPreferences.Editor editor;
@@ -116,6 +115,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
116
115
  private Boolean allowManualBundleError = false;
117
116
  private Boolean allowSetDefaultChannel = true;
118
117
 
118
+ // Used for activity-based foreground/background detection on Android < 14
119
119
  private Boolean isPreviousMainActivity = true;
120
120
 
121
121
  private volatile Thread backgroundDownloadTask;
@@ -138,6 +138,9 @@ public class CapacitorUpdaterPlugin extends Plugin {
138
138
  private FrameLayout splashscreenLoaderOverlay;
139
139
  private Runnable splashscreenTimeoutRunnable;
140
140
 
141
+ // App lifecycle observer using ProcessLifecycleOwner for reliable foreground/background detection
142
+ private AppLifecycleObserver appLifecycleObserver;
143
+
141
144
  // Play Store In-App Updates
142
145
  private AppUpdateManager appUpdateManager;
143
146
  private AppUpdateInfo cachedAppUpdateInfo;
@@ -431,6 +434,31 @@ public class CapacitorUpdaterPlugin extends Plugin {
431
434
  this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
432
435
 
433
436
  this.checkForUpdateAfterDelay();
437
+
438
+ // On Android 14+ (API 34+), topActivity in RecentTaskInfo returns null due to
439
+ // security restrictions (StrandHogg task hijacking mitigations). Use ProcessLifecycleOwner
440
+ // for reliable app-level foreground/background detection on these versions.
441
+ // On older versions, we use the traditional activity lifecycle callbacks in handleOnStart/handleOnStop.
442
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
443
+ this.appLifecycleObserver = new AppLifecycleObserver(
444
+ new AppLifecycleObserver.AppLifecycleListener() {
445
+ @Override
446
+ public void onAppMovedToForeground() {
447
+ CapacitorUpdaterPlugin.this.appMovedToForeground();
448
+ }
449
+
450
+ @Override
451
+ public void onAppMovedToBackground() {
452
+ CapacitorUpdaterPlugin.this.appMovedToBackground();
453
+ }
454
+ },
455
+ logger
456
+ );
457
+ this.appLifecycleObserver.register();
458
+ logger.info("Using ProcessLifecycleOwner for foreground/background detection (Android 14+)");
459
+ } else {
460
+ logger.info("Using activity lifecycle callbacks for foreground/background detection (Android <14)");
461
+ }
434
462
  }
435
463
 
436
464
  private void semaphoreWait(Number waitTime) {
@@ -2129,16 +2157,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
2129
2157
  }
2130
2158
  }
2131
2159
 
2160
+ /**
2161
+ * Check if the current activity is the main activity.
2162
+ * Used for activity-based foreground/background detection on Android < 14.
2163
+ * On Android 14+, topActivity returns null due to security restrictions, so we use
2164
+ * ProcessLifecycleOwner instead.
2165
+ */
2132
2166
  private boolean isMainActivity() {
2133
2167
  try {
2134
2168
  Context mContext = this.getContext();
2135
- ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
2136
- List<ActivityManager.AppTask> runningTasks = activityManager.getAppTasks();
2169
+ android.app.ActivityManager activityManager = (android.app.ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
2170
+ java.util.List<android.app.ActivityManager.AppTask> runningTasks = activityManager.getAppTasks();
2137
2171
  if (runningTasks.isEmpty()) {
2138
2172
  return false;
2139
2173
  }
2140
- ActivityManager.RecentTaskInfo runningTask = runningTasks.get(0).getTaskInfo();
2141
- String className = Objects.requireNonNull(runningTask.baseIntent.getComponent()).getClassName();
2174
+ android.app.ActivityManager.RecentTaskInfo runningTask = runningTasks.get(0).getTaskInfo();
2175
+ String className = java.util.Objects.requireNonNull(runningTask.baseIntent.getComponent()).getClassName();
2142
2176
  if (runningTask.topActivity == null) {
2143
2177
  return false;
2144
2178
  }
@@ -2152,12 +2186,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
2152
2186
  @Override
2153
2187
  public void handleOnStart() {
2154
2188
  try {
2155
- if (isPreviousMainActivity) {
2156
- logger.info("handleOnStart: appMovedToForeground");
2157
- this.appMovedToForeground();
2158
- }
2159
2189
  logger.info("handleOnStart: onActivityStarted " + getActivity().getClass().getName());
2160
- isPreviousMainActivity = true;
2190
+
2191
+ // On Android < 14, use activity lifecycle for foreground detection
2192
+ // On Android 14+, ProcessLifecycleOwner handles this via AppLifecycleObserver
2193
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
2194
+ if (isPreviousMainActivity) {
2195
+ logger.info("handleOnStart: appMovedToForeground (Android <14 path)");
2196
+ this.appMovedToForeground();
2197
+ }
2198
+ isPreviousMainActivity = true;
2199
+ }
2161
2200
 
2162
2201
  // Initialize shake menu if enabled and activity is BridgeActivity
2163
2202
  if (shakeMenuEnabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
@@ -2176,10 +2215,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
2176
2215
  @Override
2177
2216
  public void handleOnStop() {
2178
2217
  try {
2179
- isPreviousMainActivity = isMainActivity();
2180
- if (isPreviousMainActivity) {
2181
- logger.info("handleOnStop: appMovedToBackground");
2182
- this.appMovedToBackground();
2218
+ logger.info("handleOnStop: onActivityStopped");
2219
+
2220
+ // On Android < 14, use activity lifecycle for background detection
2221
+ // On Android 14+, ProcessLifecycleOwner handles this via AppLifecycleObserver
2222
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
2223
+ isPreviousMainActivity = isMainActivity();
2224
+ if (isPreviousMainActivity) {
2225
+ logger.info("handleOnStop: appMovedToBackground (Android <14 path)");
2226
+ this.appMovedToBackground();
2227
+ }
2183
2228
  }
2184
2229
  } catch (Exception e) {
2185
2230
  logger.error("Failed to run handleOnStop: " + e.getMessage());
@@ -2604,6 +2649,17 @@ public class CapacitorUpdaterPlugin extends Plugin {
2604
2649
  logger.error("Failed to clean up shake menu: " + e.getMessage());
2605
2650
  }
2606
2651
  }
2652
+
2653
+ // Clean up app lifecycle observer
2654
+ if (appLifecycleObserver != null) {
2655
+ try {
2656
+ appLifecycleObserver.unregister();
2657
+ appLifecycleObserver = null;
2658
+ logger.info("AppLifecycleObserver cleaned up");
2659
+ } catch (Exception e) {
2660
+ logger.error("Failed to clean up AppLifecycleObserver: " + e.getMessage());
2661
+ }
2662
+ }
2607
2663
  } catch (Exception e) {
2608
2664
  logger.error("Failed to run handleOnDestroy: " + e.getMessage());
2609
2665
  }
@@ -60,7 +60,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
60
60
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
61
61
  ]
62
62
  public var implementation = CapgoUpdater()
63
- private let pluginVersion: String = "8.41.3"
63
+ private let pluginVersion: String = "8.41.5"
64
64
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
65
65
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
66
66
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -382,6 +382,32 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
382
382
  self.logger.info("Cleanup complete")
383
383
  }
384
384
 
385
+ // Michael (WcaleNieWolny) at 04.01.2026
386
+ // The following line of code contains a bug. After having evaluated it, I have decided not to fix it.
387
+ // The initial report: https://discord.com/channels/912707985829163099/1456985639345061969
388
+ // The bug happens in a very specific scenario. Here is the reproduction steps, followed by the lackof busniess impact
389
+ // Reproduction steps:
390
+ // 1. Install iOS app via app store. Version: 10.13.0. Version v10 of the app uses Capacitor 6 (6.3.13) - a version where the key was still "LatestVersionNative"
391
+ // 2. The plugin writes "10.13.0" to the key "LatestVersionNative"
392
+ // 3. Update the app to version 10.17.0 via Capgo.
393
+ // 4. Update the app via testflight to version 11.0.0. This version uses Capacitor 8 (8.41.3) - a version where the key was changed to "LatestNativeBuildVersion"
394
+ // 5. During the initial load of then new native version, the plugin will read "LatestNativeBuildVersion", not find it, read "LatestVersionNative", find it and revert to builtin version sucessfully.
395
+ // 6. The plugin writes "11.0.0" to the key "LatestNativeBuildVersion"
396
+ // 7. The app is now in a state where it is using the builtin version, but the key "LatestNativeBuildVersion" is still set to "11.0.0" and "LatestVersionNative" is still set to "10.13.0".
397
+ // 8. The user downgrades using app store back to version 10.13.0.
398
+ // 9. The old plugin reads "LatestVersionNative", finds "10.13.0," so it doesn't revert to builtin version. // <--- THIS IS THE FIRST PART OF THE BUG
399
+ // 10. "LatestVersionNative" is written to "10.13.0" but "LatestNativeBuildVersion" is not touched, and stays at "11.0.0"
400
+ // 11. A capgo update happesn to version 10.17.0.
401
+ // 12. The user updates again to version 11.0.0 via Testflight.
402
+ // 13. The plugin reads "LatestNativeBuildVersion", finds "11.0.0", so it doesn't revert to builtin version. It is unaware of the native update that happended.
403
+ // 14. Capgo loads the 10.13.0 version, while it should have loaded the builtin 11.0.0 version. // <--- THIS IS THE SECOND PART OF THE BUG
404
+ // The business impact:
405
+ // None - no one will ever be affected by this bug as reverting via app store should in practice never happen. You are not SUPPOSE to go from Capacitor v8 to v6.
406
+ // Downgrading isn't supported.
407
+ // Possible fixes:
408
+ // 1. Write "LatestVersionNative" - this fixes the part 1 of this bug
409
+ // 2. Compare both keys. If any is not equal to "currentBuildVersion", then revert to builtin version. This fixes the part 2 of this bug
410
+
385
411
  let previous = UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
386
412
  if previous != "0" && self.currentBuildVersion != previous {
387
413
  _ = self._reset(toLastSuccessful: false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.41.3",
3
+ "version": "8.41.5",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",