@capgo/capacitor-updater 6.14.25 → 6.14.29

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