@capgo/capacitor-updater 8.0.0 → 8.0.1

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 (40) hide show
  1. package/CapgoCapacitorUpdater.podspec +2 -2
  2. package/Package.swift +35 -0
  3. package/README.md +667 -206
  4. package/android/build.gradle +16 -11
  5. package/android/proguard-rules.pro +28 -0
  6. package/android/src/main/AndroidManifest.xml +0 -1
  7. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +134 -194
  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/CapacitorUpdater.java +967 -1027
  11. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1283 -1180
  12. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +276 -0
  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 +45 -48
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
  16. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +440 -113
  17. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +101 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +32 -0
  19. package/dist/docs.json +1316 -473
  20. package/dist/esm/definitions.d.ts +518 -248
  21. package/dist/esm/definitions.js.map +1 -1
  22. package/dist/esm/index.d.ts +2 -2
  23. package/dist/esm/index.js +4 -4
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/web.d.ts +25 -41
  26. package/dist/esm/web.js +67 -35
  27. package/dist/esm/web.js.map +1 -1
  28. package/dist/plugin.cjs.js +67 -35
  29. package/dist/plugin.cjs.js.map +1 -1
  30. package/dist/plugin.js +67 -35
  31. package/dist/plugin.js.map +1 -1
  32. package/ios/Plugin/CapacitorUpdater.swift +736 -361
  33. package/ios/Plugin/CapacitorUpdaterPlugin.swift +436 -136
  34. package/ios/Plugin/CryptoCipherV2.swift +310 -0
  35. package/ios/Plugin/InternalUtils.swift +258 -0
  36. package/package.json +33 -29
  37. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +0 -153
  38. package/ios/Plugin/CapacitorUpdaterPlugin.h +0 -10
  39. package/ios/Plugin/CapacitorUpdaterPlugin.m +0 -27
  40. package/ios/Plugin/CryptoCipher.swift +0 -240
@@ -8,17 +8,13 @@ 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;
16
15
  import android.os.Build;
17
- import android.os.Bundle;
16
+ import android.os.Looper;
18
17
  import android.util.Log;
19
- import androidx.annotation.NonNull;
20
- import androidx.annotation.Nullable;
21
- import com.android.volley.toolbox.Volley;
22
18
  import com.getcapacitor.CapConfig;
23
19
  import com.getcapacitor.JSArray;
24
20
  import com.getcapacitor.JSObject;
@@ -36,1272 +32,1379 @@ import java.net.MalformedURLException;
36
32
  import java.net.URL;
37
33
  import java.text.SimpleDateFormat;
38
34
  import java.util.ArrayList;
35
+ import java.util.Arrays;
39
36
  import java.util.Date;
40
37
  import java.util.Iterator;
41
38
  import java.util.List;
39
+ import java.util.Objects;
40
+ import java.util.Timer;
41
+ import java.util.TimerTask;
42
42
  import java.util.UUID;
43
+ import java.util.concurrent.Phaser;
44
+ import java.util.concurrent.Semaphore;
45
+ import java.util.concurrent.TimeUnit;
46
+ import java.util.concurrent.TimeoutException;
47
+ import java.util.concurrent.atomic.AtomicReference;
48
+ import okhttp3.OkHttpClient;
49
+ import okhttp3.Protocol;
50
+ import org.json.JSONArray;
43
51
  import org.json.JSONException;
44
52
 
45
53
  @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
