@capgo/capacitor-updater 6.14.26 → 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 -508
  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 -1030
  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,24 +50,35 @@ import java.util.concurrent.Semaphore;
45
50
  import java.util.concurrent.TimeUnit;
46
51
  import java.util.concurrent.TimeoutException;
47
52
  import java.util.concurrent.atomic.AtomicReference;
48
- import okhttp3.OkHttpClient;
49
- import okhttp3.Protocol;
53
+ // Removed OkHttpClient and Protocol imports - using shared client in DownloadService instead
50
54
  import org.json.JSONArray;
51
55
  import org.json.JSONException;
56
+ import org.json.JSONObject;
52
57
 
53
58
  @CapacitorPlugin(name = "CapacitorUpdater")
54
59
  public class CapacitorUpdaterPlugin extends Plugin {
55
60
 
61
+ private final Logger logger = new Logger("CapgoUpdater");
62
+
56
63
  private static final String updateUrlDefault = "https://plugin.capgo.app/updates";
57
64
  private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
58
65
  private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
59
-
60
- private final String PLUGIN_VERSION = "6.14.26";
66
+ private static final String KEEP_URL_FLAG_KEY = "__capgo_keep_url_path_after_reload";
67
+ private static final String CUSTOM_ID_PREF_KEY = "CapacitorUpdater.customId";
68
+ private static final String UPDATE_URL_PREF_KEY = "CapacitorUpdater.updateUrl";
69
+ private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
70
+ private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
71
+ private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
72
+ private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
73
+
74
+ private final String PLUGIN_VERSION = "6.14.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
84
  private Integer periodCheckDelay = 0;
@@ -71,9 +87,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
71
87
  private Boolean autoUpdate = false;
72
88
  private String updateUrl = "";
73
89
  private Version currentVersionNative;
90
+ private String currentBuildVersion;
74
91
  private Thread backgroundTask;
75
92
  private Boolean taskRunning = false;
76
93
  private Boolean keepUrlPathAfterReload = false;
94
+ private Boolean autoSplashscreen = false;
95
+ private Boolean autoSplashscreenLoader = false;
96
+ private Integer autoSplashscreenTimeout = 10000;
97
+ private Boolean autoSplashscreenTimedOut = false;
98
+ private String directUpdateMode = "false";
99
+ private Boolean wasRecentlyInstalledOrUpdated = false;
100
+ Boolean shakeMenuEnabled = false;
101
+ private Boolean allowManualBundleError = false;
77
102
 
78
103
  private Boolean isPreviousMainActivity = true;
79
104
 
@@ -85,6 +110,63 @@ public class CapacitorUpdaterPlugin extends Plugin {
85
110
 
86
111
  private int lastNotifiedStatPercent = 0;
87
112
 
113
+ private DelayUpdateUtils delayUpdateUtils;
114
+
115
+ private ShakeMenu shakeMenu;
116
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
117
+ private FrameLayout splashscreenLoaderOverlay;
118
+ private Runnable splashscreenTimeoutRunnable;
119
+
120
+ private void notifyBreakingEvents(final String version) {
121
+ if (version == null || version.isEmpty()) {
122
+ return;
123
+ }
124
+ for (final String eventName : BREAKING_EVENT_NAMES) {
125
+ final JSObject payload = new JSObject();
126
+ payload.put("version", version);
127
+ CapacitorUpdaterPlugin.this.notifyListeners(eventName, payload);
128
+ }
129
+ }
130
+
131
+ private JSObject mapToJSObject(Map<String, Object> map) {
132
+ JSObject jsObject = new JSObject();
133
+ for (Map.Entry<String, Object> entry : map.entrySet()) {
134
+ jsObject.put(entry.getKey(), entry.getValue());
135
+ }
136
+ return jsObject;
137
+ }
138
+
139
+ private void persistLastFailedBundle(BundleInfo bundle) {
140
+ if (this.prefs == null) {
141
+ return;
142
+ }
143
+ final SharedPreferences.Editor localEditor = this.prefs.edit();
144
+ if (bundle == null) {
145
+ localEditor.remove(LAST_FAILED_BUNDLE_PREF_KEY);
146
+ } else {
147
+ final JSONObject json = new JSONObject(bundle.toJSONMap());
148
+ localEditor.putString(LAST_FAILED_BUNDLE_PREF_KEY, json.toString());
149
+ }
150
+ localEditor.apply();
151
+ }
152
+
153
+ private BundleInfo readLastFailedBundle() {
154
+ if (this.prefs == null) {
155
+ return null;
156
+ }
157
+ final String raw = this.prefs.getString(LAST_FAILED_BUNDLE_PREF_KEY, null);
158
+ if (raw == null || raw.trim().isEmpty()) {
159
+ return null;
160
+ }
161
+ try {
162
+ return BundleInfo.fromJSON(raw);
163
+ } catch (final JSONException e) {
164
+ logger.error("Failed to parse failed bundle info: " + e.getMessage());
165
+ this.persistLastFailedBundle(null);
166
+ return null;
167
+ }
168
+ }
169
+
88
170
  public Thread startNewThread(final Runnable function, Number waitTime) {
89
171
  Thread bgTask = new Thread(() -> {
90
172
  try {
@@ -111,54 +193,90 @@ public class CapacitorUpdaterPlugin extends Plugin {
111
193
  this.editor = this.prefs.edit();
112
194
 
113
195
  try {
114
- this.implementation = new CapacitorUpdater() {
196
+ this.implementation = new CapgoUpdater(logger) {
115
197
  @Override
116
198
  public void notifyDownload(final String id, final int percent) {
117
- CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
199
+ activity.runOnUiThread(() -> {
200
+ CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
201
+ });
118
202
  }
119
203
 
120
204
  @Override
121
205
  public void directUpdateFinish(final BundleInfo latest) {
122
- CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
206
+ activity.runOnUiThread(() -> {
207
+ CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
208
+ });
123
209
  }
124
210
 
125
211
  @Override
126
- public void notifyListeners(final String id, final JSObject res) {
127
- CapacitorUpdaterPlugin.this.notifyListeners(id, res);
212
+ public void notifyListeners(final String id, final Map<String, Object> res) {
213
+ activity.runOnUiThread(() -> {
214
+ CapacitorUpdaterPlugin.this.notifyListeners(id, CapacitorUpdaterPlugin.this.mapToJSObject(res));
215
+ });
128
216
  }
129
217
  };
130
218
  final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
131
219
  this.implementation.activity = this.getActivity();
132
220
  this.implementation.versionBuild = this.getConfig().getString("version", pInfo.versionName);
221
+ this.implementation.CAP_SERVER_PATH = WebView.CAP_SERVER_PATH;
133
222
  this.implementation.PLUGIN_VERSION = this.PLUGIN_VERSION;
134
223
  this.implementation.versionCode = Integer.toString(pInfo.versionCode);
135
- this.implementation.client = new OkHttpClient.Builder()
136
- .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
137
- .connectTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
138
- .readTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
139
- .writeTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
140
- .build();
141
-
142
- this.implementation.directUpdate = this.getConfig().getBoolean("directUpdate", false);
224
+ // Removed unused OkHttpClient creation - using shared client in DownloadService instead
225
+ // Handle directUpdate configuration - support string values and backward compatibility
226
+ String directUpdateConfig = this.getConfig().getString("directUpdate", null);
227
+ if (directUpdateConfig != null) {
228
+ this.directUpdateMode = directUpdateConfig;
229
+ this.implementation.directUpdate = directUpdateConfig.equals("always") || directUpdateConfig.equals("atInstall");
230
+ } else {
231
+ Boolean directUpdateBool = this.getConfig().getBoolean("directUpdate", false);
232
+ if (directUpdateBool) {
233
+ this.directUpdateMode = "always"; // backward compatibility: true = always
234
+ this.implementation.directUpdate = true;
235
+ } else {
236
+ this.directUpdateMode = "false";
237
+ this.implementation.directUpdate = false;
238
+ }
239
+ }
143
240
  this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
241
+ this.currentBuildVersion = Integer.toString(pInfo.versionCode);
242
+ this.delayUpdateUtils = new DelayUpdateUtils(this.prefs, this.editor, this.currentVersionNative, logger);
144
243
  } catch (final PackageManager.NameNotFoundException e) {
145
- Log.e(CapacitorUpdater.TAG, "Error instantiating implementation", e);
244
+ logger.error("Error instantiating implementation " + e.getMessage());
146
245
  return;
147
246
  } catch (final Exception e) {
148
- Log.e(CapacitorUpdater.TAG, "Error getting current native app version", e);
247
+ logger.error("Error getting current native app version " + e.getMessage());
149
248
  return;
150
249
  }
250
+
251
+ boolean disableJSLogging = this.getConfig().getBoolean("disableJSLogging", false);
252
+ // Set the bridge in the Logger when webView is available
253
+ if (this.bridge != null && this.bridge.getWebView() != null && !disableJSLogging) {
254
+ logger.setBridge(this.bridge);
255
+ logger.info("WebView set successfully for logging");
256
+ } else {
257
+ logger.info("WebView not ready yet, will be set later");
258
+ }
259
+
260
+ // Set logger for shared classes
261
+ CryptoCipherV1.setLogger(logger);
262
+ CryptoCipherV2.setLogger(logger);
263
+ DownloadService.setLogger(logger);
264
+ DownloadWorkerManager.setLogger(logger);
265
+
151
266
  final CapConfig config = CapConfig.loadDefault(this.getActivity());
152
267
  this.implementation.appId = InternalUtils.getPackageName(getContext().getPackageManager(), getContext().getPackageName());
153
268
  this.implementation.appId = config.getString("appId", this.implementation.appId);
154
269
  this.implementation.appId = this.getConfig().getString("appId", this.implementation.appId);
155
270
  if (this.implementation.appId == null || this.implementation.appId.isEmpty()) {
156
- // crash the app
271
+ // crash the app on purpose it should not happen
157
272
  throw new RuntimeException(
158
273
  "appId is missing in capacitor.config.json or plugin config, and cannot be retrieved from the native app, please add it globally or in the plugin config"
159
274
  );
160
275
  }
161
- Log.i(CapacitorUpdater.TAG, "appId: " + implementation.appId);
276
+ logger.info("appId: " + implementation.appId);
277
+
278
+ this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
279
+ this.persistModifyUrl = this.getConfig().getBoolean("persistModifyUrl", false);
162
280
  this.implementation.publicKey = this.getConfig().getString("publicKey", "");
163
281
  this.implementation.privateKey = this.getConfig().getString("privateKey", "");
164
282
  if (this.implementation.privateKey != null && !this.implementation.privateKey.isEmpty()) {
@@ -166,6 +284,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
166
284
  }
167
285
  this.implementation.statsUrl = this.getConfig().getString("statsUrl", statsUrlDefault);
168
286
  this.implementation.channelUrl = this.getConfig().getString("channelUrl", channelUrlDefault);
287
+ if (Boolean.TRUE.equals(this.persistModifyUrl)) {
288
+ if (this.prefs.contains(STATS_URL_PREF_KEY)) {
289
+ final String storedStatsUrl = this.prefs.getString(STATS_URL_PREF_KEY, this.implementation.statsUrl);
290
+ if (storedStatsUrl != null) {
291
+ this.implementation.statsUrl = storedStatsUrl;
292
+ logger.info("Loaded persisted statsUrl");
293
+ }
294
+ }
295
+ if (this.prefs.contains(CHANNEL_URL_PREF_KEY)) {
296
+ final String storedChannelUrl = this.prefs.getString(CHANNEL_URL_PREF_KEY, this.implementation.channelUrl);
297
+ if (storedChannelUrl != null) {
298
+ this.implementation.channelUrl = storedChannelUrl;
299
+ logger.info("Loaded persisted channelUrl");
300
+ }
301
+ }
302
+ }
169
303
  int userValue = this.getConfig().getInt("periodCheckDelay", 0);
170
304
  this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
171
305
 
@@ -181,103 +315,364 @@ public class CapacitorUpdaterPlugin extends Plugin {
181
315
  this.implementation.versionOs = Build.VERSION.RELEASE;
182
316
  this.implementation.deviceID = this.prefs.getString("appUUID", UUID.randomUUID().toString()).toLowerCase();
183
317
  this.editor.putString("appUUID", this.implementation.deviceID);
184
- this.editor.commit();
185
- Log.i(CapacitorUpdater.TAG, "init for device " + this.implementation.deviceID);
186
- Log.i(CapacitorUpdater.TAG, "version native " + this.currentVersionNative.getOriginalString());
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());
187
332
  this.autoDeleteFailed = this.getConfig().getBoolean("autoDeleteFailed", true);
188
333
  this.autoDeletePrevious = this.getConfig().getBoolean("autoDeletePrevious", true);
189
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
+ }
190
344
  this.autoUpdate = this.getConfig().getBoolean("autoUpdate", true);
191
345
  this.appReadyTimeout = this.getConfig().getInt("appReadyTimeout", 10000);
192
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);
193
353
  this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
354
+ this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
194
355
  boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
195
356
 
357
+ // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
358
+ this.wasRecentlyInstalledOrUpdated = this.checkIfRecentlyInstalledOrUpdated();
359
+
196
360
  this.implementation.autoReset();
197
361
  if (resetWhenUpdate) {
198
362
  this.cleanupObsoleteVersions();
199
363
  }
200
364
 
201
- // Check for 'kill' delay condition on app launch
202
- // This handles cases where the app was killed by the system (onDestroy is not reliable)
203
- this._checkCancelDelay(true);
204
-
205
365
  this.checkForUpdateAfterDelay();
206
366
  }
207
367
 
208
368
  private void semaphoreWait(Number waitTime) {
209
- Log.i(CapacitorUpdater.TAG, "semaphoreWait " + waitTime);
210
369
  try {
211
- // Log.i(CapacitorUpdater.TAG, "semaphoreReady count " + CapacitorUpdaterPlugin.this.semaphoreReady.getCount());
212
370
  semaphoreReady.awaitAdvanceInterruptibly(semaphoreReady.getPhase(), waitTime.longValue(), TimeUnit.SECONDS);
213
- // Log.i(CapacitorUpdater.TAG, "semaphoreReady await " + res);
214
- Log.i(CapacitorUpdater.TAG, "semaphoreReady count " + semaphoreReady.getPhase());
371
+ logger.info("semaphoreReady count " + semaphoreReady.getPhase());
215
372
  } catch (InterruptedException e) {
216
- Log.i(CapacitorUpdater.TAG, "semaphoreWait InterruptedException");
217
- e.printStackTrace();
373
+ logger.info("semaphoreWait InterruptedException");
374
+ Thread.currentThread().interrupt(); // Restore interrupted status
218
375
  } catch (TimeoutException e) {
219
- throw new RuntimeException(e);
376
+ logger.error("Semaphore timeout: " + e.getMessage());
377
+ // Don't throw runtime exception, just log and continue
220
378
  }
221
379
  }
222
380
 
223
381
  private void semaphoreUp() {
224
- Log.i(CapacitorUpdater.TAG, "semaphoreUp");
382
+ logger.info("semaphoreUp");
225
383
  semaphoreReady.register();
226
384
  }
227
385
 
228
386
  private void semaphoreDown() {
229
- Log.i(CapacitorUpdater.TAG, "semaphoreDown");
230
- Log.i(CapacitorUpdater.TAG, "semaphoreDown count " + semaphoreReady.getPhase());
387
+ logger.info("semaphoreDown");
388
+ logger.info("semaphoreDown count " + semaphoreReady.getPhase());
231
389
  semaphoreReady.arriveAndDeregister();
232
390
  }
233
391
 
234
392
  private void sendReadyToJs(final BundleInfo current, final String msg) {
235
- 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);
236
398
  final JSObject ret = new JSObject();
237
- ret.put("bundle", current.toJSON());
399
+ ret.put("bundle", mapToJSObject(current.toJSONMap()));
238
400
  ret.put("status", msg);
239
- startNewThread(() -> {
240
- Log.i(CapacitorUpdater.TAG, "semaphoreReady sendReadyToJs");
241
- semaphoreWait(CapacitorUpdaterPlugin.this.appReadyTimeout);
242
- Log.i(CapacitorUpdater.TAG, "semaphoreReady sendReadyToJs done");
243
- CapacitorUpdaterPlugin.this.notifyListeners("appReady", ret);
244
- });
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);
245
637
  }
246
638
 
247
639
  private void directUpdateFinish(final BundleInfo latest) {
248
640
  CapacitorUpdaterPlugin.this.implementation.set(latest);
249
641
  CapacitorUpdaterPlugin.this._reload();
250
- sendReadyToJs(latest, "update installed");
642
+ sendReadyToJs(latest, "update installed", true);
251
643
  }
252
644
 
253
645
  private void cleanupObsoleteVersions() {
254
- try {
255
- final Version previous = new Version(this.prefs.getString("LatestVersionNative", ""));
646
+ startNewThread(() -> {
256
647
  try {
257
- if (
258
- !"".equals(previous.getOriginalString()) &&
259
- !Objects.equals(this.currentVersionNative.getOriginalString(), previous.getOriginalString())
260
- ) {
261
- Log.i(CapacitorUpdater.TAG, "New native version detected: " + this.currentVersionNative);
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);
262
651
  this.implementation.reset(true);
263
652
  final List<BundleInfo> installed = this.implementation.list(false);
264
653
  for (final BundleInfo bundle : installed) {
265
654
  try {
266
- Log.i(CapacitorUpdater.TAG, "Deleting obsolete bundle: " + bundle.getId());
655
+ logger.info("Deleting obsolete bundle: " + bundle.getId());
267
656
  this.implementation.delete(bundle.getId());
268
657
  } catch (final Exception e) {
269
- 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());
270
666
  }
271
667
  }
668
+ this.implementation.cleanupDownloadDirectories(allowedIds);
272
669
  }
273
- } catch (final Exception e) {
274
- 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());
275
674
  }
276
- } catch (final Exception e) {
277
- Log.e(CapacitorUpdater.TAG, "Error calculating previous native version", e);
278
- }
279
- this.editor.putString("LatestVersionNative", this.currentVersionNative.toString());
280
- this.editor.commit();
675
+ });
281
676
  }
282
677
 
283
678
  public void notifyDownload(final String id, final int percent) {
@@ -285,7 +680,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
285
680
  final JSObject ret = new JSObject();
286
681
  ret.put("percent", percent);
287
682
  final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
288
- ret.put("bundle", bundleInfo.toJSON());
683
+ ret.put("bundle", mapToJSObject(bundleInfo.toJSONMap()));
289
684
  this.notifyListeners("download", ret);
290
685
 
291
686
  if (percent == 100) {
@@ -301,58 +696,70 @@ public class CapacitorUpdaterPlugin extends Plugin {
301
696
  }
302
697
  }
303
698
  } catch (final Exception e) {
304
- Log.e(CapacitorUpdater.TAG, "Could not notify listeners", e);
699
+ logger.error("Could not notify listeners " + e.getMessage());
305
700
  }
306
701
  }
