@capgo/capacitor-updater 6.14.26 → 6.14.33

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 (56) hide show
  1. package/CapgoCapacitorUpdater.podspec +3 -2
  2. package/Package.swift +2 -2
  3. package/README.md +350 -74
  4. package/android/build.gradle +20 -8
  5. package/android/proguard-rules.pro +22 -5
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +52 -16
  7. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1202 -510
  9. package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +566 -154
  10. package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipher.java → CryptoCipherV1.java} +17 -9
  11. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +15 -26
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +300 -119
  16. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +63 -25
  17. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  19. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
  20. package/dist/docs.json +652 -63
  21. package/dist/esm/definitions.d.ts +274 -15
  22. package/dist/esm/definitions.js.map +1 -1
  23. package/dist/esm/history.d.ts +1 -0
  24. package/dist/esm/history.js +283 -0
  25. package/dist/esm/history.js.map +1 -0
  26. package/dist/esm/index.d.ts +1 -0
  27. package/dist/esm/index.js +1 -0
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/web.d.ts +12 -1
  30. package/dist/esm/web.js +29 -2
  31. package/dist/esm/web.js.map +1 -1
  32. package/dist/plugin.cjs.js +311 -2
  33. package/dist/plugin.cjs.js.map +1 -1
  34. package/dist/plugin.js +311 -2
  35. package/dist/plugin.js.map +1 -1
  36. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/AES.swift +6 -3
  37. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1578 -0
  38. package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +408 -139
  39. package/ios/{Plugin/CryptoCipher.swift → Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift} +13 -6
  40. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/CryptoCipherV2.swift +33 -27
  41. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
  42. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  43. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +47 -0
  44. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  45. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/RSA.swift +1 -0
  46. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
  47. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
  48. package/package.json +20 -16
  49. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -1030
  50. /package/{LICENCE → LICENSE} +0 -0
  51. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BigInt.swift +0 -0
  52. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +0 -0
  53. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +0 -0
  54. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  55. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  56. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
@@ -12,31 +12,36 @@ import android.content.Context;
12
12
  import android.content.SharedPreferences;
13
13
  import android.content.pm.PackageInfo;
14
14
  import android.content.pm.PackageManager;
15
+ import android.graphics.Color;
15
16
  import android.os.Build;
17
+ import android.os.Handler;
16
18
  import android.os.Looper;
17
- import android.util.Log;
19
+ import android.view.Gravity;
20
+ import android.view.View;
21
+ import android.view.ViewGroup;
22
+ import android.widget.FrameLayout;
23
+ import android.widget.ProgressBar;
18
24
  import com.getcapacitor.CapConfig;
19
25
  import com.getcapacitor.JSArray;
20
26
  import com.getcapacitor.JSObject;
21
27
  import com.getcapacitor.Plugin;
22
28
  import com.getcapacitor.PluginCall;
29
+ import com.getcapacitor.PluginHandle;
23
30
  import com.getcapacitor.PluginMethod;
24
31
  import com.getcapacitor.annotation.CapacitorPlugin;
25
32
  import com.getcapacitor.plugin.WebView;
26
- import com.google.gson.Gson;
27
- import com.google.gson.reflect.TypeToken;
28
33
  import io.github.g00fy2.versioncompare.Version;
29
34
  import java.io.IOException;
30
- import java.lang.reflect.Type;
31
35
  import java.net.MalformedURLException;
32
36
  import java.net.URL;
33
- import java.text.SimpleDateFormat;
34
37
  import java.util.ArrayList;
35
38
  import java.util.Arrays;
36
39
  import java.util.Date;
37
- import java.util.Iterator;
40
+ import java.util.HashSet;
38
41
  import java.util.List;
42
+ import java.util.Map;
39
43
  import java.util.Objects;
44
+ import java.util.Set;
40
45
  import java.util.Timer;
41
46
  import java.util.TimerTask;
42
47
  import java.util.UUID;
@@ -45,24 +50,35 @@ import java.util.concurrent.Semaphore;
45
50
  import java.util.concurrent.TimeUnit;
46
51
  import java.util.concurrent.TimeoutException;
47
52
  import java.util.concurrent.atomic.AtomicReference;
48
- import okhttp3.OkHttpClient;
49
- import okhttp3.Protocol;
53
+ // Removed OkHttpClient and Protocol imports - using shared client in DownloadService instead
50
54
  import org.json.JSONArray;
51
55
  import org.json.JSONException;
56
+ import org.json.JSONObject;
52
57
 
53
58
  @CapacitorPlugin(name = "CapacitorUpdater")
