@capgo/capacitor-updater 8.0.1 → 8.1.0

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