307
702
 
308
703
  @PluginMethod
309
704
  public void setUpdateUrl(final PluginCall call) {
310
705
  if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
311
- 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");
312
707
  call.reject("setUpdateUrl not allowed");
313
708
  return;
314
709
  }
315
710
  final String url = call.getString("url");
316
711
  if (url == null) {
317
- Log.e(CapacitorUpdater.TAG, "setUpdateUrl called without url");
712
+ logger.error("setUpdateUrl called without url");
318
713
  call.reject("setUpdateUrl called without url");
319
714
  return;
320
715
  }
321
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
+ }
322
721
  call.resolve();
323
722
  }
324
723
 
325
724
  @PluginMethod
326
725
  public void setStatsUrl(final PluginCall call) {
327
726
  if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
328
- 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");
329
728
  call.reject("setStatsUrl not allowed");
330
729
  return;
331
730
  }
332
731
  final String url = call.getString("url");
333
732
  if (url == null) {
334
- Log.e(CapacitorUpdater.TAG, "setStatsUrl called without url");
733
+ logger.error("setStatsUrl called without url");
335
734
  call.reject("setStatsUrl called without url");
336
735
  return;
337
736
  }
338
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
+ }
339
742
  call.resolve();
340
743
  }
341
744
 
342
745
  @PluginMethod