- }
103
- };
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
- }
54
+ public class CapacitorUpdaterPlugin extends Plugin {
128
55
 
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) {
56
+ private static final String updateUrlDefault = "https://plugin.capgo.app/updates";
57
+ private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
58
+ private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
59
+
60
+ private final String PLUGIN_VERSION = "8.0.1";
61
+ private static final String DELAY_CONDITION_PREFERENCES = "";
62
+
63
+ private SharedPreferences.Editor editor;
64
+ private SharedPreferences prefs;
65
+ protected CapacitorUpdater implementation;
66
+
67
+ private Integer appReadyTimeout = 10000;
68
+ private Integer counterActivityCreate = 0;
69
+ private Integer periodCheckDelay = 0;
70
+ private Boolean autoDeleteFailed = true;
71
+ private Boolean autoDeletePrevious = true;
72
+ private Boolean autoUpdate = false;
73
+ private String updateUrl = "";
74
+ private Version currentVersionNative;
75
+ private Thread backgroundTask;
76
+ private Boolean taskRunning = false;
77
+ private Boolean keepUrlPathAfterReload = false;
78
+
79
+ private Boolean isPreviousMainActivity = true;
80
+
81
+ private volatile Thread backgroundDownloadTask;
82
+ private volatile Thread appReadyCheck;
83
+
84
+ // private static final CountDownLatch semaphoreReady = new CountDownLatch(1);
85
+ private static final Phaser semaphoreReady = new Phaser(1);
86
+
87
+ private int lastNotifiedStatPercent = 0;
88
+
89
+ public Thread startNewThread(final Runnable function, Number waitTime) {
90
+ Thread bgTask = new Thread(() -> {
187
91
  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
- );
92
+ if (waitTime.longValue() > 0) {
93
+ Thread.sleep(waitTime.longValue());
94
+ }
95
+ function.run();
96
+ } catch (Exception e) {
97
+ e.printStackTrace();
199
98
  }
200
- }
99
+ });
100
+ bgTask.start();
101
+ return bgTask;
102
+ }
103
+
104
+ public Thread startNewThread(final Runnable function) {
105
+ return startNewThread(function, 0);
106
+ }
107
+
108
+ @Override
109
+ public void load() {
110
+ super.load();
111
+ this.counterActivityCreate++;
112
+ this.prefs = this.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
113
+ this.editor = this.prefs.edit();
114
+
115
+ try {
116
+ this.implementation = new CapacitorUpdater() {
117
+ @Override
118
+ public void notifyDownload(final String id, final int percent) {
119
+ CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
120
+ }
121
+
122
+ @Override
123
+ public void directUpdateFinish(final BundleInfo latest) {
124
+ CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
125
+ }
126
+
127
+ @Override
128
+ public void notifyListeners(final String id, final JSObject res) {
129
+ CapacitorUpdaterPlugin.this.notifyListeners(id, res);
130
+ }
131
+ };
132
+ final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
133
+ this.implementation.activity = this.getActivity();
134
+ this.implementation.versionBuild = this.getConfig().getString("version", pInfo.versionName);
135
+ this.implementation.PLUGIN_VERSION = this.PLUGIN_VERSION;
136
+ this.implementation.versionCode = Integer.toString(pInfo.versionCode);
137
+ this.implementation.client = new OkHttpClient.Builder()
138
+ .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
139
+ .connectTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
140
+ .readTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
141
+ .writeTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
142
+ .build();
143
+
144
+ this.implementation.directUpdate = this.getConfig().getBoolean("directUpdate", false);
145
+ this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
146
+ } catch (final PackageManager.NameNotFoundException e) {
147
+ Log.e(CapacitorUpdater.TAG, "Error instantiating implementation", e);
148
+ return;
149
+ } catch (final Exception e) {
150
+ Log.e(CapacitorUpdater.TAG, "Error getting current native app version", e);
151
+ return;
201
152
  }
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
- );
153
+ final CapConfig config = CapConfig.loadDefault(this.getActivity());
154
+ this.implementation.appId = InternalUtils.getPackageName(getContext().getPackageManager(), getContext().getPackageName());
155
+ this.implementation.appId = config.getString("appId", this.implementation.appId);
156
+ this.implementation.appId = this.getConfig().getString("appId", this.implementation.appId);
157
+ if (this.implementation.appId == null || this.implementation.appId.isEmpty()) {
158
+ // crash the app
159
+ throw new RuntimeException(
160
+ "appId is missing in capacitor.config.json or plugin config, and cannot be retrieved from the native app, please add it globally or in the plugin config"
161
+ );
162
+ }
163
+ Log.i(CapacitorUpdater.TAG, "appId: " + implementation.appId);
164
+ this.implementation.publicKey = this.getConfig().getString("publicKey", "");
165
+ this.implementation.statsUrl = this.getConfig().getString("statsUrl", statsUrlDefault);
166
+ this.implementation.channelUrl = this.getConfig().getString("channelUrl", channelUrlDefault);
167
+ int userValue = this.getConfig().getInt("periodCheckDelay", 0);
168
+ this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
169
+
170
+ if (userValue >= 0 && userValue <= 600) {
171
+ this.periodCheckDelay = 600 * 1000;
172
+ } else if (userValue > 600) {
173
+ this.periodCheckDelay = userValue * 1000;
174
+ }
175
+
176
+ this.implementation.documentsDir = this.getContext().getFilesDir();
177
+ this.implementation.prefs = this.prefs;
178
+ this.implementation.editor = this.editor;
179
+ this.implementation.versionOs = Build.VERSION.RELEASE;
180
+ this.implementation.deviceID = this.prefs.getString("appUUID", UUID.randomUUID().toString()).toLowerCase();
181
+ this.editor.putString("appUUID", this.implementation.deviceID);
182
+ this.editor.commit();
183
+ Log.i(CapacitorUpdater.TAG, "init for device " + this.implementation.deviceID);
184
+ Log.i(CapacitorUpdater.TAG, "version native " + this.currentVersionNative.getOriginalString());
185
+ this.autoDeleteFailed = this.getConfig().getBoolean("autoDeleteFailed", true);
186
+ this.autoDeletePrevious = this.getConfig().getBoolean("autoDeletePrevious", true);
187
+ this.updateUrl = this.getConfig().getString("updateUrl", updateUrlDefault);
188
+ this.autoUpdate = this.getConfig().getBoolean("autoUpdate", true);
189
+ this.appReadyTimeout = this.getConfig().getInt("appReadyTimeout", 10000);
190
+ this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
191
+ this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
192
+ boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
193
+
194
+ this.implementation.autoReset();
195
+ if (resetWhenUpdate) {
196
+ this.cleanupObsoleteVersions();
197
+ }
198
+ this.checkForUpdateAfterDelay();
215
199
  }
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);
200
+
201
+ private void semaphoreWait(Number waitTime) {
202
+ Log.i(CapacitorUpdater.TAG, "semaphoreWait " + waitTime);
203
+ try {
204
+ // Log.i(CapacitorUpdater.TAG, "semaphoreReady count " + CapacitorUpdaterPlugin.this.semaphoreReady.getCount());
205
+ semaphoreReady.awaitAdvanceInterruptibly(semaphoreReady.getPhase(), waitTime.longValue(), TimeUnit.SECONDS);
206
+ // Log.i(CapacitorUpdater.TAG, "semaphoreReady await " + res);
207
+ Log.i(CapacitorUpdater.TAG, "semaphoreReady count " + semaphoreReady.getPhase());
208
+ } catch (InterruptedException e) {
209
+ Log.i(CapacitorUpdater.TAG, "semaphoreWait InterruptedException");
210
+ e.printStackTrace();
211
+ } catch (TimeoutException e) {
212
+ throw new RuntimeException(e);
213
+ }
244
214
  }
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);
215
+
216
+ private void semaphoreUp() {
217
+ Log.i(CapacitorUpdater.TAG, "semaphoreUp");
218
+ semaphoreReady.register();
256
219
  }
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;
220
+
221
+ private void semaphoreDown() {
222
+ Log.i(CapacitorUpdater.TAG, "semaphoreDown");
223
+ Log.i(CapacitorUpdater.TAG, "semaphoreDown count " + semaphoreReady.getPhase());
224
+ semaphoreReady.arriveAndDeregister();
266
225
  }
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);
226
+
227
+ private void sendReadyToJs(final BundleInfo current, final String msg) {
228
+ Log.i(CapacitorUpdater.TAG, "sendReadyToJs");
229
+ final JSObject ret = new JSObject();
230
+ ret.put("bundle", current.toJSON());
231
+ ret.put("status", msg);
232
+ startNewThread(() -> {
233
+ Log.i(CapacitorUpdater.TAG, "semaphoreReady sendReadyToJs");
234
+ semaphoreWait(CapacitorUpdaterPlugin.this.appReadyTimeout);
235
+ Log.i(CapacitorUpdater.TAG, "semaphoreReady sendReadyToJs done");
236
+ CapacitorUpdaterPlugin.this.notifyListeners("appReady", ret);
237
+ });
279
238
  }
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;
239
+
240
+ private void directUpdateFinish(final BundleInfo latest) {
241
+ CapacitorUpdaterPlugin.this.implementation.set(latest);
242
+ CapacitorUpdaterPlugin.this._reload();
243
+ sendReadyToJs(latest, "update installed");
289
244
  }
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
- }
245
+
246
+ private void cleanupObsoleteVersions() {
247
+ try {
248
+ final Version previous = new Version(this.prefs.getString("LatestVersionNative", ""));
249
+ try {
250
+ if (
251
+ !"".equals(previous.getOriginalString()) &&
252
+ !Objects.equals(this.currentVersionNative.getOriginalString(), previous.getOriginalString())
253
+ ) {
254
+ Log.i(CapacitorUpdater.TAG, "New native version detected: " + this.currentVersionNative);
255
+ this.implementation.reset(true);
256
+ final List<BundleInfo> installed = this.implementation.list(false);
257
+ for (final BundleInfo bundle : installed) {
258
+ try {
259
+ Log.i(CapacitorUpdater.TAG, "Deleting obsolete bundle: " + bundle.getId());
260
+ this.implementation.delete(bundle.getId());
261
+ } catch (final Exception e) {
262
+ Log.e(CapacitorUpdater.TAG, "Failed to delete: " + bundle.getId(), e);
263
+ }
264
+ }
304
265
  }
305
- );
306
- }
266
+ } catch (final Exception e) {
267
+ Log.e(CapacitorUpdater.TAG, "Could not determine the current version", e);
268
+ }
269
+ } catch (final Exception e) {
270
+ Log.e(CapacitorUpdater.TAG, "Error calculating previous native version", e);
307
271
  }
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);
272
+ this.editor.putString("LatestVersionNative", this.currentVersionNative.toString());
273
+ this.editor.commit();
313
274
  }
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);
275
+
276
+ public void notifyDownload(final String id, final int percent) {
277
+ try {
278
+ final JSObject ret = new JSObject();
279
+ ret.put("percent", percent);
280
+ final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
281
+ ret.put("bundle", bundleInfo.toJSON());
282
+ this.notifyListeners("download", ret);
283
+
284
+ if (percent == 100) {
285
+ final JSObject retDownloadComplete = new JSObject(ret, new String[] { "bundle" });
286
+ this.notifyListeners("downloadComplete", retDownloadComplete);
287
+ this.implementation.sendStats("download_complete", bundleInfo.getVersionName());
288
+ lastNotifiedStatPercent = 100;
289
+ } else {
290
+ int currentStatPercent = (percent / 10) * 10; // Round down to nearest 10
291
+ if (currentStatPercent > lastNotifiedStatPercent) {
292
+ this.implementation.sendStats("download_" + currentStatPercent, bundleInfo.getVersionName());
293
+ lastNotifiedStatPercent = currentStatPercent;
329
294
  }
330
- });
331
- }
295
+ }
296
+ } catch (final Exception e) {
297
+ Log.e(CapacitorUpdater.TAG, "Could not notify listeners", e);
332
298
  }
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
299
  }
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
- );
388
- }
389
- }
300
+
301
+ @PluginMethod
302
+ public void setUpdateUrl(final PluginCall call) {
303
+ if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
304
+ Log.e(CapacitorUpdater.TAG, "setUpdateUrl not allowed set allowModifyUrl in your config to true to allow it");
305
+ call.reject("setUpdateUrl not allowed");
306
+ return;
390
307
  }
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
- );
308
+ final String url = call.getString("url");
309
+ if (url == null) {
310
+ Log.e(CapacitorUpdater.TAG, "setUpdateUrl called without url");
311
+ call.reject("setUpdateUrl called without url");
312
+ return;
313
+ }
314
+ this.updateUrl = url;
315
+ call.resolve();
405
316
  }
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);
317
+
318
+ @PluginMethod
319
+ public void setStatsUrl(final PluginCall call) {
320
+ if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
321
+ Log.e(CapacitorUpdater.TAG, "setStatsUrl not allowed set allowModifyUrl in your config to true to allow it");
322
+ call.reject("setStatsUrl not allowed");
323
+ return;
324
+ }
325
+ final String url = call.getString("url");
326
+ if (url == null) {
327
+ Log.e(CapacitorUpdater.TAG, "setStatsUrl called without url");
328
+ call.reject("setStatsUrl called without url");
329
+ return;
330
+ }
331
+ this.implementation.statsUrl = url;
332
+ call.resolve();
415
333
  }
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()) {
334
+
335
+ @PluginMethod
336
+ public void setChannelUrl(final PluginCall call) {
337
+ if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
338
+ Log.e(CapacitorUpdater.TAG, "setChannelUrl not allowed set allowModifyUrl in your config to true to allow it");
339
+ call.reject("setChannelUrl not allowed");
340
+ return;
341
+ }
342
+ final String url = call.getString("url");
343
+ if (url == null) {
344
+ Log.e(CapacitorUpdater.TAG, "setChannelUrl called without url");
345
+ call.reject("setChannelUrl called without url");
346
+ return;
347
+ }
348
+ this.implementation.channelUrl = url;
425
349
  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
350
  }
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;
351
+
352
+ @PluginMethod
353
+ public void getBuiltinVersion(final PluginCall call) {
354
+ try {
355
+ final JSObject ret = new JSObject();
356
+ ret.put("version", this.implementation.versionBuild);
357
+ call.resolve(ret);
358
+ } catch (final Exception e) {
359
+ Log.e(CapacitorUpdater.TAG, "Could not get version", e);
360
+ call.reject("Could not get version", e);
361
+ }
443
362
  }
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);
363
+
364
+ @PluginMethod
365
+ public void getDeviceId(final PluginCall call) {
366
+ try {
367
+ final JSObject ret = new JSObject();
368
+ ret.put("deviceId", this.implementation.deviceID);
369
+ call.resolve(ret);
370
+ } catch (final Exception e) {
371
+ Log.e(CapacitorUpdater.TAG, "Could not get device id", e);
372
+ call.reject("Could not get device id", e);
373
+ }
458
374
  }
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;
375
+
376
+ @PluginMethod
377
+ public void setCustomId(final PluginCall call) {
378
+ final String customId = call.getString("customId");
379
+ if (customId == null) {
380
+ Log.e(CapacitorUpdater.TAG, "setCustomId called without customId");
381
+ call.reject("setCustomId called without customId");
382
+ return;
383
+ }
384
+ this.implementation.customId = customId;
468
385
  }
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);
386
+
387
+ @PluginMethod
388
+ public void getPluginVersion(final PluginCall call) {
389
+ try {
390
+ final JSObject ret = new JSObject();
391
+ ret.put("version", this.PLUGIN_VERSION);
392
+ call.resolve(ret);
393
+ } catch (final Exception e) {
394
+ Log.e(CapacitorUpdater.TAG, "Could not get plugin version", e);
395
+ call.reject("Could not get plugin version", e);
396
+ }
481
397
  }
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;
398
+
399
+ @PluginMethod
400
+ public void unsetChannel(final PluginCall call) {
401
+ final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
402
+
403
+ try {
404
+ Log.i(CapacitorUpdater.TAG, "unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
405
+ startNewThread(() ->
406
+ CapacitorUpdaterPlugin.this.implementation.unsetChannel(res -> {
407
+ if (res.has("error")) {
408
+ call.reject(res.getString("error"));
409
+ } else {
410
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
411
+ Log.i(CapacitorUpdater.TAG, "Calling autoupdater after channel change!");
412
+ backgroundDownload();
413
+ }
414
+ call.resolve(res);
415
+ }
416
+ })
417
+ );
418
+ } catch (final Exception e) {
419
+ Log.e(CapacitorUpdater.TAG, "Failed to unsetChannel: ", e);
420
+ call.reject("Failed to unsetChannel: ", e);
421
+ }
491
422
  }
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);
423
+
424
+ @PluginMethod
425
+ public void setChannel(final PluginCall call) {
426
+ final String channel = call.getString("channel");
427
+ final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
428
+
429
+ if (channel == null) {
430
+ Log.e(CapacitorUpdater.TAG, "setChannel called without channel");
431
+ call.reject("setChannel called without channel");
432
+ return;
433
+ }
434
+ try {
435
+ Log.i(CapacitorUpdater.TAG, "setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
436
+ startNewThread(() ->
437
+ CapacitorUpdaterPlugin.this.implementation.setChannel(channel, res -> {
438
+ if (res.has("error")) {
439
+ call.reject(res.getString("error"));
440
+ } else {
441
+ if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
442
+ Log.i(CapacitorUpdater.TAG, "Calling autoupdater after channel change!");
443
+ backgroundDownload();
444
+ }
445
+ call.resolve(res);
446
+ }
447
+ })
448
+ );
449
+ } catch (final Exception e) {
450
+ Log.e(CapacitorUpdater.TAG, "Failed to setChannel: " + channel, e);
451
+ call.reject("Failed to setChannel: " + channel, e);
452
+ }
507
453
  }
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);
454
+
455
+ @PluginMethod
456
+ public void getChannel(final PluginCall call) {
457
+ try {
458
+ Log.i(CapacitorUpdater.TAG, "getChannel");
459
+ startNewThread(() ->
460
+ CapacitorUpdaterPlugin.this.implementation.getChannel(res -> {
461
+ if (res.has("error")) {
462
+ call.reject(res.getString("error"));
463
+ } else {
464
+ call.resolve(res);
465
+ }
466
+ })
467
+ );
468
+ } catch (final Exception e) {
469
+ Log.e(CapacitorUpdater.TAG, "Failed to getChannel", e);
470
+ call.reject("Failed to getChannel", e);
471
+ }
524
472
  }
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"));
539
- 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
- }
473
+
474
+ @PluginMethod
475
+ public void download(final PluginCall call) {
476
+ final String url = call.getString("url");
477
+ final String version = call.getString("version");
478
+ final String sessionKey = call.getString("sessionKey", "");
479
+ final String checksum = call.getString("checksum", "");
480
+ if (url == null) {
481
+ Log.e(CapacitorUpdater.TAG, "Download called without url");
482
+ call.reject("Download called without url");
483
+ return;
484
+ }
485
+ if (version == null) {
486
+ Log.e(CapacitorUpdater.TAG, "Download called without version");
487
+ call.reject("Download called without version");
488
+ return;
489
+ }
490
+ try {
491
+ Log.i(CapacitorUpdater.TAG, "Downloading " + url);
492
+ startNewThread(() -> {
493
+ try {
494
+ final BundleInfo downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
495
+ if (downloaded.isErrorStatus()) {
496
+ throw new RuntimeException("Download failed: " + downloaded.getStatus());
497
+ } else {
498
+ call.resolve(downloaded.toJSON());
553
499
  }
554
- }
555
- call.resolve(ret);
500
+ } catch (final Exception e) {
501
+ Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
502
+ call.reject("Failed to download from: " + url, e);
503
+ final JSObject ret = new JSObject();
504
+ ret.put("version", version);
505
+ CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
506
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
507
+ CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
556
508
  }
557
- );
558
- }
509
+ });
510
+ } catch (final Exception e) {
511
+ Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
512
+ call.reject("Failed to download from: " + url, e);
513
+ final JSObject ret = new JSObject();
514
+ ret.put("version", version);
515
+ CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
516
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
517
+ CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
559
518
  }
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
519
  }
