@capgo/capacitor-updater 8.0.0 → 8.1.0

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