343
746
  public void setChannelUrl(final PluginCall call) {
344
747
  if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
345
- 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");
346
749
  call.reject("setChannelUrl not allowed");
347
750
  return;
348
751
  }
349
752
  final String url = call.getString("url");
350
753
  if (url == null) {
351
- Log.e(CapacitorUpdater.TAG, "setChannelUrl called without url");
754
+ logger.error("setChannelUrl called without url");
352
755
  call.reject("setChannelUrl called without url");
353
756
  return;
354
757
  }
355
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
+ }
356
763
  call.resolve();
357
764
  }
358
765
 
@@ -363,7 +770,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
363
770
  ret.put("version", this.implementation.versionBuild);
364
771
  call.resolve(ret);
365
772
  } catch (final Exception e) {
366
- Log.e(CapacitorUpdater.TAG, "Could not get version", e);
773
+ logger.error("Could not get version " + e.getMessage());
367
774
  call.reject("Could not get version", e);
368
775
  }
369
776
  }
@@ -375,7 +782,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
375
782
  ret.put("deviceId", this.implementation.deviceID);
376
783
  call.resolve(ret);
377
784
  } catch (final Exception e) {
378
- Log.e(CapacitorUpdater.TAG, "Could not get device id", e);
785
+ logger.error("Could not get device id " + e.getMessage());
379
786
  call.reject("Could not get device id", e);
380
787
  }
381
788
  }
@@ -384,11 +791,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
384
791
  public void setCustomId(final PluginCall call) {
385
792
  final String customId = call.getString("customId");
386
793
  if (customId == null) {
387
- Log.e(CapacitorUpdater.TAG, "setCustomId called without customId");
794
+ logger.error("setCustomId called without customId");
388
795
  call.reject("setCustomId called without customId");
389
796
  return;
390
797
  }
391
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();
392
808
  }
393
809
 
394
810
  @PluginMethod
@@ -398,7 +814,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
398
814
  ret.put("version", this.PLUGIN_VERSION);
399
815
  call.resolve(ret);
400
816
  } catch (final Exception e) {
401
- Log.e(CapacitorUpdater.TAG, "Could not get plugin version", e);
817
+ logger.error("Could not get plugin version " + e.getMessage());
402
818
  call.reject("Could not get plugin version", e);
403
819
  }
404
820
  }
@@ -408,22 +824,30 @@ public class CapacitorUpdaterPlugin extends Plugin {
408
824
  final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
409
825
 
410
826
  try {
411
- Log.i(CapacitorUpdater.TAG, "unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
827
+ logger.info("unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
412
828
  startNewThread(() ->
413
- CapacitorUpdaterPlugin.this.implementation.unsetChannel(res -> {
414
- if (res.has("error")) {
415
- call.reject(res.getString("error"));
416
- } else {
417
- if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
418
- Log.i(CapacitorUpdater.TAG, "Calling autoupdater after channel change!");
419
- backgroundDownload();
420
- }
421
- call.resolve(res);
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();
422
844
  }
423
- })
845
+ call.resolve(jsRes);
846
+ }
847
+ })
424
848
  );
425
849
  } catch (final Exception e) {
426
- Log.e(CapacitorUpdater.TAG, "Failed to unsetChannel: ", e);
850
+ logger.error("Failed to unsetChannel: " + e.getMessage());
427
851
  call.reject("Failed to unsetChannel: ", e);
428
852
  }
429
853
  }
@@ -434,27 +858,38 @@ public class CapacitorUpdaterPlugin extends Plugin {
434
858
  final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
435
859
 
436
860
  if (channel == null) {
437
- Log.e(CapacitorUpdater.TAG, "setChannel called without channel");
438
- 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);
439
866
  return;
440
867
  }
441
868
  try {
442
- Log.i(CapacitorUpdater.TAG, "setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
869
+ logger.info("setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
443
870
  startNewThread(() ->
444
- CapacitorUpdaterPlugin.this.implementation.setChannel(channel, res -> {
445
- if (res.has("error")) {
446
- call.reject(res.getString("error"));
447
- } else {
448
- if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
449
- Log.i(CapacitorUpdater.TAG, "Calling autoupdater after channel change!");
450
- backgroundDownload();
451
- }
452
- call.resolve(res);
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();
453
886
  }
454
- })
887
+ call.resolve(jsRes);
888
+ }
889
+ })
455
890
  );
456
891
  } catch (final Exception e) {
457
- Log.e(CapacitorUpdater.TAG, "Failed to setChannel: " + channel, e);
892
+ logger.error("Failed to setChannel: " + channel + " " + e.getMessage());
458
893
  call.reject("Failed to setChannel: " + channel, e);
459
894
  }
460
895
  }
@@ -462,50 +897,98 @@ public class CapacitorUpdaterPlugin extends Plugin {
462
897
  @PluginMethod
463
898
  public void getChannel(final PluginCall call) {
464
899
  try {
465
- Log.i(CapacitorUpdater.TAG, "getChannel");
900
+ logger.info("getChannel");
466
901
  startNewThread(() ->
467
- CapacitorUpdaterPlugin.this.implementation.getChannel(res -> {
468
- if (res.has("error")) {
469
- call.reject(res.getString("error"));
470
- } else {
471
- call.resolve(res);
472
- }
473
- })
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
+ })
474
917
  );
475
918
  } catch (final Exception e) {
476
- Log.e(CapacitorUpdater.TAG, "Failed to getChannel", e);
919
+ logger.error("Failed to getChannel " + e.getMessage());
477
920
  call.reject("Failed to getChannel", e);
478
921
  }
479
922
  }
480
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
+
481
951
  @PluginMethod