566
- }
567
520
 
568
- private boolean _reset(final Boolean toLastSuccessful) {
569
- final BundleInfo fallback = this.implementation.getFallbackBundle();
570
- this.implementation.reset();
521
+ protected boolean _reload() {
522
+ final String path = this.implementation.getCurrentBundlePath();
523
+ this.semaphoreUp();
524
+ Log.i(CapacitorUpdater.TAG, "Reloading: " + path);
525
+
526
+ AtomicReference<URL> url = new AtomicReference<>();
527
+ if (this.keepUrlPathAfterReload) {
528
+ try {
529
+ if (Looper.myLooper() != Looper.getMainLooper()) {
530
+ Semaphore mainThreadSemaphore = new Semaphore(0);
531
+ this.bridge.executeOnMainThread(() -> {
532
+ try {
533
+ url.set(new URL(this.bridge.getWebView().getUrl()));
534
+ } catch (Exception e) {
535
+ Log.e(CapacitorUpdater.TAG, "Error executing on main thread", e);
536
+ }
537
+ mainThreadSemaphore.release();
538
+ });
539
+ mainThreadSemaphore.acquire();
540
+ } else {
541
+ try {
542
+ url.set(new URL(this.bridge.getWebView().getUrl()));
543
+ } catch (Exception e) {
544
+ Log.e(CapacitorUpdater.TAG, "Error executing on main thread", e);
545
+ }
546
+ }
547
+ } catch (InterruptedException e) {
548
+ Log.e(CapacitorUpdater.TAG, "Error waiting for main thread or getting the current URL from webview", e);
549
+ }
550
+ }
551
+
552
+ if (url.get() != null) {
553
+ if (this.implementation.isUsingBuiltin()) {
554
+ this.bridge.getLocalServer().hostAssets(path);
555
+ } else {
556
+ this.bridge.getLocalServer().hostFiles(path);
557
+ }
571
558
 
572
- if (toLastSuccessful && !fallback.isBuiltin()) {
573
- Log.i(CapacitorUpdater.TAG, "Resetting to: " + fallback);
574
- return this.implementation.set(fallback) && this._reload();
559
+ try {
560
+ URL finalUrl = null;
561
+ finalUrl = new URL(this.bridge.getAppUrl());
562
+ finalUrl = new URL(finalUrl.getProtocol(), finalUrl.getHost(), finalUrl.getPort(), url.get().getPath());
563
+ URL finalUrl1 = finalUrl;
564
+ this.bridge.getWebView()
565
+ .post(() -> {
566
+ this.bridge.getWebView().loadUrl(finalUrl1.toString());
567
+ this.bridge.getWebView().clearHistory();
568
+ });
569
+ } catch (MalformedURLException e) {
570
+ Log.e(CapacitorUpdater.TAG, "Cannot get finalUrl from capacitor bridge", e);
571
+
572
+ if (this.implementation.isUsingBuiltin()) {
573
+ this.bridge.setServerAssetPath(path);
574
+ } else {
575
+ this.bridge.setServerBasePath(path);
576
+ }
577
+ }
578
+ } else {
579
+ if (this.implementation.isUsingBuiltin()) {
580
+ this.bridge.setServerAssetPath(path);
581
+ } else {
582
+ this.bridge.setServerBasePath(path);
583
+ }
584
+ }
585
+
586
+ this.checkAppReady();
587
+ this.notifyListeners("appReloaded", new JSObject());
588
+ return true;
575
589
  }
576
590
 
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);
591
+ @PluginMethod
592
+ public void reload(final PluginCall call) {
593
+ try {
594
+ if (this._reload()) {
595
+ call.resolve();
596
+ } else {
597
+ Log.e(CapacitorUpdater.TAG, "Reload failed");
598
+ call.reject("Reload failed");
599
+ }
600
+ } catch (final Exception e) {
601
+ Log.e(CapacitorUpdater.TAG, "Could not reload", e);
602
+ call.reject("Could not reload", e);
603
+ }
597
604
  }
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);
605
+
606
+ @PluginMethod
607
+ public void next(final PluginCall call) {
608
+ final String id = call.getString("id");
609
+ if (id == null) {
610
+ Log.e(CapacitorUpdater.TAG, "Next called without id");
611
+ call.reject("Next called without id");
612
+ return;
613
+ }
614
+ try {
615
+ Log.i(CapacitorUpdater.TAG, "Setting next active id " + id);
616
+ if (!this.implementation.setNextBundle(id)) {
617
+ Log.e(CapacitorUpdater.TAG, "Set next id failed. Bundle " + id + " does not exist.");
618
+ call.reject("Set next id failed. Bundle " + id + " does not exist.");
619
+ } else {
620
+ call.resolve(this.implementation.getBundleInfo(id).toJSON());
621
+ }
622
+ } catch (final Exception e) {
623
+ Log.e(CapacitorUpdater.TAG, "Could not set next id " + id, e);
624
+ call.reject("Could not set next id: " + id, e);
625
+ }
611
626
  }
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);
627
+
628
+ @PluginMethod
629
+ public void set(final PluginCall call) {
630
+ final String id = call.getString("id");
631
+ if (id == null) {
632
+ Log.e(CapacitorUpdater.TAG, "Set called without id");
633
+ call.reject("Set called without id");
634
+ return;
635
+ }
636
+ try {
637
+ Log.i(CapacitorUpdater.TAG, "Setting active bundle " + id);
638
+ if (!this.implementation.set(id)) {
639
+ Log.i(CapacitorUpdater.TAG, "No such bundle " + id);
640
+ call.reject("Update failed, id " + id + " does not exist.");
641
+ } else {
642
+ Log.i(CapacitorUpdater.TAG, "Bundle successfully set to " + id);
643
+ this.reload(call);
644
+ }
645
+ } catch (final Exception e) {
646
+ Log.e(CapacitorUpdater.TAG, "Could not set id " + id, e);
647
+ call.reject("Could not set id " + id, e);
648
+ }
632
649
  }
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"
643
- );
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);
650
+
651
+ @PluginMethod
652
+ public void delete(final PluginCall call) {
653
+ final String id = call.getString("id");
654
+ if (id == null) {
655
+ Log.e(CapacitorUpdater.TAG, "missing id");
656
+ call.reject("missing id");
657
+ return;
658
+ }
659
+ Log.i(CapacitorUpdater.TAG, "Deleting id " + id);
660
+ try {
661
+ final Boolean res = this.implementation.delete(id);
662
+ if (res) {
663
+ call.resolve();
664
+ } else {
665
+ Log.e(CapacitorUpdater.TAG, "Delete failed, id " + id + " does not exist");
666
+ call.reject("Delete failed, id " + id + " does not exist or it cannot be deleted (perhaps it is the 'next' bundle)");
667
+ }
668
+ } catch (final Exception e) {
669
+ Log.e(CapacitorUpdater.TAG, "Could not delete id " + id, e);
670
+ call.reject("Could not delete id " + id, e);
671
+ }
659
672
  }
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;
673
+
674
+ @PluginMethod
675
+ public void list(final PluginCall call) {
676
+ try {
677
+ final List<BundleInfo> res = this.implementation.list(call.getBoolean("raw", false));
678
+ final JSObject ret = new JSObject();
679
+ final JSArray values = new JSArray();
680
+ for (final BundleInfo bundle : res) {
681
+ values.put(bundle.toJSON());
682
+ }
683
+ ret.put("bundles", values);
684
+ call.resolve(ret);
685
+ } catch (final Exception e) {
686
+ Log.e(CapacitorUpdater.TAG, "Could not list bundles", e);
687
+ call.reject("Could not list bundles", e);
688
+ }
675
689
  }
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;
690
+
691
+ @PluginMethod
692
+ public void getLatest(final PluginCall call) {
693
+ final String channel = call.getString("channel");
694
+ startNewThread(() ->
695
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, res -> {
696
+ if (res.has("error")) {
697
+ call.reject(res.getString("error"));
698
+ return;
699
+ } else if (res.has("message")) {
700
+ call.reject(res.getString("message"));
701
+ return;
702
+ } else {
703
+ call.resolve(res);
704
+ }
705
+ final JSObject ret = new JSObject();
706
+ Iterator<String> keys = res.keys();
707
+ while (keys.hasNext()) {
708
+ String key = keys.next();
709
+ if (res.has(key)) {
710
+ try {
711
+ ret.put(key, res.get(key));
712
+ } catch (JSONException e) {
713
+ e.printStackTrace();
714
+ }
715
+ }
716
+ }
717
+ call.resolve(ret);
718
+ })
719
+ );
687
720
  }
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");
721
+
722
+ private boolean _reset(final Boolean toLastSuccessful) {
723
+ final BundleInfo fallback = this.implementation.getFallbackBundle();
724
+ this.implementation.reset();
725
+
726
+ if (toLastSuccessful && !fallback.isBuiltin()) {
727
+ Log.i(CapacitorUpdater.TAG, "Resetting to: " + fallback);
728
+ return this.implementation.set(fallback) && this._reload();
729
+ }
730
+
731
+ Log.i(CapacitorUpdater.TAG, "Resetting to native.");
732
+ return this._reload();
696
733
  }
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");
734
+
735
+ @PluginMethod
736
+ public void reset(final PluginCall call) {
737
+ try {
738
+ final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
739
+ if (this._reset(toLastSuccessful)) {
740
+ call.resolve();
741
+ return;
718
742
  }
719
- break;
720
- case "kill":
721
- if (killed) {
722
- this._cancelDelay("kill check");
723
- this.installNext();
743
+ Log.e(CapacitorUpdater.TAG, "Reset failed");
744
+ call.reject("Reset failed");
745
+ } catch (final Exception e) {
746
+ Log.e(CapacitorUpdater.TAG, "Reset failed", e);
747
+ call.reject("Reset failed", e);
748
+ }
749
+ }
750
+
751
+ @PluginMethod
752
+ public void current(final PluginCall call) {
753
+ try {
754
+ final JSObject ret = new JSObject();
755
+ final BundleInfo bundle = this.implementation.getCurrentBundle();
756
+ ret.put("bundle", bundle.toJSON());
757
+ ret.put("native", this.currentVersionNative);
758
+ call.resolve(ret);
759
+ } catch (final Exception e) {
760
+ Log.e(CapacitorUpdater.TAG, "Could not get current bundle", e);
761
+ call.reject("Could not get current bundle", e);
762
+ }
763
+ }
764
+
765
+ @PluginMethod
766
+ public void getNextBundle(final PluginCall call) {
767
+ try {
768
+ final BundleInfo bundle = this.implementation.getNextBundle();
769
+ if (bundle == null) {
770
+ call.resolve(null);
771
+ return;
724
772
  }
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");
773
+
774
+ call.resolve(bundle.toJSON());
775
+ } catch (final Exception e) {
776
+ Log.e(CapacitorUpdater.TAG, "Could not get next bundle", e);
777
+ call.reject("Could not get next bundle", e);
778
+ }
779
+ }
780
+
781
+ public void checkForUpdateAfterDelay() {
782
+ if (this.periodCheckDelay == 0 || !this._isAutoUpdateEnabled()) {
783
+ return;
784
+ }
785
+ final Timer timer = new Timer();
786
+ timer.schedule(
787
+ new TimerTask() {
788
+ @Override
789
+ public void run() {
790
+ try {
791
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, res -> {
792
+ if (res.has("error")) {
793
+ Log.e(CapacitorUpdater.TAG, Objects.requireNonNull(res.getString("error")));
794
+ } else if (res.has("version")) {
795
+ String newVersion = res.getString("version");
796
+ String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
797
+ if (!Objects.equals(newVersion, currentVersion)) {
798
+ Log.i(CapacitorUpdater.TAG, "New version found: " + newVersion);
799
+ CapacitorUpdaterPlugin.this.backgroundDownload();
800
+ }
801
+ }
802
+ });
803
+ } catch (final Exception e) {
804
+ Log.e(CapacitorUpdater.TAG, "Failed to check for update", e);
805
+ }
736
806
  }
737
- } catch (final Exception e) {
738
- this._cancelDelay("date parsing issue");
739
- }
740
- } else {
741
- this._cancelDelay("delayVal absent");
807
+ },
808
+ this.periodCheckDelay,
809
+ this.periodCheckDelay
810
+ );
811
+ }
812
+
813
+ @PluginMethod
814
+ public void notifyAppReady(final PluginCall call) {
815
+ try {
816
+ final BundleInfo bundle = this.implementation.getCurrentBundle();
817
+ this.implementation.setSuccess(bundle, this.autoDeletePrevious);
818
+ Log.i(CapacitorUpdater.TAG, "Current bundle loaded successfully. ['notifyAppReady()' was called] " + bundle);
819
+ Log.i(CapacitorUpdater.TAG, "semaphoreReady countDown");
820
+ this.semaphoreDown();
821
+ Log.i(CapacitorUpdater.TAG, "semaphoreReady countDown done");
822
+ final JSObject ret = new JSObject();
823
+ ret.put("bundle", bundle.toJSON());
824
+ call.resolve(ret);
825
+ } catch (final Exception e) {
826
+ Log.e(CapacitorUpdater.TAG, "Failed to notify app ready state. [Error calling 'notifyAppReady()']", e);
827
+ call.reject("Failed to commit app ready state.", e);
828
+ }
829
+ }
830
+
831
+ @PluginMethod
832
+ public void setMultiDelay(final PluginCall call) {
833
+ try {
834
+ final Object delayConditions = call.getData().opt("delayConditions");
835
+ if (delayConditions == null) {
836
+ Log.e(CapacitorUpdater.TAG, "setMultiDelay called without delayCondition");
837
+ call.reject("setMultiDelay called without delayCondition");
838
+ return;
742
839
  }
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");
750
- }
751
- } catch (final Exception e) {
752
- this._cancelDelay("nativeVersion parsing issue");
753
- }
840
+ if (_setMultiDelay(delayConditions.toString())) {
841
+ call.resolve();
754
842
  } else {
755
- this._cancelDelay("delayVal absent");
843
+ call.reject("Failed to delay update");
756
844
  }
