@capgo/capacitor-updater 4.41.0 → 4.43.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.
Files changed (67) hide show
  1. package/CapgoCapacitorUpdater.podspec +7 -5
  2. package/Package.swift +40 -0
  3. package/README.md +1913 -303
  4. package/android/build.gradle +41 -8
  5. package/android/proguard-rules.pro +45 -0
  6. package/android/src/main/AndroidManifest.xml +1 -3
  7. package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +88 -0
  8. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +223 -195
  9. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +23 -23
  10. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +13 -0
  11. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +2720 -1242
  12. package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1854 -0
  13. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +359 -121
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +28 -0
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +44 -49
  16. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
  17. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +296 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +215 -0
  19. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +858 -117
  20. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +156 -0
  21. package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +45 -0
  22. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +360 -0
  23. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  24. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +603 -0
  25. package/dist/docs.json +3022 -765
  26. package/dist/esm/definitions.d.ts +1717 -198
  27. package/dist/esm/definitions.js +103 -1
  28. package/dist/esm/definitions.js.map +1 -1
  29. package/dist/esm/history.d.ts +1 -0
  30. package/dist/esm/history.js +283 -0
  31. package/dist/esm/history.js.map +1 -0
  32. package/dist/esm/index.d.ts +3 -2
  33. package/dist/esm/index.js +5 -4
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/web.d.ts +43 -42
  36. package/dist/esm/web.js +122 -37
  37. package/dist/esm/web.js.map +1 -1
  38. package/dist/plugin.cjs.js +512 -37
  39. package/dist/plugin.cjs.js.map +1 -1
  40. package/dist/plugin.js +512 -37
  41. package/dist/plugin.js.map +1 -1
  42. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +87 -0
  43. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
  44. package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +177 -0
  45. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +12 -12
  46. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +2020 -0
  47. package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1959 -0
  48. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +313 -0
  49. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +257 -0
  50. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  51. package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +392 -0
  52. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  53. package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
  54. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +441 -0
  55. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +1 -2
  56. package/package.json +49 -41
  57. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +0 -1131
  58. package/ios/Plugin/BundleInfo.swift +0 -113
  59. package/ios/Plugin/CapacitorUpdater.swift +0 -850
  60. package/ios/Plugin/CapacitorUpdaterPlugin.h +0 -10
  61. package/ios/Plugin/CapacitorUpdaterPlugin.m +0 -27
  62. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -678
  63. package/ios/Plugin/CryptoCipher.swift +0 -240
  64. /package/{LICENCE → LICENSE} +0 -0
  65. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  66. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  67. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
@@ -7,1298 +7,2776 @@
7
7
  package ee.forgr.capacitor_updater;
8
8
 
9
9
  import android.app.Activity;
10
- import android.app.ActivityManager;
11
- import android.app.Application;
12
10
  import android.content.Context;
11
+ import android.content.Intent;
13
12
  import android.content.SharedPreferences;
14
13
  import android.content.pm.PackageInfo;
15
14
  import android.content.pm.PackageManager;
15
+ import android.graphics.Color;
16
+ import android.net.Uri;
16
17
  import android.os.Build;
17
- import android.os.Bundle;
18
- import android.util.Log;
19
- import androidx.annotation.NonNull;
20
- import androidx.annotation.Nullable;
21
- import com.android.volley.toolbox.Volley;
18
+ import android.os.Handler;
19
+ import android.os.Looper;
20
+ import android.view.Gravity;
21
+ import android.view.View;
22
+ import android.view.ViewGroup;
23
+ import android.widget.FrameLayout;
24
+ import android.widget.ProgressBar;
22
25
  import com.getcapacitor.CapConfig;
23
26
  import com.getcapacitor.JSArray;
24
27
  import com.getcapacitor.JSObject;
25
28
  import com.getcapacitor.Plugin;
26
29
  import com.getcapacitor.PluginCall;
30
+ import com.getcapacitor.PluginHandle;
27
31
  import com.getcapacitor.PluginMethod;
28
32
  import com.getcapacitor.annotation.CapacitorPlugin;
29
33
  import com.getcapacitor.plugin.WebView;
30
- import com.google.gson.Gson;
31
- import com.google.gson.reflect.TypeToken;
34
+ import com.google.android.gms.tasks.Task;
35
+ // Play Store In-App Updates
36
+ import com.google.android.play.core.appupdate.AppUpdateInfo;
37
+ import com.google.android.play.core.appupdate.AppUpdateManager;
38
+ import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
39
+ import com.google.android.play.core.appupdate.AppUpdateOptions;
40
+ import com.google.android.play.core.install.InstallState;
41
+ import com.google.android.play.core.install.InstallStateUpdatedListener;
42
+ import com.google.android.play.core.install.model.AppUpdateType;
43
+ import com.google.android.play.core.install.model.InstallStatus;
44
+ import com.google.android.play.core.install.model.UpdateAvailability;
32
45
  import io.github.g00fy2.versioncompare.Version;
33
46
  import java.io.IOException;
34
- import java.lang.reflect.Type;
35
47
  import java.net.MalformedURLException;
36
48
  import java.net.URL;
37
- import java.text.SimpleDateFormat;
38
49
  import java.util.ArrayList;
50
+ import java.util.Arrays;
39
51
  import java.util.Date;
40
- import java.util.Iterator;
52
+ import java.util.HashSet;
41
53
  import java.util.List;
54
+ import java.util.Map;
55
+ import java.util.Objects;
56
+ import java.util.Set;
57
+ import java.util.Timer;
58
+ import java.util.TimerTask;
42
59
  import java.util.UUID;
60
+ import java.util.concurrent.Phaser;
61
+ import java.util.concurrent.Semaphore;
62
+ import java.util.concurrent.TimeUnit;
63
+ import java.util.concurrent.TimeoutException;
64
+ import java.util.concurrent.atomic.AtomicReference;
65
+ // Removed OkHttpClient and Protocol imports - using shared client in DownloadService instead
66
+ import org.json.JSONArray;
43
67
  import org.json.JSONException;
68
+ import org.json.JSONObject;
44
69
 
45
70
  @CapacitorPlugin(name = "CapacitorUpdater")