482
952
  public void download(final PluginCall call) {
483
953
  final String url = call.getString("url");
484
954
  final String version = call.getString("version");
485
955
  final String sessionKey = call.getString("sessionKey", "");
486
956
  final String checksum = call.getString("checksum", "");
957
+ final JSONArray manifest = call.getData().optJSONArray("manifest");
487
958
  if (url == null) {
488
- Log.e(CapacitorUpdater.TAG, "Download called without url");
959
+ logger.error("Download called without url");
489
960
  call.reject("Download called without url");
490
961
  return;
491
962
  }
492
963
  if (version == null) {
493
- Log.e(CapacitorUpdater.TAG, "Download called without version");
964
+ logger.error("Download called without version");
494
965
  call.reject("Download called without version");
495
966
  return;
496
967
  }
497
968
  try {
498
- Log.i(CapacitorUpdater.TAG, "Downloading " + url);
969
+ logger.info("Downloading " + url);
499
970
  startNewThread(() -> {
500
971
  try {
501
- 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
+ }
502
985
  if (downloaded.isErrorStatus()) {
503
986
  throw new RuntimeException("Download failed: " + downloaded.getStatus());
504
987
  } else {
505
- call.resolve(downloaded.toJSON());
988
+ call.resolve(mapToJSObject(downloaded.toJSONMap()));
506
989
  }
507
990
  } catch (final Exception e) {
508
- Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
991
+ logger.error("Failed to download from: " + url + " " + e.getMessage());
509
992
  call.reject("Failed to download from: " + url, e);
510
993
  final JSObject ret = new JSObject();
511
994
  ret.put("version", version);
@@ -515,7 +998,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
515
998
  }
516
999
  });
517
1000
  } catch (final Exception e) {
518
- Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
1001
+ logger.error("Failed to download from: " + url + " " + e.getMessage());
519
1002
  call.reject("Failed to download from: " + url, e);
520
1003
  final JSObject ret = new JSObject();
521
1004
  ret.put("version", version);
@@ -525,10 +1008,27 @@ public class CapacitorUpdaterPlugin extends Plugin {
525
1008
  }
526
1009
  }
527
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
+
528
1025
  protected boolean _reload() {
529
1026
  final String path = this.implementation.getCurrentBundlePath();
1027
+ if (this.keepUrlPathAfterReload) {
1028
+ this.syncKeepUrlPathFlag(true);
1029
+ }
530
1030
  this.semaphoreUp();
531
- Log.i(CapacitorUpdater.TAG, "Reloading: " + path);
1031
+ logger.info("Reloading: " + path);
532
1032
 
533
1033
  AtomicReference<URL> url = new AtomicReference<>();
534
1034
  if (this.keepUrlPathAfterReload) {
@@ -536,23 +1036,38 @@ public class CapacitorUpdaterPlugin extends Plugin {
536
1036
  if (Looper.myLooper() != Looper.getMainLooper()) {
537
1037
  Semaphore mainThreadSemaphore = new Semaphore(0);
538
1038
  this.bridge.executeOnMainThread(() -> {
539
- try {
540
- url.set(new URL(this.bridge.getWebView().getUrl()));
541
- } catch (Exception e) {
542
- Log.e(CapacitorUpdater.TAG, "Error executing on main thread", e);
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
+ }
543
1045
  }
544
- mainThreadSemaphore.release();
545
- });
546
- 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
+ }
547
1056
  } else {
548
1057
  try {
549
- 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
+ }
550
1064
  } catch (Exception e) {
551
- Log.e(CapacitorUpdater.TAG, "Error executing on main thread", e);
1065
+ logger.error("Error executing on main thread " + e.getMessage());
552
1066
  }
553
1067
  }
554
1068
  } catch (InterruptedException e) {
555
- 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
556
1071
  }
557
1072
  }
558
1073
 
@@ -568,13 +1083,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
568
1083
  finalUrl = new URL(this.bridge.getAppUrl());
569
1084
  finalUrl = new URL(finalUrl.getProtocol(), finalUrl.getHost(), finalUrl.getPort(), url.get().getPath());
570
1085
  URL finalUrl1 = finalUrl;
571
- this.bridge.getWebView()
572
- .post(() -> {
573
- this.bridge.getWebView().loadUrl(finalUrl1.toString());
1086
+ this.bridge.getWebView().post(() -> {
1087
+ this.bridge.getWebView().loadUrl(finalUrl1.toString());
1088
+ if (!this.keepUrlPathAfterReload) {
574
1089
  this.bridge.getWebView().clearHistory();
575
- });
1090
+ }
1091
+ });
576
1092
  } catch (MalformedURLException e) {
577
- Log.e(CapacitorUpdater.TAG, "Cannot get finalUrl from capacitor bridge", e);
1093
+ logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
578
1094
 
579
1095
  if (this.implementation.isUsingBuiltin()) {
580
1096
  this.bridge.setServerAssetPath(path);
@@ -588,10 +1104,29 @@ public class CapacitorUpdaterPlugin extends Plugin {
588
1104
  } else {
589
1105
  this.bridge.setServerBasePath(path);
590
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
+ }
591
1117
  }
592
1118
 
593
1119
  this.checkAppReady();
594
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
+
595
1130
  return true;
596
1131
  }
597
1132
 
@@ -601,11 +1136,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
601
1136
  if (this._reload()) {
602
1137
  call.resolve();
603
1138
  } else {
604
- Log.e(CapacitorUpdater.TAG, "Reload failed");
1139
+ logger.error("Reload failed");
605
1140
  call.reject("Reload failed");
606
1141
  }
607
1142
  } catch (final Exception e) {
608
- Log.e(CapacitorUpdater.TAG, "Could not reload", e);
1143
+ logger.error("Could not reload " + e.getMessage());
609
1144
  call.reject("Could not reload", e);
610
1145
  }
611
1146
  }
@@ -614,20 +1149,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
614
1149
  public void next(final PluginCall call) {
615
1150
  final String id = call.getString("id");
616
1151
  if (id == null) {
617
- Log.e(CapacitorUpdater.TAG, "Next called without id");
1152
+ logger.error("Next called without id");
618
1153
  call.reject("Next called without id");
619
1154
  return;
620
1155
  }
621
1156
  try {
622
- Log.i(CapacitorUpdater.TAG, "Setting next active id " + id);
1157
+ logger.info("Setting next active id " + id);
623
1158
  if (!this.implementation.setNextBundle(id)) {
624
- 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.");
625
1160
  call.reject("Set next id failed. Bundle " + id + " does not exist.");
626
1161
  } else {
627
- call.resolve(this.implementation.getBundleInfo(id).toJSON());
1162
+ call.resolve(mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
628
1163
  }
629
1164
  } catch (final Exception e) {
630
- Log.e(CapacitorUpdater.TAG, "Could not set next id " + id, e);
1165
+ logger.error("Could not set next id " + id + " " + e.getMessage());
631
1166
  call.reject("Could not set next id: " + id, e);
632
1167
  }
633
1168
  }
@@ -636,21 +1171,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
636
1171
  public void set(final PluginCall call) {
637
1172
  final String id = call.getString("id");
638
1173
  if (id == null) {
639
- Log.e(CapacitorUpdater.TAG, "Set called without id");
1174
+ logger.error("Set called without id");
640
1175
  call.reject("Set called without id");
641
1176
  return;
642
1177
  }
643
1178
  try {
644
- Log.i(CapacitorUpdater.TAG, "Setting active bundle " + id);
1179
+ logger.info("Setting active bundle " + id);
645
1180
  if (!this.implementation.set(id)) {
646
- Log.i(CapacitorUpdater.TAG, "No such bundle " + id);
1181
+ logger.info("No such bundle " + id);
647
1182
  call.reject("Update failed, id " + id + " does not exist.");
648
1183
  } else {
649
- Log.i(CapacitorUpdater.TAG, "Bundle successfully set to " + id);
1184
+ logger.info("Bundle successfully set to " + id);
650
1185
  this.reload(call);
651
1186
  }
652
1187
  } catch (final Exception e) {
653
- Log.e(CapacitorUpdater.TAG, "Could not set id " + id, e);
1188
+ logger.error("Could not set id " + id + " " + e.getMessage());
654
1189
  call.reject("Could not set id " + id, e);
655
1190
  }
656
1191
  }
@@ -659,25 +1194,63 @@ public class CapacitorUpdaterPlugin extends Plugin {
659
1194
  public void delete(final PluginCall call) {
660
1195
  final String id = call.getString("id");
661
1196
  if (id == null) {
662
- Log.e(CapacitorUpdater.TAG, "missing id");
1197
+ logger.error("missing id");
663
1198
  call.reject("missing id");
664
1199
  return;
665
1200
  }
666
- Log.i(CapacitorUpdater.TAG, "Deleting id " + id);
1201
+ logger.info("Deleting id " + id);
667
1202
  try {
668
1203
  final Boolean res = this.implementation.delete(id);
669
1204
  if (res) {
670
1205
  call.resolve();
671
1206
  } else {
672
- Log.e(CapacitorUpdater.TAG, "Delete failed, id " + id + " does not exist");
1207
+ logger.error("Delete failed, id " + id + " does not exist");
673
1208
  call.reject("Delete failed, id " + id + " does not exist or it cannot be deleted (perhaps it is the 'next' bundle)");
674
1209
  }
675
1210
  } catch (final Exception e) {
676
- Log.e(CapacitorUpdater.TAG, "Could not delete id " + id, e);
1211
+ logger.error("Could not delete id " + id + " " + e.getMessage());
677
1212
  call.reject("Could not delete id " + id, e);
678
1213
  }
679
1214
  }
680
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
+
681
1254
  @PluginMethod
682
1255
  public void list(final PluginCall call) {
683
1256
  try {
@@ -685,12 +1258,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
685
1258
  final JSObject ret = new JSObject();
686
1259
  final JSArray values = new JSArray();
687
1260
  for (final BundleInfo bundle : res) {
688
- values.put(bundle.toJSON());
1261
+ values.put(mapToJSObject(bundle.toJSONMap()));
689
1262
  }
690
1263
  ret.put("bundles", values);
691
1264
  call.resolve(ret);
692
1265
  } catch (final Exception e) {
693
- Log.e(CapacitorUpdater.TAG, "Could not list bundles", e);
1266
+ logger.error("Could not list bundles " + e.getMessage());
694
1267
  call.reject("Could not list bundles", e);
695
1268
  }
696
1269
  }
@@ -699,30 +1272,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
699
1272
  public void getLatest(final PluginCall call) {
700
1273
  final String channel = call.getString("channel");
701
1274
  startNewThread(() ->
702
- CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, res -> {
703
- if (res.has("error")) {
704
- call.reject(res.getString("error"));
705
- return;
706
- } else if (res.has("message")) {
707
- call.reject(res.getString("message"));
708
- return;
709
- } else {
710
- call.resolve(res);
711
- }
712
- final JSObject ret = new JSObject();
713
- Iterator<String> keys = res.keys();
714
- while (keys.hasNext()) {
715
- String key = keys.next();
716
- if (res.has(key)) {
717
- try {
718
- ret.put(key, res.get(key));
719
- } catch (JSONException e) {
720
- e.printStackTrace();
721
- }
722
- }
723
- }
724
- call.resolve(ret);
725
- })
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
+ })
726
1287
  );