757
- break;
845
+ } catch (final Exception e) {
846
+ Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling 'setMultiDelay()']", e);
847
+ call.reject("Failed to delay update", e);
758
848
  }
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
849
  }
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);
850
+
851
+ private Boolean _setMultiDelay(String delayConditions) {
852
+ try {
853
+ this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions);
854
+ this.editor.commit();
855
+ Log.i(CapacitorUpdater.TAG, "Delay update saved");
856
+ return true;
857
+ } catch (final Exception e) {
858
+ Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling '_setMultiDelay()']", e);
859
+ return false;
860
+ }
790
861
  }
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
- );
862
+
863
+ private boolean _cancelDelay(String source) {
864
+ try {
865
+ this.editor.remove(DELAY_CONDITION_PREFERENCES);
866
+ this.editor.commit();
867
+ Log.i(CapacitorUpdater.TAG, "All delays canceled from " + source);
868
+ return true;
869
+ } catch (final Exception e) {
870
+ Log.e(CapacitorUpdater.TAG, "Failed to cancel update delay", e);
871
+ return false;
872
+ }
806
873
  }
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;
874
+
875
+ @PluginMethod
876
+ public void cancelDelay(final PluginCall call) {
877
+ if (this._cancelDelay("JS")) {
878
+ call.resolve();
879
+ } else {
880
+ call.reject("Failed to cancel delay");
881
+ }
815
882
  }