54
59
  public class CapacitorUpdaterPlugin extends Plugin {
55
60
 
61
+ private final Logger logger = new Logger("CapgoUpdater");
62
+
56
63
  private static final String updateUrlDefault = "https://plugin.capgo.app/updates";
57
64
  private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
58
65
  private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
59
-
60
- private final String PLUGIN_VERSION = "6.14.26";
66
+ private static final String KEEP_URL_FLAG_KEY = "__capgo_keep_url_path_after_reload";
67
+ private static final String CUSTOM_ID_PREF_KEY = "CapacitorUpdater.customId";
68
+ private static final String UPDATE_URL_PREF_KEY = "CapacitorUpdater.updateUrl";
69
+ private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
70
+ private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
71
+ private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
72
+ private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
73
+
74
+ private final String PLUGIN_VERSION = "6.14.31";
61
75
  private static final String DELAY_CONDITION_PREFERENCES = "";
62
76
 
63
77
  private SharedPreferences.Editor editor;
64
78
  private SharedPreferences prefs;
65
- protected CapacitorUpdater implementation;
79
+ protected CapgoUpdater implementation;
80
+ private Boolean persistCustomId = false;
81
+ private Boolean persistModifyUrl = false;
66
82
 
67
83
  private Integer appReadyTimeout = 10000;
68
84
  private Integer periodCheckDelay = 0;
@@ -71,9 +87,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
71
87
  private Boolean autoUpdate = false;
72
88
  private String updateUrl = "";
73
89
  private Version currentVersionNative;
90
+ private String currentBuildVersion;
74
91
  private Thread backgroundTask;
75
92
  private Boolean taskRunning = false;
76
93
  private Boolean keepUrlPathAfterReload = false;
94
+ private Boolean autoSplashscreen = false;
95
+ private Boolean autoSplashscreenLoader = false;
96
+ private Integer autoSplashscreenTimeout = 10000;
97
+ private Boolean autoSplashscreenTimedOut = false;
98
+ private String directUpdateMode = "false";
99
+ private Boolean wasRecentlyInstalledOrUpdated = false;
100
+ Boolean shakeMenuEnabled = false;
101
+ private Boolean allowManualBundleError = false;
77
102
 
78
103
  private Boolean isPreviousMainActivity = true;
79
104
 
@@ -85,6 +110,63 @@ public class CapacitorUpdaterPlugin extends Plugin {
85
110
 
86
111
  private int lastNotifiedStatPercent = 0;
87
112
 
113
+ private DelayUpdateUtils delayUpdateUtils;
114
+
115
+ private ShakeMenu shakeMenu;
116
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
117
+ private FrameLayout splashscreenLoaderOverlay;
118
+ private Runnable splashscreenTimeoutRunnable;
119
+
120
+ private void notifyBreakingEvents(final String version) {
121
+ if (version == null || version.isEmpty()) {
122
+ return;
123
+ }
124
+ for (final String eventName : BREAKING_EVENT_NAMES) {
125
+ final JSObject payload = new JSObject();
126
+ payload.put("version", version);
127
+ CapacitorUpdaterPlugin.this.notifyListeners(eventName, payload);
128
+ }
129
+ }
130
+
131
+ private JSObject mapToJSObject(Map<String, Object> map) {
132
+ JSObject jsObject = new JSObject();
133
+ for (Map.Entry<String, Object> entry : map.entrySet()) {
134
+ jsObject.put(entry.getKey(), entry.getValue());
135
+ }
136
+ return jsObject;
137
+ }
138
+
139
+ private void persistLastFailedBundle(BundleInfo bundle) {
140
+ if (this.prefs == null) {
141
+ return;
142
+ }
143
+ final SharedPreferences.Editor localEditor = this.prefs.edit();
144
+ if (bundle == null) {
145
+ localEditor.remove(LAST_FAILED_BUNDLE_PREF_KEY);
146
+ } else {
147
+ final JSONObject json = new JSONObject(bundle.toJSONMap());
148
+ localEditor.putString(LAST_FAILED_BUNDLE_PREF_KEY, json.toString());
149
+ }
150
+ localEditor.apply();
151
+ }
152
+
153
+ private BundleInfo readLastFailedBundle() {
154
+ if (this.prefs == null) {
155
+ return null;
156
+ }
157
+ final String raw = this.prefs.getString(LAST_FAILED_BUNDLE_PREF_KEY, null);
158
+ if (raw == null || raw.trim().isEmpty()) {
159
+ return null;
160
+ }
161
+ try {
162
+ return BundleInfo.fromJSON(raw);
163
+ } catch (final JSONException e) {
164
+ logger.error("Failed to parse failed bundle info: " + e.getMessage());
165
+ this.persistLastFailedBundle(null);
166
+ return null;
167
+ }
168
+ }
169
+
88
170
  public Thread startNewThread(final Runnable function, Number waitTime) {
89
171
  Thread bgTask = new Thread(() -> {
90
172
  try {
@@ -111,54 +193,90 @@ public class CapacitorUpdaterPlugin extends Plugin {
111
193
  this.editor = this.prefs.edit();
112
194
 
113
195
  try {
114
- this.implementation = new CapacitorUpdater() {
196
+ this.implementation = new CapgoUpdater(logger) {
115
197
  @Override
116
198
  public void notifyDownload(final String id, final int percent) {
117
- CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
199
+ activity.runOnUiThread(() -> {
200
+ CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
201
+ });
118
202
  }
119
203
 
120
204
  @Override
121
205
  public void directUpdateFinish(final BundleInfo latest) {
122
- CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
206
+ activity.runOnUiThread(() -> {
207
+ CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
208
+ });
123
209
  }
124
210
 
125
211
  @Override
126
- public void notifyListeners(final String id, final JSObject res) {
127
- CapacitorUpdaterPlugin.this.notifyListeners(id, res);
212
+ public void notifyListeners(final String id, final Map<String, Object> res) {
213
+ activity.runOnUiThread(() -> {
214
+ CapacitorUpdaterPlugin.this.notifyListeners(id, CapacitorUpdaterPlugin.this.mapToJSObject(res));
215
+ });
128
216
  }
129
217
  };
130
218
  final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
131
219
  this.implementation.activity = this.getActivity();
132
220
  this.implementation.versionBuild = this.getConfig().getString("version", pInfo.versionName);
221
+ this.implementation.CAP_SERVER_PATH = WebView.CAP_SERVER_PATH;
133
222
  this.implementation.PLUGIN_VERSION = this.PLUGIN_VERSION;
134
223
  this.implementation.versionCode = Integer.toString(pInfo.versionCode);
135
- this.implementation.client = new OkHttpClient.Builder()
136
- .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
137
- .connectTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
138
- .readTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
139
- .writeTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
140
- .build();
141
-
142
- this.implementation.directUpdate = this.getConfig().getBoolean("directUpdate", false);
224
+ // Removed unused OkHttpClient creation - using shared client in DownloadService instead
225
+ // Handle directUpdate configuration - support string values and backward compatibility
226
+ String directUpdateConfig = this.getConfig().getString("directUpdate", null);
227
+ if (directUpdateConfig != null) {
228
+ this.directUpdateMode = directUpdateConfig;
229
+ this.implementation.directUpdate = directUpdateConfig.equals("always") || directUpdateConfig.equals("atInstall");
230
+ } else {
231
+ Boolean directUpdateBool = this.getConfig().getBoolean("directUpdate", false);
232
+ if (directUpdateBool) {
233
+ this.directUpdateMode = "always"; // backward compatibility: true = always
234
+ this.implementation.directUpdate = true;
235
+ } else {
236
+ this.directUpdateMode = "false";
237
+ this.implementation.directUpdate = false;
238
+ }
239
+ }
143
240
  this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
241
+ this.currentBuildVersion = Integer.toString(pInfo.versionCode);
242
+ this.delayUpdateUtils = new DelayUpdateUtils(this.prefs, this.editor, this.currentVersionNative, logger);
144
243
  } catch (final PackageManager.NameNotFoundException e) {
145
- Log.e(CapacitorUpdater.TAG, "Error instantiating implementation", e);
244
+ logger.error("Error instantiating implementation " + e.getMessage());
146
245
  return;
147
246
  } catch (final Exception e) {
148
- Log.e(CapacitorUpdater.TAG, "Error getting current native app version", e);
247
+ logger.error("Error getting current native app version " + e.getMessage());
149
248
  return;
150
249
  }
250
+
251
+ boolean disableJSLogging = this.getConfig().getBoolean("disableJSLogging", false);
252
+ // Set the bridge in the Logger when webView is available
253
+ if (this.bridge != null && this.bridge.getWebView() != null && !disableJSLogging) {
254
+ logger.setBridge(this.bridge);
255
+ logger.info("WebView set successfully for logging");
256
+ } else {
257
+ logger.info("WebView not ready yet, will be set later");
258
+ }
259
+
260
+ // Set logger for shared classes
261
+ CryptoCipherV1.setLogger(logger);
262
+ CryptoCipherV2.setLogger(logger);
263
+ DownloadService.setLogger(logger);
264
+ DownloadWorkerManager.setLogger(logger);
265
+
151
266
  final CapConfig config = CapConfig.loadDefault(this.getActivity());
152
267
  this.implementation.appId = InternalUtils.getPackageName(getContext().getPackageManager(), getContext().getPackageName());
153
268
  this.implementation.appId = config.getString("appId", this.implementation.appId);
154
269
  this.implementation.appId = this.getConfig().getString("appId", this.implementation.appId);
155
270
  if (this.implementation.appId == null || this.implementation.appId.isEmpty()) {
156
- // crash the app
271
+ // crash the app on purpose it should not happen
157
272
  throw new RuntimeException(
158
273
  "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"
159
274
  );
160
275
  }
161
- Log.i(CapacitorUpdater.TAG, "appId: " + implementation.appId);
276
+ logger.info("appId: " + implementation.appId);
277
+
278
+ this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
279
+ this.persistModifyUrl = this.getConfig().getBoolean("persistModifyUrl", false);
162
280
  this.implementation.publicKey = this.getConfig().getString("publicKey", "");
163
281
  this.implementation.privateKey = this.getConfig().getString("privateKey", "");
164
282
  if (this.implementation.privateKey != null && !this.implementation.privateKey.isEmpty()) {
@@ -166,6 +284,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
166
284
  }
167
285
  this.implementation.statsUrl = this.getConfig().getString("statsUrl", statsUrlDefault);
168
286
  this.implementation.channelUrl = this.getConfig().getString("channelUrl", channelUrlDefault);
287
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
288
+ if (this.prefs.contains(STATS_URL_PREF_KEY)) {
289
+ final String storedStatsUrl = this.prefs.getString(STATS_URL_PREF_KEY, this.implementation.statsUrl);
290
+ if (storedStatsUrl != null) {
291
+ this.implementation.statsUrl = storedStatsUrl;
292
+ logger.info("Loaded persisted statsUrl");
293
+ }
294
+ }
295
+ if (this.prefs.contains(CHANNEL_URL_PREF_KEY)) {
296
+ final String storedChannelUrl = this.prefs.getString(CHANNEL_URL_PREF_KEY, this.implementation.channelUrl);
297
+ if (storedChannelUrl != null) {
298
+ this.implementation.channelUrl = storedChannelUrl;
299
+ logger.info("Loaded persisted channelUrl");
300
+ }
301
+ }
302
+ }
169
303
  int userValue = this.getConfig().getInt("periodCheckDelay", 0);
170
304
  this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
171
305
 
@@ -179,105 +313,365 @@ public class CapacitorUpdaterPlugin extends Plugin {
179
313
  this.implementation.prefs = this.prefs;
180
314
  this.implementation.editor = this.editor;
181
315
  this.implementation.versionOs = Build.VERSION.RELEASE;
182
- this.implementation.deviceID = this.prefs.getString("appUUID", UUID.randomUUID().toString()).toLowerCase();
183
- this.editor.putString("appUUID", this.implementation.deviceID);
184
- this.editor.commit();
185
- Log.i(CapacitorUpdater.TAG, "init for device " + this.implementation.deviceID);
186
- Log.i(CapacitorUpdater.TAG, "version native " + this.currentVersionNative.getOriginalString());
316
+ // Use DeviceIdHelper to get or create device ID that persists across reinstalls
317
+ this.implementation.deviceID = DeviceIdHelper.getOrCreateDeviceId(this.getContext(), this.prefs);
318
+
319
+ // Update User-Agent for shared OkHttpClient with OS version
320
+ DownloadService.updateUserAgent(this.implementation.appId, this.PLUGIN_VERSION, this.implementation.versionOs);
321
+
322
+ if (Boolean.TRUE.equals(this.persistCustomId)) {
323
+ final String storedCustomId = this.prefs.getString(CUSTOM_ID_PREF_KEY, "");
324
+ if (storedCustomId != null && !storedCustomId.isEmpty()) {
325
+ this.implementation.customId = storedCustomId;
326
+ logger.info("Loaded persisted customId");
327
+ }
328
+ }
329
+ logger.info("init for device " + this.implementation.deviceID);
330
+ logger.info("version native " + this.currentVersionNative.getOriginalString());
187
331
  this.autoDeleteFailed = this.getConfig().getBoolean("autoDeleteFailed", true);
188
332
  this.autoDeletePrevious = this.getConfig().getBoolean("autoDeletePrevious", true);
189
333
  this.updateUrl = this.getConfig().getString("updateUrl", updateUrlDefault);
334
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
335
+ if (this.prefs.contains(UPDATE_URL_PREF_KEY)) {
336
+ final String storedUpdateUrl = this.prefs.getString(UPDATE_URL_PREF_KEY, this.updateUrl);
337
+ if (storedUpdateUrl != null) {
338
+ this.updateUrl = storedUpdateUrl;
339
+ logger.info("Loaded persisted updateUrl");
340
+ }
341
+ }
342
+ }
190
343
  this.autoUpdate = this.getConfig().getBoolean("autoUpdate", true);
191
344
  this.appReadyTimeout = this.getConfig().getInt("appReadyTimeout", 10000);
192
345
  this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
346
+ this.syncKeepUrlPathFlag(this.keepUrlPathAfterReload);
347
+ this.allowManualBundleError = this.getConfig().getBoolean("allowManualBundleError", false);
348
+ this.autoSplashscreen = this.getConfig().getBoolean("autoSplashscreen", false);
349
+ this.autoSplashscreenLoader = this.getConfig().getBoolean("autoSplashscreenLoader", false);
350
+ int splashscreenTimeoutValue = this.getConfig().getInt("autoSplashscreenTimeout", 10000);
351
+ this.autoSplashscreenTimeout = Math.max(0, splashscreenTimeoutValue);
193
352
  this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
353
+ this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
194
354
  boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
195
355
 
356
+ // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
357
+ this.wasRecentlyInstalledOrUpdated = this.checkIfRecentlyInstalledOrUpdated();
358
+
196
359
  this.implementation.autoReset();
197
360
  if (resetWhenUpdate) {
198
361
  this.cleanupObsoleteVersions();
199
362
  }
200
363
 
201
- // Check for 'kill' delay condition on app launch
202
- // This handles cases where the app was killed by the system (onDestroy is not reliable)
203
- this._checkCancelDelay(true);
204
-
205
364
  this.checkForUpdateAfterDelay();
206
365
  }
207
366
 
208
367
  private void semaphoreWait(Number waitTime) {
209
- Log.i(CapacitorUpdater.TAG, "semaphoreWait " + waitTime);
210
368
  try {
211
- // Log.i(CapacitorUpdater.TAG, "semaphoreReady count " + CapacitorUpdaterPlugin.this.semaphoreReady.getCount());
212
369
  semaphoreReady.awaitAdvanceInterruptibly(semaphoreReady.getPhase(), waitTime.longValue(), TimeUnit.SECONDS);
213
- // Log.i(CapacitorUpdater.TAG, "semaphoreReady await " + res);
214
- Log.i(CapacitorUpdater.TAG, "semaphoreReady count " + semaphoreReady.getPhase());
370
+ logger.info("semaphoreReady count " + semaphoreReady.getPhase());
215
371
  } catch (InterruptedException e) {
216
- Log.i(CapacitorUpdater.TAG, "semaphoreWait InterruptedException");
217
- e.printStackTrace();
372
+ logger.info("semaphoreWait InterruptedException");
373
+ Thread.currentThread().interrupt(); // Restore interrupted status
218
374
  } catch (TimeoutException e) {
219
- throw new RuntimeException(e);
375
+ logger.error("Semaphore timeout: " + e.getMessage());
376
+ // Don't throw runtime exception, just log and continue
220
377
  }
221
378
  }
222
379
 
223
380
  private void semaphoreUp() {
224
- Log.i(CapacitorUpdater.TAG, "semaphoreUp");
381
+ logger.info("semaphoreUp");
225
382
  semaphoreReady.register();
226
383
  }
227
384
 
228
385
  private void semaphoreDown() {
229
- Log.i(CapacitorUpdater.TAG, "semaphoreDown");
230
- Log.i(CapacitorUpdater.TAG, "semaphoreDown count " + semaphoreReady.getPhase());
386
+ logger.info("semaphoreDown");
387
+ logger.info("semaphoreDown count " + semaphoreReady.getPhase());
231
388
  semaphoreReady.arriveAndDeregister();
232
389
  }
233
390
 
234
391
  private void sendReadyToJs(final BundleInfo current, final String msg) {
235
- Log.i(CapacitorUpdater.TAG, "sendReadyToJs");
392
+ sendReadyToJs(current, msg, false);
393
+ }
394
+
395
+ private void sendReadyToJs(final BundleInfo current, final String msg, final boolean isDirectUpdate) {
396
+ logger.info("sendReadyToJs: " + msg);
236
397
  final JSObject ret = new JSObject();
237
- ret.put("bundle", current.toJSON());
398
+ ret.put("bundle", mapToJSObject(current.toJSONMap()));
238
399
  ret.put("status", msg);
239
- startNewThread(() -> {
240
- Log.i(CapacitorUpdater.TAG, "semaphoreReady sendReadyToJs");
241
- semaphoreWait(CapacitorUpdaterPlugin.this.appReadyTimeout);
242
- Log.i(CapacitorUpdater.TAG, "semaphoreReady sendReadyToJs done");
243
- CapacitorUpdaterPlugin.this.notifyListeners("appReady", ret);
244
- });
400
+
401
+ // No need to wait for semaphore anymore since _reload() has already waited
402
+ this.notifyListeners("appReady", ret, true);
403
+
404
+ // Auto hide splashscreen if enabled
405
+ // We show it on background when conditions are met, so we should hide it on foreground regardless of update outcome
406
+ if (this.autoSplashscreen) {
407
+ this.hideSplashscreen();
408
+ }
409
+ }
410
+
411
+ private void hideSplashscreen() {
412
+ if (Looper.myLooper() == Looper.getMainLooper()) {
413
+ hideSplashscreenInternal();
414
+ } else {
415
+ this.mainHandler.post(this::hideSplashscreenInternal);
416
+ }
417
+ }
418
+
419
+ private void hideSplashscreenInternal() {
420
+ cancelSplashscreenTimeout();
421
+ removeSplashscreenLoader();
422
+
423
+ try {
424
+ if (getBridge() == null) {
425
+ logger.warn("Bridge not ready for hiding splashscreen with autoSplashscreen");
426
+ return;
427
+ }
428
+
429
+ // Try to call the SplashScreen plugin directly through the bridge
430
+ PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
431
+ if (splashScreenPlugin != null) {
432
+ try {
433
+ // Create a plugin call for the hide method using reflection to access private msgHandler
434
+ JSObject options = new JSObject();
435
+ java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
436
+ msgHandlerField.setAccessible(true);
437
+ Object msgHandler = msgHandlerField.get(getBridge());
438
+
439
+ PluginCall call = new PluginCall(
440
+ (com.getcapacitor.MessageHandler) msgHandler,
441
+ "SplashScreen",
442
+ "FAKE_CALLBACK_ID_HIDE",
443
+ "hide",
444
+ options
445
+ );
446
+
447
+ // Call the hide method directly
448
+ splashScreenPlugin.invoke("hide", call);
449
+ logger.info("Splashscreen hidden automatically via direct plugin call");
450
+ } catch (Exception e) {
451
+ logger.error("Failed to call SplashScreen hide method: " + e.getMessage());
452
+ }
453
+ } else {
454
+ logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.");
455
+ }
456
+ } catch (Exception e) {
457
+ logger.error(
458
+ "Error hiding splashscreen with autoSplashscreen: " +
459
+ e.getMessage() +
460
+ ". Make sure @capacitor/splash-screen plugin is installed and configured."
461
+ );
462
+ }
463
+ }
464
+
465
+ private void showSplashscreen() {
466
+ if (Looper.myLooper() == Looper.getMainLooper()) {
467
+ showSplashscreenNow();
468
+ } else {
469
+ this.mainHandler.post(this::showSplashscreenNow);
470
+ }
471
+ }
472
+
473
+ private void showSplashscreenNow() {
474
+ cancelSplashscreenTimeout();
475
+ this.autoSplashscreenTimedOut = false;
476
+
477
+ try {
478
+ if (getBridge() == null) {
479
+ logger.warn("Bridge not ready for showing splashscreen with autoSplashscreen");
480
+ } else {
481
+ PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
482
+ if (splashScreenPlugin != null) {
483
+ JSObject options = new JSObject();
484
+ java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
485
+ msgHandlerField.setAccessible(true);
486
+ Object msgHandler = msgHandlerField.get(getBridge());
487
+
488
+ PluginCall call = new PluginCall(
489
+ (com.getcapacitor.MessageHandler) msgHandler,
490
+ "SplashScreen",
491
+ "FAKE_CALLBACK_ID_SHOW",
492
+ "show",
493
+ options
494
+ );
495
+
496
+ splashScreenPlugin.invoke("show", call);
497
+ logger.info("Splashscreen shown synchronously to prevent flash");
498
+ } else {
499
+ logger.warn("autoSplashscreen: SplashScreen plugin not found");
500
+ }
501
+ }
502
+ } catch (Exception e) {
503
+ logger.error("Failed to show splashscreen synchronously: " + e.getMessage());
504
+ }
505
+
506
+ addSplashscreenLoaderIfNeeded();
507
+ scheduleSplashscreenTimeout();
508
+ }
509
+
510
+ private void addSplashscreenLoaderIfNeeded() {
511
+ if (!Boolean.TRUE.equals(this.autoSplashscreenLoader)) {
512
+ return;
513
+ }
514
+
515
+ Runnable addLoader = () -> {
516
+ if (this.splashscreenLoaderOverlay != null) {
517
+ return;
518
+ }
519
+
520
+ Activity activity = getActivity();
521
+ if (activity == null) {
522
+ logger.warn("autoSplashscreen: Activity not available for loader overlay");
523
+ return;
524
+ }
525
+
526
+ ProgressBar progressBar = new ProgressBar(activity);
527
+ progressBar.setIndeterminate(true);
528
+
529
+ FrameLayout overlay = new FrameLayout(activity);
530
+ overlay.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
531
+ overlay.setClickable(false);
532
+ overlay.setFocusable(false);
533
+ overlay.setBackgroundColor(Color.TRANSPARENT);
534
+ overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
535
+
536
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
537
+ ViewGroup.LayoutParams.WRAP_CONTENT,
538
+ ViewGroup.LayoutParams.WRAP_CONTENT
539
+ );
540
+ params.gravity = Gravity.CENTER;
541
+ overlay.addView(progressBar, params);
542
+
543
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
544
+ decorView.addView(overlay);
545
+
546
+ this.splashscreenLoaderOverlay = overlay;
547
+ };
548
+
549
+ if (Looper.myLooper() == Looper.getMainLooper()) {
550
+ addLoader.run();
551
+ } else {
552
+ this.mainHandler.post(addLoader);
553
+ }
554
+ }
555
+
556
+ private void removeSplashscreenLoader() {
557
+ Runnable removeLoader = () -> {
558
+ if (this.splashscreenLoaderOverlay != null) {
559
+ ViewGroup parent = (ViewGroup) this.splashscreenLoaderOverlay.getParent();
560
+ if (parent != null) {
561
+ parent.removeView(this.splashscreenLoaderOverlay);
562
+ }
563
+ this.splashscreenLoaderOverlay = null;
564
+ }
565
+ };
566
+
567
+ if (Looper.myLooper() == Looper.getMainLooper()) {
568
+ removeLoader.run();
569
+ } else {
570
+ this.mainHandler.post(removeLoader);
571
+ }
572
+ }
573
+
574
+ private void scheduleSplashscreenTimeout() {
575
+ if (this.autoSplashscreenTimeout == null || this.autoSplashscreenTimeout <= 0) {
576
+ return;
577
+ }
578
+
579
+ cancelSplashscreenTimeout();
580
+
581
+ this.splashscreenTimeoutRunnable = () -> {
582
+ logger.info("autoSplashscreen timeout reached, hiding splashscreen");
583
+ this.autoSplashscreenTimedOut = true;
584
+ this.implementation.directUpdate = false;
585
+ hideSplashscreen();
586
+ };
587
+
588
+ this.mainHandler.postDelayed(this.splashscreenTimeoutRunnable, this.autoSplashscreenTimeout);
589
+ }
590
+
591
+ private void cancelSplashscreenTimeout() {
592
+ if (this.splashscreenTimeoutRunnable != null) {
593
+ this.mainHandler.removeCallbacks(this.splashscreenTimeoutRunnable);
594
+ this.splashscreenTimeoutRunnable = null;
595
+ }
596
+ }
597
+
598
+ private boolean checkIfRecentlyInstalledOrUpdated() {
599
+ String currentVersion = this.currentBuildVersion;
600
+ String lastKnownVersion = this.prefs.getString("LatestNativeBuildVersion", "");
601
+
602
+ if (lastKnownVersion.isEmpty()) {
603
+ // First time running, consider it as recently installed
604
+ return true;
605
+ } else if (!lastKnownVersion.equals(currentVersion)) {
606
+ // Version changed, consider it as recently updated
607
+ return true;
608
+ }
609
+
610
+ return false;
611
+ }
612
+
613
+ private boolean shouldUseDirectUpdate() {
614
+ if (Boolean.TRUE.equals(this.autoSplashscreenTimedOut)) {
615
+ return false;
616
+ }
617
+ switch (this.directUpdateMode) {
618
+ case "false":
619
+ return false;
620
+ case "always":
621
+ return true;
622
+ case "atInstall":
623
+ if (this.wasRecentlyInstalledOrUpdated) {
624
+ // Reset the flag after first use to prevent subsequent foreground events from using direct update
625
+ this.wasRecentlyInstalledOrUpdated = false;
626
+ return true;
627
+ }
628
+ return false;
629
+ default:
630
+ return false;
631
+ }
632
+ }
633
+
634
+ private boolean isDirectUpdateCurrentlyAllowed(final boolean plannedDirectUpdate) {
635
+ return plannedDirectUpdate && !Boolean.TRUE.equals(this.autoSplashscreenTimedOut);
245
636
  }
246
637
 
247
638
  private void directUpdateFinish(final BundleInfo latest) {
248
639
  CapacitorUpdaterPlugin.this.implementation.set(latest);
249
640
  CapacitorUpdaterPlugin.this._reload();
250
- sendReadyToJs(latest, "update installed");
641
+ sendReadyToJs(latest, "update installed", true);
251
642
  }
252
643
 
253
644
  private void cleanupObsoleteVersions() {
254
- try {
255
- final Version previous = new Version(this.prefs.getString("LatestVersionNative", ""));
645
+ startNewThread(() -> {
256
646
  try {
257
- if (
258
- !"".equals(previous.getOriginalString()) &&
259
- !Objects.equals(this.currentVersionNative.getOriginalString(), previous.getOriginalString())
260
- ) {
261
- Log.i(CapacitorUpdater.TAG, "New native version detected: " + this.currentVersionNative);
647
+ final String previous = this.prefs.getString("LatestNativeBuildVersion", "");
648
+ if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
649
+ logger.info("New native build version detected: " + this.currentBuildVersion);
262
650
  this.implementation.reset(true);
263
651
  final List<BundleInfo> installed = this.implementation.list(false);
264
652
  for (final BundleInfo bundle : installed) {
265
653
  try {
266
- Log.i(CapacitorUpdater.TAG, "Deleting obsolete bundle: " + bundle.getId());
654
+ logger.info("Deleting obsolete bundle: " + bundle.getId());
267
655
  this.implementation.delete(bundle.getId());
268
656
  } catch (final Exception e) {
269
- Log.e(CapacitorUpdater.TAG, "Failed to delete: " + bundle.getId(), e);
657
+ logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
658
+ }
659
+ }
660
+ final List<BundleInfo> storedBundles = this.implementation.list(true);
661
+ final Set<String> allowedIds = new HashSet<>();
662
+ for (final BundleInfo info : storedBundles) {
663
+ if (info != null && info.getId() != null && !info.getId().isEmpty()) {
664
+ allowedIds.add(info.getId());
270
665
  }
271
666
  }
667
+ this.implementation.cleanupDownloadDirectories(allowedIds);
272
668
  }
273
- } catch (final Exception e) {
274
- Log.e(CapacitorUpdater.TAG, "Could not determine the current version", e);
669
+ this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
670
+ this.editor.apply();
671
+ } catch (Exception e) {
672
+ logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
275
673
  }
276
- } catch (final Exception e) {
277
- Log.e(CapacitorUpdater.TAG, "Error calculating previous native version", e);
278
- }
279
- this.editor.putString("LatestVersionNative", this.currentVersionNative.toString());
280
- this.editor.commit();
674
+ });
281
675
  }
282
676
 
283
677
  public void notifyDownload(final String id, final int percent) {
@@ -285,7 +679,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
285
679
  final JSObject ret = new JSObject();
286
680
  ret.put("percent", percent);
287
681
  final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
288
- ret.put("bundle", bundleInfo.toJSON());
682
+ ret.put("bundle", mapToJSObject(bundleInfo.toJSONMap()));
289
683
  this.notifyListeners("download", ret);
290
684
 
291
685
  if (percent == 100) {
@@ -301,58 +695,70 @@ public class CapacitorUpdaterPlugin extends Plugin {
301
695
  }
302
696
  }
303
697
  } catch (final Exception e) {
304
- Log.e(CapacitorUpdater.TAG, "Could not notify listeners", e);
698
+ logger.error("Could not notify listeners " + e.getMessage());
305
699
  }
306
700
  }
307
701
 
308
702
  @PluginMethod
309
703
  public void setUpdateUrl(final PluginCall call) {
310
704
  if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
311
- Log.e(CapacitorUpdater.TAG, "setUpdateUrl not allowed set allowModifyUrl in your config to true to allow it");
705
+ logger.error("setUpdateUrl not allowed set allowModifyUrl in your config to true to allow it");
312
706
  call.reject("setUpdateUrl not allowed");
313
707
  return;
314
708
  }
315
709
  final String url = call.getString("url");
316
710
  if (url == null) {
317
- Log.e(CapacitorUpdater.TAG, "setUpdateUrl called without url");
711
+ logger.error("setUpdateUrl called without url");
318
712
  call.reject("setUpdateUrl called without url");
319
713
  return;
320
714
  }
321
715
  this.updateUrl = url;
716
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
717
+ this.editor.putString(UPDATE_URL_PREF_KEY, url);
718
+ this.editor.apply();
719
+ }
322
720
  call.resolve();
323
721
  }
324
722
 
325
723
  @PluginMethod
326
724
  public void setStatsUrl(final PluginCall call) {
327
725
  if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
328
- Log.e(CapacitorUpdater.TAG, "setStatsUrl not allowed set allowModifyUrl in your config to true to allow it");
726
+ logger.error("setStatsUrl not allowed set allowModifyUrl in your config to true to allow it");
329
727
  call.reject("setStatsUrl not allowed");
330
728
  return;
331
729
  }
332
730
  final String url = call.getString("url");
333
731
  if (url == null) {
334
- Log.e(CapacitorUpdater.TAG, "setStatsUrl called without url");
732
+ logger.error("setStatsUrl called without url");
335
733
  call.reject("setStatsUrl called without url");
336
734
  return;
337
735
  }
338
736
  this.implementation.statsUrl = url;
737
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
738
+ this.editor.putString(STATS_URL_PREF_KEY, url);
739
+ this.editor.apply();
740
+ }
339
741
  call.resolve();
340
742
  }
341
743
 
342
744
  @PluginMethod
343
745
  public void setChannelUrl(final PluginCall call) {
344
746
  if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
345
- Log.e(CapacitorUpdater.TAG, "setChannelUrl not allowed set allowModifyUrl in your config to true to allow it");
747
+ logger.error("setChannelUrl not allowed set allowModifyUrl in your config to true to allow it");
346
748
  call.reject("setChannelUrl not allowed");
347
749
  return;
348
750
  }
349
751
  final String url = call.getString("url");
350
752
  if (url == null) {
351
- Log.e(CapacitorUpdater.TAG, "setChannelUrl called without url");
753
+ logger.error("setChannelUrl called without url");
352
754
  call.reject("setChannelUrl called without url");
353
755
  return;
354
756
  }
355
757
  this.implementation.channelUrl = url;
758
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
759
+ this.editor.putString(CHANNEL_URL_PREF_KEY, url);
760
+ this.editor.apply();
761
+ }
356
762
  call.resolve();