727
1288
  }
728
1289
 
@@ -731,11 +1292,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
731
1292
  this.implementation.reset();
732
1293
 
733
1294
  if (toLastSuccessful && !fallback.isBuiltin()) {
734
- Log.i(CapacitorUpdater.TAG, "Resetting to: " + fallback);
1295
+ logger.info("Resetting to: " + fallback);
735
1296
  return this.implementation.set(fallback) && this._reload();
736
1297
  }
737
1298
 
738
- Log.i(CapacitorUpdater.TAG, "Resetting to native.");
1299
+ logger.info("Resetting to native.");
739
1300
  return this._reload();
740
1301
  }
741
1302
 
@@ -747,24 +1308,25 @@ public class CapacitorUpdaterPlugin extends Plugin {
747
1308
  call.resolve();
748
1309
  return;
749
1310
  }
750
- Log.e(CapacitorUpdater.TAG, "Reset failed");
1311
+ logger.error("Reset failed");
751
1312
  call.reject("Reset failed");
752
1313
  } catch (final Exception e) {
753
- Log.e(CapacitorUpdater.TAG, "Reset failed", e);
1314
+ logger.error("Reset failed " + e.getMessage());
754
1315
  call.reject("Reset failed", e);
755
1316
  }
756
1317
  }
757
1318
 
758
1319
  @PluginMethod
759
1320
  public void current(final PluginCall call) {
1321
+ ensureBridgeSet();
760
1322
  try {
761
1323
  final JSObject ret = new JSObject();
762
1324
  final BundleInfo bundle = this.implementation.getCurrentBundle();
763
- ret.put("bundle", bundle.toJSON());
1325
+ ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
764
1326
  ret.put("native", this.currentVersionNative);
765
1327
  call.resolve(ret);
766
1328
  } catch (final Exception e) {
767
- Log.e(CapacitorUpdater.TAG, "Could not get current bundle", e);
1329
+ logger.error("Could not get current bundle " + e.getMessage());
768
1330
  call.reject("Could not get current bundle", e);
769
1331
  }
770
1332
  }
@@ -778,13 +1340,33 @@ public class CapacitorUpdaterPlugin extends Plugin {
778
1340
  return;
779
1341
  }
780
1342
 
781
- call.resolve(bundle.toJSON());
1343
+ call.resolve(mapToJSObject(bundle.toJSONMap()));
782
1344
  } catch (final Exception e) {
783
- Log.e(CapacitorUpdater.TAG, "Could not get next bundle", e);
1345
+ logger.error("Could not get next bundle " + e.getMessage());
784
1346
  call.reject("Could not get next bundle", e);
785
1347
  }
786
1348
  }
787
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
+
788
1370
  public void checkForUpdateAfterDelay() {
789
1371
  if (this.periodCheckDelay == 0 || !this._isAutoUpdateEnabled()) {
790
1372
  return;
@@ -795,20 +1377,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
795
1377
  @Override
796
1378
  public void run() {
797
1379
  try {
798
- CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, res -> {
799
- if (res.has("error")) {
800
- Log.e(CapacitorUpdater.TAG, Objects.requireNonNull(res.getString("error")));
801
- } else if (res.has("version")) {
802
- String newVersion = res.getString("version");
803
- String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
804
- if (!Objects.equals(newVersion, currentVersion)) {
805
- Log.i(CapacitorUpdater.TAG, "New version found: " + newVersion);
806
- CapacitorUpdaterPlugin.this.backgroundDownload();
807
- }
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();
808
1390
  }
809
- });
1391
+ }
1392
+ });
810
1393
  } catch (final Exception e) {
811
- Log.e(CapacitorUpdater.TAG, "Failed to check for update", e);
1394
+ logger.error("Failed to check for update " + e.getMessage());
812
1395
  }
813
1396
  }
814
1397
  },
@@ -819,18 +1402,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
819
1402
 
820
1403
  @PluginMethod
821
1404
  public void notifyAppReady(final PluginCall call) {
1405
+ ensureBridgeSet();
822
1406
  try {
823
1407
  final BundleInfo bundle = this.implementation.getCurrentBundle();
824
1408
  this.implementation.setSuccess(bundle, this.autoDeletePrevious);
825
- Log.i(CapacitorUpdater.TAG, "Current bundle loaded successfully. ['notifyAppReady()' was called] " + bundle);
826
- Log.i(CapacitorUpdater.TAG, "semaphoreReady countDown");
1409
+ logger.info("Current bundle loaded successfully. ['notifyAppReady()' was called] " + bundle);
1410
+ logger.info("semaphoreReady countDown");
827
1411
  this.semaphoreDown();
828
- Log.i(CapacitorUpdater.TAG, "semaphoreReady countDown done");
1412
+ logger.info("semaphoreReady countDown done");
829
1413
  final JSObject ret = new JSObject();
830
- ret.put("bundle", bundle.toJSON());
1414
+ ret.put("bundle", mapToJSObject(bundle.toJSONMap()));
831
1415
  call.resolve(ret);
832
1416
  } catch (final Exception e) {
833
- 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());
834
1418
  call.reject("Failed to commit app ready state.", e);
835
1419
  }
836
1420
  }
@@ -838,118 +1422,46 @@ public class CapacitorUpdaterPlugin extends Plugin {
838
1422
  @PluginMethod
839
1423
  public void setMultiDelay(final PluginCall call) {
840
1424
  try {
841
- final Object delayConditions = call.getData().opt("delayConditions");
1425
+ final JSONArray delayConditions = call.getData().optJSONArray("delayConditions");
842
1426
  if (delayConditions == null) {
843
- Log.e(CapacitorUpdater.TAG, "setMultiDelay called without delayCondition");
1427
+ logger.error("setMultiDelay called without delayCondition");
844
1428
  call.reject("setMultiDelay called without delayCondition");
845
1429
  return;
846
1430
  }
847
- 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())) {
848
1440
  call.resolve();
849
1441
  } else {
850
1442
  call.reject("Failed to delay update");
851
1443
  }
852
1444
  } catch (final Exception e) {
853
- Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling 'setMultiDelay()']", e);
1445
+ logger.error("Failed to delay update, [Error calling 'setMultiDelay()'] " + e.getMessage());
854
1446
  call.reject("Failed to delay update", e);
855
1447
  }
856
1448
  }
857
1449
 