816
- }
817
883
 
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."
889
- );
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."
902
- );
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
- 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
- );
884
+ private void _checkCancelDelay(Boolean killed) {
885
+ Gson gson = new Gson();
886
+ String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
887
+ Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
888
+ ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
889
+ for (DelayCondition condition : delayConditionList) {
890
+ String kind = condition.getKind().toString();
891
+ String value = condition.getValue();
892
+ if (!kind.isEmpty()) {
893
+ switch (kind) {
894
+ case "background":
895
+ if (!killed) {
896
+ this._cancelDelay("background check");
939
897
  }
940
- }
941
- }
942
-
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."
955
- );
956
-
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
975
- );
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
- }
898
+ break;
899
+ case "kill":
900
+ if (killed) {
901
+ this._cancelDelay("kill check");
902
+ this.installNext();
903
+ }
904
+ break;
905
+ case "date":
906
+ if (!"".equals(value)) {
907
+ try {
908
+ final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
909
+ Date date = sdf.parse(value);
910
+ assert date != null;
911
+ if (new Date().compareTo(date) > 0) {
912
+ this._cancelDelay("date expired");
913
+ }
914
+ } catch (final Exception e) {
915
+ this._cancelDelay("date parsing issue");
916
+ }
917
+ } else {
918
+ this._cancelDelay("delayVal absent");
919
+ }
920
+ break;
921
+ case "nativeVersion":
922
+ if (!"".equals(value)) {
923
+ try {
924
+ final Version versionLimit = new Version(value);
925
+ if (this.currentVersionNative.isAtLeast(versionLimit)) {
926
+ this._cancelDelay("nativeVersion above limit");
927
+ }
928
+ } catch (final Exception e) {
929
+ this._cancelDelay("nativeVersion parsing issue");
930
+ }
931
+ } else {
932
+ this._cancelDelay("delayVal absent");
995
933
  }
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."
1005
- );
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
1020
- );
934
+ break;
1021
935
  }