357
763
  }
358
764
 
@@ -363,7 +769,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
363
769
  ret.put("version", this.implementation.versionBuild);
364
770
  call.resolve(ret);
365
771
  } catch (final Exception e) {
366
- Log.e(CapacitorUpdater.TAG, "Could not get version", e);
772
+ logger.error("Could not get version " + e.getMessage());
367
773
  call.reject("Could not get version", e);
368
774
  }
369
775
  }
@@ -375,7 +781,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
375
781
  ret.put("deviceId", this.implementation.deviceID);
376
782
  call.resolve(ret);
377
783
  } catch (final Exception e) {
378
- Log.e(CapacitorUpdater.TAG, "Could not get device id", e);
784
+ logger.error("Could not get device id " + e.getMessage());
379
785
  call.reject("Could not get device id", e);
380
786
  }
381
787
  }
@@ -384,11 +790,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
384
790
  public void setCustomId(final PluginCall call) {
385
791
  final String customId = call.getString("customId");
386
792
  if (customId == null) {
387
- Log.e(CapacitorUpdater.TAG, "setCustomId called without customId");
793
+ logger.error("setCustomId called without customId");
388
794
  call.reject("setCustomId called without customId");
389
795
  return;
390
796
  }
391
797
  this.implementation.customId = customId;
798
+ if (Boolean.TRUE.equals(this.persistCustomId)) {
799
+ if (customId.isEmpty()) {
800
+ this.editor.remove(CUSTOM_ID_PREF_KEY);
801
+ } else {
802
+ this.editor.putString(CUSTOM_ID_PREF_KEY, customId);
803
+ }
804
+ this.editor.apply();
805
+ }
806
+ call.resolve();
392
807
  }
393
808
 
394
809
  @PluginMethod
@@ -398,7 +813,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
398
813
  ret.put("version", this.PLUGIN_VERSION);
399
814
  call.resolve(ret);
400
815
  } catch (final Exception e) {
401
- Log.e(CapacitorUpdater.TAG, "Could not get plugin version", e);
816
+ logger.error("Could not get plugin version " + e.getMessage());
402
817
  call.reject("Could not get plugin version", e);
403
818
  }
404
819
  }
@@ -408,22 +823,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
408
823
  final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
409
824
 
