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