1022
- }
1023
- );
936
+ }
1024
937
  }
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
- );
938
+ }
939
+
940
+ private Boolean _isAutoUpdateEnabled() {
941
+ final CapConfig config = CapConfig.loadDefault(this.getActivity());
942
+ String serverUrl = config.getServerUrl();
943
+ if (serverUrl != null && !serverUrl.isEmpty()) {
944
+ // log warning autoupdate disabled when serverUrl is set
945
+ Log.w(CapacitorUpdater.TAG, "AutoUpdate is automatic disabled when serverUrl is set.");
1067
946
  }
1068
- }
1069
- } catch (final Exception e) {
1070
- Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
947
+ return (
948
+ CapacitorUpdaterPlugin.this.autoUpdate &&
949
+ !"".equals(CapacitorUpdaterPlugin.this.updateUrl) &&
950
+ (serverUrl == null || serverUrl.isEmpty())
951
+ );
1071
952
  }
1072
- }
1073
953
 
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();
954
+ @PluginMethod
955
+ public void isAutoUpdateEnabled(final PluginCall call) {
956
+ try {
957
+ final JSObject ret = new JSObject();
958
+ ret.put("enabled", this._isAutoUpdateEnabled());
959
+ call.resolve(ret);
960
+ } catch (final Exception e) {
961
+ Log.e(CapacitorUpdater.TAG, "Could not get autoUpdate status", e);
962
+ call.reject("Could not get autoUpdate status", e);
963
+ }
964
+ }
1077
965
 
1078
- if (current.isBuiltin()) {
1079
- Log.i(CapacitorUpdater.TAG, "Built-in bundle is active. Nothing to do.");
1080
- return;
966
+ @PluginMethod
967
+ public void isAutoUpdateAvailable(final PluginCall call) {
968
+ try {
969
+ final CapConfig config = CapConfig.loadDefault(this.getActivity());
970
+ String serverUrl = config.getServerUrl();
971
+ final JSObject ret = new JSObject();
972
+ ret.put("available", serverUrl == null || serverUrl.isEmpty());
973
+ call.resolve(ret);
974
+ } catch (final Exception e) {
975
+ Log.e(CapacitorUpdater.TAG, "Could not get autoUpdate availability", e);
976
+ call.reject("Could not get autoUpdate availability", e);
977
+ }
1081
978
  }
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
- );
979
+
980
+ private void checkAppReady() {
981
+ try {
982
+ if (this.appReadyCheck != null) {
983
+ this.appReadyCheck.interrupt();
984
+ }
985
+ this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck());
986
+ } catch (final Exception e) {
987
+ Log.e(CapacitorUpdater.TAG, "Failed to start " + DeferredNotifyAppReadyCheck.class.getName(), e);
988
+ }
989
+ }
990
+
991
+ private boolean isValidURL(String urlStr) {
1107
992
  try {
1108
- final Boolean res =
1109
- this.implementation.delete(current.getId(), false);
1110
- if (res) {
993
+ new URL(urlStr);
994
+ return true;
995
+ } catch (MalformedURLException e) {
996
+ return false;
997
+ }
998
+ }
999
+
1000
+ private void endBackGroundTaskWithNotif(String msg, String latestVersionName, BundleInfo current, Boolean error) {
1001
+ if (error) {
1111
1002
  Log.i(
1112
- CapacitorUpdater.TAG,
1113
- "Failed bundle deleted: " + current.getVersionName()
1003
+ CapacitorUpdater.TAG,
1004
+ "endBackGroundTaskWithNotif error: " +
1005
+ error +
1006
+ " current: " +
1007
+ current.getVersionName() +
1008
+ "latestVersionName: " +
1009
+ latestVersionName
1114
1010
  );
1115
- }
1116
- } catch (final IOException e) {
1117
- Log.e(
1118
- CapacitorUpdater.TAG,
1119
- "Failed to delete failed bundle: " + current.getVersionName(),
1120
- e
1121
- );
1011
+ this.implementation.sendStats("download_fail", current.getVersionName());
1012
+ final JSObject ret = new JSObject();
1013
+ ret.put("version", latestVersionName);
1014
+ this.notifyListeners("downloadFailed", ret);
1122
1015
  }
1123
- }
1124
- } else {
1125
- Log.i(
1126
- CapacitorUpdater.TAG,
1127
- "notifyAppReady was called. This is fine: " + current.getId()
1128
- );
1016
+ final JSObject ret = new JSObject();
1017
+ ret.put("bundle", current.toJSON());
1018
+ this.notifyListeners("noNeedUpdate", ret);
1019
+ this.sendReadyToJs(current, msg);
1020
+ this.backgroundDownloadTask = null;
1021
+ Log.i(CapacitorUpdater.TAG, "endBackGroundTaskWithNotif " + msg);
1129
1022
  }
1130
- }
1131
1023
 