410
825
  try {
411
- Log.i(CapacitorUpdater.TAG, "unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
826
+ logger.info("unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
412
827
  startNewThread(() ->
413
- CapacitorUpdaterPlugin.this.implementation.unsetChannel(res -> {
414
- if (res.has("error")) {
415
- call.reject(res.getString("error"));
416
- } else {
417
- if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
418
- Log.i(CapacitorUpdater.TAG, "Calling autoupdater after channel change!");
419
- backgroundDownload();
420
- }
421
- call.resolve(res);
828
+ CapacitorUpdaterPlugin.this.implementation.unsetChannel((res) -> {
829
+ JSObject jsRes = mapToJSObject(res);
830
+ if (jsRes.has("error")) {
831
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
832
+ String errorCode = jsRes.getString("error");
833
+
834
+ JSObject errorObj = new JSObject();
835
+ errorObj.put("message", errorMessage);
836
+ errorObj.put("error", errorCode);
837
+
838
+ call.reject(errorMessage, "UNSETCHANNEL_FAILED", null, errorObj);
839
+ } else {
840
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
841
+ logger.info("Calling autoupdater after channel change!");
842
+ backgroundDownload();
422
843
  }
423
- })
844
+ call.resolve(jsRes);
845
+ }
846
+ })
424
847
  );
425
848
  } catch (final Exception e) {
426
- Log.e(CapacitorUpdater.TAG, "Failed to unsetChannel: ", e);
849
+ logger.error("Failed to unsetChannel: " + e.getMessage());
427
850
  call.reject("Failed to unsetChannel: ", e);
428
851
  }
429
852
  }
@@ -434,27 +857,38 @@ public class CapacitorUpdaterPlugin extends Plugin {
434
857
  final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
435
858
 
436
859
  if (channel == null) {
437
- Log.e(CapacitorUpdater.TAG, "setChannel called without channel");
438
- call.reject("setChannel called without channel");
860
+ logger.error("setChannel called without channel");
861
+ JSObject errorObj = new JSObject();
862
+ errorObj.put("message", "setChannel called without channel");
863
+ errorObj.put("error", "missing_parameter");
864
+ call.reject("setChannel called without channel", "SETCHANNEL_INVALID_PARAMS", null, errorObj);
439
865
  return;
440
866
  }
441
867
  try {
442
- Log.i(CapacitorUpdater.TAG, "setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
868
+ logger.info("setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
443
869
  startNewThread(() ->
444
- CapacitorUpdaterPlugin.this.implementation.setChannel(channel, res -> {
445
- if (res.has("error")) {
446
- call.reject(res.getString("error"));
447
- } else {
448
- if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
449
- Log.i(CapacitorUpdater.TAG, "Calling autoupdater after channel change!");
450
- backgroundDownload();
451
- }
452
- call.resolve(res);
870
+ CapacitorUpdaterPlugin.this.implementation.setChannel(channel, (res) -> {
871
+ JSObject jsRes = mapToJSObject(res);
872
+ if (jsRes.has("error")) {
873
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
874
+ String errorCode = jsRes.getString("error");
875
+
876
+ JSObject errorObj = new JSObject();
877
+ errorObj.put("message", errorMessage);
878
+ errorObj.put("error", errorCode);
879
+
880
+ call.reject(errorMessage, "SETCHANNEL_FAILED", null, errorObj);
881
+ } else {
882
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
883
+ logger.info("Calling autoupdater after channel change!");
884
+ backgroundDownload();
453
885
  }
454
- })
886
+ call.resolve(jsRes);
887
+ }
888
+ })
455
889
  );
456
890
  } catch (final Exception e) {
457
- Log.e(CapacitorUpdater.TAG, "Failed to setChannel: " + channel, e);
891
+ logger.error("Failed to setChannel: " + channel + " " + e.getMessage());
458
892
  call.reject("Failed to setChannel: " + channel, e);
459
893
  }
460
894
  }
@@ -462,50 +896,98 @@ public class CapacitorUpdaterPlugin extends Plugin {
462
896
  @PluginMethod
463
897
  public void getChannel(final PluginCall call) {
464
898
  try {
465
- Log.i(CapacitorUpdater.TAG, "getChannel");
899
+ logger.info("getChannel");
466
900
  startNewThread(() ->
467
- CapacitorUpdaterPlugin.this.implementation.getChannel(res -> {
468
- if (res.has("error")) {
469
- call.reject(res.getString("error"));
470
- } else {
471
- call.resolve(res);
472
- }
473
- })
901
+ CapacitorUpdaterPlugin.this.implementation.getChannel((res) -> {
902
+ JSObject jsRes = mapToJSObject(res);
903
+ if (jsRes.has("error")) {
904
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
905
+ String errorCode = jsRes.getString("error");
906
+
907
+ JSObject errorObj = new JSObject();
908
+ errorObj.put("message", errorMessage);
909
+ errorObj.put("error", errorCode);
910
+
911
+ call.reject(errorMessage, "GETCHANNEL_FAILED", null, errorObj);
912
+ } else {
913
+ call.resolve(jsRes);
914
+ }
915
+ })
474
916
  );
475
917
  } catch (final Exception e) {
476
- Log.e(CapacitorUpdater.TAG, "Failed to getChannel", e);
918
+ logger.error("Failed to getChannel " + e.getMessage());
477
919
  call.reject("Failed to getChannel", e);
478
920
  }
479
921
  }
480
922
 
923
+ @PluginMethod
924
+ public void listChannels(final PluginCall call) {
925
+ try {
926
+ logger.info("listChannels");
927
+ startNewThread(() ->
928
+ CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
929
+ JSObject jsRes = mapToJSObject(res);
930
+ if (jsRes.has("error")) {
931
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
932
+ String errorCode = jsRes.getString("error");
933
+
934
+ JSObject errorObj = new JSObject();
935
+ errorObj.put("message", errorMessage);
936
+ errorObj.put("error", errorCode);
937
+
938
+ call.reject(errorMessage, "LISTCHANNELS_FAILED", null, errorObj);
939
+ } else {
940
+ call.resolve(jsRes);
941
+ }
942
+ })
943
+ );
944
+ } catch (final Exception e) {
945
+ logger.error("Failed to listChannels: " + e.getMessage());
946
+ call.reject("Failed to listChannels", e);
947
+ }
948
+ }
949
+
481
950
  @PluginMethod
482
951
  public void download(final PluginCall call) {
483
952
  final String url = call.getString("url");
484
953
  final String version = call.getString("version");
485
954
  final String sessionKey = call.getString("sessionKey", "");
486
955
  final String checksum = call.getString("checksum", "");
956
+ final JSONArray manifest = call.getData().optJSONArray("manifest");
487
957
  if (url == null) {
488
- Log.e(CapacitorUpdater.TAG, "Download called without url");
958
+ logger.error("Download called without url");
489
959
  call.reject("Download called without url");
490
960
  return;
491
961
  }
492
962
  if (version == null) {
493
- Log.e(CapacitorUpdater.TAG, "Download called without version");
963
+ logger.error("Download called without version");
494
964
  call.reject("Download called without version");
495
965
  return;
496
966
  }
497
967
  try {
498
- Log.i(CapacitorUpdater.TAG, "Downloading " + url);
968
+ logger.info("Downloading " + url);
499
969
  startNewThread(() -> {
500
970
  try {
501
- final BundleInfo downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
971
+ final BundleInfo downloaded;
972
+ if (manifest != null) {
973
+ // For manifest downloads, we need to handle this asynchronously
974
+ // since there's no synchronous downloadManifest method in Java
975
+ CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest);
976
+ // Return immediately with a pending status - the actual result will come via listeners
977
+ final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
978
+ downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
979
+ call.resolve(mapToJSObject(downloaded.toJSONMap()));
980
+ return;
981
+ } else {
982
+ downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
983
+ }
502
984
  if (downloaded.isErrorStatus()) {
503
985
  throw new RuntimeException("Download failed: " + downloaded.getStatus());
504
986
  } else {
505
- call.resolve(downloaded.toJSON());
987
+ call.resolve(mapToJSObject(downloaded.toJSONMap()));
506
988
  }
507
989
  } catch (final Exception e) {
508
- Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
990
+ logger.error("Failed to download from: " + url + " " + e.getMessage());
509
991
  call.reject("Failed to download from: " + url, e);
510
992
  final JSObject ret = new JSObject();
511
993
  ret.put("version", version);
@@ -515,7 +997,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
515
997
  }
516
998
  });
517
999
  } catch (final Exception e) {
518
- Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
1000
+ logger.error("Failed to download from: " + url + " " + e.getMessage());
519
1001
  call.reject("Failed to download from: " + url, e);
520
1002
  final JSObject ret = new JSObject();
521
1003
  ret.put("version", version);
@@ -525,10 +1007,27 @@ public class CapacitorUpdaterPlugin extends Plugin {
525
1007
  }
526
1008
  }
527
1009
 
1010
+ private void syncKeepUrlPathFlag(final boolean enabled) {
1011
+ if (this.bridge == null || this.bridge.getWebView() == null) {
1012
+ return;
1013
+ }
1014
+ final String script = enabled
1015
+ ? "(function(){try{localStorage.setItem('" +
1016
+ KEEP_URL_FLAG_KEY +
1017
+ "','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);})();"
1018
+ : "(function(){try{localStorage.removeItem('" +
1019
+ KEEP_URL_FLAG_KEY +
1020
+ "');}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);})();";
1021
+ this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
1022
+ }
1023
+
528
1024
  protected boolean _reload() {
529
1025
  final String path = this.implementation.getCurrentBundlePath();
1026
+ if (this.keepUrlPathAfterReload) {
1027
+ this.syncKeepUrlPathFlag(true);
1028
+ }
530
1029
  this.semaphoreUp();
531
- Log.i(CapacitorUpdater.TAG, "Reloading: " + path);
1030
+ logger.info("Reloading: " + path);
532
1031
 
533
1032
  AtomicReference<URL> url = new AtomicReference<>();
534
1033
  if (this.keepUrlPathAfterReload) {
@@ -536,23 +1035,38 @@ public class CapacitorUpdaterPlugin extends Plugin {
536
1035
  if (Looper.myLooper() != Looper.getMainLooper()) {
537
1036
  Semaphore mainThreadSemaphore = new Semaphore(0);
538
1037
  this.bridge.executeOnMainThread(() -> {
539
- try {
540
- url.set(new URL(this.bridge.getWebView().getUrl()));
541
- } catch (Exception e) {
542
- Log.e(CapacitorUpdater.TAG, "Error executing on main thread", e);
1038
+ try {
1039
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1040
+ String currentUrl = this.bridge.getWebView().getUrl();
1041
+ if (currentUrl != null) {
1042
+ url.set(new URL(currentUrl));
1043
+ }
543
1044
  }
544
- mainThreadSemaphore.release();
545
- });
546
- mainThreadSemaphore.acquire();
1045
+ } catch (Exception e) {
1046
+ logger.error("Error executing on main thread " + e.getMessage());
1047
+ }
1048
+ mainThreadSemaphore.release();
1049
+ });
1050
+
1051
+ // Add timeout to prevent indefinite blocking
1052
+ if (!mainThreadSemaphore.tryAcquire(10, TimeUnit.SECONDS)) {
1053
+ logger.error("Timeout waiting for main thread operation");
1054
+ }
547
1055
  } else {
548
1056
  try {
549
- url.set(new URL(this.bridge.getWebView().getUrl()));
1057
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1058
+ String currentUrl = this.bridge.getWebView().getUrl();
1059
+ if (currentUrl != null) {
1060
+ url.set(new URL(currentUrl));
1061
+ }
1062
+ }
550
1063
  } catch (Exception e) {
551
- Log.e(CapacitorUpdater.TAG, "Error executing on main thread", e);
1064
+ logger.error("Error executing on main thread " + e.getMessage());
552
1065
  }
553
1066
  }
554
1067
  } catch (InterruptedException e) {
555
- Log.e(CapacitorUpdater.TAG, "Error waiting for main thread or getting the current URL from webview", e);
1068
+ logger.error("Error waiting for main thread or getting the current URL from webview " + e.getMessage());
1069
+ Thread.currentThread().interrupt(); // Restore interrupted status
556
1070
  }
557
1071
  }
558
1072
 
@@ -568,13 +1082,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
568
1082
  finalUrl = new URL(this.bridge.getAppUrl());
569
1083
  finalUrl = new URL(finalUrl.getProtocol(), finalUrl.getHost(), finalUrl.getPort(), url.get().getPath());
570
1084
  URL finalUrl1 = finalUrl;
571
- this.bridge.getWebView()
572
- .post(() -> {
573
- this.bridge.getWebView().loadUrl(finalUrl1.toString());
1085
+ this.bridge.getWebView().post(() -> {
1086
+ this.bridge.getWebView().loadUrl(finalUrl1.toString());
1087
+ if (!this.keepUrlPathAfterReload) {
574
1088
  this.bridge.getWebView().clearHistory();
575
- });
1089
+ }
1090
+ });
576
1091
  } catch (MalformedURLException e) {
577
- Log.e(CapacitorUpdater.TAG, "Cannot get finalUrl from capacitor bridge", e);
1092
+ logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
578
1093
 
579
1094
  if (this.implementation.isUsingBuiltin()) {
580
1095
  this.bridge.setServerAssetPath(path);
@@ -588,10 +1103,29 @@ public class CapacitorUpdaterPlugin extends Plugin {
588
1103
  } else {
589
1104
  this.bridge.setServerBasePath(path);
590
1105
  }
1106
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1107
+ this.bridge.getWebView().post(() -> {
1108
+ if (this.bridge.getWebView() != null) {
1109
+ this.bridge.getWebView().loadUrl(this.bridge.getAppUrl());
1110
+ if (!this.keepUrlPathAfterReload) {
1111
+ this.bridge.getWebView().clearHistory();
1112
+ }
1113
+ }
1114
+ });
1115
+ }
591
1116
  }
592
1117
 
593
1118
  this.checkAppReady();
594
1119
  this.notifyListeners("appReloaded", new JSObject());
1120
+
1121
+ // Wait for the reload to complete (until notifyAppReady is called)
1122
+ try {
1123
+ this.semaphoreWait(this.appReadyTimeout);
1124
+ } catch (Exception e) {
1125
+ logger.error("Error waiting for app ready: " + e.getMessage());
1126
+ return false;
1127
+ }
1128
+
595
1129
  return true;
596
1130
  }
597
1131
 