46
- public class CapacitorUpdaterPlugin
47
- extends Plugin
48
- implements Application.ActivityLifecycleCallbacks {
49
-
50
- private static final String updateUrlDefault =
51
- "https://api.capgo.app/updates";
52
- private static final String statsUrlDefault = "https://api.capgo.app/stats";
53
- private static final String defaultPrivateKey =
54
- "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA4pW9olT0FBXXivRCzd3xcImlWZrqkwcF2xTkX/FwXmj9eh9H\nkBLrsQmfsC+PJisRXIOGq6a0z3bsGq6jBpp3/Jr9jiaW5VuPGaKeMaZZBRvi/N5f\nIMG3hZXSOcy0IYg+E1Q7RkYO1xq5GLHseqG+PXvJsNe4R8R/Bmd/ngq0xh/cvcrH\nHpXwO0Aj9tfprlb+rHaVV79EkVRWYPidOLnK1n0EFHFJ1d/MyDIp10TEGm2xHpf/\nBrlb1an8wXEuzoC0DgYaczgTjovwR+ewSGhSHJliQdM0Qa3o1iN87DldWtydImMs\nPjJ3DUwpsjAMRe5X8Et4+udFW2ciYnQo9H0CkwIDAQABAoIBAQCtjlMV/4qBxAU4\nu0ZcWA9yywwraX0aJ3v1xrfzQYV322Wk4Ea5dbSxA5UcqCE29DA1M824t1Wxv/6z\npWbcTP9xLuresnJMtmgTE7umfiubvTONy2sENT20hgDkIwcq1CfwOEm61zjQzPhQ\nkSB5AmEsyR/BZEsUNc+ygR6AWOUFB7tj4yMc32LOTWSbE/znnF2BkmlmnQykomG1\n2oVqM3lUFP7+m8ux1O7scO6IMts+Z/eFXjWfxpbebUSvSIR83GXPQZ34S/c0ehOg\nyHdmCSOel1r3VvInMe+30j54Jr+Ml/7Ee6axiwyE2e/bd85MsK9sVdp0OtelXaqA\nOZZqWvN5AoGBAP2Hn3lSq+a8GsDH726mHJw60xM0LPbVJTYbXsmQkg1tl3NKJTMM\nQqz41+5uys+phEgLHI9gVJ0r+HaGHXnJ4zewlFjsudstb/0nfctUvTqnhEhfNo9I\ny4kufVKPRF3sMEeo7CDVJs4GNBLycEyIBy6Mbv0VcO7VaZqggRwu4no9AoGBAOTK\n6NWYs1BWlkua2wmxexGOzehNGedInp0wGr2l4FDayWjkZLqvB+nNXUQ63NdHlSs4\nWB2Z1kQXZxVaI2tPYexGUKXEo2uFob63uflbuE029ovDXIIPFTPtGNdNXwhHT5a+\nPhmy3sMc+s2BSNM5qaNmfxQxhdd6gRU6oikE+c0PAoGAMn3cKNFqIt27hkFLUgIL\nGKIuf1iYy9/PNWNmEUaVj88PpopRtkTu0nwMpROzmH/uNFriKTvKHjMvnItBO4wV\nkHW+VadvrFL0Rrqituf9d7z8/1zXBNo+juePVe3qc7oiM2NVA4Tv4YAixtM5wkQl\nCgQ15nlqsGYYTg9BJ1e/CxECgYEAjEYPzO2reuUrjr0p8F59ev1YJ0YmTJRMk0ks\nC/yIdGo/tGzbiU3JB0LfHPcN8Xu07GPGOpfYM7U5gXDbaG6qNgfCaHAQVdr/mQPi\nJQ1kCQtay8QCkscWk9iZM1//lP7LwDtxraXqSCwbZSYP9VlUNZeg8EuQqNU2EUL6\nqzWexmcCgYEA0prUGNBacraTYEknB1CsbP36UPWsqFWOvevlz+uEC5JPxPuW5ZHh\nSQN7xl6+PHyjPBM7ttwPKyhgLOVTb3K7ex/PXnudojMUK5fh7vYfChVTSlx2p6r0\nDi58PdD+node08cJH+ie0Yphp7m+D4+R9XD0v0nEvnu4BtAW6DrJasw=\n-----END RSA PRIVATE KEY-----\n";
55
- private static final String channelUrlDefault =
56
- "https://api.capgo.app/channel_self";
57
-
58
- private final String PLUGIN_VERSION = "4.41.0";
59
- private static final String DELAY_CONDITION_PREFERENCES = "";
60
-
61
- private SharedPreferences.Editor editor;
62
- private SharedPreferences prefs;
63
- private CapacitorUpdater implementation;
64
-
65
- private Integer appReadyTimeout = 10000;
66
- private Boolean autoDeleteFailed = true;
67
- private Boolean autoDeletePrevious = true;
68
- private Boolean autoUpdate = false;
69
- private String updateUrl = "";
70
- private Version currentVersionNative;
71
- private Boolean resetWhenUpdate = true;
72
- private Thread backgroundTask;
73
- private Boolean taskRunning = false;
74
-
75
- private Boolean isPreviousMainActivity = true;
76
-
77
- private volatile Thread appReadyCheck;
78
-
79
- @Override
80
- public void load() {
81
- super.load();
82
- this.prefs =
83
- this.getContext()
84
- .getSharedPreferences(
85
- WebView.WEBVIEW_PREFS_NAME,
86
- Activity.MODE_PRIVATE
87
- );
88
- this.editor = this.prefs.edit();
89
-
90
- try {
91
- this.implementation =
92
- new CapacitorUpdater() {
93
- @Override
94
- public void notifyDownload(final String id, final int percent) {
95
- CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
96
- }
97
-
98
- @Override
99
- public void notifyListeners(final String id, final JSObject res) {
100
- CapacitorUpdaterPlugin.this.notifyListeners(id, res);
101
- }
102
- };
103
- final PackageInfo pInfo =
104
- this.getContext()
105
- .getPackageManager()
106
- .getPackageInfo(this.getContext().getPackageName(), 0);
107
- this.implementation.activity = this.getActivity();
108
- this.implementation.versionBuild = pInfo.versionName;
109
- this.implementation.PLUGIN_VERSION = this.PLUGIN_VERSION;
110
- this.implementation.versionCode = Integer.toString(pInfo.versionCode);
111
- this.implementation.requestQueue =
112
- Volley.newRequestQueue(this.getContext());
113
- this.currentVersionNative =
114
- new Version(this.getConfig().getString("version", pInfo.versionName));
115
- } catch (final PackageManager.NameNotFoundException e) {
116
- Log.e(CapacitorUpdater.TAG, "Error instantiating implementation", e);
117
- return;
118
- } catch (final Exception e) {
119
- Log.e(
120
- CapacitorUpdater.TAG,
121
- "Error getting current native app version",
122
- e
123
- );
124
- return;
125
- }
126
-
127
- final CapConfig config = CapConfig.loadDefault(this.getActivity());
128
- this.implementation.appId = config.getString("appId", "");
129
- this.implementation.privateKey =
130
- this.getConfig().getString("privateKey", defaultPrivateKey);
131
- this.implementation.statsUrl =
132
- this.getConfig().getString("statsUrl", statsUrlDefault);
133
- this.implementation.channelUrl =
134
- this.getConfig().getString("channelUrl", channelUrlDefault);
135
- this.implementation.documentsDir = this.getContext().getFilesDir();
136
- this.implementation.prefs = this.prefs;
137
- this.implementation.editor = this.editor;
138
- this.implementation.versionOs = Build.VERSION.RELEASE;
139
- this.implementation.deviceID =
140
- this.prefs.getString("appUUID", UUID.randomUUID().toString());
141
- this.editor.putString("appUUID", this.implementation.deviceID);
142
- Log.i(
143
- CapacitorUpdater.TAG,
144
- "init for device " + this.implementation.deviceID
145
- );
146
-
147
- this.autoDeleteFailed =
148
- this.getConfig().getBoolean("autoDeleteFailed", true);
149
- this.autoDeletePrevious =
150
- this.getConfig().getBoolean("autoDeletePrevious", true);
151
- this.updateUrl = this.getConfig().getString("updateUrl", updateUrlDefault);
152
- this.autoUpdate = this.getConfig().getBoolean("autoUpdate", true);
153
- this.appReadyTimeout = this.getConfig().getInt("appReadyTimeout", 10000);
154
- this.resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
155
-
156
- if (this.resetWhenUpdate) {
157
- this.cleanupObsoleteVersions();
158
- }
159
- final Application application = (Application) this.getContext()
160
- .getApplicationContext();
161
- application.registerActivityLifecycleCallbacks(this);
162
- }
163
-
164
- private void cleanupObsoleteVersions() {
165
- try {
166
- final Version previous = new Version(
167
- this.prefs.getString("LatestVersionNative", "")
168
- );
169
- try {
170
- if (
171
- !"".equals(previous.getOriginalString()) &&
172
- this.currentVersionNative.getMajor() > previous.getMajor()
173
- ) {
174
- Log.i(
175
- CapacitorUpdater.TAG,
176
- "New native major version detected: " + this.currentVersionNative
177
- );
178
- this.implementation.reset(true);
179
- final List<BundleInfo> installed = this.implementation.list();
180
- for (final BundleInfo bundle : installed) {
71
+ public class CapacitorUpdaterPlugin extends Plugin {
72
+
73
+ private Logger logger;
74
+
75
+ private static final String updateUrlDefault = "https://plugin.capgo.app/updates";
76
+ private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
77
+ private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
78
+ private static final String KEEP_URL_FLAG_KEY = "__capgo_keep_url_path_after_reload";
79
+ private static final String CUSTOM_ID_PREF_KEY = "CapacitorUpdater.customId";
80
+ private static final String UPDATE_URL_PREF_KEY = "CapacitorUpdater.updateUrl";
81
+ private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
82
+ private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
83
+ private static final String DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.defaultChannel";
84
+ private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
85
+ private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
86
+
87
+ private final String pluginVersion = "4.43.5";
88
+ private static final String DELAY_CONDITION_PREFERENCES = "";
89
+
90
+ private SharedPreferences.Editor editor;
91
+ private SharedPreferences prefs;
92
+ protected CapgoUpdater implementation;
93
+ private Boolean persistCustomId = false;
94
+ private Boolean persistModifyUrl = false;
95
+
96
+ private Integer appReadyTimeout = 10000;
97
+ private Integer periodCheckDelay = 0;
98
+ private Boolean autoDeleteFailed = true;
99
+ private Boolean autoDeletePrevious = true;
100
+ private Boolean autoUpdate = false;
101
+ private String updateUrl = "";
102
+ private Version currentVersionNative;
103
+ private String currentBuildVersion;
104
+ private Thread backgroundTask;
105
+ private Boolean taskRunning = false;
106
+ private Boolean keepUrlPathAfterReload = false;
107
+ private Boolean autoSplashscreen = false;
108
+ private Boolean autoSplashscreenLoader = false;
109
+ private Integer autoSplashscreenTimeout = 10000;
110
+ private Boolean autoSplashscreenTimedOut = false;
111
+ private String directUpdateMode = "false";
112
+ private Boolean wasRecentlyInstalledOrUpdated = false;
113
+ private Boolean onLaunchDirectUpdateUsed = false;
114
+ Boolean shakeMenuEnabled = false;
115
+ Boolean shakeChannelSelectorEnabled = false;
116
+ private Boolean allowManualBundleError = false;
117
+ Boolean allowSetDefaultChannel = true;
118
+
119
+ String getUpdateUrl() {
120
+ return this.updateUrl;
121
+ }
122
+
123
+ // Used for activity-based foreground/background detection on Android < 14
124
+ private Boolean isPreviousMainActivity = true;
125
+
126
+ private volatile Thread backgroundDownloadTask;
127
+ private volatile Thread appReadyCheck;
128
+ private volatile long downloadStartTimeMs = 0;
129
+ private static final long DOWNLOAD_TIMEOUT_MS = 3600000; // 1 hour timeout
130
+
131
+ // private static final CountDownLatch semaphoreReady = new CountDownLatch(1);
132
+ private static final Phaser semaphoreReady = new Phaser(1);
133
+
134
+ // Lock to ensure cleanup completes before downloads start
135
+ private final Object cleanupLock = new Object();
136
+ private volatile boolean cleanupComplete = false;
137
+ private volatile Thread cleanupThread = null;
138
+
139
+ private int lastNotifiedStatPercent = 0;
140
+
141
+ private DelayUpdateUtils delayUpdateUtils;
142
+
143
+ private ShakeMenu shakeMenu;
144
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
145
+ private FrameLayout splashscreenLoaderOverlay;
146
+ private Runnable splashscreenTimeoutRunnable;
147
+
148
+ // App lifecycle observer using ProcessLifecycleOwner for reliable foreground/background detection
149
+ private AppLifecycleObserver appLifecycleObserver;
150
+
151
+ // Play Store In-App Updates
152
+ private AppUpdateManager appUpdateManager;
153
+ private AppUpdateInfo cachedAppUpdateInfo;
154
+ private static final int APP_UPDATE_REQUEST_CODE = 9001;
155
+ private InstallStateUpdatedListener installStateUpdatedListener;
156
+
157
+ private void notifyBreakingEvents(final String version) {
158
+ if (version == null || version.isEmpty()) {
159
+ return;
160
+ }
161
+ for (final String eventName : BREAKING_EVENT_NAMES) {
162
+ final JSObject payload = new JSObject();
163
+ payload.put("version", version);
164
+ CapacitorUpdaterPlugin.this.notifyListeners(eventName, payload);
165
+ }
166
+ }
167
+
168
+ private void persistLastFailedBundle(BundleInfo bundle) {
169
+ if (this.prefs == null) {
170
+ return;
171
+ }
172
+ final SharedPreferences.Editor localEditor = this.prefs.edit();
173
+ if (bundle == null) {
174
+ localEditor.remove(LAST_FAILED_BUNDLE_PREF_KEY);
175
+ } else {
176
+ final JSONObject json = new JSONObject(bundle.toJSONMap());
177
+ localEditor.putString(LAST_FAILED_BUNDLE_PREF_KEY, json.toString());
178
+ }
179
+ localEditor.apply();
180
+ }
181
+
182
+ private BundleInfo readLastFailedBundle() {
183
+ if (this.prefs == null) {
184
+ return null;
185
+ }
186
+ final String raw = this.prefs.getString(LAST_FAILED_BUNDLE_PREF_KEY, null);
187
+ if (raw == null || raw.trim().isEmpty()) {
188
+ return null;
189
+ }
190
+ try {
191
+ return BundleInfo.fromJSON(raw);
192
+ } catch (final JSONException e) {
193
+ logger.error("Failed to parse failed bundle info: " + e.getMessage());
194
+ this.persistLastFailedBundle(null);
195
+ return null;
196
+ }
197
+ }
198
+
199
+ public Thread startNewThread(final Runnable function, Number waitTime) {
200
+ Thread bgTask = new Thread(() -> {
181
201
  try {
182
- Log.i(
183
- CapacitorUpdater.TAG,
184
- "Deleting obsolete bundle: " + bundle.getId()
185
- );
186
- this.implementation.delete(bundle.getId());
187
- } catch (final Exception e) {
188
- Log.e(
189
- CapacitorUpdater.TAG,
190
- "Failed to delete: " + bundle.getId(),
191
- e
192
- );
193
- }
194
- }
195
- }
196
- } catch (final Exception e) {
197
- Log.e(
198
- CapacitorUpdater.TAG,
199
- "Could not determine the current version",
200
- e
201
- );
202
- }
203
- } catch (final Exception e) {
204
- Log.e(
205
- CapacitorUpdater.TAG,
206
- "Error calculating previous native version",
207
- e
208
- );
209
- }
210
- this.editor.putString(
211
- "LatestVersionNative",
212
- this.currentVersionNative.toString()
213
- );
214
- this.editor.commit();
215
- }
216
-
217
- public void notifyDownload(final String id, final int percent) {
218
- try {
219
- final JSObject ret = new JSObject();
220
- ret.put("percent", percent);
221
- final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
222
- ret.put("bundle", bundleInfo.toJSON());
223
- this.notifyListeners("download", ret);
224
- if (percent == 100) {
225
- this.notifyListeners("downloadComplete", bundleInfo.toJSON());
226
- this.implementation.sendStats(
227
- "download_complete",
228
- bundleInfo.getVersionName()
229
- );
230
- } else if (percent % 10 == 0) {
231
- this.implementation.sendStats(
232
- "download_" + percent,
233
- bundleInfo.getVersionName()
234
- );
235
- }
236
- } catch (final Exception e) {
237
- Log.e(CapacitorUpdater.TAG, "Could not notify listeners", e);
238
- }
239
- }
240
-
241
- @PluginMethod
242
- public void getDeviceId(final PluginCall call) {
243
- try {
244
- final JSObject ret = new JSObject();
245
- ret.put("deviceId", this.implementation.deviceID);
246
- call.resolve(ret);
247
- } catch (final Exception e) {
248
- Log.e(CapacitorUpdater.TAG, "Could not get device id", e);
249
- call.reject("Could not get device id", e);
250
- }
251
- }
252
-
253
- @PluginMethod
254
- public void setCustomId(final PluginCall call) {
255
- final String customId = call.getString("customId");
256
- if (customId == null) {
257
- Log.e(CapacitorUpdater.TAG, "setCustomId called without customId");
258
- call.reject("setCustomId called without customId");
259
- return;
260
- }
261
- this.implementation.customId = customId;
262
- }
263
-
264
- @PluginMethod
265
- public void getPluginVersion(final PluginCall call) {
266
- try {
267
- final JSObject ret = new JSObject();
268
- ret.put("version", this.PLUGIN_VERSION);
269
- call.resolve(ret);
270
- } catch (final Exception e) {
271
- Log.e(CapacitorUpdater.TAG, "Could not get plugin version", e);
272
- call.reject("Could not get plugin version", e);
273
- }
274
- }
275
-
276
- @PluginMethod
277
- public void setChannel(final PluginCall call) {
278
- final String channel = call.getString("channel");
279
- if (channel == null) {
280
- Log.e(CapacitorUpdater.TAG, "setChannel called without channel");
281
- call.reject("setChannel called without channel");
282
- return;
283
- }
284
- try {
285
- Log.i(CapacitorUpdater.TAG, "setChannel " + channel);
286
- new Thread(
287
- new Runnable() {
288
- @Override
289
- public void run() {
290
- CapacitorUpdaterPlugin.this.implementation.setChannel(
291
- channel,
292
- res -> {
293
- if (res.has("error")) {
294
- call.reject(res.getString("error"));
295
- } else {
296
- call.resolve(res);
297
- }
298
- }
299
- );
300
- }
301
- }
302
- )
303
- .start();
304
- } catch (final Exception e) {
305
- Log.e(CapacitorUpdater.TAG, "Failed to setChannel: " + channel, e);
306
- call.reject("Failed to setChannel: " + channel, e);
307
- }
308
- }
309
-
310
- @PluginMethod
311
- public void getChannel(final PluginCall call) {
312
- try {
313
- Log.i(CapacitorUpdater.TAG, "getChannel");
314
- new Thread(
315
- new Runnable() {
316
- @Override
317
- public void run() {
318
- CapacitorUpdaterPlugin.this.implementation.getChannel(res -> {
319
- if (res.has("error")) {
320
- call.reject(res.getString("error"));
321
- } else {
322
- call.resolve(res);
202
+ if (waitTime.longValue() > 0) {
203
+ Thread.sleep(waitTime.longValue());
323
204
  }
324
- });
325
- }
326
- }
327
- )
328
- .start();
329
- } catch (final Exception e) {
330
- Log.e(CapacitorUpdater.TAG, "Failed to getChannel", e);
331
- call.reject("Failed to getChannel", e);
332
- }
333
- }
334
-
335
- @PluginMethod
336
- public void download(final PluginCall call) {
337
- final String url = call.getString("url");
338
- final String version = call.getString("version");
339
- final String sessionKey = call.getString("sessionKey", "");
340
- final String checksum = call.getString("checksum", "");
341
- if (url == null) {
342
- Log.e(CapacitorUpdater.TAG, "Download called without url");
343
- call.reject("Download called without url");
344
- return;
345
- }
346
- if (version == null) {
347
- Log.e(CapacitorUpdater.TAG, "Download called without version");
348
- call.reject("Download called without version");
349
- return;
350
- }
351
- try {
352
- Log.i(CapacitorUpdater.TAG, "Downloading " + url);
353
- new Thread(
354
- new Runnable() {
355
- @Override
356
- public void run() {
357
- try {
358
- final BundleInfo downloaded =
359
- CapacitorUpdaterPlugin.this.implementation.download(
360
- url,
361
- version,
362
- sessionKey,
363
- checksum
364
- );
365
-
366
- call.resolve(downloaded.toJSON());
367
- } catch (final IOException e) {
368
- Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
369
- call.reject("Failed to download from: " + url, e);
370
- final JSObject ret = new JSObject();
371
- ret.put("version", version);
372
- CapacitorUpdaterPlugin.this.notifyListeners(
373
- "downloadFailed",
374
- ret
375
- );
376
- final BundleInfo current =
377
- CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
378
- CapacitorUpdaterPlugin.this.implementation.sendStats(
379
- "download_fail",
380
- current.getVersionName()
381
- );
205
+ function.run();
206
+ } catch (Exception e) {
207
+ e.printStackTrace();
382
208
  }
383
- }
384
- }
385
- )
386
- .start();
387
- } catch (final Exception e) {
388
- Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
389
- call.reject("Failed to download from: " + url, e);
390
- final JSObject ret = new JSObject();
391
- ret.put("version", version);
392
- CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
393
- final BundleInfo current =
394
- CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
395
- CapacitorUpdaterPlugin.this.implementation.sendStats(
396
- "download_fail",
397
- current.getVersionName()
398
- );
209
+ });
210
+ bgTask.start();
211
+ return bgTask;
399
212
  }
400
- }
401
-
402
- private boolean _reload() {
403
- final String path = this.implementation.getCurrentBundlePath();
404
- Log.i(CapacitorUpdater.TAG, "Reloading: " + path);
405
- if (this.implementation.isUsingBuiltin()) {
406
- this.bridge.setServerAssetPath(path);
407
- } else {
408
- this.bridge.setServerBasePath(path);
409
- }
410
- this.checkAppReady();
411
- this.notifyListeners("appReloaded", new JSObject());
412
- return true;
413
- }
414
-
415
- @PluginMethod
416
- public void reload(final PluginCall call) {
417
- try {
418
- if (this._reload()) {
419
- call.resolve();
420
- } else {
421
- Log.e(CapacitorUpdater.TAG, "Reload failed");
422
- call.reject("Reload failed");
423
- }
424
- } catch (final Exception e) {
425
- Log.e(CapacitorUpdater.TAG, "Could not reload", e);
426
- call.reject("Could not reload", e);
427
- }
428
- }
429
-
430
- @PluginMethod
431
- public void next(final PluginCall call) {
432
- final String id = call.getString("id");
433
- if (id == null) {
434
- Log.e(CapacitorUpdater.TAG, "Next called without id");
435
- call.reject("Next called without id");
436
- return;
437
- }
438
- try {
439
- Log.i(CapacitorUpdater.TAG, "Setting next active id " + id);
440
- if (!this.implementation.setNextBundle(id)) {
441
- Log.e(
442
- CapacitorUpdater.TAG,
443
- "Set next id failed. Bundle " + id + " does not exist."
444
- );
445
- call.reject("Set next id failed. Bundle " + id + " does not exist.");
446
- } else {
447
- call.resolve(this.implementation.getBundleInfo(id).toJSON());
448
- }
449
- } catch (final Exception e) {
450
- Log.e(CapacitorUpdater.TAG, "Could not set next id " + id, e);
451
- call.reject("Could not set next id: " + id, e);
452
- }
453
- }
454
-
455
- @PluginMethod
456
- public void set(final PluginCall call) {
457
- final String id = call.getString("id");
458
- if (id == null) {
459
- Log.e(CapacitorUpdater.TAG, "Set called without id");
460
- call.reject("Set called without id");
461
- return;
462
- }
463
- try {
464
- Log.i(CapacitorUpdater.TAG, "Setting active bundle " + id);
465
- if (!this.implementation.set(id)) {
466
- Log.i(CapacitorUpdater.TAG, "No such bundle " + id);
467
- call.reject("Update failed, id " + id + " does not exist.");
468
- } else {
469
- Log.i(CapacitorUpdater.TAG, "Bundle successfully set to " + id);
470
- this.reload(call);
471
- }
472
- } catch (final Exception e) {
473
- Log.e(CapacitorUpdater.TAG, "Could not set id " + id, e);
474
- call.reject("Could not set id " + id, e);
475
- }
476
- }
477
-
478
- @PluginMethod
479
- public void delete(final PluginCall call) {
480
- final String id = call.getString("id");
481
- if (id == null) {
482
- Log.e(CapacitorUpdater.TAG, "missing id");
483
- call.reject("missing id");
484
- return;
485
- }
486
- Log.i(CapacitorUpdater.TAG, "Deleting id " + id);
487
- try {
488
- final Boolean res = this.implementation.delete(id);
489
- if (res) {
490
- call.resolve();
491
- } else {
492
- Log.e(
493
- CapacitorUpdater.TAG,
494
- "Delete failed, id " + id + " does not exist"
495
- );
496
- call.reject("Delete failed, id " + id + " does not exist");
497
- }
498
- } catch (final Exception e) {
499
- Log.e(CapacitorUpdater.TAG, "Could not delete id " + id, e);
500
- call.reject("Could not delete id " + id, e);
501
- }
502
- }
503
-
504
- @PluginMethod
505
- public void list(final PluginCall call) {
506
- try {
507
- final List<BundleInfo> res = this.implementation.list();
508
- final JSObject ret = new JSObject();
509
- final JSArray values = new JSArray();
510
- for (final BundleInfo bundle : res) {
511
- values.put(bundle.toJSON());
512
- }
513
- ret.put("bundles", values);
514
- call.resolve(ret);
515
- } catch (final Exception e) {
516
- Log.e(CapacitorUpdater.TAG, "Could not list bundles", e);
517
- call.reject("Could not list bundles", e);
518
- }
519
- }
520
-
521
- @PluginMethod
522
- public void getLatest(final PluginCall call) {
523
- try {
524
- new Thread(
525
- new Runnable() {
526
- @Override
527
- public void run() {
528
- CapacitorUpdaterPlugin.this.implementation.getLatest(
529
- CapacitorUpdaterPlugin.this.updateUrl,
530
- res -> {
531
- if (res.has("error")) {
532
- call.reject(res.getString("error"));
533
- return;
534
- } else {
535
- call.resolve(res);
536
- }
537
- final JSObject ret = new JSObject();
538
- Iterator<String> keys = res.keys();
539
- while (keys.hasNext()) {
540
- String key = keys.next();
541
- if (res.has(key)) {
542
- try {
543
- ret.put(key, res.get(key));
544
- } catch (JSONException e) {
545
- e.printStackTrace();
546
- }
213
+
214
+ public Thread startNewThread(final Runnable function) {
215
+ return startNewThread(function, 0);
216
+ }
217
+
218
+ @Override
219
+ public void load() {
220
+ super.load();
221
+
222
+ // Initialize logger with osLogging config
223
+ // Default to true for both platforms to enable system logging by default
224
+ boolean osLogging = this.getConfig().getBoolean("osLogging", true);
225
+ Logger.Options loggerOptions = new Logger.Options(osLogging);
226
+ this.logger = new Logger("CapgoUpdater", loggerOptions);
227
+
228
+ this.prefs = this.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
229
+ this.editor = this.prefs.edit();
230
+
231
+ try {
232
+ this.implementation = new CapgoUpdater(logger) {
233
+ @Override
234
+ public void notifyDownload(final String id, final int percent) {
235
+ if (activity != null) {
236
+ activity.runOnUiThread(() -> {
237
+ CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
238
+ });
239
+ } else {
240
+ logger.warn("notifyDownload: Activity is null, skipping notification");
547
241
  }
548
- }
549
- call.resolve(ret);
550
242
  }
551
- );
552
- }
553
- }
554
- )
555
- .start();
556
- } catch (final Exception e) {
557
- Log.e(CapacitorUpdater.TAG, "Failed to getLatest", e);
558
- call.reject("Failed to getLatest", e);
559
- }
560
- }
561
-
562
- private boolean _reset(final Boolean toLastSuccessful) {
563
- final BundleInfo fallback = this.implementation.getFallbackBundle();
564
- this.implementation.reset();
565
-
566
- if (toLastSuccessful && !fallback.isBuiltin()) {
567
- Log.i(CapacitorUpdater.TAG, "Resetting to: " + fallback);
568
- return this.implementation.set(fallback) && this._reload();
569
- }
570
-
571
- Log.i(CapacitorUpdater.TAG, "Resetting to native.");
572
- return this._reload();
573
- }
574
-
575
- @PluginMethod
576
- public void reset(final PluginCall call) {
577
- try {
578
- final Boolean toLastSuccessful = call.getBoolean(
579
- "toLastSuccessful",
580
- false
581
- );
582
- if (this._reset(toLastSuccessful)) {
583
- call.resolve();
584
- return;
585
- }
586
- Log.e(CapacitorUpdater.TAG, "Reset failed");
587
- call.reject("Reset failed");
588
- } catch (final Exception e) {
589
- Log.e(CapacitorUpdater.TAG, "Reset failed", e);
590
- call.reject("Reset failed", e);
591
- }
592
- }
593
-
594
- @PluginMethod
595
- public void current(final PluginCall call) {
596
- try {
597
- final JSObject ret = new JSObject();
598
- final BundleInfo bundle = this.implementation.getCurrentBundle();
599
- ret.put("bundle", bundle.toJSON());
600
- ret.put("native", this.currentVersionNative);
601
- call.resolve(ret);
602
- } catch (final Exception e) {
603
- Log.e(CapacitorUpdater.TAG, "Could not get current bundle", e);
604
- call.reject("Could not get current bundle", e);
605
- }
606
- }
607
-
608
- @PluginMethod
609
- public void notifyAppReady(final PluginCall call) {
610
- try {
611
- final BundleInfo bundle = this.implementation.getCurrentBundle();
612
- this.implementation.setSuccess(bundle, this.autoDeletePrevious);
613
- Log.i(
614
- CapacitorUpdater.TAG,
615
- "Current bundle loaded successfully. ['notifyAppReady()' was called] " +
616
- bundle
617
- );
618
- call.resolve();
619
- } catch (final Exception e) {
620
- Log.e(
621
- CapacitorUpdater.TAG,
622
- "Failed to notify app ready state. [Error calling 'notifyAppReady()']",
623
- e
624
- );
625
- call.reject("Failed to commit app ready state.", e);
626
- }
627
- }
628
-
629
- @PluginMethod
630
- public void setMultiDelay(final PluginCall call) {
631
- try {
632
- final Object delayConditions = call.getData().opt("delayConditions");
633
- if (delayConditions == null) {
634
- Log.e(
635
- CapacitorUpdater.TAG,
636
- "setMultiDelay called without delayCondition"
637
- );
638
- call.reject("setMultiDelay called without delayCondition");
639
- return;
640
- }
641
- if (_setMultiDelay(delayConditions.toString())) {
642
- call.resolve();
643
- } else {
644
- call.reject("Failed to delay update");
645
- }
646
- } catch (final Exception e) {
647
- Log.e(
648
- CapacitorUpdater.TAG,
649
- "Failed to delay update, [Error calling 'setMultiDelay()']",
650
- e
651
- );
652
- call.reject("Failed to delay update", e);
653
- }
654
- }
655
-
656
- private Boolean _setMultiDelay(String delayConditions) {
657
- try {
658
- this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions);
659
- this.editor.commit();
660
- Log.i(CapacitorUpdater.TAG, "Delay update saved");
661
- return true;
662
- } catch (final Exception e) {
663
- Log.e(
664
- CapacitorUpdater.TAG,
665
- "Failed to delay update, [Error calling '_setMultiDelay()']",
666
- e
667
- );
668
- return false;
669
- }
670
- }
671
-
672
- @Deprecated
673
- @PluginMethod
674
- public void setDelay(final PluginCall call) {
675
- try {
676
- String kind = call.getString("kind");
677
- String value = call.getString("value");
678
- String delayConditions =
679
- "[{\"kind\":\"" +
680
- kind +
681
- "\", \"value\":\"" +
682
- (value != null ? value : "") +
683
- "\"}]";
684
- if (_setMultiDelay(delayConditions)) {
685
- call.resolve();
686
- } else {
687
- call.reject("Failed to delay update");
688
- }
689
- } catch (final Exception e) {
690
- Log.e(
691
- CapacitorUpdater.TAG,
692
- "Failed to delay update, [Error calling 'setDelay()']",
693
- e
694
- );
695
- call.reject("Failed to delay update", e);
696
- }
697
- }
698
-
699
- private boolean _cancelDelay(String source) {
700
- try {
701
- this.editor.remove(DELAY_CONDITION_PREFERENCES);
702
- this.editor.commit();
703
- Log.i(CapacitorUpdater.TAG, "All delays canceled from " + source);
704
- return true;
705
- } catch (final Exception e) {
706
- Log.e(CapacitorUpdater.TAG, "Failed to cancel update delay", e);
707
- return false;
708
- }
709
- }
710
-
711
- @PluginMethod
712
- public void cancelDelay(final PluginCall call) {
713
- if (this._cancelDelay("JS")) {
714
- call.resolve();
715
- } else {
716
- call.reject("Failed to cancel delay");
717
- }
718
- }
719
-
720
- private void _checkCancelDelay(Boolean killed) {
721
- Gson gson = new Gson();
722
- String delayUpdatePreferences = prefs.getString(
723
- DELAY_CONDITION_PREFERENCES,
724
- "[]"
725
- );
726
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
727
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(
728
- delayUpdatePreferences,
729
- type
730
- );
731
- for (DelayCondition condition : delayConditionList) {
732
- String kind = condition.getKind().toString();
733
- String value = condition.getValue();
734
- if (!"".equals(kind)) {
735
- switch (kind) {
736
- case "background":
737
- if (!killed) {
738
- this._cancelDelay("background check");
739
- }
740
- break;
741
- case "kill":
742
- if (killed) {
743
- this._cancelDelay("kill check");
744
- this.installNext();
745
- }
746
- break;
747
- case "date":
748
- if (!"".equals(value)) {
749
- try {
750
- final SimpleDateFormat sdf = new SimpleDateFormat(
751
- "yyyy-MM-dd'T'HH:mm:ss.SSS"
752
- );
753
- Date date = sdf.parse(value);
754
- assert date != null;
755
- if (new Date().compareTo(date) > 0) {
756
- this._cancelDelay("date expired");
243
+
244
+ @Override
245
+ public void directUpdateFinish(final BundleInfo latest) {
246
+ if (activity != null) {
247
+ activity.runOnUiThread(() -> {
248
+ CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
249
+ });
250
+ } else {
251
+ logger.warn("directUpdateFinish: Activity is null, skipping notification");
252
+ }
757
253
  }
758
- } catch (final Exception e) {
759
- this._cancelDelay("date parsing issue");
760
- }
761
- } else {
762
- this._cancelDelay("delayVal absent");
763
- }
764
- break;
765
- case "nativeVersion":
766
- if (!"".equals(value)) {
767
- try {
768
- final Version versionLimit = new Version(value);
769
- if (this.currentVersionNative.isAtLeast(versionLimit)) {
770
- this._cancelDelay("nativeVersion above limit");
254
+
255
+ @Override
256
+ public void notifyListeners(final String id, final Map<String, Object> res) {
257
+ if (activity != null) {
258
+ activity.runOnUiThread(() -> {
259
+ CapacitorUpdaterPlugin.this.notifyListeners(id, InternalUtils.mapToJSObject(res));
260
+ });
261
+ } else {
262
+ logger.warn("notifyListeners: Activity is null, skipping notification for event: " + id);
263
+ }
771
264
  }
772
- } catch (final Exception e) {
773
- this._cancelDelay("nativeVersion parsing issue");
774
- }
775
- } else {
776
- this._cancelDelay("delayVal absent");
777
- }
778
- break;
779
- }
780
- }
781
- }
782
- }
783
-
784
- private Boolean _isAutoUpdateEnabled() {
785
- final CapConfig config = CapConfig.loadDefault(this.getActivity());
786
- String serverUrl = config.getServerUrl();
787
- if (serverUrl != null && !"".equals(serverUrl)) {
788
- // log warning autoupdate disabled when serverUrl is set
789
- Log.w(
790
- CapacitorUpdater.TAG,
791
- "AutoUpdate is automatic disabled when serverUrl is set."
792
- );
793
- }
794
- return (
795
- CapacitorUpdaterPlugin.this.autoUpdate &&
796
- !"".equals(CapacitorUpdaterPlugin.this.updateUrl) &&
797
- serverUrl != null &&
798
- !"".equals(serverUrl)
799
- );
800
- }
801
-
802
- @PluginMethod
803
- public void isAutoUpdateEnabled(final PluginCall call) {
804
- try {
805
- final JSObject ret = new JSObject();
806
- ret.put("enabled", this._isAutoUpdateEnabled());
807
- call.resolve(ret);
808
- } catch (final Exception e) {
809
- Log.e(CapacitorUpdater.TAG, "Could not get autoUpdate status", e);
810
- call.reject("Could not get autoUpdate status", e);
811
- }
812
- }
813
-
814
- private void checkAppReady() {
815
- try {
816
- if (this.appReadyCheck != null) {
817
- this.appReadyCheck.interrupt();
818
- }
819
- this.appReadyCheck = new Thread(new DeferredNotifyAppReadyCheck());
820
- this.appReadyCheck.start();
821
- } catch (final Exception e) {
822
- Log.e(
823
- CapacitorUpdater.TAG,
824
- "Failed to start " + DeferredNotifyAppReadyCheck.class.getName(),
825
- e
826
- );
827
- }
828
- }
829
-
830
- private boolean isValidURL(String urlStr) {
831
- try {
832
- URL url = new URL(urlStr);
833
- return true;
834
- } catch (MalformedURLException e) {
835
- return false;
836
- }
837
- }
838
-
839
- private void backgroundDownload() {
840
- new Thread(
841
- new Runnable() {
842
- @Override
843
- public void run() {
844
- Log.i(
845
- CapacitorUpdater.TAG,
846
- "Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl
847
- );
848
- CapacitorUpdaterPlugin.this.implementation.getLatest(
849
- CapacitorUpdaterPlugin.this.updateUrl,
850
- res -> {
851
- final BundleInfo current =
852
- CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
853
- try {
854
- if (res.has("message")) {
855
- Log.i(
856
- CapacitorUpdater.TAG,
857
- "message " + res.get("message")
858
- );
265
+ };
266
+ final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
267
+ this.implementation.activity = this.getActivity();
268
+ this.implementation.versionBuild = this.getConfig().getString("version", pInfo.versionName);
269
+ this.implementation.CAP_SERVER_PATH = WebView.CAP_SERVER_PATH;
270
+ this.implementation.pluginVersion = this.pluginVersion;
271
+ this.implementation.versionCode = Integer.toString(pInfo.versionCode);
272
+ // Removed unused OkHttpClient creation - using shared client in DownloadService instead
273
+ // Handle directUpdate configuration - support string values and backward compatibility
274
+ String directUpdateConfig = this.getConfig().getString("directUpdate", null);
275
+ if (directUpdateConfig != null) {
276
+ // Handle backward compatibility for boolean true
277
+ if (directUpdateConfig.equals("true")) {
278
+ this.directUpdateMode = "always";
279
+ this.implementation.directUpdate = true;
280
+ } else {
281
+ this.directUpdateMode = directUpdateConfig;
282
+ this.implementation.directUpdate =
283
+ directUpdateConfig.equals("always") ||
284
+ directUpdateConfig.equals("atInstall") ||
285
+ directUpdateConfig.equals("onLaunch");
286
+ // Validate directUpdate value
859
287
  if (
860
- res.has("major") &&
861
- res.getBoolean("major") &&
862
- res.has("version")
288
+ !directUpdateConfig.equals("false") &&
289
+ !directUpdateConfig.equals("always") &&
290
+ !directUpdateConfig.equals("atInstall") &&
291
+ !directUpdateConfig.equals("onLaunch")
863
292
  ) {
864
- final JSObject majorAvailable = new JSObject();
865
- majorAvailable.put("version", res.getString("version"));
866
- CapacitorUpdaterPlugin.this.notifyListeners(
867
- "majorAvailable",
868
- majorAvailable
293
+ logger.error(
294
+ "Invalid directUpdate value: \"" +
295
+ directUpdateConfig +
296
+ "\". Supported values are: \"false\", \"true\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\"."
869
297
  );
298
+ this.directUpdateMode = "false";
299
+ this.implementation.directUpdate = false;
870
300
  }
871
- final JSObject retNoNeed = new JSObject();
872
- retNoNeed.put("bundle", current.toJSON());
873
- CapacitorUpdaterPlugin.this.notifyListeners(
874
- "noNeedUpdate",
875
- retNoNeed
876
- );
877
- return;
878
- }
879
-
880
- if (
881
- !res.has("url") ||
882
- !CapacitorUpdaterPlugin.this.isValidURL(
883
- res.getString("url")
884
- )
885
- ) {
886
- Log.e(CapacitorUpdater.TAG, "Error no url or wrong format");
887
- final JSObject retNoNeed = new JSObject();
888
- retNoNeed.put("bundle", current.toJSON());
889
- CapacitorUpdaterPlugin.this.notifyListeners(
890
- "noNeedUpdate",
891
- retNoNeed
892
- );
893
- }
894
- final String latestVersionName = res.getString("version");
895
-
896
- if (
897
- latestVersionName != null &&
898
- !"".equals(latestVersionName) &&
899
- !current.getVersionName().equals(latestVersionName)
900
- ) {
901
- final BundleInfo latest =
902
- CapacitorUpdaterPlugin.this.implementation.getBundleInfoByName(
903
- latestVersionName
904
- );
905
- if (latest != null) {
906
- if (latest.isErrorStatus()) {
907
- Log.e(
908
- CapacitorUpdater.TAG,
909
- "Latest bundle already exists, and is in error state. Aborting update."
910
- );
911
- final JSObject retNoNeed = new JSObject();
912
- retNoNeed.put("bundle", current.toJSON());
913
- CapacitorUpdaterPlugin.this.notifyListeners(
914
- "noNeedUpdate",
915
- retNoNeed
916
- );
917
- return;
918
- }
919
- if (latest.isDownloaded()) {
920
- Log.i(
921
- CapacitorUpdater.TAG,
922
- "Latest bundle already exists and download is NOT required. Update will occur next time app moves to background."
923
- );
924
- final JSObject ret = new JSObject();
925
- ret.put("bundle", latest.toJSON());
926
- CapacitorUpdaterPlugin.this.notifyListeners(
927
- "updateAvailable",
928
- ret
929
- );
930
- CapacitorUpdaterPlugin.this.implementation.setNextBundle(
931
- latest.getId()
932
- );
933
- return;
934
- }
935
- if (latest.isDeleted()) {
936
- Log.i(
937
- CapacitorUpdater.TAG,
938
- "Latest bundle already exists and will be deleted, download will overwrite it."
939
- );
940
- try {
941
- final Boolean deleted =
942
- CapacitorUpdaterPlugin.this.implementation.delete(
943
- latest.getId(),
944
- true
945
- );
946
- if (deleted) {
947
- Log.i(
948
- CapacitorUpdater.TAG,
949
- "Failed bundle deleted: " +
950
- latest.getVersionName()
951
- );
952
- }
953
- } catch (final IOException e) {
954
- Log.e(
955
- CapacitorUpdater.TAG,
956
- "Failed to delete failed bundle: " +
957
- latest.getVersionName(),
958
- e
959
- );
960
- }
961
- }
301
+ }
302
+ } else {
303
+ Boolean directUpdateBool = this.getConfig().getBoolean("directUpdate", false);
304
+ if (directUpdateBool) {
305
+ this.directUpdateMode = "always"; // backward compatibility: true = always
306
+ this.implementation.directUpdate = true;
307
+ } else {
308
+ this.directUpdateMode = "false";
309
+ this.implementation.directUpdate = false;
310
+ }
311
+ }
312
+ this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
313
+ this.currentBuildVersion = Integer.toString(pInfo.versionCode);
314
+ this.delayUpdateUtils = new DelayUpdateUtils(this.prefs, this.editor, this.currentVersionNative, logger);
315
+ } catch (final PackageManager.NameNotFoundException e) {
316
+ logger.error("Error instantiating implementation " + e.getMessage());
317
+ return;
318
+ } catch (final Exception e) {
319
+ logger.error("Error getting current native app version " + e.getMessage());
320
+ return;
321
+ }
322
+
323
+ boolean disableJSLogging = this.getConfig().getBoolean("disableJSLogging", false);
324
+ // Set the bridge in the Logger when webView is available
325
+ if (this.bridge != null && this.bridge.getWebView() != null && !disableJSLogging) {
326
+ logger.setBridge(this.bridge);
327
+ logger.info("WebView set successfully for logging");
328
+ } else {
329
+ logger.info("WebView not ready yet, will be set later");
330
+ }
331
+
332
+ // Set logger for shared classes
333
+ CryptoCipher.setLogger(logger);
334
+ DownloadService.setLogger(logger);
335
+ DownloadWorkerManager.setLogger(logger);
336
+
337
+ final CapConfig config = CapConfig.loadDefault(this.getActivity());
338
+ this.implementation.appId = InternalUtils.getPackageName(getContext().getPackageManager(), getContext().getPackageName());
339
+ this.implementation.appId = config.getString("appId", this.implementation.appId);
340
+ this.implementation.appId = this.getConfig().getString("appId", this.implementation.appId);
341
+ if (this.implementation.appId == null || this.implementation.appId.isEmpty()) {
342
+ // crash the app on purpose it should not happen
343
+ throw new RuntimeException(
344
+ "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"
345
+ );
346
+ }
347
+ logger.info("appId: " + implementation.appId);
348
+
349
+ this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
350
+ this.persistModifyUrl = this.getConfig().getBoolean("persistModifyUrl", false);
351
+ this.allowSetDefaultChannel = this.getConfig().getBoolean("allowSetDefaultChannel", true);
352
+ this.implementation.setPublicKey(this.getConfig().getString("publicKey", ""));
353
+ // Log public key prefix if encryption is enabled
354
+ String keyId = this.implementation.getKeyId();
355
+ if (keyId != null && !keyId.isEmpty()) {
356
+ logger.info("Public key prefix: " + keyId);
357
+ }
358
+ this.implementation.statsUrl = this.getConfig().getString("statsUrl", statsUrlDefault);
359
+ this.implementation.channelUrl = this.getConfig().getString("channelUrl", channelUrlDefault);
360
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
361
+ if (this.prefs.contains(STATS_URL_PREF_KEY)) {
362
+ final String storedStatsUrl = this.prefs.getString(STATS_URL_PREF_KEY, this.implementation.statsUrl);
363
+ if (storedStatsUrl != null) {
364
+ this.implementation.statsUrl = storedStatsUrl;
365
+ logger.info("Loaded persisted statsUrl");
366
+ }
367
+ }
368
+ if (this.prefs.contains(CHANNEL_URL_PREF_KEY)) {
369
+ final String storedChannelUrl = this.prefs.getString(CHANNEL_URL_PREF_KEY, this.implementation.channelUrl);
370
+ if (storedChannelUrl != null) {
371
+ this.implementation.channelUrl = storedChannelUrl;
372
+ logger.info("Loaded persisted channelUrl");
373
+ }
374
+ }
375
+ }
376
+
377
+ // Load defaultChannel: first try from persistent storage (set via setChannel), then fall back to config
378
+ if (this.prefs.contains(DEFAULT_CHANNEL_PREF_KEY)) {
379
+ final String storedDefaultChannel = this.prefs.getString(DEFAULT_CHANNEL_PREF_KEY, "");
380
+ if (storedDefaultChannel != null && !storedDefaultChannel.isEmpty()) {
381
+ this.implementation.defaultChannel = storedDefaultChannel;
382
+ logger.info("Loaded persisted defaultChannel from setChannel()");
383
+ } else {
384
+ this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
385
+ }
386
+ } else {
387
+ this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
388
+ }
389
+
390
+ int userValue = this.getConfig().getInt("periodCheckDelay", 0);
391
+
392
+ if (userValue >= 0 && userValue <= 600) {
393
+ this.periodCheckDelay = 600 * 1000;
394
+ } else if (userValue > 600) {
395
+ this.periodCheckDelay = userValue * 1000;
396
+ }
397
+
398
+ this.implementation.documentsDir = this.getContext().getFilesDir();
399
+ this.implementation.prefs = this.prefs;
400
+ this.implementation.editor = this.editor;
401
+ this.implementation.versionOs = Build.VERSION.RELEASE;
402
+ // Use DeviceIdHelper to get or create device ID that persists across reinstalls
403
+ this.implementation.deviceID = DeviceIdHelper.getOrCreateDeviceId(this.getContext(), this.prefs);
404
+
405
+ // Update User-Agent for shared OkHttpClient with OS version
406
+ DownloadService.updateUserAgent(this.implementation.appId, this.pluginVersion, this.implementation.versionOs);
407
+
408
+ if (Boolean.TRUE.equals(this.persistCustomId)) {
409
+ final String storedCustomId = this.prefs.getString(CUSTOM_ID_PREF_KEY, "");
410
+ if (storedCustomId != null && !storedCustomId.isEmpty()) {
411
+ this.implementation.customId = storedCustomId;
412
+ logger.info("Loaded persisted customId");
413
+ }
414
+ }
415
+ logger.info("init for device " + this.implementation.deviceID);
416
+ logger.info("version native " + this.currentVersionNative.getOriginalString());
417
+ this.autoDeleteFailed = this.getConfig().getBoolean("autoDeleteFailed", true);
418
+ this.autoDeletePrevious = this.getConfig().getBoolean("autoDeletePrevious", true);
419
+ this.updateUrl = this.getConfig().getString("updateUrl", updateUrlDefault);
420
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
421
+ if (this.prefs.contains(UPDATE_URL_PREF_KEY)) {
422
+ final String storedUpdateUrl = this.prefs.getString(UPDATE_URL_PREF_KEY, this.updateUrl);
423
+ if (storedUpdateUrl != null) {
424
+ this.updateUrl = storedUpdateUrl;
425
+ logger.info("Loaded persisted updateUrl");
426
+ }
427
+ }
428
+ }
429
+ this.autoUpdate = this.getConfig().getBoolean("autoUpdate", true);
430
+ this.appReadyTimeout = Math.max(1000, this.getConfig().getInt("appReadyTimeout", 10000)); // Minimum 1 second
431
+ this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
432
+ this.syncKeepUrlPathFlag(this.keepUrlPathAfterReload);
433
+ this.allowManualBundleError = this.getConfig().getBoolean("allowManualBundleError", false);
434
+ this.autoSplashscreen = this.getConfig().getBoolean("autoSplashscreen", false);
435
+ this.autoSplashscreenLoader = this.getConfig().getBoolean("autoSplashscreenLoader", false);
436
+ int splashscreenTimeoutValue = this.getConfig().getInt("autoSplashscreenTimeout", 10000);
437
+ this.autoSplashscreenTimeout = Math.max(0, splashscreenTimeoutValue);
438
+ this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
439
+ this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
440
+ this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
441
+ boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
442
+
443
+ // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
444
+ this.wasRecentlyInstalledOrUpdated = this.checkIfRecentlyInstalledOrUpdated();
445
+
446
+ this.implementation.autoReset();
447
+ if (resetWhenUpdate) {
448
+ this.cleanupObsoleteVersions();
449
+ }
450
+
451
+ // Check for 'kill' delay condition on app launch
452
+ // This handles cases where the app was killed by the system (onDestroy is not reliable)
453
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
454
+
455
+ this.checkForUpdateAfterDelay();
456
+
457
+ // On Android 14+ (API 34+), topActivity in RecentTaskInfo returns null due to
458
+ // security restrictions (StrandHogg task hijacking mitigations). Use ProcessLifecycleOwner
459
+ // for reliable app-level foreground/background detection on these versions.
460
+ // On older versions, we use the traditional activity lifecycle callbacks in handleOnStart/handleOnStop.
461
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
462
+ this.appLifecycleObserver = new AppLifecycleObserver(
463
+ new AppLifecycleObserver.AppLifecycleListener() {
464
+ @Override
465
+ public void onAppMovedToForeground() {
466
+ CapacitorUpdaterPlugin.this.appMovedToForeground();
962
467
  }
963
468
 
964
- new Thread(
965
- new Runnable() {
966
- @Override
967
- public void run() {
968
- try {
969
- Log.i(
970
- CapacitorUpdater.TAG,
971
- "New bundle: " +
972
- latestVersionName +
973
- " found. Current is: " +
974
- current.getVersionName() +
975
- ". Update will occur next time app moves to background."
976
- );
469
+ @Override
470
+ public void onAppMovedToBackground() {
471
+ CapacitorUpdaterPlugin.this.appMovedToBackground();
472
+ }
473
+ },
474
+ logger
475
+ );
476
+ this.appLifecycleObserver.register();
477
+ logger.info("Using ProcessLifecycleOwner for foreground/background detection (Android 14+)");
478
+ } else {
479
+ logger.info("Using activity lifecycle callbacks for foreground/background detection (Android <14)");
480
+ }
481
+ }
977
482
 
978
- final String url = res.getString("url");
979
- final String sessionKey = res.has("sessionKey")
980
- ? res.getString("sessionKey")
981
- : "";
982
- final String checksum = res.has("checksum")
983
- ? res.getString("checksum")
984
- : "";
985
- CapacitorUpdaterPlugin.this.implementation.downloadBackground(
986
- url,
987
- latestVersionName,
988
- sessionKey,
989
- checksum
990
- );
991
- } catch (final Exception e) {
992
- Log.e(
993
- CapacitorUpdater.TAG,
994
- "error downloading file",
995
- e
996
- );
997
- final JSObject ret = new JSObject();
998
- ret.put("version", latestVersionName);
999
- CapacitorUpdaterPlugin.this.notifyListeners(
1000
- "downloadFailed",
1001
- ret
1002
- );
1003
- final BundleInfo current =
1004
- CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1005
- CapacitorUpdaterPlugin.this.implementation.sendStats(
1006
- "download_fail",
1007
- current.getVersionName()
1008
- );
1009
- final JSObject retNoNeed = new JSObject();
1010
- retNoNeed.put("bundle", current.toJSON());
1011
- CapacitorUpdaterPlugin.this.notifyListeners(
1012
- "noNeedUpdate",
1013
- retNoNeed
1014
- );
1015
- }
1016
- }
1017
- }
1018
- )
1019
- .start();
1020
- } else {
1021
- Log.i(
1022
- CapacitorUpdater.TAG,
1023
- "No need to update, " +
1024
- current.getId() +
1025
- " is the latest bundle."
1026
- );
1027
- final JSObject retNoNeed = new JSObject();
1028
- retNoNeed.put("bundle", current.toJSON());
1029
- CapacitorUpdaterPlugin.this.notifyListeners(
1030
- "noNeedUpdate",
1031
- retNoNeed
1032
- );
1033
- }
1034
- } catch (final JSONException e) {
1035
- Log.e(CapacitorUpdater.TAG, "error parsing JSON", e);
1036
- final JSObject retNoNeed = new JSObject();
1037
- retNoNeed.put("bundle", current.toJSON());
1038
- CapacitorUpdaterPlugin.this.notifyListeners(
1039
- "noNeedUpdate",
1040
- retNoNeed
483
+ private void semaphoreWait(Number waitTime) {
484
+ try {
485
+ semaphoreReady.awaitAdvanceInterruptibly(semaphoreReady.getPhase(), waitTime.longValue(), TimeUnit.SECONDS);
486
+ logger.info("semaphoreReady count " + semaphoreReady.getPhase());
487
+ } catch (InterruptedException e) {
488
+ logger.info("semaphoreWait InterruptedException");
489
+ Thread.currentThread().interrupt(); // Restore interrupted status
490
+ } catch (TimeoutException e) {
491
+ logger.error("Semaphore timeout: " + e.getMessage());
492
+ // Don't throw runtime exception, just log and continue
493
+ }
494
+ }
495
+
496
+ private void semaphoreUp() {
497
+ logger.info("semaphoreUp");
498
+ semaphoreReady.register();
499
+ }
500
+
501
+ private void semaphoreDown() {
502
+ logger.info("semaphoreDown");
503
+ logger.info("semaphoreDown count " + semaphoreReady.getPhase());
504
+ semaphoreReady.arriveAndDeregister();
505
+ }
506
+
507
+ private void sendReadyToJs(final BundleInfo current, final String msg) {
508
+ sendReadyToJs(current, msg, false);
509
+ }
510
+
511
+ private void sendReadyToJs(final BundleInfo current, final String msg, final boolean isDirectUpdate) {
512
+ logger.info("sendReadyToJs: " + msg);
513
+ final JSObject ret = new JSObject();
514
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
515
+ ret.put("status", msg);
516
+
517
+ // No need to wait for semaphore anymore since _reload() has already waited
518
+ this.notifyListeners("appReady", ret, true);
519
+
520
+ // Auto hide splashscreen if enabled
521
+ // We show it on background when conditions are met, so we should hide it on foreground regardless of update outcome
522
+ if (this.autoSplashscreen) {
523
+ this.hideSplashscreen();
524
+ }
525
+ }
526
+
527
+ private void hideSplashscreen() {
528
+ if (Looper.myLooper() == Looper.getMainLooper()) {
529
+ hideSplashscreenInternal();
530
+ } else {
531
+ this.mainHandler.post(this::hideSplashscreenInternal);
532
+ }
533
+ }
534
+
535
+ private void hideSplashscreenInternal() {
536
+ cancelSplashscreenTimeout();
537
+ removeSplashscreenLoader();
538
+
539
+ try {
540
+ if (getBridge() == null) {
541
+ logger.warn("Bridge not ready for hiding splashscreen with autoSplashscreen");
542
+ return;
543
+ }
544
+
545
+ // Try to call the SplashScreen plugin directly through the bridge
546
+ PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
547
+ if (splashScreenPlugin != null) {
548
+ try {
549
+ // Create a plugin call for the hide method using reflection to access private msgHandler
550
+ JSObject options = new JSObject();
551
+ java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
552
+ msgHandlerField.setAccessible(true);
553
+ Object msgHandler = msgHandlerField.get(getBridge());
554
+
555
+ PluginCall call = new PluginCall(
556
+ (com.getcapacitor.MessageHandler) msgHandler,
557
+ "SplashScreen",
558
+ "FAKE_CALLBACK_ID_HIDE",
559
+ "hide",
560
+ options
1041
561
  );
562
+
563
+ // Call the hide method directly
564
+ splashScreenPlugin.invoke("hide", call);
565
+ logger.info("Splashscreen hidden automatically via direct plugin call");
566
+ } catch (Exception e) {
567
+ logger.error("Failed to call SplashScreen hide method: " + e.getMessage());
1042
568
  }
1043
- }
569
+ } else {
570
+ logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.");
571
+ }
572
+ } catch (Exception e) {
573
+ logger.error(
574
+ "Error hiding splashscreen with autoSplashscreen: " +
575
+ e.getMessage() +
576
+ ". Make sure @capacitor/splash-screen plugin is installed and configured."
1044
577
  );
1045
578
  }
1046
- }
1047
- )
1048
- .start();
1049
- }
1050
-
1051
- private void installNext() {
1052
- try {
1053
- Gson gson = new Gson();
1054
- String delayUpdatePreferences = prefs.getString(
1055
- DELAY_CONDITION_PREFERENCES,
1056
- "[]"
1057
- );
1058
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1059
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(
1060
- delayUpdatePreferences,
1061
- type
1062
- );
1063
- if (delayConditionList != null && delayConditionList.size() != 0) {
1064
- Log.i(CapacitorUpdater.TAG, "Update delayed to next backgrounding");
1065
- return;
1066
- }
1067
- final BundleInfo current = this.implementation.getCurrentBundle();
1068
- final BundleInfo next = this.implementation.getNextBundle();
1069
-
1070
- if (
1071
- next != null &&
1072
- !next.isErrorStatus() &&
1073
- !next.getId().equals(current.getId())
1074
- ) {
1075
- // There is a next bundle waiting for activation
1076
- Log.d(CapacitorUpdater.TAG, "Next bundle is: " + next.getVersionName());
1077
- if (this.implementation.set(next) && this._reload()) {
1078
- Log.i(
1079
- CapacitorUpdater.TAG,
1080
- "Updated to bundle: " + next.getVersionName()
1081
- );
1082
- this.implementation.setNextBundle(null);
579
+ }
580
+
581
+ private void showSplashscreen() {
582
+ if (Looper.myLooper() == Looper.getMainLooper()) {
583
+ showSplashscreenNow();
1083
584
  } else {
1084
- Log.e(
1085
- CapacitorUpdater.TAG,
1086
- "Update to bundle: " + next.getVersionName() + " Failed!"
1087
- );
1088
- }
1089
- }
1090
- } catch (final Exception e) {
1091
- Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1092
- }
1093
- }
1094
-
1095
- private void checkRevert() {
1096
- // Automatically roll back to fallback version if notifyAppReady has not been called yet
1097
- final BundleInfo current = this.implementation.getCurrentBundle();
1098
-
1099
- if (current.isBuiltin()) {
1100
- Log.i(CapacitorUpdater.TAG, "Built-in bundle is active. Nothing to do.");
1101
- return;
1102
- }
1103
- Log.d(CapacitorUpdater.TAG, "Current bundle is: " + current);
1104
-
1105
- if (BundleStatus.SUCCESS != current.getStatus()) {
1106
- Log.e(
1107
- CapacitorUpdater.TAG,
1108
- "notifyAppReady was not called, roll back current bundle: " +
1109
- current.getId()
1110
- );
1111
- Log.i(
1112
- CapacitorUpdater.TAG,
1113
- "Did you forget to call 'notifyAppReady()' in your Capacitor App code?"
1114
- );
1115
- final JSObject ret = new JSObject();
1116
- ret.put("bundle", current.toJSON());
1117
- this.notifyListeners("updateFailed", ret);
1118
- this.implementation.sendStats("update_fail", current.getVersionName());
1119
- this.implementation.setError(current);
1120
- this._reset(true);
1121
- if (
1122
- CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()
1123
- ) {
1124
- Log.i(
1125
- CapacitorUpdater.TAG,
1126
- "Deleting failing bundle: " + current.getVersionName()
1127
- );
585
+ this.mainHandler.post(this::showSplashscreenNow);
586
+ }
587
+ }
588
+
589
+ private void showSplashscreenNow() {
590
+ cancelSplashscreenTimeout();
591
+ this.autoSplashscreenTimedOut = false;
592
+
1128
593
  try {
1129
- final Boolean res =
1130
- this.implementation.delete(current.getId(), false);
1131
- if (res) {
1132
- Log.i(
1133
- CapacitorUpdater.TAG,
1134
- "Failed bundle deleted: " + current.getVersionName()
594
+ if (getBridge() == null) {
595
+ logger.warn("Bridge not ready for showing splashscreen with autoSplashscreen");
596
+ } else {
597
+ PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
598
+ if (splashScreenPlugin != null) {
599
+ JSObject options = new JSObject();
600
+ java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
601
+ msgHandlerField.setAccessible(true);
602
+ Object msgHandler = msgHandlerField.get(getBridge());
603
+
604
+ PluginCall call = new PluginCall(
605
+ (com.getcapacitor.MessageHandler) msgHandler,
606
+ "SplashScreen",
607
+ "FAKE_CALLBACK_ID_SHOW",
608
+ "show",
609
+ options
610
+ );
611
+
612
+ splashScreenPlugin.invoke("show", call);
613
+ logger.info("Splashscreen shown synchronously to prevent flash");
614
+ } else {
615
+ logger.warn("autoSplashscreen: SplashScreen plugin not found");
616
+ }
617
+ }
618
+ } catch (Exception e) {
619
+ logger.error("Failed to show splashscreen synchronously: " + e.getMessage());
620
+ }
621
+
622
+ addSplashscreenLoaderIfNeeded();
623
+ scheduleSplashscreenTimeout();
624
+ }
625
+
626
+ private void addSplashscreenLoaderIfNeeded() {
627
+ if (!Boolean.TRUE.equals(this.autoSplashscreenLoader)) {
628
+ return;
629
+ }
630
+
631
+ Runnable addLoader = () -> {
632
+ if (this.splashscreenLoaderOverlay != null) {
633
+ return;
634
+ }
635
+
636
+ Activity activity = getActivity();
637
+ if (activity == null) {
638
+ logger.warn("autoSplashscreen: Activity not available for loader overlay");
639
+ return;
640
+ }
641
+
642
+ ProgressBar progressBar = new ProgressBar(activity);
643
+ progressBar.setIndeterminate(true);
644
+
645
+ FrameLayout overlay = new FrameLayout(activity);
646
+ overlay.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
647
+ overlay.setClickable(false);
648
+ overlay.setFocusable(false);
649
+ overlay.setBackgroundColor(Color.TRANSPARENT);
650
+ overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
651
+
652
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
653
+ ViewGroup.LayoutParams.WRAP_CONTENT,
654
+ ViewGroup.LayoutParams.WRAP_CONTENT
1135
655
  );
1136
- }
1137
- } catch (final IOException e) {
1138
- Log.e(
1139
- CapacitorUpdater.TAG,
1140
- "Failed to delete failed bundle: " + current.getVersionName(),
1141
- e
1142
- );
656
+ params.gravity = Gravity.CENTER;
657
+ overlay.addView(progressBar, params);
658
+
659
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
660
+ decorView.addView(overlay);
661
+
662
+ this.splashscreenLoaderOverlay = overlay;
663
+ };
664
+
665
+ if (Looper.myLooper() == Looper.getMainLooper()) {
666
+ addLoader.run();
667
+ } else {
668
+ this.mainHandler.post(addLoader);
669
+ }
670
+ }
671
+
672
+ private void removeSplashscreenLoader() {
673
+ Runnable removeLoader = () -> {
674
+ if (this.splashscreenLoaderOverlay != null) {
675
+ ViewGroup parent = (ViewGroup) this.splashscreenLoaderOverlay.getParent();
676
+ if (parent != null) {
677
+ parent.removeView(this.splashscreenLoaderOverlay);
678
+ }
679
+ this.splashscreenLoaderOverlay = null;
680
+ }
681
+ };
682
+
683
+ if (Looper.myLooper() == Looper.getMainLooper()) {
684
+ removeLoader.run();
685
+ } else {
686
+ this.mainHandler.post(removeLoader);
1143
687
  }
1144
- }
1145
- } else {
1146
- Log.i(
1147
- CapacitorUpdater.TAG,
1148
- "notifyAppReady was called. This is fine: " + current.getId()
1149
- );
1150
688
  }
1151
- }
1152
689
 
1153
- private class DeferredNotifyAppReadyCheck implements Runnable {
690
+ private void scheduleSplashscreenTimeout() {
691
+ if (this.autoSplashscreenTimeout == null || this.autoSplashscreenTimeout <= 0) {
692
+ return;
693
+ }
694
+
695
+ cancelSplashscreenTimeout();
1154
696
 
1155
- @Override
1156
- public void run() {
1157
- try {
1158
- Log.i(
1159
- CapacitorUpdater.TAG,
1160
- "Wait for " +
1161
- CapacitorUpdaterPlugin.this.appReadyTimeout +
1162
- "ms, then check for notifyAppReady"
1163
- );
1164
- Thread.sleep(CapacitorUpdaterPlugin.this.appReadyTimeout);
1165
- CapacitorUpdaterPlugin.this.checkRevert();
1166
- CapacitorUpdaterPlugin.this.appReadyCheck = null;
1167
- } catch (final InterruptedException e) {
1168
- Log.i(
1169
- CapacitorUpdater.TAG,
1170
- DeferredNotifyAppReadyCheck.class.getName() + " was interrupted."
1171
- );
1172
- }
1173
- }
1174
- }
1175
-
1176
- public void appMovedToForeground() {
1177
- this._checkCancelDelay(true);
1178
- if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled()) {
1179
- this.backgroundDownload();
1180
- }
1181
- this.checkAppReady();
1182
- }
1183
-
1184
- public void appMovedToBackground() {
1185
- Log.i(CapacitorUpdater.TAG, "Checking for pending update");
1186
- try {
1187
- Gson gson = new Gson();
1188
- String delayUpdatePreferences = prefs.getString(
1189
- DELAY_CONDITION_PREFERENCES,
1190
- "[]"
1191
- );
1192
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1193
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(
1194
- delayUpdatePreferences,
1195
- type
1196
- );
1197
- String backgroundValue = null;
1198
- for (DelayCondition delayCondition : delayConditionList) {
1199
- if (delayCondition.getKind().toString().equals("background")) {
1200
- String value = delayCondition.getValue();
1201
- backgroundValue = (value != null && !value.isEmpty()) ? value : "0";
1202
- }
1203
- }
1204
- if (backgroundValue != null) {
1205
- taskRunning = true;
1206
- final Long timeout = Long.parseLong(backgroundValue);
1207
- if (backgroundTask != null) {
1208
- backgroundTask.interrupt();
1209
- }
1210
- backgroundTask =
1211
- new Thread(
1212
- new Runnable() {
1213
- @Override
1214
- public void run() {
697
+ this.splashscreenTimeoutRunnable = () -> {
698
+ logger.info("autoSplashscreen timeout reached, hiding splashscreen");
699
+ this.autoSplashscreenTimedOut = true;
700
+ this.implementation.directUpdate = false;
701
+ hideSplashscreen();
702
+ };
703
+
704
+ this.mainHandler.postDelayed(this.splashscreenTimeoutRunnable, this.autoSplashscreenTimeout);
705
+ }
706
+
707
+ private void cancelSplashscreenTimeout() {
708
+ if (this.splashscreenTimeoutRunnable != null) {
709
+ this.mainHandler.removeCallbacks(this.splashscreenTimeoutRunnable);
710
+ this.splashscreenTimeoutRunnable = null;
711
+ }
712
+ }
713
+
714
+ private boolean checkIfRecentlyInstalledOrUpdated() {
715
+ String currentVersion = this.currentBuildVersion;
716
+ String lastKnownVersion = this.prefs.getString("LatestNativeBuildVersion", "");
717
+
718
+ if (lastKnownVersion.isEmpty()) {
719
+ // First time running, consider it as recently installed
720
+ return true;
721
+ } else if (!lastKnownVersion.equals(currentVersion)) {
722
+ // Version changed, consider it as recently updated
723
+ return true;
724
+ }
725
+
726
+ return false;
727
+ }
728
+
729
+ private boolean shouldUseDirectUpdate() {
730
+ if (Boolean.TRUE.equals(this.autoSplashscreenTimedOut)) {
731
+ return false;
732
+ }
733
+ switch (this.directUpdateMode) {
734
+ case "false":
735
+ return false;
736
+ case "always":
737
+ return true;
738
+ case "atInstall":
739
+ if (this.wasRecentlyInstalledOrUpdated) {
740
+ // Reset the flag after first use to prevent subsequent foreground events from using direct update
741
+ this.wasRecentlyInstalledOrUpdated = false;
742
+ return true;
743
+ }
744
+ return false;
745
+ case "onLaunch":
746
+ if (!this.onLaunchDirectUpdateUsed) {
747
+ return true;
748
+ }
749
+ return false;
750
+ default:
751
+ logger.error(
752
+ "Invalid directUpdateMode: \"" +
753
+ this.directUpdateMode +
754
+ "\". Supported values are: \"false\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\" behavior."
755
+ );
756
+ return false;
757
+ }
758
+ }
759
+
760
+ private boolean isDirectUpdateCurrentlyAllowed(final boolean plannedDirectUpdate) {
761
+ return plannedDirectUpdate && !Boolean.TRUE.equals(this.autoSplashscreenTimedOut);
762
+ }
763
+
764
+ private void directUpdateFinish(final BundleInfo latest) {
765
+ if ("onLaunch".equals(this.directUpdateMode)) {
766
+ this.onLaunchDirectUpdateUsed = true;
767
+ this.implementation.directUpdate = false;
768
+ }
769
+ CapacitorUpdaterPlugin.this.implementation.set(latest);
770
+ CapacitorUpdaterPlugin.this._reload();
771
+ sendReadyToJs(latest, "update installed", true);
772
+ }
773
+
774
+ private void cleanupObsoleteVersions() {
775
+ cleanupThread = startNewThread(() -> {
776
+ synchronized (cleanupLock) {
1215
777
  try {
1216
- Thread.sleep(timeout);
1217
- taskRunning = false;
1218
- _checkCancelDelay(false);
1219
- installNext();
1220
- } catch (InterruptedException e) {
1221
- Log.i(
1222
- CapacitorUpdater.TAG,
1223
- "Background Task canceled, Activity resumed before timer completes"
1224
- );
778
+ final String previous = this.prefs.getString("LatestNativeBuildVersion", "");
779
+ if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
780
+ logger.info("New native build version detected: " + this.currentBuildVersion);
781
+ this.implementation.reset(true);
782
+ final List<BundleInfo> installed = this.implementation.list(false);
783
+ for (final BundleInfo bundle : installed) {
784
+ // Check if thread was interrupted (cancelled)
785
+ if (Thread.currentThread().isInterrupted()) {
786
+ logger.warn("Cleanup was cancelled, stopping");
787
+ return;
788
+ }
789
+ try {
790
+ logger.info("Deleting obsolete bundle: " + bundle.getId());
791
+ this.implementation.delete(bundle.getId());
792
+ } catch (final Exception e) {
793
+ logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
794
+ }
795
+ }
796
+ final List<BundleInfo> storedBundles = this.implementation.list(true);
797
+ final Set<String> allowedIds = new HashSet<>();
798
+ for (final BundleInfo info : storedBundles) {
799
+ if (info != null && info.getId() != null && !info.getId().isEmpty()) {
800
+ allowedIds.add(info.getId());
801
+ }
802
+ }
803
+ this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
804
+ this.implementation.cleanupOrphanedTempFolders(Thread.currentThread());
805
+
806
+ // Check again before the expensive delta cache cleanup
807
+ if (Thread.currentThread().isInterrupted()) {
808
+ logger.warn("Cleanup was cancelled before delta cache cleanup");
809
+ return;
810
+ }
811
+ this.implementation.cleanupDeltaCache(Thread.currentThread());
812
+ }
813
+ this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
814
+ this.editor.apply();
815
+ } catch (Exception e) {
816
+ logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
817
+ } finally {
818
+ cleanupComplete = true;
819
+ logger.info("Cleanup complete");
820
+ }
821
+ }
822
+ });
823
+
824
+ // Start a timeout watchdog thread to cancel cleanup if it takes too long
825
+ final long timeout = this.appReadyTimeout / 2;
826
+ startNewThread(() -> {
827
+ try {
828
+ Thread.sleep(timeout);
829
+ if (cleanupThread != null && cleanupThread.isAlive() && !cleanupComplete) {
830
+ logger.warn("Cleanup timeout exceeded (" + timeout + "ms), interrupting cleanup thread");
831
+ cleanupThread.interrupt();
832
+ }
833
+ } catch (InterruptedException e) {
834
+ // Watchdog thread was interrupted, that's fine
835
+ }
836
+ });
837
+ }
838
+
839
+ private void waitForCleanupIfNeeded() {
840
+ if (cleanupComplete) {
841
+ return; // Already done, no need to wait
842
+ }
843
+
844
+ logger.info("Waiting for cleanup to complete before starting download...");
845
+
846
+ // Wait for cleanup to complete - blocks until lock is released
847
+ synchronized (cleanupLock) {
848
+ logger.info("Cleanup finished, proceeding with download");
849
+ }
850
+ }
851
+
852
+ public void notifyDownload(final String id, final int percent) {
853
+ try {
854
+ final JSObject ret = new JSObject();
855
+ ret.put("percent", percent);
856
+ final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
857
+ ret.put("bundle", InternalUtils.mapToJSObject(bundleInfo.toJSONMap()));
858
+ this.notifyListeners("download", ret);
859
+
860
+ if (percent == 100) {
861
+ final JSObject retDownloadComplete = new JSObject(ret, new String[] { "bundle" });
862
+ this.notifyListeners("downloadComplete", retDownloadComplete);
863
+ this.implementation.sendStats("download_complete", bundleInfo.getVersionName());
864
+ lastNotifiedStatPercent = 100;
865
+ } else {
866
+ int currentStatPercent = (percent / 10) * 10; // Round down to nearest 10
867
+ if (currentStatPercent > lastNotifiedStatPercent) {
868
+ this.implementation.sendStats("download_" + currentStatPercent, bundleInfo.getVersionName());
869
+ lastNotifiedStatPercent = currentStatPercent;
1225
870
  }
1226
- }
1227
- }
1228
- );
1229
- backgroundTask.start();
1230
- } else {
1231
- this._checkCancelDelay(false);
1232
- this.installNext();
1233
- }
1234
- } catch (final Exception e) {
1235
- Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1236
- }
1237
- }
1238
-
1239
- private boolean isMainActivity() {
1240
- Context mContext = this.getContext();
1241
- ActivityManager activityManager =
1242
- (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
1243
- List<ActivityManager.AppTask> runningTasks = activityManager.getAppTasks();
1244
- ActivityManager.RecentTaskInfo runningTask = runningTasks
1245
- .get(0)
1246
- .getTaskInfo();
1247
- String className = runningTask.baseIntent.getComponent().getClassName();
1248
- String runningActivity = runningTask.topActivity.getClassName();
1249
- boolean isThisAppActivity = className.equals(runningActivity);
1250
- return isThisAppActivity;
1251
- }
1252
-
1253
- @Override
1254
- public void onActivityStarted(@NonNull final Activity activity) {
1255
- if (isPreviousMainActivity) {
1256
- this.appMovedToForeground();
1257
- }
1258
- isPreviousMainActivity = true;
1259
- }
1260
-
1261
- @Override
1262
- public void onActivityStopped(@NonNull final Activity activity) {
1263
- isPreviousMainActivity = isMainActivity();
1264
- if (isPreviousMainActivity) {
1265
- this.appMovedToBackground();
1266
- }
1267
- }
1268
-
1269
- @Override
1270
- public void onActivityResumed(@NonNull final Activity activity) {
1271
- if (backgroundTask != null && taskRunning) {
1272
- backgroundTask.interrupt();
1273
- }
1274
- this.implementation.activity = activity;
1275
- this.implementation.onResume();
1276
- }
1277
-
1278
- @Override
1279
- public void onActivityPaused(@NonNull final Activity activity) {
1280
- this.implementation.activity = activity;
1281
- this.implementation.onPause();
1282
- }
1283
-
1284
- @Override
1285
- public void onActivityCreated(
1286
- @NonNull final Activity activity,
1287
- @Nullable final Bundle savedInstanceState
1288
- ) {
1289
- this.implementation.activity = activity;
1290
- }
1291
-
1292
- @Override
1293
- public void onActivitySaveInstanceState(
1294
- @NonNull final Activity activity,
1295
- @NonNull final Bundle outState
1296
- ) {
1297
- this.implementation.activity = activity;
1298
- }
1299
-
1300
- @Override
1301
- public void onActivityDestroyed(@NonNull final Activity activity) {
1302
- this.implementation.activity = activity;
1303
- }
871
+ }
872
+ } catch (final Exception e) {
873
+ logger.error("Could not notify listeners " + e.getMessage());
874
+ }
875
+ }
876
+
877
+ @PluginMethod
878
+ public void setUpdateUrl(final PluginCall call) {
879
+ if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
880
+ logger.error("setUpdateUrl not allowed set allowModifyUrl in your config to true to allow it");
881
+ call.reject("setUpdateUrl not allowed");
882
+ return;
883
+ }
884
+ final String url = call.getString("url");
885
+ if (url == null) {
886
+ logger.error("setUpdateUrl called without url");
887
+ call.reject("setUpdateUrl called without url");
888
+ return;
889
+ }
890
+ this.updateUrl = url;
891
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
892
+ this.editor.putString(UPDATE_URL_PREF_KEY, url);
893
+ this.editor.apply();
894
+ }
895
+ call.resolve();
896
+ }
897
+
898
+ @PluginMethod
899
+ public void setStatsUrl(final PluginCall call) {
900
+ if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
901
+ logger.error("setStatsUrl not allowed set allowModifyUrl in your config to true to allow it");
902
+ call.reject("setStatsUrl not allowed");
903
+ return;
904
+ }
905
+ final String url = call.getString("url");
906
+ if (url == null) {
907
+ logger.error("setStatsUrl called without url");
908
+ call.reject("setStatsUrl called without url");
909
+ return;
910
+ }
911
+ this.implementation.statsUrl = url;
912
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
913
+ this.editor.putString(STATS_URL_PREF_KEY, url);
914
+ this.editor.apply();
915
+ }
916
+ call.resolve();
917
+ }
918
+
919
+ @PluginMethod
920
+ public void setChannelUrl(final PluginCall call) {
921
+ if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
922
+ logger.error("setChannelUrl not allowed set allowModifyUrl in your config to true to allow it");
923
+ call.reject("setChannelUrl not allowed");
924
+ return;
925
+ }
926
+ final String url = call.getString("url");
927
+ if (url == null) {
928
+ logger.error("setChannelUrl called without url");
929
+ call.reject("setChannelUrl called without url");
930
+ return;
931
+ }
932
+ this.implementation.channelUrl = url;
933
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
934
+ this.editor.putString(CHANNEL_URL_PREF_KEY, url);
935
+ this.editor.apply();
936
+ }
937
+ call.resolve();
938
+ }
939
+
940
+ @PluginMethod
941
+ public void getBuiltinVersion(final PluginCall call) {
942
+ try {
943
+ final JSObject ret = new JSObject();
944
+ ret.put("version", this.implementation.versionBuild);
945
+ call.resolve(ret);
946
+ } catch (final Exception e) {
947
+ logger.error("Could not get version " + e.getMessage());
948
+ call.reject("Could not get version", e);
949
+ }
950
+ }
951
+
952
+ @PluginMethod
953
+ public void getDeviceId(final PluginCall call) {
954
+ try {
955
+ final JSObject ret = new JSObject();
956
+ ret.put("deviceId", this.implementation.deviceID);
957
+ call.resolve(ret);
958
+ } catch (final Exception e) {
959
+ logger.error("Could not get device id " + e.getMessage());
960
+ call.reject("Could not get device id", e);
961
+ }
962
+ }
963
+
964
+ @PluginMethod
965
+ public void setCustomId(final PluginCall call) {
966
+ final String customId = call.getString("customId");
967
+ if (customId == null) {
968
+ logger.error("setCustomId called without customId");
969
+ call.reject("setCustomId called without customId");
970
+ return;
971
+ }
972
+ this.implementation.customId = customId;
973
+ if (Boolean.TRUE.equals(this.persistCustomId)) {
974
+ if (customId.isEmpty()) {
975
+ this.editor.remove(CUSTOM_ID_PREF_KEY);
976
+ } else {
977
+ this.editor.putString(CUSTOM_ID_PREF_KEY, customId);
978
+ }
979
+ this.editor.apply();
980
+ }
981
+ call.resolve();
982
+ }
983
+
984
+ @PluginMethod
985
+ public void getPluginVersion(final PluginCall call) {
986
+ try {
987
+ final JSObject ret = new JSObject();
988
+ ret.put("version", this.pluginVersion);
989
+ call.resolve(ret);
990
+ } catch (final Exception e) {
991
+ logger.error("Could not get plugin version " + e.getMessage());
992
+ call.reject("Could not get plugin version", e);
993
+ }
994
+ }
995
+
996
+ @PluginMethod
997
+ public void unsetChannel(final PluginCall call) {
998
+ final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
999
+
1000
+ try {
1001
+ logger.info("unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
1002
+ startNewThread(() -> {
1003
+ String configDefaultChannel = CapacitorUpdaterPlugin.this.getConfig().getString("defaultChannel", "");
1004
+ CapacitorUpdaterPlugin.this.implementation.unsetChannel(
1005
+ CapacitorUpdaterPlugin.this.editor,
1006
+ DEFAULT_CHANNEL_PREF_KEY,
1007
+ configDefaultChannel,
1008
+ (res) -> {
1009
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1010
+ if (jsRes.has("error")) {
1011
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1012
+ String errorCode = jsRes.getString("error");
1013
+
1014
+ JSObject errorObj = new JSObject();
1015
+ errorObj.put("message", errorMessage);
1016
+ errorObj.put("error", errorCode);
1017
+
1018
+ call.reject(errorMessage, "UNSETCHANNEL_FAILED", null, errorObj);
1019
+ } else {
1020
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
1021
+ logger.info("Calling autoupdater after channel change!");
1022
+ // Check if download is already in progress (with timeout protection)
1023
+ if (!this.isDownloadStuckOrTimedOut()) {
1024
+ backgroundDownload();
1025
+ } else {
1026
+ logger.info("Download already in progress, skipping duplicate download request");
1027
+ }
1028
+ }
1029
+ call.resolve(jsRes);
1030
+ }
1031
+ }
1032
+ );
1033
+ });
1034
+ } catch (final Exception e) {
1035
+ logger.error("Failed to unsetChannel: " + e.getMessage());
1036
+ call.reject("Failed to unsetChannel: ", e);
1037
+ }
1038
+ }
1039
+
1040
+ @PluginMethod
1041
+ public void setChannel(final PluginCall call) {
1042
+ final String channel = call.getString("channel");
1043
+ final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
1044
+
1045
+ if (channel == null) {
1046
+ logger.error("setChannel called without channel");
1047
+ JSObject errorObj = new JSObject();
1048
+ errorObj.put("message", "setChannel called without channel");
1049
+ errorObj.put("error", "missing_parameter");
1050
+ call.reject("setChannel called without channel", "SETCHANNEL_INVALID_PARAMS", null, errorObj);
1051
+ return;
1052
+ }
1053
+ try {
1054
+ logger.info("setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
1055
+ startNewThread(() ->
1056
+ CapacitorUpdaterPlugin.this.implementation.setChannel(
1057
+ channel,
1058
+ CapacitorUpdaterPlugin.this.editor,
1059
+ DEFAULT_CHANNEL_PREF_KEY,
1060
+ CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
1061
+ (res) -> {
1062
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1063
+ if (jsRes.has("error")) {
1064
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1065
+ String errorCode = jsRes.getString("error");
1066
+
1067
+ // Fire channelPrivate event if channel doesn't allow self-assignment
1068
+ if (
1069
+ errorCode.contains("cannot_update_via_private_channel") ||
1070
+ errorCode.contains("channel_self_set_not_allowed")
1071
+ ) {
1072
+ JSObject eventData = new JSObject();
1073
+ eventData.put("channel", channel);
1074
+ eventData.put("message", errorMessage);
1075
+ notifyListeners("channelPrivate", eventData);
1076
+ }
1077
+
1078
+ JSObject errorObj = new JSObject();
1079
+ errorObj.put("message", errorMessage);
1080
+ errorObj.put("error", errorCode);
1081
+
1082
+ call.reject(errorMessage, "SETCHANNEL_FAILED", null, errorObj);
1083
+ } else {
1084
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
1085
+ logger.info("Calling autoupdater after channel change!");
1086
+ // Check if download is already in progress (with timeout protection)
1087
+ if (!this.isDownloadStuckOrTimedOut()) {
1088
+ backgroundDownload();
1089
+ } else {
1090
+ logger.info("Download already in progress, skipping duplicate download request");
1091
+ }
1092
+ }
1093
+ call.resolve(jsRes);
1094
+ }
1095
+ }
1096
+ )
1097
+ );
1098
+ } catch (final Exception e) {
1099
+ logger.error("Failed to setChannel: " + channel + " " + e.getMessage());
1100
+ call.reject("Failed to setChannel: " + channel, e);
1101
+ }
1102
+ }
1103
+
1104
+ @PluginMethod
1105
+ public void getChannel(final PluginCall call) {
1106
+ try {
1107
+ logger.info("getChannel");
1108
+ startNewThread(() ->
1109
+ CapacitorUpdaterPlugin.this.implementation.getChannel((res) -> {
1110
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1111
+ if (jsRes.has("error")) {
1112
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1113
+ String errorCode = jsRes.getString("error");
1114
+
1115
+ JSObject errorObj = new JSObject();
1116
+ errorObj.put("message", errorMessage);
1117
+ errorObj.put("error", errorCode);
1118
+
1119
+ call.reject(errorMessage, "GETCHANNEL_FAILED", null, errorObj);
1120
+ } else {
1121
+ call.resolve(jsRes);
1122
+ }
1123
+ })
1124
+ );
1125
+ } catch (final Exception e) {
1126
+ logger.error("Failed to getChannel " + e.getMessage());
1127
+ call.reject("Failed to getChannel", e);
1128
+ }
1129
+ }
1130
+
1131
+ @PluginMethod
1132
+ public void listChannels(final PluginCall call) {
1133
+ try {
1134
+ logger.info("listChannels");
1135
+ startNewThread(() ->
1136
+ CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
1137
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1138
+ if (jsRes.has("error")) {
1139
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
1140
+ String errorCode = jsRes.getString("error");
1141
+
1142
+ JSObject errorObj = new JSObject();
1143
+ errorObj.put("message", errorMessage);
1144
+ errorObj.put("error", errorCode);
1145
+
1146
+ call.reject(errorMessage, "LISTCHANNELS_FAILED", null, errorObj);
1147
+ } else {
1148
+ call.resolve(jsRes);
1149
+ }
1150
+ })
1151
+ );
1152
+ } catch (final Exception e) {
1153
+ logger.error("Failed to listChannels: " + e.getMessage());
1154
+ call.reject("Failed to listChannels", e);
1155
+ }
1156
+ }
1157
+
1158
+ @PluginMethod
1159
+ public void download(final PluginCall call) {
1160
+ final String url = call.getString("url");
1161
+ final String version = call.getString("version");
1162
+ final String sessionKey = call.getString("sessionKey", "");
1163
+ final String checksum = call.getString("checksum", "");
1164
+ final JSONArray manifest = call.getData().optJSONArray("manifest");
1165
+ if (url == null) {
1166
+ logger.error("Download called without url");
1167
+ call.reject("Download called without url");
1168
+ return;
1169
+ }
1170
+ if (version == null) {
1171
+ logger.error("Download called without version");
1172
+ call.reject("Download called without version");
1173
+ return;
1174
+ }
1175
+ try {
1176
+ logger.info("Downloading " + url);
1177
+ startNewThread(() -> {
1178
+ try {
1179
+ final BundleInfo downloaded;
1180
+ if (manifest != null) {
1181
+ // For manifest downloads, we need to handle this asynchronously
1182
+ // to avoid automatically scheduling/applying the downloaded bundle.
1183
+ // Manual download must not schedule/apply the bundle automatically.
1184
+ CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest, false);
1185
+ // Return immediately with a pending status - the actual result will come via listeners
1186
+ final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
1187
+ downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
1188
+ call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
1189
+ return;
1190
+ } else {
1191
+ downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
1192
+ }
1193
+ if (downloaded.isErrorStatus()) {
1194
+ throw new RuntimeException("Download failed: " + downloaded.getStatus());
1195
+ } else {
1196
+ call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
1197
+ }
1198
+ } catch (final Exception e) {
1199
+ logger.error("Failed to download from: " + url + " " + e.getMessage());
1200
+ call.reject("Failed to download from: " + url, e);
1201
+ final JSObject ret = new JSObject();
1202
+ ret.put("version", version);
1203
+ CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
1204
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1205
+ CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
1206
+ }
1207
+ });
1208
+ } catch (final Exception e) {
1209
+ logger.error("Failed to download from: " + url + " " + e.getMessage());
1210
+ call.reject("Failed to download from: " + url, e);
1211
+ final JSObject ret = new JSObject();
1212
+ ret.put("version", version);
1213
+ CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
1214
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1215
+ CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
1216
+ }
1217
+ }
1218
+
1219
+ private void syncKeepUrlPathFlag(final boolean enabled) {
1220
+ if (this.bridge == null || this.bridge.getWebView() == null) {
1221
+ return;
1222
+ }
1223
+ final String script = enabled
1224
+ ? "(function(){try{localStorage.setItem('" +
1225
+ KEEP_URL_FLAG_KEY +
1226
+ "','1');}catch(e){}window.__capgoKeepUrlPathAfterReload=true;var evt;try{evt=new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',{detail:{enabled:true}});}catch(err){evt=document.createEvent('CustomEvent');evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',false,false,{enabled:true});}window.dispatchEvent(evt);})();"
1227
+ : "(function(){try{localStorage.removeItem('" +
1228
+ KEEP_URL_FLAG_KEY +
1229
+ "');}catch(e){}delete window.__capgoKeepUrlPathAfterReload;var evt;try{evt=new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',{detail:{enabled:false}});}catch(err){evt=document.createEvent('CustomEvent');evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',false,false,{enabled:false});}window.dispatchEvent(evt);})();";
1230
+ this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
1231
+ }
1232
+
1233
+ protected boolean _reload() {
1234
+ final String path = this.implementation.getCurrentBundlePath();
1235
+ if (this.keepUrlPathAfterReload) {
1236
+ this.syncKeepUrlPathFlag(true);
1237
+ }
1238
+ this.semaphoreUp();
1239
+ logger.info("Reloading: " + path);
1240
+
1241
+ AtomicReference<URL> url = new AtomicReference<>();
1242
+ if (this.keepUrlPathAfterReload) {
1243
+ try {
1244
+ if (Looper.myLooper() != Looper.getMainLooper()) {
1245
+ Semaphore mainThreadSemaphore = new Semaphore(0);
1246
+ this.bridge.executeOnMainThread(() -> {
1247
+ try {
1248
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1249
+ String currentUrl = this.bridge.getWebView().getUrl();
1250
+ if (currentUrl != null) {
1251
+ url.set(new URL(currentUrl));
1252
+ }
1253
+ }
1254
+ } catch (Exception e) {
1255
+ logger.error("Error executing on main thread " + e.getMessage());
1256
+ }
1257
+ mainThreadSemaphore.release();
1258
+ });
1259
+
1260
+ // Add timeout to prevent indefinite blocking
1261
+ if (!mainThreadSemaphore.tryAcquire(10, TimeUnit.SECONDS)) {
1262
+ logger.error("Timeout waiting for main thread operation");
1263
+ }
1264
+ } else {
1265
+ try {
1266
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1267
+ String currentUrl = this.bridge.getWebView().getUrl();
1268
+ if (currentUrl != null) {
1269
+ url.set(new URL(currentUrl));
1270
+ }
1271
+ }
1272
+ } catch (Exception e) {
1273
+ logger.error("Error executing on main thread " + e.getMessage());
1274
+ }
1275
+ }
1276
+ } catch (InterruptedException e) {
1277
+ logger.error("Error waiting for main thread or getting the current URL from webview " + e.getMessage());
1278
+ Thread.currentThread().interrupt(); // Restore interrupted status
1279
+ }
1280
+ }
1281
+
1282
+ if (url.get() != null) {
1283
+ if (this.implementation.isUsingBuiltin()) {
1284
+ this.bridge.getLocalServer().hostAssets(path);
1285
+ } else {
1286
+ this.bridge.getLocalServer().hostFiles(path);
1287
+ }
1288
+
1289
+ try {
1290
+ URL finalUrl = null;
1291
+ finalUrl = new URL(this.bridge.getAppUrl());
1292
+ finalUrl = new URL(finalUrl.getProtocol(), finalUrl.getHost(), finalUrl.getPort(), url.get().getPath());
1293
+ URL finalUrl1 = finalUrl;
1294
+ this.bridge.getWebView().post(() -> {
1295
+ this.bridge.getWebView().loadUrl(finalUrl1.toString());
1296
+ if (!this.keepUrlPathAfterReload) {
1297
+ this.bridge.getWebView().clearHistory();
1298
+ }
1299
+ });
1300
+ } catch (MalformedURLException e) {
1301
+ logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
1302
+
1303
+ if (this.implementation.isUsingBuiltin()) {
1304
+ this.bridge.setServerAssetPath(path);
1305
+ } else {
1306
+ this.bridge.setServerBasePath(path);
1307
+ }
1308
+ }
1309
+ } else {
1310
+ if (this.implementation.isUsingBuiltin()) {
1311
+ this.bridge.setServerAssetPath(path);
1312
+ } else {
1313
+ this.bridge.setServerBasePath(path);
1314
+ }
1315
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1316
+ this.bridge.getWebView().post(() -> {
1317
+ if (this.bridge.getWebView() != null) {
1318
+ this.bridge.getWebView().loadUrl(this.bridge.getAppUrl());
1319
+ if (!this.keepUrlPathAfterReload) {
1320
+ this.bridge.getWebView().clearHistory();
1321
+ }
1322
+ }
1323
+ });
1324
+ }
1325
+ }
1326
+
1327
+ this.checkAppReady();
1328
+ this.notifyListeners("appReloaded", new JSObject());
1329
+
1330
+ // Wait for the reload to complete (until notifyAppReady is called)
1331
+ try {
1332
+ this.semaphoreWait(this.appReadyTimeout);
1333
+ } catch (Exception e) {
1334
+ logger.error("Error waiting for app ready: " + e.getMessage());
1335
+ return false;
1336
+ }
1337
+
1338
+ return true;
1339
+ }
1340
+
1341
+ @PluginMethod
1342
+ public void reload(final PluginCall call) {
1343
+ try {
1344
+ if (this._reload()) {
1345
+ call.resolve();
1346
+ } else {
1347
+ logger.error("Reload failed");
1348
+ call.reject("Reload failed");
1349
+ }
1350
+ } catch (final Exception e) {
1351
+ logger.error("Could not reload " + e.getMessage());
1352
+ call.reject("Could not reload", e);
1353
+ }
1354
+ }
1355
+
1356
+ @PluginMethod
1357
+ public void next(final PluginCall call) {
1358
+ final String id = call.getString("id");
1359
+ if (id == null) {
1360
+ logger.error("Next called without id");
1361
+ call.reject("Next called without id");
1362
+ return;
1363
+ }
1364
+ try {
1365
+ logger.info("Setting next active id " + id);
1366
+ if (!this.implementation.setNextBundle(id)) {
1367
+ logger.error("Set next id failed. Bundle " + id + " does not exist.");
1368
+ call.reject("Set next id failed. Bundle " + id + " does not exist.");
1369
+ } else {
1370
+ call.resolve(InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1371
+ }
1372
+ } catch (final Exception e) {
1373
+ logger.error("Could not set next id " + id + " " + e.getMessage());
1374
+ call.reject("Could not set next id: " + id, e);
1375
+ }
1376
+ }
1377
+
1378
+ @PluginMethod
1379
+ public void set(final PluginCall call) {
1380
+ final String id = call.getString("id");
1381
+ if (id == null) {
1382
+ logger.error("Set called without id");
1383
+ call.reject("Set called without id");
1384
+ return;
1385
+ }
1386
+ try {
1387
+ logger.info("Setting active bundle " + id);
1388
+ if (!this.implementation.set(id)) {
1389
+ logger.info("No such bundle " + id);
1390
+ call.reject("Update failed, id " + id + " does not exist.");
1391
+ } else {
1392
+ logger.info("Bundle successfully set to " + id);
1393
+ this.reload(call);
1394
+ }
1395
+ } catch (final Exception e) {
1396
+ logger.error("Could not set id " + id + " " + e.getMessage());
1397
+ call.reject("Could not set id " + id, e);
1398
+ }
1399
+ }
1400
+
1401
+ @PluginMethod
1402
+ public void delete(final PluginCall call) {
1403
+ final String id = call.getString("id");
1404
+ if (id == null) {
1405
+ logger.error("missing id");
1406
+ call.reject("missing id");
1407
+ return;
1408
+ }
1409
+ logger.info("Deleting id " + id);
1410
+ try {
1411
+ final Boolean res = this.implementation.delete(id);
1412
+ if (res) {
1413
+ call.resolve();
1414
+ } else {
1415
+ logger.error("Delete failed, id " + id + " does not exist");
1416
+ call.reject("Delete failed, id " + id + " does not exist or it cannot be deleted (perhaps it is the 'next' bundle)");
1417
+ }
1418
+ } catch (final Exception e) {
1419
+ logger.error("Could not delete id " + id + " " + e.getMessage());
1420
+ call.reject("Could not delete id " + id, e);
1421
+ }
1422
+ }
1423
+
1424
+ @PluginMethod
1425
+ public void setBundleError(final PluginCall call) {
1426
+ if (!Boolean.TRUE.equals(this.allowManualBundleError)) {
1427
+ logger.error("setBundleError called without allowManualBundleError");
1428
+ call.reject("setBundleError not allowed. Set allowManualBundleError to true in your config to enable it.");
1429
+ return;
1430
+ }
1431
+ final String id = call.getString("id");
1432
+ if (id == null) {
1433
+ logger.error("setBundleError called without id");
1434
+ call.reject("setBundleError called without id");
1435
+ return;
1436
+ }
1437
+ try {
1438
+ final BundleInfo bundle = this.implementation.getBundleInfo(id);
1439
+ if (bundle == null || bundle.isUnknown()) {
1440
+ logger.error("setBundleError called with unknown bundle " + id);
1441
+ call.reject("Bundle " + id + " does not exist");
1442
+ return;
1443
+ }
1444
+ if (bundle.isBuiltin()) {
1445
+ logger.error("setBundleError called on builtin bundle");
1446
+ call.reject("Cannot set builtin bundle to error state");
1447
+ return;
1448
+ }
1449
+ if (Boolean.TRUE.equals(this.autoUpdate)) {
1450
+ logger.warn("setBundleError used while autoUpdate is enabled; this method is intended for manual mode");
1451
+ }
1452
+ this.implementation.setError(bundle);
1453
+ final JSObject ret = new JSObject();
1454
+ ret.put("bundle", InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1455
+ call.resolve(ret);
1456
+ } catch (final Exception e) {
1457
+ logger.error("Could not set bundle error for id " + id + " " + e.getMessage());
1458
+ call.reject("Could not set bundle error for id " + id, e);
1459
+ }
1460
+ }
1461
+
1462
+ @PluginMethod
1463
+ public void list(final PluginCall call) {
1464
+ try {
1465
+ final List<BundleInfo> res = this.implementation.list(call.getBoolean("raw", false));
1466
+ final JSObject ret = new JSObject();
1467
+ final JSArray values = new JSArray();
1468
+ for (final BundleInfo bundle : res) {
1469
+ values.put(InternalUtils.mapToJSObject(bundle.toJSONMap()));
1470
+ }
1471
+ ret.put("bundles", values);
1472
+ call.resolve(ret);
1473
+ } catch (final Exception e) {
1474
+ logger.error("Could not list bundles " + e.getMessage());
1475
+ call.reject("Could not list bundles", e);
1476
+ }
1477
+ }
1478
+
1479
+ @PluginMethod
1480
+ public void getLatest(final PluginCall call) {
1481
+ final String channel = call.getString("channel");
1482
+ startNewThread(() ->
1483
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
1484
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1485
+ if (jsRes.has("error")) {
1486
+ String error = jsRes.getString("error");
1487
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1488
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1489
+ call.reject(jsRes.getString("error"));
1490
+ return;
1491
+ } else if (jsRes.has("message")) {
1492
+ call.reject(jsRes.getString("message"));
1493
+ return;
1494
+ } else {
1495
+ call.resolve(jsRes);
1496
+ }
1497
+ })
1498
+ );
1499
+ }
1500
+
1501
+ private boolean _reset(final Boolean toLastSuccessful) {
1502
+ final BundleInfo fallback = this.implementation.getFallbackBundle();
1503
+ this.implementation.reset();
1504
+
1505
+ if (toLastSuccessful && !fallback.isBuiltin()) {
1506
+ logger.info("Resetting to: " + fallback);
1507
+ return this.implementation.set(fallback) && this._reload();
1508
+ }
1509
+
1510
+ logger.info("Resetting to native.");
1511
+ return this._reload();
1512
+ }
1513
+
1514
+ @PluginMethod
1515
+ public void reset(final PluginCall call) {
1516
+ try {
1517
+ final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
1518
+ if (this._reset(toLastSuccessful)) {
1519
+ call.resolve();
1520
+ return;
1521
+ }
1522
+ logger.error("Reset failed");
1523
+ call.reject("Reset failed");
1524
+ } catch (final Exception e) {
1525
+ logger.error("Reset failed " + e.getMessage());
1526
+ call.reject("Reset failed", e);
1527
+ }
1528
+ }
1529
+
1530
+ @PluginMethod
1531
+ public void current(final PluginCall call) {
1532
+ ensureBridgeSet();
1533
+ try {
1534
+ final JSObject ret = new JSObject();
1535
+ final BundleInfo bundle = this.implementation.getCurrentBundle();
1536
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
1537
+ ret.put("native", this.currentVersionNative);
1538
+ call.resolve(ret);
1539
+ } catch (final Exception e) {
1540
+ logger.error("Could not get current bundle " + e.getMessage());
1541
+ call.reject("Could not get current bundle", e);
1542
+ }
1543
+ }
1544
+
1545
+ @PluginMethod
1546
+ public void getNextBundle(final PluginCall call) {
1547
+ try {
1548
+ final BundleInfo bundle = this.implementation.getNextBundle();
1549
+ if (bundle == null) {
1550
+ call.resolve(null);
1551
+ return;
1552
+ }
1553
+
1554
+ call.resolve(InternalUtils.mapToJSObject(bundle.toJSONMap()));
1555
+ } catch (final Exception e) {
1556
+ logger.error("Could not get next bundle " + e.getMessage());
1557
+ call.reject("Could not get next bundle", e);
1558
+ }
1559
+ }
1560
+
1561
+ @PluginMethod
1562
+ public void getFailedUpdate(final PluginCall call) {
1563
+ try {
1564
+ final BundleInfo bundle = this.readLastFailedBundle();
1565
+ if (bundle == null || bundle.isUnknown()) {
1566
+ call.resolve(null);
1567
+ return;
1568
+ }
1569
+
1570
+ this.persistLastFailedBundle(null);
1571
+
1572
+ final JSObject ret = new JSObject();
1573
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
1574
+ call.resolve(ret);
1575
+ } catch (final Exception e) {
1576
+ logger.error("Could not get failed update " + e.getMessage());
1577
+ call.reject("Could not get failed update", e);
1578
+ }
1579
+ }
1580
+
1581
+ public void checkForUpdateAfterDelay() {
1582
+ if (this.periodCheckDelay == 0 || !this._isAutoUpdateEnabled()) {
1583
+ return;
1584
+ }
1585
+ final Timer timer = new Timer();
1586
+ timer.schedule(
1587
+ new TimerTask() {
1588
+ @Override
1589
+ public void run() {
1590
+ try {
1591
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1592
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1593
+ if (jsRes.has("error")) {
1594
+ String error = jsRes.getString("error");
1595
+ String errorMessage = jsRes.has("message")
1596
+ ? jsRes.getString("message")
1597
+ : "server did not provide a message";
1598
+ logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
1599
+ } else if (jsRes.has("version")) {
1600
+ String newVersion = jsRes.getString("version");
1601
+ String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
1602
+ if (!Objects.equals(newVersion, currentVersion)) {
1603
+ logger.info("New version found: " + newVersion);
1604
+ // Check if download is already in progress (with timeout protection)
1605
+ if (!CapacitorUpdaterPlugin.this.isDownloadStuckOrTimedOut()) {
1606
+ CapacitorUpdaterPlugin.this.backgroundDownload();
1607
+ } else {
1608
+ logger.info("Download already in progress, skipping duplicate download request");
1609
+ }
1610
+ }
1611
+ }
1612
+ });
1613
+ } catch (final Exception e) {
1614
+ logger.error("Failed to check for update " + e.getMessage());
1615
+ }
1616
+ }
1617
+ },
1618
+ this.periodCheckDelay,
1619
+ this.periodCheckDelay
1620
+ );
1621
+ }
1622
+
1623
+ @PluginMethod
1624
+ public void notifyAppReady(final PluginCall call) {
1625
+ ensureBridgeSet();
1626
+ try {
1627
+ final BundleInfo bundle = this.implementation.getCurrentBundle();
1628
+ this.implementation.setSuccess(bundle, this.autoDeletePrevious);
1629
+ logger.info("Current bundle loaded successfully. ['notifyAppReady()' was called] " + bundle);
1630
+ logger.info("semaphoreReady countDown");
1631
+ this.semaphoreDown();
1632
+ logger.info("semaphoreReady countDown done");
1633
+ final JSObject ret = new JSObject();
1634
+ ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
1635
+ call.resolve(ret);
1636
+ } catch (final Exception e) {
1637
+ logger.error("Failed to notify app ready state. [Error calling 'notifyAppReady()'] " + e.getMessage());
1638
+ call.reject("Failed to commit app ready state.", e);
1639
+ }
1640
+ }
1641
+
1642
+ @PluginMethod
1643
+ public void setMultiDelay(final PluginCall call) {
1644
+ try {
1645
+ final JSONArray delayConditions = call.getData().optJSONArray("delayConditions");
1646
+ if (delayConditions == null) {
1647
+ logger.error("setMultiDelay called without delayCondition");
1648
+ call.reject("setMultiDelay called without delayCondition");
1649
+ return;
1650
+ }
1651
+ for (int i = 0; i < delayConditions.length(); i++) {
1652
+ final JSONObject object = delayConditions.optJSONObject(i);
1653
+ if (object != null && object.optString("kind").equals("background") && object.optString("value").isEmpty()) {
1654
+ object.put("value", "0");
1655
+ delayConditions.put(i, object);
1656
+ }
1657
+ }
1658
+
1659
+ if (this.delayUpdateUtils.setMultiDelay(delayConditions.toString())) {
1660
+ call.resolve();
1661
+ } else {
1662
+ call.reject("Failed to delay update");
1663
+ }
1664
+ } catch (final Exception e) {
1665
+ logger.error("Failed to delay update, [Error calling 'setMultiDelay()'] " + e.getMessage());
1666
+ call.reject("Failed to delay update", e);
1667
+ }
1668
+ }
1669
+
1670
+ @PluginMethod
1671
+ public void cancelDelay(final PluginCall call) {
1672
+ if (this.delayUpdateUtils.cancelDelay("JS")) {
1673
+ call.resolve();
1674
+ } else {
1675
+ call.reject("Failed to cancel delay");
1676
+ }
1677
+ }
1678
+
1679
+ private Boolean _isAutoUpdateEnabled() {
1680
+ final CapConfig config = CapConfig.loadDefault(this.getActivity());
1681
+ String serverUrl = config.getServerUrl();
1682
+ if (serverUrl != null && !serverUrl.isEmpty()) {
1683
+ // log warning autoupdate disabled when serverUrl is set
1684
+ logger.warn("AutoUpdate is automatic disabled when serverUrl is set.");
1685
+ }
1686
+ return (
1687
+ CapacitorUpdaterPlugin.this.autoUpdate &&
1688
+ !"".equals(CapacitorUpdaterPlugin.this.updateUrl) &&
1689
+ (serverUrl == null || serverUrl.isEmpty())
1690
+ );
1691
+ }
1692
+
1693
+ @PluginMethod
1694
+ public void isAutoUpdateEnabled(final PluginCall call) {
1695
+ try {
1696
+ final JSObject ret = new JSObject();
1697
+ ret.put("enabled", this._isAutoUpdateEnabled());
1698
+ call.resolve(ret);
1699
+ } catch (final Exception e) {
1700
+ logger.error("Could not get autoUpdate status " + e.getMessage());
1701
+ call.reject("Could not get autoUpdate status", e);
1702
+ }
1703
+ }
1704
+
1705
+ @PluginMethod
1706
+ public void isAutoUpdateAvailable(final PluginCall call) {
1707
+ try {
1708
+ final CapConfig config = CapConfig.loadDefault(this.getActivity());
1709
+ String serverUrl = config.getServerUrl();
1710
+ final JSObject ret = new JSObject();
1711
+ ret.put("available", serverUrl == null || serverUrl.isEmpty());
1712
+ call.resolve(ret);
1713
+ } catch (final Exception e) {
1714
+ logger.error("Could not get autoUpdate availability " + e.getMessage());
1715
+ call.reject("Could not get autoUpdate availability", e);
1716
+ }
1717
+ }
1718
+
1719
+ private void checkAppReady() {
1720
+ try {
1721
+ if (this.appReadyCheck != null) {
1722
+ this.appReadyCheck.interrupt();
1723
+ }
1724
+ this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck());
1725
+ } catch (final Exception e) {
1726
+ logger.error("Failed to start " + DeferredNotifyAppReadyCheck.class.getName() + " " + e.getMessage());
1727
+ }
1728
+ }
1729
+
1730
+ private boolean isValidURL(String urlStr) {
1731
+ try {
1732
+ new URL(urlStr);
1733
+ return true;
1734
+ } catch (MalformedURLException e) {
1735
+ return false;
1736
+ }
1737
+ }
1738
+
1739
+ private void ensureBridgeSet() {
1740
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1741
+ logger.setBridge(this.bridge);
1742
+ }
1743
+ }
1744
+
1745
+ private void endBackGroundTaskWithNotif(String msg, String latestVersionName, BundleInfo current, Boolean error) {
1746
+ endBackGroundTaskWithNotif(msg, latestVersionName, current, error, false, "download_fail", "downloadFailed", true);
1747
+ }
1748
+
1749
+ private void endBackGroundTaskWithNotif(
1750
+ String msg,
1751
+ String latestVersionName,
1752
+ BundleInfo current,
1753
+ Boolean error,
1754
+ Boolean isDirectUpdate
1755
+ ) {
1756
+ endBackGroundTaskWithNotif(msg, latestVersionName, current, error, isDirectUpdate, "download_fail", "downloadFailed", true);
1757
+ }
1758
+
1759
+ private void endBackGroundTaskWithNotif(
1760
+ String msg,
1761
+ String latestVersionName,
1762
+ BundleInfo current,
1763
+ Boolean error,
1764
+ Boolean isDirectUpdate,
1765
+ String failureAction,
1766
+ String failureEvent
1767
+ ) {
1768
+ endBackGroundTaskWithNotif(msg, latestVersionName, current, error, isDirectUpdate, failureAction, failureEvent, true);
1769
+ }
1770
+
1771
+ private void endBackGroundTaskWithNotif(
1772
+ String msg,
1773
+ String latestVersionName,
1774
+ BundleInfo current,
1775
+ Boolean error,
1776
+ Boolean isDirectUpdate,
1777
+ String failureAction,
1778
+ String failureEvent,
1779
+ boolean shouldSendStats
1780
+ ) {
1781
+ if (error) {
1782
+ logger.info(
1783
+ "endBackGroundTaskWithNotif error: " +
1784
+ error +
1785
+ " current: " +
1786
+ current.getVersionName() +
1787
+ "latestVersionName: " +
1788
+ latestVersionName
1789
+ );
1790
+ if (shouldSendStats) {
1791
+ this.implementation.sendStats(failureAction, current.getVersionName());
1792
+ }
1793
+ final JSObject ret = new JSObject();
1794
+ ret.put("version", latestVersionName);
1795
+ this.notifyListeners(failureEvent, ret);
1796
+ }
1797
+ final JSObject ret = new JSObject();
1798
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
1799
+ this.notifyListeners("noNeedUpdate", ret);
1800
+ this.sendReadyToJs(current, msg, isDirectUpdate);
1801
+ this.backgroundDownloadTask = null;
1802
+ this.downloadStartTimeMs = 0;
1803
+ logger.info("endBackGroundTaskWithNotif " + msg);
1804
+ }
1805
+
1806
+ private boolean isDownloadStuckOrTimedOut() {
1807
+ if (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive()) {
1808
+ return false;
1809
+ }
1810
+
1811
+ // Check if download has timed out
1812
+ if (this.downloadStartTimeMs > 0) {
1813
+ long elapsed = System.currentTimeMillis() - this.downloadStartTimeMs;
1814
+ if (elapsed > DOWNLOAD_TIMEOUT_MS) {
1815
+ logger.warn(
1816
+ "Download has been in progress for " +
1817
+ elapsed +
1818
+ " ms, exceeding timeout of " +
1819
+ DOWNLOAD_TIMEOUT_MS +
1820
+ " ms. Clearing stuck state."
1821
+ );
1822
+ this.backgroundDownloadTask = null;
1823
+ this.downloadStartTimeMs = 0;
1824
+ return false; // Now it's not stuck anymore, caller can proceed
1825
+ }
1826
+ }
1827
+
1828
+ return true;
1829
+ }
1830
+
1831
+ private Thread backgroundDownload() {
1832
+ final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
1833
+ final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
1834
+ this.implementation.directUpdate = initialDirectUpdateAllowed;
1835
+ final String messageUpdate = initialDirectUpdateAllowed
1836
+ ? "Update will occur now."
1837
+ : "Update will occur next time app moves to background.";
1838
+ Thread newTask = startNewThread(() -> {
1839
+ // Wait for cleanup to complete before starting download
1840
+ waitForCleanupIfNeeded();
1841
+ logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
1842
+ try {
1843
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1844
+ JSObject jsRes = InternalUtils.mapToJSObject(res);
1845
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1846
+
1847
+ // Handle network errors and other failures first
1848
+ if (jsRes.has("error")) {
1849
+ String error = jsRes.getString("error");
1850
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
1851
+ int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
1852
+ boolean responseIsOk = statusCode >= 200 && statusCode < 300;
1853
+
1854
+ logger.error(
1855
+ "getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
1856
+ );
1857
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
1858
+
1859
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1860
+ errorMessage,
1861
+ latestVersion,
1862
+ current,
1863
+ true,
1864
+ plannedDirectUpdate,
1865
+ "download_fail",
1866
+ "downloadFailed",
1867
+ !responseIsOk
1868
+ );
1869
+ return;
1870
+ }
1871
+
1872
+ try {
1873
+ final String latestVersionName = jsRes.getString("version");
1874
+
1875
+ if ("builtin".equals(latestVersionName)) {
1876
+ logger.info("Latest version is builtin");
1877
+ final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
1878
+ plannedDirectUpdate
1879
+ );
1880
+ if (directUpdateAllowedNow) {
1881
+ logger.info("Direct update to builtin version");
1882
+ this._reset(false);
1883
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1884
+ "Updated to builtin version",
1885
+ latestVersionName,
1886
+ CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1887
+ false,
1888
+ true
1889
+ );
1890
+ } else {
1891
+ if (plannedDirectUpdate && !directUpdateAllowedNow) {
1892
+ logger.info(
1893
+ "Direct update skipped because splashscreen timeout occurred. Update will be applied later."
1894
+ );
1895
+ }
1896
+ logger.info("Setting next bundle to builtin");
1897
+ CapacitorUpdaterPlugin.this.implementation.setNextBundle(BundleInfo.ID_BUILTIN);
1898
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1899
+ "Next update will be to builtin version",
1900
+ latestVersionName,
1901
+ current,
1902
+ false
1903
+ );
1904
+ }
1905
+ return;
1906
+ }
1907
+
1908
+ if (!jsRes.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(jsRes.getString("url"))) {
1909
+ logger.error("Error no url or wrong format");
1910
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1911
+ "Error no url or wrong format",
1912
+ current.getVersionName(),
1913
+ current,
1914
+ true,
1915
+ plannedDirectUpdate
1916
+ );
1917
+ return;
1918
+ }
1919
+
1920
+ if (
1921
+ latestVersionName != null && !latestVersionName.isEmpty() && !current.getVersionName().equals(latestVersionName)
1922
+ ) {
1923
+ final BundleInfo latest = CapacitorUpdaterPlugin.this.implementation.getBundleInfoByName(latestVersionName);
1924
+ if (latest != null) {
1925
+ final JSObject ret = new JSObject();
1926
+ ret.put("bundle", InternalUtils.mapToJSObject(latest.toJSONMap()));
1927
+ if (latest.isErrorStatus()) {
1928
+ logger.error("Latest bundle already exists, and is in error state. Aborting update.");
1929
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1930
+ "Latest bundle already exists, and is in error state. Aborting update.",
1931
+ latestVersionName,
1932
+ current,
1933
+ true,
1934
+ plannedDirectUpdate
1935
+ );
1936
+ return;
1937
+ }
1938
+ if (latest.isDownloaded()) {
1939
+ logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
1940
+ final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
1941
+ plannedDirectUpdate
1942
+ );
1943
+ if (directUpdateAllowedNow) {
1944
+ String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
1945
+ ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(
1946
+ delayUpdatePreferences
1947
+ );
1948
+ if (!delayConditionList.isEmpty()) {
1949
+ logger.info("Update delayed until delay conditions met");
1950
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1951
+ "Update delayed until delay conditions met",
1952
+ latestVersionName,
1953
+ latest,
1954
+ false,
1955
+ plannedDirectUpdate
1956
+ );
1957
+ return;
1958
+ }
1959
+ CapacitorUpdaterPlugin.this.implementation.set(latest);
1960
+ CapacitorUpdaterPlugin.this._reload();
1961
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1962
+ "Update installed",
1963
+ latestVersionName,
1964
+ latest,
1965
+ false,
1966
+ true
1967
+ );
1968
+ } else {
1969
+ if (plannedDirectUpdate && !directUpdateAllowedNow) {
1970
+ logger.info(
1971
+ "Direct update skipped because splashscreen timeout occurred. Update will install on next background."
1972
+ );
1973
+ }
1974
+ CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
1975
+ CapacitorUpdaterPlugin.this.implementation.setNextBundle(latest.getId());
1976
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1977
+ "update downloaded, will install next background",
1978
+ latestVersionName,
1979
+ latest,
1980
+ false
1981
+ );
1982
+ }
1983
+ return;
1984
+ }
1985
+ if (latest.isDeleted()) {
1986
+ logger.info("Latest bundle already exists and will be deleted, download will overwrite it.");
1987
+ try {
1988
+ final Boolean deleted = CapacitorUpdaterPlugin.this.implementation.delete(latest.getId(), true);
1989
+ if (deleted) {
1990
+ logger.info("Failed bundle deleted: " + latest.getVersionName());
1991
+ }
1992
+ } catch (final IOException e) {
1993
+ logger.error("Failed to delete failed bundle: " + latest.getVersionName() + " " + e.getMessage());
1994
+ }
1995
+ }
1996
+ }
1997
+ startNewThread(() -> {
1998
+ try {
1999
+ logger.info(
2000
+ "New bundle: " +
2001
+ latestVersionName +
2002
+ " found. Current is: " +
2003
+ current.getVersionName() +
2004
+ ". " +
2005
+ messageUpdate
2006
+ );
2007
+
2008
+ final String url = jsRes.getString("url");
2009
+ final String sessionKey = jsRes.has("sessionKey") ? jsRes.getString("sessionKey") : "";
2010
+ final String checksum = jsRes.has("checksum") ? jsRes.getString("checksum") : "";
2011
+
2012
+ if (jsRes.has("manifest")) {
2013
+ // Handle manifest-based download
2014
+ JSONArray manifest = jsRes.getJSONArray("manifest");
2015
+ CapacitorUpdaterPlugin.this.implementation.downloadBackground(
2016
+ url,
2017
+ latestVersionName,
2018
+ sessionKey,
2019
+ checksum,
2020
+ manifest
2021
+ );
2022
+ } else {
2023
+ // Handle single file download (existing code)
2024
+ CapacitorUpdaterPlugin.this.implementation.downloadBackground(
2025
+ url,
2026
+ latestVersionName,
2027
+ sessionKey,
2028
+ checksum,
2029
+ null
2030
+ );
2031
+ }
2032
+ } catch (final Exception e) {
2033
+ logger.error("error downloading file " + e.getMessage());
2034
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2035
+ "Error downloading file",
2036
+ latestVersionName,
2037
+ CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
2038
+ true,
2039
+ plannedDirectUpdate
2040
+ );
2041
+ }
2042
+ });
2043
+ } else {
2044
+ logger.info("No need to update, " + current.getId() + " is the latest bundle.");
2045
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif("No need to update", latestVersionName, current, false);
2046
+ }
2047
+ } catch (final Exception e) {
2048
+ logger.error("error in update check " + e.getMessage());
2049
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2050
+ "Error in update check",
2051
+ current.getVersionName(),
2052
+ current,
2053
+ true,
2054
+ plannedDirectUpdate
2055
+ );
2056
+ }
2057
+ });
2058
+ } catch (final Exception e) {
2059
+ logger.error("getLatest call failed: " + e.getMessage());
2060
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2061
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
2062
+ "Network connection failed",
2063
+ current.getVersionName(),
2064
+ current,
2065
+ true,
2066
+ plannedDirectUpdate
2067
+ );
2068
+ }
2069
+ });
2070
+ this.backgroundDownloadTask = newTask;
2071
+ this.downloadStartTimeMs = System.currentTimeMillis();
2072
+ return newTask;
2073
+ }
2074
+
2075
+ private void installNext() {
2076
+ try {
2077
+ String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
2078
+ ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(delayUpdatePreferences);
2079
+ if (!delayConditionList.isEmpty()) {
2080
+ logger.info("Update delayed until delay conditions met");
2081
+ return;
2082
+ }
2083
+ final BundleInfo current = this.implementation.getCurrentBundle();
2084
+ final BundleInfo next = this.implementation.getNextBundle();
2085
+
2086
+ if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
2087
+ // There is a next bundle waiting for activation
2088
+ logger.debug("Next bundle is: " + next.getVersionName());
2089
+ if (this.implementation.set(next) && this._reload()) {
2090
+ logger.info("Updated to bundle: " + next.getVersionName());
2091
+ this.implementation.setNextBundle(null);
2092
+ } else {
2093
+ logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
2094
+ }
2095
+ }
2096
+ } catch (final Exception e) {
2097
+ logger.error("Error during onActivityStopped " + e.getMessage());
2098
+ }
2099
+ }
2100
+
2101
+ private void checkRevert() {
2102
+ // Automatically roll back to fallback version if notifyAppReady has not been called yet
2103
+ final BundleInfo current = this.implementation.getCurrentBundle();
2104
+
2105
+ if (current.isBuiltin()) {
2106
+ logger.info("Built-in bundle is active. We skip the check for notifyAppReady.");
2107
+ return;
2108
+ }
2109
+ logger.debug("Current bundle is: " + current);
2110
+
2111
+ if (BundleStatus.SUCCESS != current.getStatus()) {
2112
+ logger.error("notifyAppReady was not called, roll back current bundle: " + current.getId());
2113
+ logger.info("Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
2114
+ final JSObject ret = new JSObject();
2115
+ ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
2116
+ this.persistLastFailedBundle(current);
2117
+ this.notifyListeners("updateFailed", ret);
2118
+ this.implementation.sendStats("update_fail", current.getVersionName());
2119
+ this.implementation.setError(current);
2120
+ this._reset(true);
2121
+ if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
2122
+ logger.info("Deleting failing bundle: " + current.getVersionName());
2123
+ try {
2124
+ final Boolean res = this.implementation.delete(current.getId(), false);
2125
+ if (res) {
2126
+ logger.info("Failed bundle deleted: " + current.getVersionName());
2127
+ }
2128
+ } catch (final IOException e) {
2129
+ logger.error("Failed to delete failed bundle: " + current.getVersionName() + " " + e.getMessage());
2130
+ }
2131
+ }
2132
+ } else {
2133
+ logger.info("notifyAppReady was called. This is fine: " + current.getId());
2134
+ }
2135
+ }
2136
+
2137
+ private class DeferredNotifyAppReadyCheck implements Runnable {
2138
+
2139
+ @Override
2140
+ public void run() {
2141
+ try {
2142
+ logger.info("Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady");
2143
+ Thread.sleep(CapacitorUpdaterPlugin.this.appReadyTimeout);
2144
+ CapacitorUpdaterPlugin.this.checkRevert();
2145
+ CapacitorUpdaterPlugin.this.appReadyCheck = null;
2146
+ } catch (final InterruptedException e) {
2147
+ logger.info(DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
2148
+ }
2149
+ }
2150
+ }
2151
+
2152
+ public void appMovedToForeground() {
2153
+ // Ensure activity reference is up-to-date before proceeding
2154
+ // This is critical for callbacks that may be invoked during background operations
2155
+ try {
2156
+ Activity currentActivity = this.getActivity();
2157
+ if (currentActivity != null) {
2158
+ CapacitorUpdaterPlugin.this.implementation.activity = currentActivity;
2159
+ } else {
2160
+ logger.warn("appMovedToForeground: Activity is null, operations may be limited");
2161
+ }
2162
+ } catch (Exception e) {
2163
+ logger.error("appMovedToForeground: Failed to update activity reference: " + e.getMessage());
2164
+ }
2165
+
2166
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2167
+ CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_foreground", current.getVersionName());
2168
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
2169
+ this.delayUpdateUtils.unsetBackgroundTimestamp();
2170
+
2171
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && !this.isDownloadStuckOrTimedOut()) {
2172
+ this.backgroundDownload();
2173
+ } else {
2174
+ final CapConfig config = CapConfig.loadDefault(this.getActivity());
2175
+ String serverUrl = config.getServerUrl();
2176
+ if (serverUrl != null && !serverUrl.isEmpty()) {
2177
+ CapacitorUpdaterPlugin.this.implementation.sendStats("blocked_by_server_url", current.getVersionName());
2178
+ }
2179
+ logger.info("Auto update is disabled");
2180
+ this.sendReadyToJs(current, "disabled");
2181
+ }
2182
+ this.checkAppReady();
2183
+ }
2184
+
2185
+ public void appMovedToBackground() {
2186
+ // Reset timeout flag at start of each background cycle
2187
+ this.autoSplashscreenTimedOut = false;
2188
+
2189
+ // Ensure activity reference is up-to-date before proceeding
2190
+ try {
2191
+ Activity currentActivity = this.getActivity();
2192
+ if (currentActivity != null) {
2193
+ CapacitorUpdaterPlugin.this.implementation.activity = currentActivity;
2194
+ } else {
2195
+ logger.warn("appMovedToBackground: Activity is null, operations may be limited");
2196
+ }
2197
+ } catch (Exception e) {
2198
+ logger.error("appMovedToBackground: Failed to update activity reference: " + e.getMessage());
2199
+ }
2200
+
2201
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
2202
+
2203
+ // Show splashscreen FIRST, before any other background work to ensure launcher shows it
2204
+ if (this.autoSplashscreen) {
2205
+ boolean canShowSplashscreen = true;
2206
+
2207
+ if (!this._isAutoUpdateEnabled()) {
2208
+ logger.warn(
2209
+ "autoSplashscreen is enabled but autoUpdate is disabled. Splashscreen will not be shown. Enable autoUpdate or disable autoSplashscreen."
2210
+ );
2211
+ canShowSplashscreen = false;
2212
+ }
2213
+
2214
+ if (!this.shouldUseDirectUpdate()) {
2215
+ if ("false".equals(this.directUpdateMode)) {
2216
+ logger.warn(
2217
+ "autoSplashscreen is enabled but directUpdate is not configured for immediate updates. Set directUpdate to 'always' or disable autoSplashscreen."
2218
+ );
2219
+ } else if ("atInstall".equals(this.directUpdateMode) || "onLaunch".equals(this.directUpdateMode)) {
2220
+ logger.info(
2221
+ "autoSplashscreen is enabled but directUpdate is set to \"" +
2222
+ this.directUpdateMode +
2223
+ "\". This is normal. Skipping autoSplashscreen logic."
2224
+ );
2225
+ }
2226
+ canShowSplashscreen = false;
2227
+ }
2228
+
2229
+ if (canShowSplashscreen) {
2230
+ logger.info("Showing splashscreen for launcher/task switcher");
2231
+ this.showSplashscreen();
2232
+ }
2233
+ }
2234
+
2235
+ // Do other background work after splashscreen is shown
2236
+ CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_background", current.getVersionName());
2237
+ logger.info("Checking for pending update");
2238
+
2239
+ try {
2240
+ // We need to set "backgrounded time"
2241
+ this.delayUpdateUtils.setBackgroundTimestamp(System.currentTimeMillis());
2242
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.BACKGROUND);
2243
+ this.installNext();
2244
+ } catch (final Exception e) {
2245
+ logger.error("Error during onActivityStopped " + e.getMessage());
2246
+ }
2247
+ }
2248
+
2249
+ /**
2250
+ * Check if the current activity is the main activity.
2251
+ * Used for activity-based foreground/background detection on Android < 14.
2252
+ * On Android 14+, topActivity returns null due to security restrictions, so we use
2253
+ * ProcessLifecycleOwner instead.
2254
+ */
2255
+ private boolean isMainActivity() {
2256
+ try {
2257
+ Context mContext = this.getContext();
2258
+ android.app.ActivityManager activityManager = (android.app.ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
2259
+ java.util.List<android.app.ActivityManager.AppTask> runningTasks = activityManager.getAppTasks();
2260
+ if (runningTasks.isEmpty()) {
2261
+ return false;
2262
+ }
2263
+ android.app.ActivityManager.RecentTaskInfo runningTask = runningTasks.get(0).getTaskInfo();
2264
+ String className = java.util.Objects.requireNonNull(runningTask.baseIntent.getComponent()).getClassName();
2265
+ if (runningTask.topActivity == null) {
2266
+ return false;
2267
+ }
2268
+ String runningActivity = runningTask.topActivity.getClassName();
2269
+ return className.equals(runningActivity);
2270
+ } catch (NullPointerException e) {
2271
+ return false;
2272
+ }
2273
+ }
2274
+
2275
+ @Override
2276
+ public void handleOnStart() {
2277
+ try {
2278
+ logger.info("handleOnStart: onActivityStarted " + getActivity().getClass().getName());
2279
+
2280
+ // On Android < 14, use activity lifecycle for foreground detection
2281
+ // On Android 14+, ProcessLifecycleOwner handles this via AppLifecycleObserver
2282
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
2283
+ if (isPreviousMainActivity) {
2284
+ logger.info("handleOnStart: appMovedToForeground (Android <14 path)");
2285
+ this.appMovedToForeground();
2286
+ }
2287
+ isPreviousMainActivity = true;
2288
+ }
2289
+
2290
+ // Initialize shake menu if enabled and activity is BridgeActivity
2291
+ if (shakeMenuEnabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
2292
+ try {
2293
+ shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
2294
+ logger.info("Shake menu initialized");
2295
+ } catch (Exception e) {
2296
+ logger.error("Failed to initialize shake menu: " + e.getMessage());
2297
+ }
2298
+ }
2299
+ } catch (Exception e) {
2300
+ logger.error("Failed to run handleOnStart: " + e.getMessage());
2301
+ }
2302
+ }
2303
+
2304
+ @Override
2305
+ public void handleOnStop() {
2306
+ try {
2307
+ logger.info("handleOnStop: onActivityStopped");
2308
+
2309
+ // On Android < 14, use activity lifecycle for background detection
2310
+ // On Android 14+, ProcessLifecycleOwner handles this via AppLifecycleObserver
2311
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
2312
+ isPreviousMainActivity = isMainActivity();
2313
+ if (isPreviousMainActivity) {
2314
+ logger.info("handleOnStop: appMovedToBackground (Android <14 path)");
2315
+ this.appMovedToBackground();
2316
+ }
2317
+ }
2318
+ } catch (Exception e) {
2319
+ logger.error("Failed to run handleOnStop: " + e.getMessage());
2320
+ }
2321
+ }
2322
+
2323
+ @Override
2324
+ public void handleOnResume() {
2325
+ try {
2326
+ if (backgroundTask != null && taskRunning) {
2327
+ backgroundTask.interrupt();
2328
+ }
2329
+ this.implementation.activity = getActivity();
2330
+ } catch (Exception e) {
2331
+ logger.error("Failed to run handleOnResume: " + e.getMessage());
2332
+ }
2333
+ }
2334
+
2335
+ @Override
2336
+ public void handleOnPause() {
2337
+ try {
2338
+ this.implementation.activity = getActivity();
2339
+ } catch (Exception e) {
2340
+ logger.error("Failed to run handleOnPause: " + e.getMessage());
2341
+ }
2342
+ }
2343
+
2344
+ @PluginMethod
2345
+ public void setShakeMenu(final PluginCall call) {
2346
+ final Boolean enabled = call.getBoolean("enabled");
2347
+ if (enabled == null) {
2348
+ logger.error("setShakeMenu called without enabled parameter");
2349
+ call.reject("setShakeMenu called without enabled parameter");
2350
+ return;
2351
+ }
2352
+
2353
+ this.shakeMenuEnabled = enabled;
2354
+ logger.info("Shake menu " + (enabled ? "enabled" : "disabled"));
2355
+
2356
+ // Manage shake menu instance based on enabled state
2357
+ if (enabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
2358
+ try {
2359
+ shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
2360
+ logger.info("Shake menu initialized");
2361
+ } catch (Exception e) {
2362
+ logger.error("Failed to initialize shake menu: " + e.getMessage());
2363
+ }
2364
+ } else if (!enabled && shakeMenu != null) {
2365
+ try {
2366
+ shakeMenu.stop();
2367
+ shakeMenu = null;
2368
+ logger.info("Shake menu stopped");
2369
+ } catch (Exception e) {
2370
+ logger.error("Failed to stop shake menu: " + e.getMessage());
2371
+ }
2372
+ }
2373
+
2374
+ call.resolve();
2375
+ }
2376
+
2377
+ @PluginMethod
2378
+ public void isShakeMenuEnabled(final PluginCall call) {
2379
+ try {
2380
+ final JSObject ret = new JSObject();
2381
+ ret.put("enabled", this.shakeMenuEnabled);
2382
+ call.resolve(ret);
2383
+ } catch (final Exception e) {
2384
+ logger.error("Could not get shake menu status " + e.getMessage());
2385
+ call.reject("Could not get shake menu status", e);
2386
+ }
2387
+ }
2388
+
2389
+ @PluginMethod
2390
+ public void setShakeChannelSelector(final PluginCall call) {
2391
+ final Boolean enabled = call.getBoolean("enabled");
2392
+ if (enabled == null) {
2393
+ logger.error("setShakeChannelSelector called without enabled parameter");
2394
+ call.reject("setShakeChannelSelector called without enabled parameter");
2395
+ return;
2396
+ }
2397
+
2398
+ this.shakeChannelSelectorEnabled = enabled;
2399
+ logger.info("Shake channel selector " + (enabled ? "enabled" : "disabled"));
2400
+ call.resolve();
2401
+ }
2402
+
2403
+ @PluginMethod
2404
+ public void isShakeChannelSelectorEnabled(final PluginCall call) {
2405
+ try {
2406
+ final JSObject ret = new JSObject();
2407
+ ret.put("enabled", this.shakeChannelSelectorEnabled);
2408
+ call.resolve(ret);
2409
+ } catch (final Exception e) {
2410
+ logger.error("Could not get shake channel selector status " + e.getMessage());
2411
+ call.reject("Could not get shake channel selector status", e);
2412
+ }
2413
+ }
2414
+
2415
+ @PluginMethod
2416
+ public void getAppId(final PluginCall call) {
2417
+ try {
2418
+ final JSObject ret = new JSObject();
2419
+ ret.put("appId", this.implementation.appId);
2420
+ call.resolve(ret);
2421
+ } catch (final Exception e) {
2422
+ logger.error("Could not get appId " + e.getMessage());
2423
+ call.reject("Could not get appId", e);
2424
+ }
2425
+ }
2426
+
2427
+ @PluginMethod
2428
+ public void setAppId(final PluginCall call) {
2429
+ if (!this.getConfig().getBoolean("allowModifyAppId", false)) {
2430
+ logger.error("setAppId not allowed set allowModifyAppId in your config to true to allow it");
2431
+ call.reject("setAppId not allowed");
2432
+ return;
2433
+ }
2434
+ final String appId = call.getString("appId");
2435
+ if (appId == null) {
2436
+ logger.error("setAppId called without appId");
2437
+ call.reject("setAppId called without appId");
2438
+ return;
2439
+ }
2440
+ this.implementation.appId = appId;
2441
+ call.resolve();
2442
+ }
2443
+
2444
+ // ============================================================================
2445
+ // Play Store In-App Update Methods
2446
+ // ============================================================================
2447
+
2448
+ // AppUpdateAvailability enum values matching TypeScript definitions
2449
+ private static final int UPDATE_AVAILABILITY_UNKNOWN = 0;
2450
+ private static final int UPDATE_AVAILABILITY_NOT_AVAILABLE = 1;
2451
+ private static final int UPDATE_AVAILABILITY_AVAILABLE = 2;
2452
+ private static final int UPDATE_AVAILABILITY_IN_PROGRESS = 3;
2453
+
2454
+ // AppUpdateResultCode enum values matching TypeScript definitions
2455
+ private static final int RESULT_OK = 0;
2456
+ private static final int RESULT_CANCELED = 1;
2457
+ private static final int RESULT_FAILED = 2;
2458
+ private static final int RESULT_NOT_AVAILABLE = 3;
2459
+ private static final int RESULT_NOT_ALLOWED = 4;
2460
+ private static final int RESULT_INFO_MISSING = 5;
2461
+
2462
+ private AppUpdateManager getAppUpdateManager() {
2463
+ if (appUpdateManager == null) {
2464
+ appUpdateManager = AppUpdateManagerFactory.create(getContext());
2465
+ }
2466
+ return appUpdateManager;
2467
+ }
2468
+
2469
+ private int mapUpdateAvailability(int playStoreAvailability) {
2470
+ switch (playStoreAvailability) {
2471
+ case UpdateAvailability.UPDATE_AVAILABLE:
2472
+ return UPDATE_AVAILABILITY_AVAILABLE;
2473
+ case UpdateAvailability.UPDATE_NOT_AVAILABLE:
2474
+ return UPDATE_AVAILABILITY_NOT_AVAILABLE;
2475
+ case UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS:
2476
+ return UPDATE_AVAILABILITY_IN_PROGRESS;
2477
+ default:
2478
+ return UPDATE_AVAILABILITY_UNKNOWN;
2479
+ }
2480
+ }
2481
+
2482
+ @PluginMethod
2483
+ public void getAppUpdateInfo(final PluginCall call) {
2484
+ logger.info("Getting Play Store update info");
2485
+
2486
+ try {
2487
+ AppUpdateManager manager = getAppUpdateManager();
2488
+ Task<AppUpdateInfo> appUpdateInfoTask = manager.getAppUpdateInfo();
2489
+
2490
+ appUpdateInfoTask
2491
+ .addOnSuccessListener((appUpdateInfo) -> {
2492
+ cachedAppUpdateInfo = appUpdateInfo;
2493
+
2494
+ JSObject result = new JSObject();
2495
+ try {
2496
+ PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0);
2497
+ result.put("currentVersionName", pInfo.versionName);
2498
+ result.put("currentVersionCode", String.valueOf(pInfo.versionCode));
2499
+ } catch (PackageManager.NameNotFoundException e) {
2500
+ result.put("currentVersionName", "0.0.0");
2501
+ result.put("currentVersionCode", "0");
2502
+ }
2503
+
2504
+ result.put("updateAvailability", mapUpdateAvailability(appUpdateInfo.updateAvailability()));
2505
+
2506
+ if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
2507
+ result.put("availableVersionCode", String.valueOf(appUpdateInfo.availableVersionCode()));
2508
+ // Play Store doesn't provide version name, only version code
2509
+ result.put("availableVersionName", String.valueOf(appUpdateInfo.availableVersionCode()));
2510
+ result.put("updatePriority", appUpdateInfo.updatePriority());
2511
+ result.put("immediateUpdateAllowed", appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE));
2512
+ result.put("flexibleUpdateAllowed", appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE));
2513
+
2514
+ Integer stalenessDays = appUpdateInfo.clientVersionStalenessDays();
2515
+ if (stalenessDays != null) {
2516
+ result.put("clientVersionStalenessDays", stalenessDays);
2517
+ }
2518
+ } else {
2519
+ result.put("immediateUpdateAllowed", false);
2520
+ result.put("flexibleUpdateAllowed", false);
2521
+ }
2522
+
2523
+ result.put("installStatus", appUpdateInfo.installStatus());
2524
+
2525
+ call.resolve(result);
2526
+ })
2527
+ .addOnFailureListener((e) -> {
2528
+ logger.error("Failed to get app update info: " + e.getMessage());
2529
+ call.reject("Failed to get app update info: " + e.getMessage());
2530
+ });
2531
+ } catch (Exception e) {
2532
+ logger.error("Error getting app update info: " + e.getMessage());
2533
+ call.reject("Error getting app update info: " + e.getMessage());
2534
+ }
2535
+ }
2536
+
2537
+ @PluginMethod
2538
+ public void openAppStore(final PluginCall call) {
2539
+ String packageName = call.getString("packageName");
2540
+ if (packageName == null || packageName.isEmpty()) {
2541
+ packageName = getContext().getPackageName();
2542
+ }
2543
+
2544
+ try {
2545
+ // Try to open Play Store app first
2546
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName));
2547
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2548
+ getContext().startActivity(intent);
2549
+ call.resolve();
2550
+ } catch (android.content.ActivityNotFoundException e) {
2551
+ // Fall back to browser
2552
+ try {
2553
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + packageName));
2554
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2555
+ getContext().startActivity(intent);
2556
+ call.resolve();
2557
+ } catch (Exception ex) {
2558
+ logger.error("Failed to open Play Store: " + ex.getMessage());
2559
+ call.reject("Failed to open Play Store: " + ex.getMessage());
2560
+ }
2561
+ }
2562
+ }
2563
+
2564
+ @PluginMethod
2565
+ public void performImmediateUpdate(final PluginCall call) {
2566
+ if (cachedAppUpdateInfo == null) {
2567
+ logger.error("No update info available. Call getAppUpdateInfo first.");
2568
+ JSObject result = new JSObject();
2569
+ result.put("code", RESULT_INFO_MISSING);
2570
+ call.resolve(result);
2571
+ return;
2572
+ }
2573
+
2574
+ if (cachedAppUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
2575
+ logger.info("No update available");
2576
+ JSObject result = new JSObject();
2577
+ result.put("code", RESULT_NOT_AVAILABLE);
2578
+ call.resolve(result);
2579
+ return;
2580
+ }
2581
+
2582
+ if (!cachedAppUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
2583
+ logger.info("Immediate update not allowed");
2584
+ JSObject result = new JSObject();
2585
+ result.put("code", RESULT_NOT_ALLOWED);
2586
+ call.resolve(result);
2587
+ return;
2588
+ }
2589
+
2590
+ try {
2591
+ Activity activity = getActivity();
2592
+ if (activity == null) {
2593
+ call.reject("Activity not available");
2594
+ return;
2595
+ }
2596
+
2597
+ // Save the call for later resolution
2598
+ bridge.saveCall(call);
2599
+
2600
+ AppUpdateManager manager = getAppUpdateManager();
2601
+ manager.startUpdateFlowForResult(
2602
+ cachedAppUpdateInfo,
2603
+ activity,
2604
+ AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
2605
+ APP_UPDATE_REQUEST_CODE
2606
+ );
2607
+ } catch (Exception e) {
2608
+ logger.error("Failed to start immediate update: " + e.getMessage());
2609
+ JSObject result = new JSObject();
2610
+ result.put("code", RESULT_FAILED);
2611
+ call.resolve(result);
2612
+ }
2613
+ }
2614
+
2615
+ @PluginMethod
2616
+ public void startFlexibleUpdate(final PluginCall call) {
2617
+ if (cachedAppUpdateInfo == null) {
2618
+ logger.error("No update info available. Call getAppUpdateInfo first.");
2619
+ JSObject result = new JSObject();
2620
+ result.put("code", RESULT_INFO_MISSING);
2621
+ call.resolve(result);
2622
+ return;
2623
+ }
2624
+
2625
+ if (cachedAppUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
2626
+ logger.info("No update available");
2627
+ JSObject result = new JSObject();
2628
+ result.put("code", RESULT_NOT_AVAILABLE);
2629
+ call.resolve(result);
2630
+ return;
2631
+ }
2632
+
2633
+ if (!cachedAppUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
2634
+ logger.info("Flexible update not allowed");
2635
+ JSObject result = new JSObject();
2636
+ result.put("code", RESULT_NOT_ALLOWED);
2637
+ call.resolve(result);
2638
+ return;
2639
+ }
2640
+
2641
+ try {
2642
+ Activity activity = getActivity();
2643
+ if (activity == null) {
2644
+ call.reject("Activity not available");
2645
+ return;
2646
+ }
2647
+
2648
+ // Register listener for flexible update state changes
2649
+ AppUpdateManager manager = getAppUpdateManager();
2650
+
2651
+ // Remove any existing listener
2652
+ if (installStateUpdatedListener != null) {
2653
+ manager.unregisterListener(installStateUpdatedListener);
2654
+ }
2655
+
2656
+ installStateUpdatedListener = (state) -> {
2657
+ JSObject eventData = new JSObject();
2658
+ eventData.put("installStatus", state.installStatus());
2659
+
2660
+ if (state.installStatus() == InstallStatus.DOWNLOADING) {
2661
+ eventData.put("bytesDownloaded", state.bytesDownloaded());
2662
+ eventData.put("totalBytesToDownload", state.totalBytesToDownload());
2663
+ }
2664
+
2665
+ notifyListeners("onFlexibleUpdateStateChange", eventData);
2666
+ };
2667
+
2668
+ manager.registerListener(installStateUpdatedListener);
2669
+
2670
+ // Save the call for later resolution
2671
+ bridge.saveCall(call);
2672
+
2673
+ manager.startUpdateFlowForResult(
2674
+ cachedAppUpdateInfo,
2675
+ activity,
2676
+ AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build(),
2677
+ APP_UPDATE_REQUEST_CODE
2678
+ );
2679
+ } catch (Exception e) {
2680
+ logger.error("Failed to start flexible update: " + e.getMessage());
2681
+ JSObject result = new JSObject();
2682
+ result.put("code", RESULT_FAILED);
2683
+ call.resolve(result);
2684
+ }
2685
+ }
2686
+
2687
+ @PluginMethod
2688
+ public void completeFlexibleUpdate(final PluginCall call) {
2689
+ try {
2690
+ AppUpdateManager manager = getAppUpdateManager();
2691
+ manager
2692
+ .completeUpdate()
2693
+ .addOnSuccessListener((aVoid) -> {
2694
+ // The app will restart, so this may not be called
2695
+ call.resolve();
2696
+ })
2697
+ .addOnFailureListener((e) -> {
2698
+ logger.error("Failed to complete flexible update: " + e.getMessage());
2699
+ call.reject("Failed to complete flexible update: " + e.getMessage());
2700
+ });
2701
+ } catch (Exception e) {
2702
+ logger.error("Error completing flexible update: " + e.getMessage());
2703
+ call.reject("Error completing flexible update: " + e.getMessage());
2704
+ }
2705
+ }
2706
+
2707
+ @Override
2708
+ protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {
2709
+ super.handleOnActivityResult(requestCode, resultCode, data);
2710
+
2711
+ if (requestCode == APP_UPDATE_REQUEST_CODE) {
2712
+ PluginCall savedCall = bridge.getSavedCall("com.getcapacitor.PluginCall");
2713
+ if (savedCall == null) {
2714
+ // Try to get any saved call (for backward compatibility)
2715
+ return;
2716
+ }
2717
+
2718
+ JSObject result = new JSObject();
2719
+ if (resultCode == Activity.RESULT_OK) {
2720
+ result.put("code", RESULT_OK);
2721
+ } else if (resultCode == Activity.RESULT_CANCELED) {
2722
+ result.put("code", RESULT_CANCELED);
2723
+ } else {
2724
+ result.put("code", RESULT_FAILED);
2725
+ }
2726
+ savedCall.resolve(result);
2727
+ bridge.releaseCall(savedCall);
2728
+ }
2729
+ }
2730
+
2731
+ @Override
2732
+ protected void handleOnDestroy() {
2733
+ // Clean up the install state listener
2734
+ if (installStateUpdatedListener != null && appUpdateManager != null) {
2735
+ try {
2736
+ appUpdateManager.unregisterListener(installStateUpdatedListener);
2737
+ installStateUpdatedListener = null;
2738
+ } catch (Exception e) {
2739
+ logger.error("Failed to unregister install state listener: " + e.getMessage());
2740
+ }
2741
+ }
2742
+
2743
+ handleOnDestroyInternal();
2744
+ }
2745
+
2746
+ private void handleOnDestroyInternal() {
2747
+ // Original handleOnDestroy code
2748
+ try {
2749
+ logger.info("onActivityDestroyed " + getActivity().getClass().getName());
2750
+ this.implementation.activity = getActivity();
2751
+
2752
+ // Check for 'kill' delay condition on activity destroy
2753
+ // Note: onDestroy is not reliably called - also check on next app launch
2754
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
2755
+ this.delayUpdateUtils.setBackgroundTimestamp(0);
2756
+
2757
+ // Clean up shake menu
2758
+ if (shakeMenu != null) {
2759
+ try {
2760
+ shakeMenu.stop();
2761
+ shakeMenu = null;
2762
+ logger.info("Shake menu cleaned up");
2763
+ } catch (Exception e) {
2764
+ logger.error("Failed to clean up shake menu: " + e.getMessage());
2765
+ }
2766
+ }
2767
+
2768
+ // Clean up app lifecycle observer
2769
+ if (appLifecycleObserver != null) {
2770
+ try {
2771
+ appLifecycleObserver.unregister();
2772
+ appLifecycleObserver = null;
2773
+ logger.info("AppLifecycleObserver cleaned up");
2774
+ } catch (Exception e) {
2775
+ logger.error("Failed to clean up AppLifecycleObserver: " + e.getMessage());
2776
+ }
2777
+ }
2778
+ } catch (Exception e) {
2779
+ logger.error("Failed to run handleOnDestroy: " + e.getMessage());
2780
+ }
2781
+ }
1304
2782
  }