858
- private Boolean _setMultiDelay(String delayConditions) {
859
- try {
860
- this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions);
861
- this.editor.commit();
862
- Log.i(CapacitorUpdater.TAG, "Delay update saved");
863
- return true;
864
- } catch (final Exception e) {
865
- Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling '_setMultiDelay()']", e);
866
- return false;
867
- }
868
- }
869
-
870
- private boolean _cancelDelay(String source) {
871
- try {
872
- this.editor.remove(DELAY_CONDITION_PREFERENCES);
873
- this.editor.commit();
874
- Log.i(CapacitorUpdater.TAG, "All delays canceled from " + source);
875
- return true;
876
- } catch (final Exception e) {
877
- Log.e(CapacitorUpdater.TAG, "Failed to cancel update delay", e);
878
- return false;
879
- }
880
- }
881
-
882
1450
  @PluginMethod
883
1451
  public void cancelDelay(final PluginCall call) {
884
- if (this._cancelDelay("JS")) {
1452
+ if (this.delayUpdateUtils.cancelDelay("JS")) {
885
1453
  call.resolve();
886
1454
  } else {
887
1455
  call.reject("Failed to cancel delay");
888
1456
  }
889
1457
  }
890
1458
 
891
- private void _checkCancelDelay(Boolean killed) {
892
- Gson gson = new Gson();
893
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
894
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
895
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
896
- for (DelayCondition condition : delayConditionList) {
897
- String kind = condition.getKind().toString();
898
- String value = condition.getValue();
899
- if (!kind.isEmpty()) {
900
- switch (kind) {
901
- case "background":
902
- if (!killed) {
903
- this._cancelDelay("background check");
904
- }
905
- break;
906
- case "kill":
907
- if (killed) {
908
- this._cancelDelay("kill check");
909
- this.installNext();
910
- }
911
- break;
912
- case "date":
913
- if (!"".equals(value)) {
914
- try {
915
- final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
916
- Date date = sdf.parse(value);
917
- assert date != null;
918
- if (new Date().compareTo(date) > 0) {
919
- this._cancelDelay("date expired");
920
- }
921
- } catch (final Exception e) {
922
- this._cancelDelay("date parsing issue");
923
- }
924
- } else {
925
- this._cancelDelay("delayVal absent");
926
- }
927
- break;
928
- case "nativeVersion":
929
- if (!"".equals(value)) {
930
- try {
931
- final Version versionLimit = new Version(value);
932
- if (this.currentVersionNative.isAtLeast(versionLimit)) {
933
- this._cancelDelay("nativeVersion above limit");
934
- }
935
- } catch (final Exception e) {
936
- this._cancelDelay("nativeVersion parsing issue");
937
- }
938
- } else {
939
- this._cancelDelay("delayVal absent");
940
- }
941
- break;
942
- }
943
- }
944
- }
945
- }
946
-
947
1459
  private Boolean _isAutoUpdateEnabled() {
948
1460
  final CapConfig config = CapConfig.loadDefault(this.getActivity());
949
1461
  String serverUrl = config.getServerUrl();
950
1462
  if (serverUrl != null && !serverUrl.isEmpty()) {
951
1463
  // log warning autoupdate disabled when serverUrl is set
952
- Log.w(CapacitorUpdater.TAG, "AutoUpdate is automatic disabled when serverUrl is set.");
1464
+ logger.warn("AutoUpdate is automatic disabled when serverUrl is set.");
953
1465
  }
954
1466
  return (
955
1467
  CapacitorUpdaterPlugin.this.autoUpdate &&
@@ -965,7 +1477,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
965
1477
  ret.put("enabled", this._isAutoUpdateEnabled());
966
1478
  call.resolve(ret);
967
1479
  } catch (final Exception e) {
968
- Log.e(CapacitorUpdater.TAG, "Could not get autoUpdate status", e);
1480
+ logger.error("Could not get autoUpdate status " + e.getMessage());
969
1481
  call.reject("Could not get autoUpdate status", e);
970
1482
  }
971
1483
  }
@@ -979,7 +1491,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
979
1491
  ret.put("available", serverUrl == null || serverUrl.isEmpty());
980
1492
  call.resolve(ret);
981
1493
  } catch (final Exception e) {
982
- Log.e(CapacitorUpdater.TAG, "Could not get autoUpdate availability", e);
1494
+ logger.error("Could not get autoUpdate availability " + e.getMessage());
983
1495
  call.reject("Could not get autoUpdate availability", e);
984
1496
  }
985
1497
  }
@@ -991,7 +1503,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
991
1503
  }
992
1504
  this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck());
993
1505
  } catch (final Exception e) {
994
- Log.e(CapacitorUpdater.TAG, "Failed to start " + DeferredNotifyAppReadyCheck.class.getName(), e);
1506
+ logger.error("Failed to start " + DeferredNotifyAppReadyCheck.class.getName() + " " + e.getMessage());
995
1507
  }
996
1508
  }
997
1509
 
@@ -1004,8 +1516,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
1004
1516
  }
1005
1517
  }
1006
1518
 
1519
+ private void ensureBridgeSet() {
1520
+ if (this.bridge != null && this.bridge.getWebView() != null) {
1521
+ logger.setBridge(this.bridge);
1522
+ }
1523
+ }
1524
+
1007
1525
  private void endBackGroundTaskWithNotif(String msg, String latestVersionName, BundleInfo current, Boolean error) {
1008
- endBackGroundTaskWithNotif(msg, latestVersionName, current, error, "download_fail", "downloadFailed");
1526
+ endBackGroundTaskWithNotif(msg, latestVersionName, current, error, false, "download_fail", "downloadFailed");
1009
1527
  }
1010
1528
 
1011
1529
  private void endBackGroundTaskWithNotif(
@@ -1013,18 +1531,28 @@ public class CapacitorUpdaterPlugin extends Plugin {
1013
1531
  String latestVersionName,
1014
1532
  BundleInfo current,
1015
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,
1016
1545
  String failureAction,
1017
1546
  String failureEvent
1018
1547
  ) {
1019
1548
  if (error) {
1020
- Log.i(
1021
- CapacitorUpdater.TAG,
1549
+ logger.info(
1022
1550
  "endBackGroundTaskWithNotif error: " +
1023
- error +
1024
- " current: " +
1025
- current.getVersionName() +
1026
- "latestVersionName: " +
1027
- latestVersionName
1551
+ error +
1552
+ " current: " +
1553
+ current.getVersionName() +
1554
+ "latestVersionName: " +
1555
+ latestVersionName
1028
1556
  );
1029
1557
  this.implementation.sendStats(failureAction, current.getVersionName());
1030
1558
  final JSObject ret = new JSObject();
@@ -1032,105 +1560,117 @@ public class CapacitorUpdaterPlugin extends Plugin {
1032
1560
  this.notifyListeners(failureEvent, ret);
1033
1561
  }
1034
1562
  final JSObject ret = new JSObject();
1035
- ret.put("bundle", current.toJSON());
1563
+ ret.put("bundle", mapToJSObject(current.toJSONMap()));
1036
1564
  this.notifyListeners("noNeedUpdate", ret);
1037
- this.sendReadyToJs(current, msg);
1565
+ this.sendReadyToJs(current, msg, isDirectUpdate);
1038
1566
  this.backgroundDownloadTask = null;
1039
- Log.i(CapacitorUpdater.TAG, "endBackGroundTaskWithNotif " + msg);
1567
+ logger.info("endBackGroundTaskWithNotif " + msg);
1040
1568
  }
1041
1569
 
1042
1570
  private Thread backgroundDownload() {
1043
- 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
1044
1575
  ? "Update will occur now."
1045
1576
  : "Update will occur next time app moves to background.";
1046
1577
  return startNewThread(() -> {
1047
- Log.i(CapacitorUpdater.TAG, "Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
1048
- 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);
1049
1582
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1050
- try {
1051
- if (res.has("error")) {
1052
- final String error = res.optString("error", "");
1053
- if (error != null && !error.isEmpty()) {
1054
- Log.e(CapacitorUpdater.TAG, "getLatest failed with error: " + error);
1055
- final String latestVersion = res.has("version")
1056
- ? res.optString("version", current.getVersionName())
1057
- : current.getVersionName();
1058
- if ("response_error".equals(error)) {
1059
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1060
- "Network error: " + error,
1061
- latestVersion,
1062
- current,
1063
- true
1064
- );
1065
- } else {
1066
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1067
- error,
1068
- latestVersion,
1069
- current,
1070
- true,
1071
- "backend_refusal",
1072
- "backendRefused"
1073
- );
1074
- }
1075
- return;
1076
- }
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
+ );
1077
1607
  }
1608
+ return;
1609
+ }
1078
1610
 
1079
- if (res.has("message")) {
1080
- Log.i(CapacitorUpdater.TAG, "API message: " + res.get("message"));
1081
- if (res.has("major") && res.getBoolean("major") && res.has("version")) {
1082
- final JSObject majorAvailable = new JSObject();
1083
- majorAvailable.put("version", res.getString("version"));
1084
- CapacitorUpdaterPlugin.this.notifyListeners("majorAvailable", majorAvailable);
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"));
1085
1616
  }
1086
- final String latestVersion = res.has("version")
1087
- ? res.optString("version", current.getVersionName())
1088
- : current.getVersionName();
1617
+ String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
1089
1618
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1090
- res.getString("message"),
1091
- latestVersion,
1092
- current,
1093
- true,
1094
- "backend_refusal",
1095
- "backendRefused"
1096
- );
1619
+ jsRes.getString("message"),
1620
+ latestVersion,
1621
+ current,
1622
+ true,
1623
+ plannedDirectUpdate,
1624
+ "backend_refusal",
1625
+ "backendRefused"
1626
+ );
1097
1627
  return;
1098
1628
  }
1099
1629
 
1100
- final String latestVersionName = res.getString("version");
1630
+ final String latestVersionName = jsRes.getString("version");
1101
1631
 
1102
1632
  if ("builtin".equals(latestVersionName)) {
1103
- Log.i(CapacitorUpdater.TAG, "Latest version is builtin");
1104
- if (CapacitorUpdaterPlugin.this.implementation.directUpdate) {
1105
- Log.i(CapacitorUpdater.TAG, "Direct update to builtin version");
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");
1106
1639
  this._reset(false);
1107
1640
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1108
- "Updated to builtin version",
1109
- latestVersionName,
1110
- CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1111
- false
1112
- );
1641
+ "Updated to builtin version",
1642
+ latestVersionName,
1643
+ CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1644
+ false,
1645
+ true
1646
+ );
1113
1647
  } else {
1114
- 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");
1115
1654
  CapacitorUpdaterPlugin.this.implementation.setNextBundle(BundleInfo.ID_BUILTIN);
1116
1655
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1117
- "Next update will be to builtin version",
1118
- latestVersionName,
1119
- current,
1120
- false
1121
- );
1656
+ "Next update will be to builtin version",
1657
+ latestVersionName,
1658
+ current,
1659
+ false
1660
+ );
1122
1661
  }