@@ -601,11 +1135,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
601
1135
  if (this._reload()) {
602
1136
  call.resolve();
603
1137
  } else {
604
- Log.e(CapacitorUpdater.TAG, "Reload failed");
1138
+ logger.error("Reload failed");
605
1139
  call.reject("Reload failed");
606
1140
  }
607
1141
  } catch (final Exception e) {
608
- Log.e(CapacitorUpdater.TAG, "Could not reload", e);
1142
+ logger.error("Could not reload " + e.getMessage());
609
1143
  call.reject("Could not reload", e);
610
1144
  }
611
1145
  }
@@ -614,20 +1148,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
614
1148
  public void next(final PluginCall call) {
615
1149
  final String id = call.getString("id");
616
1150
  if (id == null) {
617
- Log.e(CapacitorUpdater.TAG, "Next called without id");
1151
+ logger.error("Next called without id");
618
1152
  call.reject("Next called without id");
619
1153
  return;
620
1154
  }
621
1155
  try {
622
- Log.i(CapacitorUpdater.TAG, "Setting next active id " + id);
1156
+ logger.info("Setting next active id " + id);
623
1157
  if (!this.implementation.setNextBundle(id)) {
624
- Log.e(CapacitorUpdater.TAG, "Set next id failed. Bundle " + id + " does not exist.");
1158
+ logger.error("Set next id failed. Bundle " + id + " does not exist.");
625
1159
  call.reject("Set next id failed. Bundle " + id + " does not exist.");
626
1160
  } else {
627
- call.resolve(this.implementation.getBundleInfo(id).toJSON());
1161
+ call.resolve(mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
628
1162
  }
629
1163
  } catch (final Exception e) {
630
- Log.e(CapacitorUpdater.TAG, "Could not set next id " + id, e);
1164
+ logger.error("Could not set next id " + id + " " + e.getMessage());
631
1165
  call.reject("Could not set next id: " + id, e);
632
1166
  }
633
1167
  }
@@ -636,21 +1170,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
636
1170
  public void set(final PluginCall call) {
637
1171
  final String id = call.getString("id");
638
1172
  if (id == null) {
639
- Log.e(CapacitorUpdater.TAG, "Set called without id");
1173
+ logger.error("Set called without id");
640
1174
  call.reject("Set called without id");
641
1175
  return;
642
1176
  }
643
1177
  try {
644
- Log.i(CapacitorUpdater.TAG, "Setting active bundle " + id);
1178
+ logger.info("Setting active bundle " + id);
645
1179
  if (!this.implementation.set(id)) {
646
- Log.i(CapacitorUpdater.TAG, "No such bundle " + id);
1180
+ logger.info("No such bundle " + id);
647
1181
  call.reject("Update failed, id " + id + " does not exist.");
648
1182
  } else {
649
- Log.i(CapacitorUpdater.TAG, "Bundle successfully set to " + id);
1183
+ logger.info("Bundle successfully set to " + id);
650
1184
  this.reload(call);
651
1185
  }
652
1186
  } catch (final Exception e) {
653
- Log.e(CapacitorUpdater.TAG, "Could not set id " + id, e);
1187
+ logger.error("Could not set id " + id + " " + e.getMessage());
654
1188
  call.reject("Could not set id " + id, e);
655
1189
  }
656
1190
  }
@@ -659,25 +1193,63 @@ public class CapacitorUpdaterPlugin extends Plugin {
659
1193
  public void delete(final PluginCall call) {
660
1194
  final String id = call.getString("id");
661
1195
  if (id == null) {
662
- Log.e(CapacitorUpdater.TAG, "missing id");
1196
+ logger.error("missing id");
663
1197
  call.reject("missing id");
664
1198
  return;
665
1199
  }
666
- Log.i(CapacitorUpdater.TAG, "Deleting id " + id);
1200
+ logger.info("Deleting id " + id);
667
1201
  try {
668
1202
  final Boolean res = this.implementation.delete(id);
669
1203
  if (res) {
670
1204
  call.resolve();
671
1205
  } else {
672
- Log.e(CapacitorUpdater.TAG, "Delete failed, id " + id + " does not exist");
1206
+ logger.error("Delete failed, id " + id + " does not exist");
673
1207
  call.reject("Delete failed, id " + id + " does not exist or it cannot be deleted (perhaps it is the 'next' bundle)");
674
1208
  }
675
1209
  } catch (final Exception e) {
676
- Log.e(CapacitorUpdater.TAG, "Could not delete id " + id, e);
1210
+ logger.error("Could not delete id " + id + " " + e.getMessage());
677
1211
  call.reject("Could not delete id " + id, e);
678
1212
  }
679
1213
  }
680
1214
 
1215
+ @PluginMethod
1216
+ public void setBundleError(final PluginCall call) {
1217
+ if (!Boolean.TRUE.equals(this.allowManualBundleError)) {
1218
+ logger.error("setBundleError called without allowManualBundleError");
1219
+ call.reject("setBundleError not allowed. Set allowManualBundleError to true in your config to enable it.");
1220
+ return;
1221
+ }
1222
+ final String id = call.getString("id");
1223
+ if (id == null) {
1224
+ logger.error("setBundleError called without id");
1225
+ call.reject("setBundleError called without id");
1226
+ return;
1227
+ }
1228
+ try {
1229
+ final BundleInfo bundle = this.implementation.getBundleInfo(id);
1230
+ if (bundle == null || bundle.isUnknown()) {
1231
+ logger.error("setBundleError called with unknown bundle " + id);
1232
+ call.reject("Bundle " + id + " does not exist");
1233
+ return;
1234
+ }
1235
+ if (bundle.isBuiltin()) {
1236
+ logger.error("setBundleError called on builtin bundle");
1237
+ call.reject("Cannot set builtin bundle to error state");
1238
+ return;
1239
+ }
1240
+ if (Boolean.TRUE.equals(this.autoUpdate)) {
1241
+ logger.warn("setBundleError used while autoUpdate is enabled; this method is intended for manual mode");
1242
+ }
1243
+ this.implementation.setError(bundle);
1244
+ final JSObject ret = new JSObject();
1245
+ ret.put("bundle", mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
1246
+ call.resolve(ret);
1247
+ } catch (final Exception e) {
1248
+ logger.error("Could not set bundle error for id " + id + " " + e.getMessage());
1249
+ call.reject("Could not set bundle error for id " + id, e);
1250
+ }
1251
+ }
1252
+
681
1253
  @PluginMethod
682
1254
  public void list(final PluginCall call) {
683
1255
  try {
@@ -685,12 +1257,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
685
1257
  final JSObject ret = new JSObject();
686
1258
  final JSArray values = new JSArray();
687
1259
  for (final BundleInfo bundle : res) {
688
- values.put(bundle.toJSON());
1260
+ values.put(mapToJSObject(bundle.toJSONMap()));
689
1261
  }
690
1262
  ret.put("bundles", values);
691
1263
  call.resolve(ret);
692
1264
  } catch (final Exception e) {
693
- Log.e(CapacitorUpdater.TAG, "Could not list bundles", e);
1265
+ logger.error("Could not list bundles " + e.getMessage());
694
1266
  call.reject("Could not list bundles", e);
695
1267
  }
696
1268
  }
@@ -699,30 +1271,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
699
1271
  public void getLatest(final PluginCall call) {
700
1272
  final String channel = call.getString("channel");
701
1273
  startNewThread(() ->
702
- CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, res -> {
703
- if (res.has("error")) {
704
- call.reject(res.getString("error"));
705
- return;
706
- } else if (res.has("message")) {
707
- call.reject(res.getString("message"));
708
- return;
709
- } else {
710
- call.resolve(res);
711
- }
712
- final JSObject ret = new JSObject();
713
- Iterator<String> keys = res.keys();
714
- while (keys.hasNext()) {
715
- String key = keys.next();
716
- if (res.has(key)) {
717
- try {
718
- ret.put(key, res.get(key));
719
- } catch (JSONException e) {
720
- e.printStackTrace();
721
- }
722
- }
723
- }
724
- call.resolve(ret);
725
- })
1274
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
1275
+ JSObject jsRes = mapToJSObject(res);
1276
+ if (jsRes.has("error")) {
1277
+ call.reject(jsRes.getString("error"));
1278
+ return;
1279
+ } else if (jsRes.has("message")) {
1280
+ call.reject(jsRes.getString("message"));
1281
+ return;
1282
+ } else {
1283
+ call.resolve(jsRes);
1284
+ }
1285
+ })
726
1286
  );
727
1287
  }
728
1288
 
@@ -731,11 +1291,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
731
1291
  this.implementation.reset();
732
1292
 
733
1293
  if (toLastSuccessful && !fallback.isBuiltin()) {
734
- Log.i(CapacitorUpdater.TAG, "Resetting to: " + fallback);
1294
+ logger.info("Resetting to: " + fallback);
735
1295
  return this.implementation.set(fallback) && this._reload();
736
1296
  }
737
1297
 
738
- Log.i(CapacitorUpdater.TAG, "Resetting to native.");
1298
+ logger.info("Resetting to native.");
739
1299
  return this._reload();
740
1300
  }
741
1301
 
@@ -747,24 +1307,25 @@ public class CapacitorUpdaterPlugin extends Plugin {
747
1307
  call.resolve();
748
1308
  return;
749
1309
  }
750
- Log.e(CapacitorUpdater.TAG, "Reset failed");
1310
+ logger.error("Reset failed");
751
1311
  call.reject("Reset failed");
752
1312
  } catch (final Exception e) {
753
- Log.e(CapacitorUpdater.TAG, "Reset failed", e);
1313
+ logger.error("Reset failed " + e.getMessage());
754
1314
  call.reject("Reset failed", e);
755
1315
  }
756
1316
  }
757
1317
 
758
1318
  @PluginMethod
759
1319
  public void current(final PluginCall call) {
1320
+ ensureBridgeSet();
760
1321
  try {
761
1322
  final JSObject ret = new JSObject();
762
1323
  final BundleInfo bundle = this.implementation.getCurrentBundle();
763
- ret.put("bundle", bundle.toJSON());
1324
+ ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
764
1325
  ret.put("native", this.currentVersionNative);
765
1326
  call.resolve(ret);
766
1327
  } catch (final Exception e) {
767
- Log.e(CapacitorUpdater.TAG, "Could not get current bundle", e);
1328
+ logger.error("Could not get current bundle " + e.getMessage());
768
1329
  call.reject("Could not get current bundle", e);
769
1330
  }
770
1331
  }
@@ -778,13 +1339,33 @@ public class CapacitorUpdaterPlugin extends Plugin {
778
1339
  return;
779
1340
  }
780
1341
 
781
- call.resolve(bundle.toJSON());
1342
+ call.resolve(mapToJSObject(bundle.toJSONMap()));
782
1343
  } catch (final Exception e) {
783
- Log.e(CapacitorUpdater.TAG, "Could not get next bundle", e);
1344
+ logger.error("Could not get next bundle " + e.getMessage());
784
1345
  call.reject("Could not get next bundle", e);
785
1346
  }
786
1347
  }
787
1348
 
1349
+ @PluginMethod
1350
+ public void getFailedUpdate(final PluginCall call) {
1351
+ try {
1352
+ final BundleInfo bundle = this.readLastFailedBundle();
1353
+ if (bundle == null || bundle.isUnknown()) {
1354
+ call.resolve(null);
1355
+ return;
1356
+ }
1357
+
1358
+ this.persistLastFailedBundle(null);
1359
+
1360
+ final JSObject ret = new JSObject();
1361
+ ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
1362
+ call.resolve(ret);
1363
+ } catch (final Exception e) {
1364
+ logger.error("Could not get failed update " + e.getMessage());
1365
+ call.reject("Could not get failed update", e);
1366
+ }
1367
+ }
1368
+
788
1369
  public void checkForUpdateAfterDelay() {
789
1370
  if (this.periodCheckDelay == 0 || !this._isAutoUpdateEnabled()) {
790
1371
  return;
@@ -795,20 +1376,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
795
1376
  @Override
796
1377
  public void run() {
797
1378
  try {
798
- CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, res -> {
799
- if (res.has("error")) {
800
- Log.e(CapacitorUpdater.TAG, Objects.requireNonNull(res.getString("error")));
801
- } else if (res.has("version")) {
802
- String newVersion = res.getString("version");
803
- String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
804
- if (!Objects.equals(newVersion, currentVersion)) {
805
- Log.i(CapacitorUpdater.TAG, "New version found: " + newVersion);
806
- CapacitorUpdaterPlugin.this.backgroundDownload();
807
- }
1379
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1380
+ JSObject jsRes = mapToJSObject(res);
1381
+ if (jsRes.has("error")) {
1382
+ logger.error(Objects.requireNonNull(jsRes.getString("error")));
1383
+ } else if (jsRes.has("version")) {
1384
+ String newVersion = jsRes.getString("version");
1385
+ String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
1386
+ if (!Objects.equals(newVersion, currentVersion)) {
1387
+ logger.info("New version found: " + newVersion);
1388
+ CapacitorUpdaterPlugin.this.backgroundDownload();
808
1389
  }
809
- });
1390
+ }
1391
+ });
810
1392
  } catch (final Exception e) {
811
- Log.e(CapacitorUpdater.TAG, "Failed to check for update", e);
1393
+ logger.error("Failed to check for update " + e.getMessage());
812
1394
  }
813
1395
  }
814
1396
  },
@@ -819,18 +1401,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
819
1401
 
820
1402
  @PluginMethod