1132
- private class DeferredNotifyAppReadyCheck implements Runnable {
1024
+ private Thread backgroundDownload() {
1025
+ String messageUpdate = this.implementation.directUpdate
1026
+ ? "Update will occur now."
1027
+ : "Update will occur next time app moves to background.";
1028
+ return startNewThread(() -> {
1029
+ Log.i(CapacitorUpdater.TAG, "Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
1030
+ CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, res -> {
1031
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1032
+ try {
1033
+ if (res.has("message")) {
1034
+ Log.i(CapacitorUpdater.TAG, "API message: " + res.get("message"));
1035
+ if (res.has("major") && res.getBoolean("major") && res.has("version")) {
1036
+ final JSObject majorAvailable = new JSObject();
1037
+ majorAvailable.put("version", res.getString("version"));
1038
+ CapacitorUpdaterPlugin.this.notifyListeners("majorAvailable", majorAvailable);
1039
+ }
1040
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1041
+ res.getString("message"),
1042
+ current.getVersionName(),
1043
+ current,
1044
+ true
1045
+ );
1046
+ return;
1047
+ }
1133
1048
 
1134
- @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();
1049
+ final String latestVersionName = res.getString("version");
1050
+
1051
+ if ("builtin".equals(latestVersionName)) {
1052
+ Log.i(CapacitorUpdater.TAG, "Latest version is builtin");
1053
+ if (CapacitorUpdaterPlugin.this.implementation.directUpdate) {
1054
+ Log.i(CapacitorUpdater.TAG, "Direct update to builtin version");
1055
+ this._reset(false);
1056
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1057
+ "Updated to builtin version",
1058
+ latestVersionName,
1059
+ CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1060
+ false
1061
+ );
1062
+ } else {
1063
+ Log.i(CapacitorUpdater.TAG, "Setting next bundle to builtin");
1064
+ CapacitorUpdaterPlugin.this.implementation.setNextBundle(BundleInfo.ID_BUILTIN);
1065
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1066
+ "Next update will be to builtin version",
1067
+ latestVersionName,
1068
+ current,
1069
+ false
1070
+ );
1071
+ }
1072
+ return;
1073
+ }
1074
+
1075
+ if (!res.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(res.getString("url"))) {
1076
+ Log.e(CapacitorUpdater.TAG, "Error no url or wrong format");
1077
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1078
+ "Error no url or wrong format",
1079
+ current.getVersionName(),
1080
+ current,
1081
+ true
1082
+ );
1083
+ return;
1084
+ }
1085
+
1086
+ if (
1087
+ latestVersionName != null && !latestVersionName.isEmpty() && !current.getVersionName().equals(latestVersionName)
1088
+ ) {
1089
+ final BundleInfo latest = CapacitorUpdaterPlugin.this.implementation.getBundleInfoByName(latestVersionName);
1090
+ if (latest != null) {
1091
+ final JSObject ret = new JSObject();
1092
+ ret.put("bundle", latest.toJSON());
1093
+ if (latest.isErrorStatus()) {
1094
+ Log.e(CapacitorUpdater.TAG, "Latest bundle already exists, and is in error state. Aborting update.");
1095
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1096
+ "Latest bundle already exists, and is in error state. Aborting update.",
1097
+ latestVersionName,
1098
+ current,
1099
+ true
1100
+ );
1101
+ return;
1102
+ }
1103
+ if (latest.isDownloaded()) {
1104
+ Log.i(
1105
+ CapacitorUpdater.TAG,
1106
+ "Latest bundle already exists and download is NOT required. " + messageUpdate
1107
+ );
1108
+ if (CapacitorUpdaterPlugin.this.implementation.directUpdate) {
1109
+ CapacitorUpdaterPlugin.this.implementation.set(latest);
1110
+ CapacitorUpdaterPlugin.this._reload();
1111
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1112
+ "Update installed",
1113
+ latestVersionName,
1114
+ latest,
1115
+ false
1116
+ );
1117
+ } else {
1118
+ CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
1119
+ CapacitorUpdaterPlugin.this.implementation.setNextBundle(latest.getId());
1120
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1121
+ "update downloaded, will install next background",
1122
+ latestVersionName,
1123
+ latest,
1124
+ false
1125
+ );
1126
+ }
1127
+ return;
1128
+ }
1129
+ if (latest.isDeleted()) {
1130
+ Log.i(
1131
+ CapacitorUpdater.TAG,
1132
+ "Latest bundle already exists and will be deleted, download will overwrite it."
1133
+ );
1134
+ try {
1135
+ final Boolean deleted = CapacitorUpdaterPlugin.this.implementation.delete(latest.getId(), true);
1136
+ if (deleted) {
1137
+ Log.i(CapacitorUpdater.TAG, "Failed bundle deleted: " + latest.getVersionName());
1138
+ }
1139
+ } catch (final IOException e) {
1140
+ Log.e(CapacitorUpdater.TAG, "Failed to delete failed bundle: " + latest.getVersionName(), e);
1141
+ }
1142
+ }
1143
+ }
1144
+ startNewThread(() -> {
1145
+ try {
1146
+ Log.i(
1147
+ CapacitorUpdater.TAG,
1148
+ "New bundle: " +
1149
+ latestVersionName +
1150
+ " found. Current is: " +
1151
+ current.getVersionName() +
1152
+ ". " +
1153
+ messageUpdate
1154
+ );
1155
+
1156
+ final String url = res.getString("url");
1157
+ final String sessionKey = res.has("sessionKey") ? res.getString("sessionKey") : "";
1158
+ final String checksum = res.has("checksum") ? res.getString("checksum") : "";
1159
+
1160
+ if (res.has("manifest")) {
1161
+ // Handle manifest-based download
1162
+ JSONArray manifest = res.getJSONArray("manifest");
1163
+ CapacitorUpdaterPlugin.this.implementation.downloadBackground(
1164
+ url,
1165
+ latestVersionName,
1166
+ sessionKey,
1167
+ checksum,
1168
+ manifest
1169
+ );
1170
+ } else {
1171
+ // Handle single file download (existing code)
1172
+ CapacitorUpdaterPlugin.this.implementation.downloadBackground(
1173
+ url,
1174
+ latestVersionName,
1175
+ sessionKey,
1176
+ checksum,
1177
+ null
1178
+ );
1179
+ }
1180
+ } catch (final Exception e) {
1181
+ Log.e(CapacitorUpdater.TAG, "error downloading file", e);
1182
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1183
+ "Error downloading file",
1184
+ latestVersionName,
1185
+ CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
1186
+ true
1187
+ );
1188
+ }
1189
+ });
1190
+ } else {
1191
+ Log.i(CapacitorUpdater.TAG, "No need to update, " + current.getId() + " is the latest bundle.");
1192
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif("No need to update", latestVersionName, current, false);
1193
+ }
1194
+ } catch (final JSONException e) {
1195
+ Log.e(CapacitorUpdater.TAG, "error parsing JSON", e);
1196
+ CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
1197
+ "Error parsing JSON",
1198
+ current.getVersionName(),
1199
+ current,
1200
+ true
1201
+ );
1202
+ }
1203
+ });
1204
+ });
1164
1205
  }
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";
1206
+
1207
+ private void installNext() {
1208
+ try {
1209
+ Gson gson = new Gson();
1210
+ String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
1211
+ Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1212
+ ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
1213
+ if (delayConditionList != null && !delayConditionList.isEmpty()) {
1214
+ Log.i(CapacitorUpdater.TAG, "Update delayed until delay conditions met");
1215
+ return;
1216
+ }
1217
+ final BundleInfo current = this.implementation.getCurrentBundle();
1218
+ final BundleInfo next = this.implementation.getNextBundle();
1219
+
1220
+ if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
1221
+ // There is a next bundle waiting for activation
1222
+ Log.d(CapacitorUpdater.TAG, "Next bundle is: " + next.getVersionName());
1223
+ if (this.implementation.set(next) && this._reload()) {
1224
+ Log.i(CapacitorUpdater.TAG, "Updated to bundle: " + next.getVersionName());
1225
+ this.implementation.setNextBundle(null);
1226
+ } else {
1227
+ Log.e(CapacitorUpdater.TAG, "Update to bundle: " + next.getVersionName() + " Failed!");
1228
+ }
1229
+ }
1230
+ } catch (final Exception e) {
1231
+ Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1192
1232
  }