1123
1662
  return;
1124
1663
  }
1125
1664
 
1126
- if (!res.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(res.getString("url"))) {
1127
- 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");
1128
1667
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1129
- "Error no url or wrong format",
1130
- current.getVersionName(),
1131
- current,
1132
- true
1133
- );
1668
+ "Error no url or wrong format",
1669
+ current.getVersionName(),
1670
+ current,
1671
+ true,
1672
+ plannedDirectUpdate
1673
+ );
1134
1674
  return;
1135
1675
  }
1136
1676
 
@@ -1140,129 +1680,158 @@ public class CapacitorUpdaterPlugin extends Plugin {
1140
1680
  final BundleInfo latest = CapacitorUpdaterPlugin.this.implementation.getBundleInfoByName(latestVersionName);
1141
1681
  if (latest != null) {
1142
1682
  final JSObject ret = new JSObject();
1143
- ret.put("bundle", latest.toJSON());
1683
+ ret.put("bundle", mapToJSObject(latest.toJSONMap()));
1144
1684
  if (latest.isErrorStatus()) {
1145
- 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.");
1146
1686
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1147
- "Latest bundle already exists, and is in error state. Aborting update.",
1148
- latestVersionName,
1149
- current,
1150
- true
1151
- );
1687
+ "Latest bundle already exists, and is in error state. Aborting update.",
1688
+ latestVersionName,
1689
+ current,
1690
+ true,
1691
+ plannedDirectUpdate
1692
+ );
1152
1693
  return;
1153
1694
  }
1154
1695
  if (latest.isDownloaded()) {
1155
- Log.i(
1156
- CapacitorUpdater.TAG,
1157
- "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
1158
1699
  );
1159
- if (CapacitorUpdaterPlugin.this.implementation.directUpdate) {
1160
- CapacitorUpdaterPlugin.this.implementation.set(latest);
1161
- CapacitorUpdaterPlugin.this._reload();
1162
- CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1163
- "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",
1164
1709
  latestVersionName,
1165
1710
  latest,
1166
- false
1711
+ false,
1712
+ plannedDirectUpdate
1167
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
+ );
1168
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
+ }
1169
1731
  CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
1170
1732
  CapacitorUpdaterPlugin.this.implementation.setNextBundle(latest.getId());
1171
1733
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1172
- "update downloaded, will install next background",
1173
- latestVersionName,
1174
- latest,
1175
- false
1176
- );
1734
+ "update downloaded, will install next background",
1735
+ latestVersionName,
1736
+ latest,
1737
+ false
1738
+ );
1177
1739
  }
1178
1740
  return;
1179
1741
  }
1180
1742
  if (latest.isDeleted()) {
1181
- Log.i(
1182
- CapacitorUpdater.TAG,
1183
- "Latest bundle already exists and will be deleted, download will overwrite it."
1184
- );
1743
+ logger.info("Latest bundle already exists and will be deleted, download will overwrite it.");
1185
1744
  try {
1186
1745
  final Boolean deleted = CapacitorUpdaterPlugin.this.implementation.delete(latest.getId(), true);
1187
1746
  if (deleted) {
1188
- Log.i(CapacitorUpdater.TAG, "Failed bundle deleted: " + latest.getVersionName());
1747
+ logger.info("Failed bundle deleted: " + latest.getVersionName());
1189
1748
  }
1190
1749
  } catch (final IOException e) {
1191
- Log.e(CapacitorUpdater.TAG, "Failed to delete failed bundle: " + latest.getVersionName(), e);
1750
+ logger.error("Failed to delete failed bundle: " + latest.getVersionName() + " " + e.getMessage());
1192
1751
  }
1193
1752
  }
1194
1753
  }
1195
1754
  startNewThread(() -> {
1196
1755
  try {
1197
- Log.i(
1198
- CapacitorUpdater.TAG,
1756
+ logger.info(
1199
1757
  "New bundle: " +
1200
- latestVersionName +
1201
- " found. Current is: " +
1202
- current.getVersionName() +
1203
- ". " +
1204
- messageUpdate
1758
+ latestVersionName +
1759
+ " found. Current is: " +
1760
+ current.getVersionName() +
1761
+ ". " +
1762
+ messageUpdate
1205
1763
  );
1206
1764
 
1207
- final String url = res.getString("url");
1208
- final String sessionKey = res.has("sessionKey") ? res.getString("sessionKey") : "";
1209
- final String checksum = res.has("checksum") ? res.getString("checksum") : "";
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") : "";
1210
1768
 
1211
- if (res.has("manifest")) {
1769
+ if (jsRes.has("manifest")) {
1212
1770
  // Handle manifest-based download
1213
- JSONArray manifest = res.getJSONArray("manifest");
1771
+ JSONArray manifest = jsRes.getJSONArray("manifest");
1214
1772
  CapacitorUpdaterPlugin.this.implementation.downloadBackground(
1215
- url,
1216
- latestVersionName,
1217
- sessionKey,
1218
- checksum,
1219
- manifest
1220
- );
1773
+ url,
1774
+ latestVersionName,
1775
+ sessionKey,
1776
+ checksum,
1777
+ manifest
1778
+ );
1221
1779
  } else {
1222
1780
  // Handle single file download (existing code)
1223
1781
  CapacitorUpdaterPlugin.this.implementation.downloadBackground(
1224
- url,
1225
- latestVersionName,
1226
- sessionKey,
1227
- checksum,
1228
- null
1229
- );
1782
+ url,
1783
+ latestVersionName,
1784
+ sessionKey,
1785
+ checksum,
1786
+ null
1787
+ );
1230
1788
  }
1231
1789
  } catch (final Exception e) {
1232
- Log.e(CapacitorUpdater.TAG, "error downloading file", e);
1790
+ logger.error("error downloading file " + e.getMessage());
1233
1791
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1234
- "Error downloading file",
1235
- latestVersionName,
1236
- CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1237
- true
1238
- );
1792
+ "Error downloading file",
1793
+ latestVersionName,
1794
+ CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1795
+ true,
1796
+ plannedDirectUpdate
1797
+ );
1239
1798
  }
1240
1799
  });
1241
1800
  } else {
1242
- 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.");
1243
1802
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif("No need to update", latestVersionName, current, false);
1244
1803
  }
1245
1804
  } catch (final JSONException e) {
1246
- Log.e(CapacitorUpdater.TAG, "error parsing JSON", e);
1805
+ logger.error("error parsing JSON " + e.getMessage());
1247
1806
  CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1248
- "Error parsing JSON",
1249
- current.getVersionName(),
1250
- current,
1251
- true
1252
- );
1807
+ "Error parsing JSON",
1808
+ current.getVersionName(),
1809
+ current,
1810
+ true,
1811
+ plannedDirectUpdate
1812
+ );
1253
1813
  }