821
1403
  public void notifyAppReady(final PluginCall call) {
1404
+ ensureBridgeSet();
822
1405
  try {
823
1406
  final BundleInfo bundle = this.implementation.getCurrentBundle();
824
1407
  this.implementation.setSuccess(bundle, this.autoDeletePrevious);
825
- Log.i(CapacitorUpdater.TAG, "Current bundle loaded successfully. ['notifyAppReady()' was called] " + bundle);
826
- Log.i(CapacitorUpdater.TAG, "semaphoreReady countDown");
1408
+ logger.info("Current bundle loaded successfully. ['notifyAppReady()' was called] " + bundle);
1409
+ logger.info("semaphoreReady countDown");
827
1410
  this.semaphoreDown();
828
- Log.i(CapacitorUpdater.TAG, "semaphoreReady countDown done");
1411
+ logger.info("semaphoreReady countDown done");
829
1412
  final JSObject ret = new JSObject();
830
- ret.put("bundle", bundle.toJSON());
1413
+ ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
831
1414
  call.resolve(ret);
832
1415
  } catch (final Exception e) {
833
- Log.e(CapacitorUpdater.TAG, "Failed to notify app ready state. [Error calling 'notifyAppReady()']", e);
1416
+ logger.error("Failed to notify app ready state. [Error calling 'notifyAppReady()'] " + e.getMessage());
834
1417
  call.reject("Failed to commit app ready state.", e);
835
1418
  }
836
1419
  }
@@ -838,118 +1421,46 @@ public class CapacitorUpdaterPlugin extends Plugin {
838
1421
  @PluginMethod
839
1422
  public void setMultiDelay(final PluginCall call) {
840
1423
  try {
841
- final Object delayConditions = call.getData().opt("delayConditions");
1424
+ final JSONArray delayConditions = call.getData().optJSONArray("delayConditions");
842
1425
  if (delayConditions == null) {
843
- Log.e(CapacitorUpdater.TAG, "setMultiDelay called without delayCondition");
1426
+ logger.error("setMultiDelay called without delayCondition");
844
1427
  call.reject("setMultiDelay called without delayCondition");
845
1428
  return;
846
1429
  }
847
- if (_setMultiDelay(delayConditions.toString())) {
1430
+ for (int i = 0; i < delayConditions.length(); i++) {
1431
+ final JSONObject object = delayConditions.optJSONObject(i);
1432
+ if (object != null && object.optString("kind").equals("background") && object.optString("value").isEmpty()) {
1433
+ object.put("value", "0");
1434
+ delayConditions.put(i, object);
1435
+ }
1436
+ }
1437
+
1438
+ if (this.delayUpdateUtils.setMultiDelay(delayConditions.toString())) {
848
1439
  call.resolve();
849
1440
  } else {
850
1441
  call.reject("Failed to delay update");
851
1442
  }
852
1443
  } catch (final Exception e) {
853
- Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling 'setMultiDelay()']", e);
1444
+ logger.error("Failed to delay update, [Error calling 'setMultiDelay()'] " + e.getMessage());
854
1445
  call.reject("Failed to delay update", e);
855
1446
  }
856
1447
  }
857
1448
 
858
- private Boolean _setMultiDelay(String delayConditions) {
859
- try {
860
- this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions);
861
- this.editor.commit();
862
- Log.i(CapacitorUpdater.TAG, "Delay update saved");
863
- return true;
864
- } catch (final Exception e) {
865
- Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling '_setMultiDelay()']", e);
866
- return false;
867
- }
868
- }
869
-
870
- private boolean _cancelDelay(String source) {
871
- try {
872
- this.editor.remove(DELAY_CONDITION_PREFERENCES);
873
- this.editor.commit();
874
- Log.i(CapacitorUpdater.TAG, "All delays canceled from " + source);
875
- return true;
876
- } catch (final Exception e) {
877
- Log.e(CapacitorUpdater.TAG, "Failed to cancel update delay", e);
878
- return false;
879
- }
880
- }
881
-
882
1449
  @PluginMethod
883
1450
  public void cancelDelay(final PluginCall call) {
884
- if (this._cancelDelay("JS")) {
1451
+ if (this.delayUpdateUtils.cancelDelay("JS")) {
885
1452
  call.resolve();
886
1453
  } else {
887
1454
  call.reject("Failed to cancel delay");
888
1455
  }
889
1456
  }
890
1457
 
891
- private void _checkCancelDelay(Boolean killed) {
892
- Gson gson = new Gson();
893
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
894
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
895
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
896
- for (DelayCondition condition : delayConditionList) {
897
- String kind = condition.getKind().toString();
898
- String value = condition.getValue();
899
- if (!kind.isEmpty()) {
900
- switch (kind) {
901
- case "background":
902
- if (!killed) {
903
- this._cancelDelay("background check");
904
- }
905
- break;
906
- case "kill":
907
- if (killed) {
908
- this._cancelDelay("kill check");
909
- this.installNext();
910
- }
911
- break;
912
- case "date":
913
- if (!"".equals(value)) {
914
- try {
915
- final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
916
- Date date = sdf.parse(value);
917
- assert date != null;
918
- if (new Date().compareTo(date) > 0) {
919
- this._cancelDelay("date expired");
920
- }
921
- } catch (final Exception e) {
922
- this._cancelDelay("date parsing issue");
923
- }
924
- } else {
925
- this._cancelDelay("delayVal absent");
926
- }
927
- break;
928
- case "nativeVersion":
929
- if (!"".equals(value)) {
930
- try {
931
- final Version versionLimit = new Version(value);
932
- if (this.currentVersionNative.isAtLeast(versionLimit)) {
933
- this._cancelDelay("nativeVersion above limit");
934
- }
935
- } catch (final Exception e) {
936
- this._cancelDelay("nativeVersion parsing issue");
937
- }
938
- } else {
939
- this._cancelDelay("delayVal absent");
940
- }
941
- break;
942
- }
943
- }
944
- }
945
- }
946
-
947
1458
  private Boolean _isAutoUpdateEnabled() {
948
1459
  final CapConfig config = CapConfig.loadDefault(this.getActivity());
949
1460
  String serverUrl = config.getServerUrl();
950
1461
  if (serverUrl != null && !serverUrl.isEmpty()) {
951
1462
  // log warning autoupdate disabled when serverUrl is set
952
- Log.w(CapacitorUpdater.TAG, "AutoUpdate is automatic disabled when serverUrl is set.");
1463
+ logger.warn("AutoUpdate is automatic disabled when serverUrl is set.");
953
1464
  }
954
1465
  return (
955
1466
  CapacitorUpdaterPlugin.this.autoUpdate &&
@@ -965,7 +1476,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
965
1476
  ret.put("enabled", this._isAutoUpdateEnabled());
966
1477
  call.resolve(ret);
967
1478
  } catch (final Exception e) {
968
- Log.e(CapacitorUpdater.TAG, "Could not get autoUpdate status", e);
1479
+ logger.error("Could not get autoUpdate status " + e.getMessage());
969
1480
  call.reject("Could not get autoUpdate status", e);
970
1481
  }
971
1482
  }
@@ -979,7 +1490,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
979
1490
  ret.put("available", serverUrl == null || serverUrl.isEmpty());
980
1491
  call.resolve(ret);
981
1492
  } catch (final Exception e) {
982
- Log.e(CapacitorUpdater.TAG, "Could not get autoUpdate availability", e);
1493
+ logger.error("Could not get autoUpdate availability " + e.getMessage());
983
1494
  call.reject("Could not get autoUpdate availability", e);
984
1495
  }
985
1496
  }
@@ -991,7 +1502,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
991
1502
  }
992
1503
  this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck());
993
1504
  } catch (final Exception e) {
994
- Log.e(CapacitorUpdater.TAG, "Failed to start " + DeferredNotifyAppReadyCheck.class.getName(), e);
1505
+ logger.error("Failed to start " + DeferredNotifyAppReadyCheck.class.getName() + " " + e.getMessage());
995
1506
  }
996
1507
  }
997
1508
 
@@ -1004,8 +1515,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
1004
1515
  }
1005
1516
  }
1006
1517
 
1518
+ private void ensureBridgeSet() {
1519
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1520
+ logger.setBridge(this.bridge);
1521
+ }
1522
+ }
1523
+
1007
1524
  private void endBackGroundTaskWithNotif(String msg, String latestVersionName, BundleInfo current, Boolean error) {
1008
- endBackGroundTaskWithNotif(msg, latestVersionName, current, error, "download_fail", "downloadFailed");
1525
+ endBackGroundTaskWithNotif(msg, latestVersionName, current, error, false, "download_fail", "downloadFailed");
1009
1526
  }
1010
1527
 