1193
- }
1194
- if (backgroundValue != null) {
1195
- taskRunning = true;
1196
- final Long timeout = Long.parseLong(backgroundValue);
1197
- if (backgroundTask != null) {
1198
- backgroundTask.interrupt();
1233
+ }
1234
+
1235
+ private void checkRevert() {
1236
+ // Automatically roll back to fallback version if notifyAppReady has not been called yet
1237
+ final BundleInfo current = this.implementation.getCurrentBundle();
1238
+
1239
+ if (current.isBuiltin()) {
1240
+ Log.i(CapacitorUpdater.TAG, "Built-in bundle is active. We skip the check for notifyAppReady.");
1241
+ return;
1199
1242
  }
1200
- backgroundTask =
1201
- new Thread(
1202
- new Runnable() {
1203
- @Override
1204
- public void run() {
1243
+ Log.d(CapacitorUpdater.TAG, "Current bundle is: " + current);
1244
+
1245
+ if (BundleStatus.SUCCESS != current.getStatus()) {
1246
+ Log.e(CapacitorUpdater.TAG, "notifyAppReady was not called, roll back current bundle: " + current.getId());
1247
+ Log.i(CapacitorUpdater.TAG, "Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
1248
+ final JSObject ret = new JSObject();
1249
+ ret.put("bundle", current.toJSON());
1250
+ this.notifyListeners("updateFailed", ret);
1251
+ this.implementation.sendStats("update_fail", current.getVersionName());
1252
+ this.implementation.setError(current);
1253
+ this._reset(true);
1254
+ if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
1255
+ Log.i(CapacitorUpdater.TAG, "Deleting failing bundle: " + current.getVersionName());
1205
1256
  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
- );
1257
+ final Boolean res = this.implementation.delete(current.getId(), false);
1258
+ if (res) {
1259
+ Log.i(CapacitorUpdater.TAG, "Failed bundle deleted: " + current.getVersionName());
1260
+ }
1261
+ } catch (final IOException e) {
1262
+ Log.e(CapacitorUpdater.TAG, "Failed to delete failed bundle: " + current.getVersionName(), e);
1215
1263
  }
1216
- }
1217
1264
  }
1218
- );
1219
- backgroundTask.start();
1220
- } else {
1265
+ } else {
1266
+ Log.i(CapacitorUpdater.TAG, "notifyAppReady was called. This is fine: " + current.getId());
1267
+ }
1268
+ }
1269
+
1270
+ private class DeferredNotifyAppReadyCheck implements Runnable {
1271
+
1272
+ @Override
1273
+ public void run() {
1274
+ try {
1275
+ Log.i(
1276
+ CapacitorUpdater.TAG,
1277
+ "Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady"
1278
+ );
1279
+ Thread.sleep(CapacitorUpdaterPlugin.this.appReadyTimeout);
1280
+ CapacitorUpdaterPlugin.this.checkRevert();
1281
+ CapacitorUpdaterPlugin.this.appReadyCheck = null;
1282
+ } catch (final InterruptedException e) {
1283
+ Log.i(CapacitorUpdater.TAG, DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
1284
+ }
1285
+ }
1286
+ }
1287
+
1288
+ public void appMovedToForeground() {
1289
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1290
+ CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_foreground", current.getVersionName());
1221
1291
  this._checkCancelDelay(false);
1222
- this.installNext();
1223
- }
1224
- } catch (final Exception e) {
1225
- Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1292
+ if (
1293
+ CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() &&
1294
+ (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive())
1295
+ ) {
1296
+ this.backgroundDownloadTask = this.backgroundDownload();
1297
+ } else {
1298
+ Log.i(CapacitorUpdater.TAG, "Auto update is disabled");
1299
+ this.sendReadyToJs(current, "disabled");
1300
+ }
1301
+ this.checkAppReady();
1226
1302
  }
1227
- }
1228
1303
 
1229
- private boolean isMainActivity() {
1230
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1231
- return false;
1304
+ public void appMovedToBackground() {
1305
+ final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1306
+ CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_background", current.getVersionName());
1307
+ Log.i(CapacitorUpdater.TAG, "Checking for pending update");
1308
+ try {
1309
+ Gson gson = new Gson();
1310
+ String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
1311
+ Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1312
+ ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
1313
+ String backgroundValue = null;
1314
+ for (DelayCondition delayCondition : delayConditionList) {
1315
+ if (delayCondition.getKind().toString().equals("background")) {
1316
+ String value = delayCondition.getValue();
1317
+ backgroundValue = (value != null && !value.isEmpty()) ? value : "0";
1318
+ }
1319
+ }
1320
+ if (backgroundValue != null) {
1321
+ taskRunning = true;
1322
+ final Long timeout = Long.parseLong(backgroundValue);
1323
+ if (backgroundTask != null) {
1324
+ backgroundTask.interrupt();
1325
+ }
1326
+ backgroundTask = startNewThread(
1327
+ () -> {
1328
+ taskRunning = false;
1329
+ _checkCancelDelay(false);
1330
+ installNext();
1331
+ },
1332
+ timeout
1333
+ );
1334
+ } else {
1335
+ this._checkCancelDelay(false);
1336
+ this.installNext();
1337
+ }
1338
+ } catch (final Exception e) {
1339
+ Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1340
+ }
1232
1341
  }
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();
1342
+
1343
+ private boolean isMainActivity() {
1344
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1345
+ return false;
1346
+ }
1347
+ try {
1348
+ Context mContext = this.getContext();
1349
+ ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
1350
+ List<ActivityManager.AppTask> runningTasks = activityManager.getAppTasks();
1351
+ if (runningTasks.isEmpty()) {
1352
+ return false;
1353
+ }
1354
+ ActivityManager.RecentTaskInfo runningTask = runningTasks.get(0).getTaskInfo();
1355
+ String className = Objects.requireNonNull(runningTask.baseIntent.getComponent()).getClassName();
1356
+ if (runningTask.topActivity == null) {
1357
+ return false;
1358
+ }
1359
+ String runningActivity = runningTask.topActivity.getClassName();
1360
+ return className.equals(runningActivity);
1361
+ } catch (NullPointerException e) {
1362
+ return false;
1363
+ }
1255
1364
  }
1256
- isPreviousMainActivity = true;
1257
- }
1258
-
1259
- @Override
1260
- public void onActivityStopped(@NonNull final Activity activity) {
1261
- isPreviousMainActivity = isMainActivity();
1262
- if (isPreviousMainActivity) {
1263
- this.appMovedToBackground();
1365
+
1366
+ private void appKilled() {
1367
+ Log.d(CapacitorUpdater.TAG, "onActivityDestroyed: all activity destroyed");
1368
+ this._checkCancelDelay(true);
1369
+ }
1370
+
1371
+ @Override
1372
+ public void handleOnStart() {
1373
+ if (isPreviousMainActivity) {
1374
+ this.appMovedToForeground();
1375
+ }
1376
+ Log.i(CapacitorUpdater.TAG, "onActivityStarted " + getActivity().getClass().getName());
1377
+ isPreviousMainActivity = true;
1264
1378
  }
1265
- }
1266
1379
 
1267
- @Override
1268
- public void onActivityResumed(@NonNull final Activity activity) {
1269
- if (backgroundTask != null && taskRunning) {
1270
- backgroundTask.interrupt();
1380
+ @Override
1381
+ public void handleOnStop() {
1382
+ isPreviousMainActivity = isMainActivity();
1383
+ if (isPreviousMainActivity) {
1384
+ this.appMovedToBackground();
1385
+ }
1271
1386
  }
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();
1387
+
1388
+ @Override
1389
+ public void handleOnResume() {
1390
+ if (backgroundTask != null && taskRunning) {
1391
+ backgroundTask.interrupt();
1392
+ }
1393
+ this.implementation.activity = getActivity();
1394
+ }
1395
+
1396
+ @Override
1397
+ public void handleOnPause() {
1398
+ this.implementation.activity = getActivity();
1399
+ }
1400
+
1401
+ @Override
1402
+ public void handleOnDestroy() {
1403
+ Log.i(CapacitorUpdater.TAG, "onActivityDestroyed " + getActivity().getClass().getName());
1404
+ this.implementation.activity = getActivity();
1405
+ counterActivityCreate--;
1406
+ if (counterActivityCreate == 0) {
1407
+ this.appKilled();
1408
+ }
1305
1409
  }
1306
- }
1307
1410
  }