1254
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
+ }
1255
1826
  });
1256
1827
  }
1257
1828
 
1258
1829
  private void installNext() {
1259
1830
  try {
1260
- Gson gson = new Gson();
1261
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
1262
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1263
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
1264
- if (delayConditionList != null && !delayConditionList.isEmpty()) {
1265
- Log.i(CapacitorUpdater.TAG, "Update delayed until delay conditions met");
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");
1266
1835
  return;
1267
1836
  }
1268
1837
  final BundleInfo current = this.implementation.getCurrentBundle();
@@ -1270,16 +1839,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
1270
1839
 
1271
1840
  if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
1272
1841
  // There is a next bundle waiting for activation
1273
- Log.d(CapacitorUpdater.TAG, "Next bundle is: " + next.getVersionName());
1842
+ logger.debug("Next bundle is: " + next.getVersionName());
1274
1843
  if (this.implementation.set(next) && this._reload()) {
1275
- Log.i(CapacitorUpdater.TAG, "Updated to bundle: " + next.getVersionName());
1844
+ logger.info("Updated to bundle: " + next.getVersionName());
1276
1845
  this.implementation.setNextBundle(null);
1277
1846
  } else {
1278
- Log.e(CapacitorUpdater.TAG, "Update to bundle: " + next.getVersionName() + " Failed!");
1847
+ logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
1279
1848
  }
1280
1849
  }
1281
1850
  } catch (final Exception e) {
1282
- Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1851
+ logger.error("Error during onActivityStopped " + e.getMessage());
1283
1852
  }
1284
1853
  }
1285
1854
 
@@ -1288,33 +1857,34 @@ public class CapacitorUpdaterPlugin extends Plugin {
1288
1857
  final BundleInfo current = this.implementation.getCurrentBundle();
1289
1858
 
1290
1859
  if (current.isBuiltin()) {
1291
- 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.");
1292
1861
  return;
1293
1862
  }
1294
- Log.d(CapacitorUpdater.TAG, "Current bundle is: " + current);
1863
+ logger.debug("Current bundle is: " + current);
1295
1864
 
1296
1865
  if (BundleStatus.SUCCESS != current.getStatus()) {
1297
- Log.e(CapacitorUpdater.TAG, "notifyAppReady was not called, roll back current bundle: " + current.getId());
1298
- Log.i(CapacitorUpdater.TAG, "Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
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?");
1299
1868
  final JSObject ret = new JSObject();
1300
- ret.put("bundle", current.toJSON());
1869
+ ret.put("bundle", mapToJSObject(current.toJSONMap()));
1870
+ this.persistLastFailedBundle(current);
1301
1871
  this.notifyListeners("updateFailed", ret);
1302
1872
  this.implementation.sendStats("update_fail", current.getVersionName());
1303
1873
  this.implementation.setError(current);
1304
1874
  this._reset(true);
1305
1875
  if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
1306
- Log.i(CapacitorUpdater.TAG, "Deleting failing bundle: " + current.getVersionName());
1876
+ logger.info("Deleting failing bundle: " + current.getVersionName());
1307
1877
  try {
1308
1878
  final Boolean res = this.implementation.delete(current.getId(), false);
1309
1879
  if (res) {
1310
- Log.i(CapacitorUpdater.TAG, "Failed bundle deleted: " + current.getVersionName());
1880
+ logger.info("Failed bundle deleted: " + current.getVersionName());
1311
1881
  }
1312
1882
  } catch (final IOException e) {
1313
- Log.e(CapacitorUpdater.TAG, "Failed to delete failed bundle: " + current.getVersionName(), e);
1883
+ logger.error("Failed to delete failed bundle: " + current.getVersionName() + " " + e.getMessage());
1314
1884
  }
1315
1885
  }
1316
1886
  } else {
1317
- Log.i(CapacitorUpdater.TAG, "notifyAppReady was called. This is fine: " + current.getId());
1887
+ logger.info("notifyAppReady was called. This is fine: " + current.getId());
1318
1888
  }
1319
1889
  }
1320
1890
 
@@ -1323,15 +1893,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
1323
1893
  @Override
1324
1894
  public void run() {
1325
1895
  try {
1326
- Log.i(
1327
- CapacitorUpdater.TAG,
1328
- "Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady"
1329
- );
1896
+ logger.info("Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady");
1330
1897
  Thread.sleep(CapacitorUpdaterPlugin.this.appReadyTimeout);
1331
1898
  CapacitorUpdaterPlugin.this.checkRevert();
1332
1899
  CapacitorUpdaterPlugin.this.appReadyCheck = null;
1333
1900
  } catch (final InterruptedException e) {
1334
- Log.i(CapacitorUpdater.TAG, DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
1901
+ logger.info(DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
1335
1902
  }
1336
1903
  }
1337
1904
  }
@@ -1339,14 +1906,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
1339
1906
  public void appMovedToForeground() {
1340
1907
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1341
1908
  CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_foreground", current.getVersionName());
1342
- this._checkCancelDelay(false);
1909
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
1910
+ this.delayUpdateUtils.unsetBackgroundTimestamp();
1911
+
1343
1912
  if (
1344
1913
  CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() &&
1345
1914
  (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive())
1346
1915
  ) {
1347
1916
  this.backgroundDownloadTask = this.backgroundDownload();
1348
1917
  } else {
1349
- Log.i(CapacitorUpdater.TAG, "Auto update is disabled");
1918
+ logger.info("Auto update is disabled");
1350
1919
  this.sendReadyToJs(current, "disabled");
1351
1920
  }
1352
1921
  this.checkAppReady();
@@ -1354,40 +1923,42 @@ public class CapacitorUpdaterPlugin extends Plugin {
1354
1923
 
1355
1924
  public void appMovedToBackground() {
1356
1925
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1357
- CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_background", current.getVersionName());
1358
- Log.i(CapacitorUpdater.TAG, "Checking for pending update");
1359
- try {
1360
- Gson gson = new Gson();
1361
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
1362
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1363
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
1364
- String backgroundValue = null;
1365
- for (DelayCondition delayCondition : delayConditionList) {
1366
- if (delayCondition.getKind().toString().equals("background")) {
1367
- String value = delayCondition.getValue();
1368
- backgroundValue = (value != null && !value.isEmpty()) ? value : "0";
1369
- }
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;
1370
1936
  }
1371
- if (backgroundValue != null) {
1372
- taskRunning = true;
1373
- final Long timeout = Long.parseLong(backgroundValue);
1374
- if (backgroundTask != null) {
1375
- backgroundTask.interrupt();
1376
- }
1377
- backgroundTask = startNewThread(
1378
- () -> {
1379
- taskRunning = false;
1380
- _checkCancelDelay(false);
1381
- installNext();
1382
- },
1383
- timeout
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."
1384
1941
  );
1385
- } else {
1386
- this._checkCancelDelay(false);
1387
- this.installNext();
1942
+ canShowSplashscreen = false;
1943
+ }
1944
+
1945
+ if (canShowSplashscreen) {
1946
+ logger.info("Showing splashscreen for launcher/task switcher");
1947
+ this.showSplashscreen();
1388
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();
1389
1960
  } catch (final Exception e) {
1390
- Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1961
+ logger.error("Error during onActivityStopped " + e.getMessage());
1391
1962
  }
1392
1963
  }
1393
1964
 
@@ -1416,37 +1987,154 @@ public class CapacitorUpdaterPlugin extends Plugin {
1416
1987
 
1417
1988
  @Override
1418
1989
  public void handleOnStart() {
1419
- if (isPreviousMainActivity) {
1420
- 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());
1421
2009
  }
1422
- Log.i(CapacitorUpdater.TAG, "onActivityStarted " + getActivity().getClass().getName());
1423
- isPreviousMainActivity = true;
1424
2010
  }
1425
2011
 
1426
2012
  @Override
1427
2013
  public void handleOnStop() {
1428
- isPreviousMainActivity = isMainActivity();
1429
- if (isPreviousMainActivity) {
1430
- 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());
1431
2022
  }
1432
2023
  }
1433
2024
 
1434
2025
  @Override
1435
2026
  public void handleOnResume() {
1436
- if (backgroundTask != null && taskRunning) {
1437
- 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());
1438
2034
  }
1439
- this.implementation.activity = getActivity();
1440
2035
  }
1441
2036
 
1442
2037
  @Override
1443
2038
  public void handleOnPause() {
1444
- 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
+ }
1445
2044
  }
1446
2045
 
1447
2046
  @Override
1448
2047
  public void handleOnDestroy() {
1449
- Log.i(CapacitorUpdater.TAG, "onActivityDestroyed " + getActivity().getClass().getName());
1450
- this.implementation.activity = getActivity();
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());
2064
+ }
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();
1451
2139
  }
1452
2140
  }