1011
1528
  private void endBackGroundTaskWithNotif(
@@ -1013,18 +1530,28 @@ public class CapacitorUpdaterPlugin extends Plugin {
1013
1530
  String latestVersionName,
1014
1531
  BundleInfo current,
1015
1532
  Boolean error,
1533
+ Boolean isDirectUpdate
1534
+ ) {
1535
+ endBackGroundTaskWithNotif(msg, latestVersionName, current, error, isDirectUpdate, "download_fail", "downloadFailed");
1536
+ }
1537
+
1538
+ private void endBackGroundTaskWithNotif(
1539
+ String msg,
1540
+ String latestVersionName,
1541
+ BundleInfo current,
1542
+ Boolean error,
1543
+ Boolean isDirectUpdate,
1016
1544
  String failureAction,
1017
1545
  String failureEvent
1018
1546
  ) {
1019
1547
  if (error) {
1020
- Log.i(
1021
- CapacitorUpdater.TAG,
1548
+ logger.info(
1022
1549
  "endBackGroundTaskWithNotif error: " +
1023
- error +
1024
- " current: " +
1025
- current.getVersionName() +
1026
- "latestVersionName: " +
1027
- latestVersionName
1550
+ error +
1551
+ " current: " +
1552
+ current.getVersionName() +
1553
+ "latestVersionName: " +
1554
+ latestVersionName
1028
1555
  );
1029
1556
  this.implementation.sendStats(failureAction, current.getVersionName());
1030
1557
  final JSObject ret = new JSObject();
@@ -1032,105 +1559,117 @@ public class CapacitorUpdaterPlugin extends Plugin {
1032
1559
  this.notifyListeners(failureEvent, ret);
1033
1560
  }
1034
1561
  final JSObject ret = new JSObject();
1035
- ret.put("bundle", current.toJSON());
1562
+ ret.put("bundle", mapToJSObject(current.toJSONMap()));
1036
1563
  this.notifyListeners("noNeedUpdate", ret);
1037
- this.sendReadyToJs(current, msg);
1564
+ this.sendReadyToJs(current, msg, isDirectUpdate);
1038
1565
  this.backgroundDownloadTask = null;
1039
- Log.i(CapacitorUpdater.TAG, "endBackGroundTaskWithNotif " + msg);
1566
+ logger.info("endBackGroundTaskWithNotif " + msg);
1040
1567
  }
1041
1568
 
1042
1569
  private Thread backgroundDownload() {
1043
- String messageUpdate = this.implementation.directUpdate
1570
+ final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
1571
+ final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
1572
+ this.implementation.directUpdate = initialDirectUpdateAllowed;
1573
+ final String messageUpdate = initialDirectUpdateAllowed
1044
1574
  ? "Update will occur now."
1045
1575
  : "Update will occur next time app moves to background.";
1046
1576
  return startNewThread(() -> {
1047
- Log.i(CapacitorUpdater.TAG, "Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
1048
- CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, res -> {
1577
+ logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
1578
+ try {
1579
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
1580
+ JSObject jsRes = mapToJSObject(res);
1049
1581
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1050
- try {
1051
- if (res.has("error")) {
1052
- final String error = res.optString("error", "");
1053
- if (error != null && !error.isEmpty()) {
1054
- Log.e(CapacitorUpdater.TAG, "getLatest failed with error: " + error);
1055
- final String latestVersion = res.has("version")
1056
- ? res.optString("version", current.getVersionName())
1057
- : current.getVersionName();
1058
- if ("response_error".equals(error)) {
1059
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1060
- "Network error: " + error,
1061
- latestVersion,
1062
- current,
1063
- true
1064
- );
1065
- } else {
1066
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1067
- error,
1068
- latestVersion,
1069
- current,
1070
- true,
1071
- "backend_refusal",
1072
- "backendRefused"
1073
- );
1074
- }
1075
- return;
1076
- }
1582
+
1583
+ // Handle network errors and other failures first
1584
+ if (jsRes.has("error")) {
1585
+ String error = jsRes.getString("error");
1586
+ logger.error("getLatest failed with error: " + error);
1587
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
1588
+ if ("response_error".equals(error)) {
1589
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1590
+ "Network error: " + error,
1591
+ latestVersion,
1592
+ current,
1593
+ true,
1594
+ plannedDirectUpdate
1595
+ );
1596
+ } else {
1597
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1598
+ error,
1599
+ latestVersion,
1600
+ current,
1601
+ true,
1602
+ plannedDirectUpdate,
1603
+ "backend_refusal",
1604
+ "backendRefused"
1605
+ );
1077
1606
  }
1607
+ return;
1608
+ }
1078
1609
 
1079
- if (res.has("message")) {
1080
- Log.i(CapacitorUpdater.TAG, "API message: " + res.get("message"));
1081
- if (res.has("major") && res.getBoolean("major") && res.has("version")) {
1082
- final JSObject majorAvailable = new JSObject();
1083
- majorAvailable.put("version", res.getString("version"));
1084
- CapacitorUpdaterPlugin.this.notifyListeners("majorAvailable", majorAvailable);
1610
+ try {
1611
+ if (jsRes.has("message")) {
1612
+ logger.info("API message: " + jsRes.get("message"));
1613
+ if (jsRes.has("version") && (jsRes.has("breaking") || jsRes.has("major"))) {
1614
+ CapacitorUpdaterPlugin.this.notifyBreakingEvents(jsRes.getString("version"));
1085
1615
  }
1086
- final String latestVersion = res.has("version")
1087
- ? res.optString("version", current.getVersionName())
1088
- : current.getVersionName();
1616
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
1089
1617
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1090
- res.getString("message"),
1091
- latestVersion,
1092
- current,
1093
- true,
1094
- "backend_refusal",
1095
- "backendRefused"
1096
- );
1618
+ jsRes.getString("message"),
1619
+ latestVersion,
1620
+ current,
1621
+ true,
1622
+ plannedDirectUpdate,
1623
+ "backend_refusal",
1624
+ "backendRefused"
1625
+ );
1097
1626
  return;
1098
1627
  }
1099
1628
 
1100
- final String latestVersionName = res.getString("version");
1629
+ final String latestVersionName = jsRes.getString("version");
1101
1630
 
1102
1631
  if ("builtin".equals(latestVersionName)) {
1103
- Log.i(CapacitorUpdater.TAG, "Latest version is builtin");
1104
- if (CapacitorUpdaterPlugin.this.implementation.directUpdate) {
1105
- Log.i(CapacitorUpdater.TAG, "Direct update to builtin version");
1632
+ logger.info("Latest version is builtin");
1633
+ final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
1634
+ plannedDirectUpdate
1635
+ );
1636
+ if (directUpdateAllowedNow) {
1637
+ logger.info("Direct update to builtin version");
1106
1638
  this._reset(false);
1107
1639
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1108
- "Updated to builtin version",
1109
- latestVersionName,
1110
- CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1111
- false
1112
- );
1640
+ "Updated to builtin version",
1641
+ latestVersionName,
1642
+ CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1643
+ false,
1644
+ true
1645
+ );
1113
1646
  } else {
1114
- Log.i(CapacitorUpdater.TAG, "Setting next bundle to builtin");
1647
+ if (plannedDirectUpdate && !directUpdateAllowedNow) {
1648
+ logger.info(
1649
+ "Direct update skipped because splashscreen timeout occurred. Update will be applied later."
1650
+ );
1651
+ }
1652
+ logger.info("Setting next bundle to builtin");
1115
1653
  CapacitorUpdaterPlugin.this.implementation.setNextBundle(BundleInfo.ID_BUILTIN);
1116
1654
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1117
- "Next update will be to builtin version",
1118
- latestVersionName,
1119
- current,
1120
- false
1121
- );
1655
+ "Next update will be to builtin version",
1656
+ latestVersionName,
1657
+ current,
1658
+ false
1659
+ );
1122
1660
  }
1123
1661
  return;
1124
1662
  }
1125
1663
 
1126
- if (!res.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(res.getString("url"))) {
1127
- Log.e(CapacitorUpdater.TAG, "Error no url or wrong format");
1664
+ if (!jsRes.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(jsRes.getString("url"))) {
1665
+ logger.error("Error no url or wrong format");
1128
1666
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1129
- "Error no url or wrong format",
1130
- current.getVersionName(),
1131
- current,
1132
- true
1133
- );
1667
+ "Error no url or wrong format",
1668
+ current.getVersionName(),
1669
+ current,
1670
+ true,
1671
+ plannedDirectUpdate
1672
+ );
1134
1673
  return;
1135
1674
  }
1136
1675
 
@@ -1140,129 +1679,158 @@ public class CapacitorUpdaterPlugin extends Plugin {
1140
1679
  final BundleInfo latest = CapacitorUpdaterPlugin.this.implementation.getBundleInfoByName(latestVersionName);
1141
1680
  if (latest != null) {
1142
1681
  final JSObject ret = new JSObject();
1143
- ret.put("bundle", latest.toJSON());
1682
+ ret.put("bundle", mapToJSObject(latest.toJSONMap()));
1144
1683
  if (latest.isErrorStatus()) {
1145
- Log.e(CapacitorUpdater.TAG, "Latest bundle already exists, and is in error state. Aborting update.");
1684
+ logger.error("Latest bundle already exists, and is in error state. Aborting update.");
1146
1685
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1147
- "Latest bundle already exists, and is in error state. Aborting update.",
1148
- latestVersionName,
1149
- current,
1150
- true
1151
- );
1686
+ "Latest bundle already exists, and is in error state. Aborting update.",
1687
+ latestVersionName,
1688
+ current,
1689
+ true,
1690
+ plannedDirectUpdate
1691
+ );
1152
1692
  return;
1153
1693
  }
1154
1694
  if (latest.isDownloaded()) {
1155
- Log.i(
1156
- CapacitorUpdater.TAG,
1157
- "Latest bundle already exists and download is NOT required. " + messageUpdate
1695
+ logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
1696
+ final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
1697
+ plannedDirectUpdate
1158
1698
  );
1159
- if (CapacitorUpdaterPlugin.this.implementation.directUpdate) {
1160
- CapacitorUpdaterPlugin.this.implementation.set(latest);
1161
- CapacitorUpdaterPlugin.this._reload();
1162
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1163
- "Update installed",
1699
+ if (directUpdateAllowedNow) {
1700
+ String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
1701
+ ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(
1702
+ delayUpdatePreferences
1703
+ );
1704
+ if (!delayConditionList.isEmpty()) {
1705
+ logger.info("Update delayed until delay conditions met");
1706
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1707
+ "Update delayed until delay conditions met",
1164
1708
  latestVersionName,
1165
1709
  latest,
1166
- false
1710
+ false,
1711
+ plannedDirectUpdate
1167
1712
  );
1713
+ return;
1714
+ }
1715
+ CapacitorUpdaterPlugin.this.implementation.set(latest);
1716
+ CapacitorUpdaterPlugin.this._reload();
1717
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1718
+ "Update installed",
1719
+ latestVersionName,
1720
+ latest,
1721
+ false,
1722
+ true
1723
+ );
1168
1724
  } else {
1725
+ if (plannedDirectUpdate && !directUpdateAllowedNow) {
1726
+ logger.info(
1727
+ "Direct update skipped because splashscreen timeout occurred. Update will install on next background."
1728
+ );
1729
+ }
1169
1730
  CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
1170
1731
  CapacitorUpdaterPlugin.this.implementation.setNextBundle(latest.getId());
1171
1732
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1172
- "update downloaded, will install next background",
1173
- latestVersionName,
1174
- latest,
1175
- false
1176
- );
1733
+ "update downloaded, will install next background",
1734
+ latestVersionName,
1735
+ latest,
1736
+ false
1737
+ );
1177
1738
  }
1178
1739
  return;
1179
1740
  }
1180
1741
  if (latest.isDeleted()) {
1181
- Log.i(
1182
- CapacitorUpdater.TAG,
1183
- "Latest bundle already exists and will be deleted, download will overwrite it."
1184
- );
1742
+ logger.info("Latest bundle already exists and will be deleted, download will overwrite it.");
1185
1743
  try {
1186
1744
  final Boolean deleted = CapacitorUpdaterPlugin.this.implementation.delete(latest.getId(), true);
1187
1745
  if (deleted) {
1188
- Log.i(CapacitorUpdater.TAG, "Failed bundle deleted: " + latest.getVersionName());
1746
+ logger.info("Failed bundle deleted: " + latest.getVersionName());
1189
1747
  }
1190
1748
  } catch (final IOException e) {
1191
- Log.e(CapacitorUpdater.TAG, "Failed to delete failed bundle: " + latest.getVersionName(), e);
1749
+ logger.error("Failed to delete failed bundle: " + latest.getVersionName() + " " + e.getMessage());
1192
1750
  }
1193
1751
  }
1194
1752
  }
1195
1753
  startNewThread(() -> {
1196
1754
  try {
1197
- Log.i(
1198
- CapacitorUpdater.TAG,
1755
+ logger.info(
1199
1756
  "New bundle: " +
1200
- latestVersionName +
1201
- " found. Current is: " +
1202
- current.getVersionName() +
1203
- ". " +
1204
- messageUpdate
1757
+ latestVersionName +
1758
+ " found. Current is: " +
1759
+ current.getVersionName() +
1760
+ ". " +
1761
+ messageUpdate
1205
1762
  );
1206
1763
 
1207
- final String url = res.getString("url");
1208
- final String sessionKey = res.has("sessionKey") ? res.getString("sessionKey") : "";
1209
- final String checksum = res.has("checksum") ? res.getString("checksum") : "";
1764
+ final String url = jsRes.getString("url");
1765
+ final String sessionKey = jsRes.has("sessionKey") ? jsRes.getString("sessionKey") : "";
1766
+ final String checksum = jsRes.has("checksum") ? jsRes.getString("checksum") : "";
1210
1767
 
1211
- if (res.has("manifest")) {
1768
+ if (jsRes.has("manifest")) {
1212
1769
  // Handle manifest-based download
1213
- JSONArray manifest = res.getJSONArray("manifest");
1770
+ JSONArray manifest = jsRes.getJSONArray("manifest");
1214
1771
  CapacitorUpdaterPlugin.this.implementation.downloadBackground(
1215
- url,
1216
- latestVersionName,
1217
- sessionKey,
1218
- checksum,
1219
- manifest
1220
- );
1772
+ url,
1773
+ latestVersionName,
1774
+ sessionKey,
1775
+ checksum,
1776
+ manifest
1777
+ );
1221
1778
  } else {
1222
1779
  // Handle single file download (existing code)
1223
1780
  CapacitorUpdaterPlugin.this.implementation.downloadBackground(
1224
- url,
1225
- latestVersionName,
1226
- sessionKey,
1227
- checksum,
1228
- null
1229
- );
1781
+ url,
1782
+ latestVersionName,
1783
+ sessionKey,
1784
+ checksum,
1785
+ null
1786
+ );
1230
1787
  }
1231
1788
  } catch (final Exception e) {
1232
- Log.e(CapacitorUpdater.TAG, "error downloading file", e);
1789
+ logger.error("error downloading file " + e.getMessage());
1233
1790
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1234
- "Error downloading file",
1235
- latestVersionName,
1236
- CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1237
- true
1238
- );
1791
+ "Error downloading file",
1792
+ latestVersionName,
1793
+ CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1794
+ true,
1795
+ plannedDirectUpdate
1796
+ );
1239
1797
  }
1240
1798
  });
1241
1799
  } else {
1242
- Log.i(CapacitorUpdater.TAG, "No need to update, " + current.getId() + " is the latest bundle.");
1800
+ logger.info("No need to update, " + current.getId() + " is the latest bundle.");
1243
1801
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif("No need to update", latestVersionName, current, false);
1244
1802
  }
1245
1803
  } catch (final JSONException e) {
1246
- Log.e(CapacitorUpdater.TAG, "error parsing JSON", e);
1804
+ logger.error("error parsing JSON " + e.getMessage());
1247
1805
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1248
- "Error parsing JSON",
1249
- current.getVersionName(),
1250
- current,
1251
- true
1252
- );
1806
+ "Error parsing JSON",
1807
+ current.getVersionName(),
1808
+ current,
1809
+ true,
1810
+ plannedDirectUpdate
1811
+ );
1253
1812
  }
1254
1813
  });
1814
+ } catch (final Exception e) {
1815
+ logger.error("getLatest call failed: " + e.getMessage());
1816
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1817
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1818
+ "Network connection failed",
1819
+ current.getVersionName(),
1820
+ current,
1821
+ true,
1822
+ plannedDirectUpdate
1823
+ );
1824
+ }
1255
1825
  });
1256
1826
  }
1257
1827
 
1258
1828
  private void installNext() {
1259
1829
  try {
1260
- Gson gson = new Gson();
1261
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
1262
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1263
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
1264
- if (delayConditionList != null && !delayConditionList.isEmpty()) {
1265
- Log.i(CapacitorUpdater.TAG, "Update delayed until delay conditions met");
1830
+ String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
1831
+ ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(delayUpdatePreferences);
1832
+ if (!delayConditionList.isEmpty()) {
1833
+ logger.info("Update delayed until delay conditions met");
1266
1834
  return;
1267
1835
  }
1268
1836
  final BundleInfo current = this.implementation.getCurrentBundle();
@@ -1270,16 +1838,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
1270
1838
 
1271
1839
  if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
1272
1840
  // There is a next bundle waiting for activation
1273
- Log.d(CapacitorUpdater.TAG, "Next bundle is: " + next.getVersionName());
1841
+ logger.debug("Next bundle is: " + next.getVersionName());
1274
1842
  if (this.implementation.set(next) && this._reload()) {
1275
- Log.i(CapacitorUpdater.TAG, "Updated to bundle: " + next.getVersionName());
1843
+ logger.info("Updated to bundle: " + next.getVersionName());
1276
1844
  this.implementation.setNextBundle(null);
1277
1845
  } else {
1278
- Log.e(CapacitorUpdater.TAG, "Update to bundle: " + next.getVersionName() + " Failed!");
1846
+ logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
1279
1847
  }
1280
1848
  }
1281
1849
  } catch (final Exception e) {
1282
- Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1850
+ logger.error("Error during onActivityStopped " + e.getMessage());
1283
1851
  }
1284
1852
  }
1285
1853
 
@@ -1288,33 +1856,34 @@ public class CapacitorUpdaterPlugin extends Plugin {
1288
1856
  final BundleInfo current = this.implementation.getCurrentBundle();
1289
1857
 
1290
1858
  if (current.isBuiltin()) {
1291
- Log.i(CapacitorUpdater.TAG, "Built-in bundle is active. We skip the check for notifyAppReady.");
1859
+ logger.info("Built-in bundle is active. We skip the check for notifyAppReady.");
1292
1860
  return;
1293
1861
  }
1294
- Log.d(CapacitorUpdater.TAG, "Current bundle is: " + current);
1862
+ logger.debug("Current bundle is: " + current);
1295
1863
 
1296
1864
  if (BundleStatus.SUCCESS != current.getStatus()) {
1297
- Log.e(CapacitorUpdater.TAG, "notifyAppReady was not called, roll back current bundle: " + current.getId());
1298
- Log.i(CapacitorUpdater.TAG, "Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
1865
+ logger.error("notifyAppReady was not called, roll back current bundle: " + current.getId());
1866
+ logger.info("Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
1299
1867
  final JSObject ret = new JSObject();
1300
- ret.put("bundle", current.toJSON());
1868
+ ret.put("bundle", mapToJSObject(current.toJSONMap()));
1869
+ this.persistLastFailedBundle(current);
1301
1870
  this.notifyListeners("updateFailed", ret);
1302
1871
  this.implementation.sendStats("update_fail", current.getVersionName());
1303
1872
  this.implementation.setError(current);
1304
1873
  this._reset(true);
1305
1874
  if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
1306
- Log.i(CapacitorUpdater.TAG, "Deleting failing bundle: " + current.getVersionName());
1875
+ logger.info("Deleting failing bundle: " + current.getVersionName());
1307
1876
  try {
1308
1877
  final Boolean res = this.implementation.delete(current.getId(), false);
1309
1878
  if (res) {
1310
- Log.i(CapacitorUpdater.TAG, "Failed bundle deleted: " + current.getVersionName());
1879
+ logger.info("Failed bundle deleted: " + current.getVersionName());
1311
1880
  }
1312
1881
  } catch (final IOException e) {
1313
- Log.e(CapacitorUpdater.TAG, "Failed to delete failed bundle: " + current.getVersionName(), e);
1882
+ logger.error("Failed to delete failed bundle: " + current.getVersionName() + " " + e.getMessage());
1314
1883
  }
1315
1884
  }
1316
1885
  } else {
1317
- Log.i(CapacitorUpdater.TAG, "notifyAppReady was called. This is fine: " + current.getId());
1886
+ logger.info("notifyAppReady was called. This is fine: " + current.getId());
1318
1887
  }
1319
1888
  }
1320
1889
 
@@ -1323,15 +1892,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1323
1892
  @Override
1324
1893
  public void run() {
1325
1894
  try {
1326
- Log.i(
1327
- CapacitorUpdater.TAG,
1328
- "Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady"
1329
- );
1895
+ logger.info("Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady");
1330
1896
  Thread.sleep(CapacitorUpdaterPlugin.this.appReadyTimeout);
1331
1897
  CapacitorUpdaterPlugin.this.checkRevert();
1332
1898
  CapacitorUpdaterPlugin.this.appReadyCheck = null;
1333
1899
  } catch (final InterruptedException e) {
1334
- Log.i(CapacitorUpdater.TAG, DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
1900
+ logger.info(DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
1335
1901
  }
1336
1902
  }
1337
1903
  }
@@ -1339,14 +1905,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
1339
1905
  public void appMovedToForeground() {
1340
1906
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1341
1907
  CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_foreground", current.getVersionName());
1342
- this._checkCancelDelay(false);
1908
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
1909
+ this.delayUpdateUtils.unsetBackgroundTimestamp();
1910
+
1343
1911
  if (
1344
1912
  CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() &&
1345
1913
  (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive())
1346
1914
  ) {
1347
1915
  this.backgroundDownloadTask = this.backgroundDownload();
1348
1916
  } else {
1349
- Log.i(CapacitorUpdater.TAG, "Auto update is disabled");
1917
+ final CapConfig config = CapConfig.loadDefault(this.getActivity());
1918
+ String serverUrl = config.getServerUrl();
1919
+ if (serverUrl != null && !serverUrl.isEmpty()) {
1920
+ CapacitorUpdaterPlugin.this.implementation.sendStats("blocked_by_server_url", current.getVersionName());
1921
+ }
1922
+ logger.info("Auto update is disabled");
1350
1923
  this.sendReadyToJs(current, "disabled");
1351
1924
  }
1352
1925
  this.checkAppReady();
@@ -1354,40 +1927,42 @@ public class CapacitorUpdaterPlugin extends Plugin {
1354
1927
 
1355
1928
  public void appMovedToBackground() {
1356
1929
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1357
- CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_background", current.getVersionName());
1358
- Log.i(CapacitorUpdater.TAG, "Checking for pending update");
1359
- try {
1360
- Gson gson = new Gson();
1361
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
1362
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1363
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
1364
- String backgroundValue = null;
1365
- for (DelayCondition delayCondition : delayConditionList) {
1366
- if (delayCondition.getKind().toString().equals("background")) {
1367
- String value = delayCondition.getValue();
1368
- backgroundValue = (value != null && !value.isEmpty()) ? value : "0";
1369
- }
1930
+
1931
+ // Show splashscreen FIRST, before any other background work to ensure launcher shows it
1932
+ if (this.autoSplashscreen) {
1933
+ boolean canShowSplashscreen = true;
1934
+
1935
+ if (!this._isAutoUpdateEnabled()) {
1936
+ logger.warn(
1937
+ "autoSplashscreen is enabled but autoUpdate is disabled. Splashscreen will not be shown. Enable autoUpdate or disable autoSplashscreen."
1938
+ );
1939
+ canShowSplashscreen = false;
1370
1940
  }
1371
- if (backgroundValue != null) {
1372
- taskRunning = true;
1373
- final Long timeout = Long.parseLong(backgroundValue);
1374
- if (backgroundTask != null) {
1375
- backgroundTask.interrupt();
1376
- }
1377
- backgroundTask = startNewThread(
1378
- () -> {
1379
- taskRunning = false;
1380
- _checkCancelDelay(false);
1381
- installNext();
1382
- },
1383
- timeout
1941
+
1942
+ if (!this.shouldUseDirectUpdate()) {
1943
+ logger.warn(
1944
+ "autoSplashscreen is enabled but directUpdate is not configured for immediate updates. Set directUpdate to 'always' or 'atInstall', or disable autoSplashscreen."
1384
1945
  );
1385
- } else {
1386
- this._checkCancelDelay(false);
1387
- this.installNext();
1946
+ canShowSplashscreen = false;
1947
+ }
1948
+
1949
+ if (canShowSplashscreen) {
1950
+ logger.info("Showing splashscreen for launcher/task switcher");
1951
+ this.showSplashscreen();
1388
1952
  }
1953
+ }
1954
+
1955
+ // Do other background work after splashscreen is shown
1956
+ CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_background", current.getVersionName());
1957
+ logger.info("Checking for pending update");
1958
+
1959
+ try {
1960
+ // We need to set "backgrounded time"
1961
+ this.delayUpdateUtils.setBackgroundTimestamp(System.currentTimeMillis());
1962
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.BACKGROUND);
1963
+ this.installNext();
1389
1964
  } catch (final Exception e) {
1390
- Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1965
+ logger.error("Error during onActivityStopped " + e.getMessage());
1391
1966
  }
1392
1967
  }
1393
1968
 
@@ -1416,37 +1991,154 @@ public class CapacitorUpdaterPlugin extends Plugin {
1416
1991
 
1417
1992
  @Override
1418
1993
  public void handleOnStart() {
1419
- if (isPreviousMainActivity) {
1420
- this.appMovedToForeground();
1994
+ try {
1995
+ if (isPreviousMainActivity) {
1996
+ logger.info("handleOnStart: appMovedToForeground");
1997
+ this.appMovedToForeground();
1998
+ }
1999
+ logger.info("handleOnStart: onActivityStarted " + getActivity().getClass().getName());
2000
+ isPreviousMainActivity = true;
2001
+
2002
+ // Initialize shake menu if enabled and activity is BridgeActivity
2003
+ if (shakeMenuEnabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
2004
+ try {
2005
+ shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
2006
+ logger.info("Shake menu initialized");
2007
+ } catch (Exception e) {
2008
+ logger.error("Failed to initialize shake menu: " + e.getMessage());
2009
+ }
2010
+ }
2011
+ } catch (Exception e) {
2012
+ logger.error("Failed to run handleOnStart: " + e.getMessage());
1421
2013
  }
1422
- Log.i(CapacitorUpdater.TAG, "onActivityStarted " + getActivity().getClass().getName());
1423
- isPreviousMainActivity = true;
1424
2014
  }
1425
2015
 
1426
2016
  @Override
1427
2017
  public void handleOnStop() {
1428
- isPreviousMainActivity = isMainActivity();
1429
- if (isPreviousMainActivity) {
1430
- this.appMovedToBackground();
2018
+ try {
2019
+ isPreviousMainActivity = isMainActivity();
2020
+ if (isPreviousMainActivity) {
2021
+ logger.info("handleOnStop: appMovedToBackground");
2022
+ this.appMovedToBackground();
2023
+ }
2024
+ } catch (Exception e) {
2025
+ logger.error("Failed to run handleOnStop: " + e.getMessage());
1431
2026
  }
1432
2027
  }
1433
2028
 
1434
2029
  @Override
1435
2030
  public void handleOnResume() {
1436
- if (backgroundTask != null && taskRunning) {
1437
- backgroundTask.interrupt();
2031
+ try {
2032
+ if (backgroundTask != null && taskRunning) {
2033
+ backgroundTask.interrupt();
2034
+ }
2035
+ this.implementation.activity = getActivity();
2036
+ } catch (Exception e) {
2037
+ logger.error("Failed to run handleOnResume: " + e.getMessage());
1438
2038
  }
1439
- this.implementation.activity = getActivity();
1440
2039
  }
1441
2040
 
1442
2041
  @Override
1443
2042
  public void handleOnPause() {
1444
- this.implementation.activity = getActivity();
2043
+ try {
2044
+ this.implementation.activity = getActivity();
2045
+ } catch (Exception e) {
2046
+ logger.error("Failed to run handleOnPause: " + e.getMessage());
2047
+ }
1445
2048
  }
1446
2049
 
1447
2050
  @Override
1448
2051
  public void handleOnDestroy() {
1449
- Log.i(CapacitorUpdater.TAG, "onActivityDestroyed " + getActivity().getClass().getName());
1450
- this.implementation.activity = getActivity();
2052
+ try {
2053
+ logger.info("onActivityDestroyed " + getActivity().getClass().getName());
2054
+ this.implementation.activity = getActivity();
2055
+
2056
+ // Clean up shake menu
2057
+ if (shakeMenu != null) {
2058
+ try {
2059
+ shakeMenu.stop();
2060
+ shakeMenu = null;
2061
+ logger.info("Shake menu cleaned up");
2062
+ } catch (Exception e) {
2063
+ logger.error("Failed to clean up shake menu: " + e.getMessage());
2064
+ }
2065
+ }
2066
+ } catch (Exception e) {
2067
+ logger.error("Failed to run handleOnDestroy: " + e.getMessage());
2068
+ }
2069
+ }
2070
+
2071
+ @PluginMethod
2072
+ public void setShakeMenu(final PluginCall call) {
2073
+ final Boolean enabled = call.getBoolean("enabled");
2074
+ if (enabled == null) {
2075
+ logger.error("setShakeMenu called without enabled parameter");
2076
+ call.reject("setShakeMenu called without enabled parameter");
2077
+ return;
2078
+ }
2079
+
2080
+ this.shakeMenuEnabled = enabled;
2081
+ logger.info("Shake menu " + (enabled ? "enabled" : "disabled"));
2082
+
2083
+ // Manage shake menu instance based on enabled state
2084
+ if (enabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
2085
+ try {
2086
+ shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
2087
+ logger.info("Shake menu initialized");
2088
+ } catch (Exception e) {
2089
+ logger.error("Failed to initialize shake menu: " + e.getMessage());
2090
+ }
2091
+ } else if (!enabled && shakeMenu != null) {
2092
+ try {
2093
+ shakeMenu.stop();
2094
+ shakeMenu = null;
2095
+ logger.info("Shake menu stopped");
2096
+ } catch (Exception e) {
2097
+ logger.error("Failed to stop shake menu: " + e.getMessage());
2098
+ }
2099
+ }
2100
+
2101
+ call.resolve();
2102
+ }
2103
+
2104
+ @PluginMethod
2105
+ public void isShakeMenuEnabled(final PluginCall call) {
2106
+ try {
2107
+ final JSObject ret = new JSObject();
2108
+ ret.put("enabled", this.shakeMenuEnabled);
2109
+ call.resolve(ret);
2110
+ } catch (final Exception e) {
2111
+ logger.error("Could not get shake menu status " + e.getMessage());
2112
+ call.reject("Could not get shake menu status", e);
2113
+ }
2114
+ }
2115
+
2116
+ @PluginMethod
2117
+ public void getAppId(final PluginCall call) {
2118
+ try {
2119
+ final JSObject ret = new JSObject();
2120
+ ret.put("appId", this.implementation.appId);
2121
+ call.resolve(ret);
2122
+ } catch (final Exception e) {
2123
+ logger.error("Could not get appId " + e.getMessage());
2124
+ call.reject("Could not get appId", e);
2125
+ }
2126
+ }
2127
+
2128
+ @PluginMethod
2129
+ public void setAppId(final PluginCall call) {
2130
+ if (!this.getConfig().getBoolean("allowModifyAppId", false)) {
2131
+ logger.error("setAppId not allowed set allowModifyAppId in your config to true to allow it");
2132
+ call.reject("setAppId not allowed");
2133
+ return;
2134
+ }
2135
+ final String appId = call.getString("appId");
2136
+ if (appId == null) {
2137
+ logger.error("setAppId called without appId");
2138
+ call.reject("setAppId called without appId");
2139
+ return;
2140
+ }
2141
+ this.implementation.appId = appId;
2142
+ call.resolve();
1451
2143
  }
1452
2144
  }