@capgo/capacitor-updater 7.9.0 → 7.9.2
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.
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +37 -18
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +15 -4
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +263 -103
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +30 -16
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +31 -8
- package/ios/Plugin/CapgoUpdater.swift +3 -1
- package/package.json +1 -1
|
@@ -44,8 +44,7 @@ import java.util.concurrent.Semaphore;
|
|
|
44
44
|
import java.util.concurrent.TimeUnit;
|
|
45
45
|
import java.util.concurrent.TimeoutException;
|
|
46
46
|
import java.util.concurrent.atomic.AtomicReference;
|
|
47
|
-
|
|
48
|
-
import okhttp3.Protocol;
|
|
47
|
+
// Removed OkHttpClient and Protocol imports - using shared client in DownloadService instead
|
|
49
48
|
import org.json.JSONArray;
|
|
50
49
|
import org.json.JSONException;
|
|
51
50
|
import org.json.JSONObject;
|
|
@@ -59,7 +58,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
59
58
|
private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
|
|
60
59
|
private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
|
|
61
60
|
|
|
62
|
-
private final String PLUGIN_VERSION = "7.9.
|
|
61
|
+
private final String PLUGIN_VERSION = "7.9.2";
|
|
63
62
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
64
63
|
|
|
65
64
|
private SharedPreferences.Editor editor;
|
|
@@ -153,12 +152,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
153
152
|
this.implementation.CAP_SERVER_PATH = WebView.CAP_SERVER_PATH;
|
|
154
153
|
this.implementation.PLUGIN_VERSION = this.PLUGIN_VERSION;
|
|
155
154
|
this.implementation.versionCode = Integer.toString(pInfo.versionCode);
|
|
156
|
-
|
|
157
|
-
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
|
|
158
|
-
.connectTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
|
|
159
|
-
.readTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
|
|
160
|
-
.writeTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
|
|
161
|
-
.build();
|
|
155
|
+
// Removed unused OkHttpClient creation - using shared client in DownloadService instead
|
|
162
156
|
// Handle directUpdate configuration - support string values and backward compatibility
|
|
163
157
|
String directUpdateConfig = this.getConfig().getString("directUpdate", null);
|
|
164
158
|
if (directUpdateConfig != null) {
|
|
@@ -209,12 +203,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
209
203
|
this.implementation.appId = config.getString("appId", this.implementation.appId);
|
|
210
204
|
this.implementation.appId = this.getConfig().getString("appId", this.implementation.appId);
|
|
211
205
|
if (this.implementation.appId == null || this.implementation.appId.isEmpty()) {
|
|
212
|
-
// crash the app
|
|
206
|
+
// crash the app on purpose it should not happen
|
|
213
207
|
throw new RuntimeException(
|
|
214
208
|
"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"
|
|
215
209
|
);
|
|
216
210
|
}
|
|
217
211
|
logger.info("appId: " + implementation.appId);
|
|
212
|
+
|
|
213
|
+
// Update User-Agent for shared OkHttpClient
|
|
214
|
+
DownloadService.updateUserAgent(this.implementation.appId, this.PLUGIN_VERSION);
|
|
215
|
+
|
|
218
216
|
this.implementation.publicKey = this.getConfig().getString("publicKey", "");
|
|
219
217
|
this.implementation.statsUrl = this.getConfig().getString("statsUrl", statsUrlDefault);
|
|
220
218
|
this.implementation.channelUrl = this.getConfig().getString("channelUrl", channelUrlDefault);
|
|
@@ -233,7 +231,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
233
231
|
this.implementation.versionOs = Build.VERSION.RELEASE;
|
|
234
232
|
this.implementation.deviceID = this.prefs.getString("appUUID", UUID.randomUUID().toString()).toLowerCase();
|
|
235
233
|
this.editor.putString("appUUID", this.implementation.deviceID);
|
|
236
|
-
this.editor.
|
|
234
|
+
this.editor.apply();
|
|
237
235
|
logger.info("init for device " + this.implementation.deviceID);
|
|
238
236
|
logger.info("version native " + this.currentVersionNative.getOriginalString());
|
|
239
237
|
this.autoDeleteFailed = this.getConfig().getBoolean("autoDeleteFailed", true);
|
|
@@ -263,9 +261,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
263
261
|
logger.info("semaphoreReady count " + semaphoreReady.getPhase());
|
|
264
262
|
} catch (InterruptedException e) {
|
|
265
263
|
logger.info("semaphoreWait InterruptedException");
|
|
266
|
-
|
|
264
|
+
Thread.currentThread().interrupt(); // Restore interrupted status
|
|
267
265
|
} catch (TimeoutException e) {
|
|
268
|
-
|
|
266
|
+
logger.error("Semaphore timeout: " + e.getMessage());
|
|
267
|
+
// Don't throw runtime exception, just log and continue
|
|
269
268
|
}
|
|
270
269
|
}
|
|
271
270
|
|
|
@@ -455,7 +454,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
455
454
|
logger.error("Error calculating previous native version " + e.getMessage());
|
|
456
455
|
}
|
|
457
456
|
this.editor.putString("LatestVersionNative", this.currentVersionNative.toString());
|
|
458
|
-
this.editor.
|
|
457
|
+
this.editor.apply();
|
|
459
458
|
}
|
|
460
459
|
|
|
461
460
|
public void notifyDownload(final String id, final int percent) {
|
|
@@ -782,22 +781,37 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
782
781
|
Semaphore mainThreadSemaphore = new Semaphore(0);
|
|
783
782
|
this.bridge.executeOnMainThread(() -> {
|
|
784
783
|
try {
|
|
785
|
-
|
|
784
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
785
|
+
String currentUrl = this.bridge.getWebView().getUrl();
|
|
786
|
+
if (currentUrl != null) {
|
|
787
|
+
url.set(new URL(currentUrl));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
786
790
|
} catch (Exception e) {
|
|
787
791
|
logger.error("Error executing on main thread " + e.getMessage());
|
|
788
792
|
}
|
|
789
793
|
mainThreadSemaphore.release();
|
|
790
794
|
});
|
|
791
|
-
|
|
795
|
+
|
|
796
|
+
// Add timeout to prevent indefinite blocking
|
|
797
|
+
if (!mainThreadSemaphore.tryAcquire(10, TimeUnit.SECONDS)) {
|
|
798
|
+
logger.error("Timeout waiting for main thread operation");
|
|
799
|
+
}
|
|
792
800
|
} else {
|
|
793
801
|
try {
|
|
794
|
-
|
|
802
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
803
|
+
String currentUrl = this.bridge.getWebView().getUrl();
|
|
804
|
+
if (currentUrl != null) {
|
|
805
|
+
url.set(new URL(currentUrl));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
795
808
|
} catch (Exception e) {
|
|
796
809
|
logger.error("Error executing on main thread " + e.getMessage());
|
|
797
810
|
}
|
|
798
811
|
}
|
|
799
812
|
} catch (InterruptedException e) {
|
|
800
813
|
logger.error("Error waiting for main thread or getting the current URL from webview " + e.getMessage());
|
|
814
|
+
Thread.currentThread().interrupt(); // Restore interrupted status
|
|
801
815
|
}
|
|
802
816
|
}
|
|
803
817
|
|
|
@@ -839,7 +853,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
839
853
|
this.notifyListeners("appReloaded", new JSObject());
|
|
840
854
|
|
|
841
855
|
// Wait for the reload to complete (until notifyAppReady is called)
|
|
842
|
-
|
|
856
|
+
try {
|
|
857
|
+
this.semaphoreWait(this.appReadyTimeout);
|
|
858
|
+
} catch (Exception e) {
|
|
859
|
+
logger.error("Error waiting for app ready: " + e.getMessage());
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
843
862
|
|
|
844
863
|
return true;
|
|
845
864
|
}
|
|
@@ -32,6 +32,7 @@ import java.util.Iterator;
|
|
|
32
32
|
import java.util.List;
|
|
33
33
|
import java.util.Map;
|
|
34
34
|
import java.util.Objects;
|
|
35
|
+
import java.util.concurrent.TimeUnit;
|
|
35
36
|
import java.util.zip.ZipEntry;
|
|
36
37
|
import java.util.zip.ZipInputStream;
|
|
37
38
|
import okhttp3.*;
|
|
@@ -57,7 +58,7 @@ public class CapgoUpdater {
|
|
|
57
58
|
public SharedPreferences.Editor editor;
|
|
58
59
|
public SharedPreferences prefs;
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
private OkHttpClient client;
|
|
61
62
|
|
|
62
63
|
public File documentsDir;
|
|
63
64
|
public Boolean directUpdate = false;
|
|
@@ -79,6 +80,8 @@ public class CapgoUpdater {
|
|
|
79
80
|
|
|
80
81
|
public CapgoUpdater(Logger logger) {
|
|
81
82
|
this.logger = logger;
|
|
83
|
+
// Simple OkHttpClient - actual configuration happens in plugin
|
|
84
|
+
this.client = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).build();
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
private final FilenameFilter filter = (f, name) -> {
|
|
@@ -248,11 +251,16 @@ public class CapgoUpdater {
|
|
|
248
251
|
id,
|
|
249
252
|
new BundleInfo(id, version, BundleStatus.ERROR, new Date(System.currentTimeMillis()), "")
|
|
250
253
|
);
|
|
254
|
+
// Cleanup download tracking
|
|
255
|
+
DownloadWorkerManager.cancelBundleDownload(activity, id, version);
|
|
251
256
|
Map<String, Object> ret = new HashMap<>();
|
|
252
257
|
ret.put("version", getCurrentBundle().getVersionName());
|
|
253
258
|
ret.put("error", "finish_download_fail");
|
|
254
259
|
sendStats("finish_download_fail", version);
|
|
255
260
|
notifyListeners("downloadFailed", ret);
|
|
261
|
+
} else {
|
|
262
|
+
// Successful download - cleanup tracking
|
|
263
|
+
DownloadWorkerManager.cancelBundleDownload(activity, id, version);
|
|
256
264
|
}
|
|
257
265
|
break;
|
|
258
266
|
case FAILED:
|
|
@@ -264,6 +272,8 @@ public class CapgoUpdater {
|
|
|
264
272
|
id,
|
|
265
273
|
new BundleInfo(id, failedVersion, BundleStatus.ERROR, new Date(System.currentTimeMillis()), "")
|
|
266
274
|
);
|
|
275
|
+
// Cleanup download tracking for failed downloads
|
|
276
|
+
DownloadWorkerManager.cancelBundleDownload(activity, id, failedVersion);
|
|
267
277
|
Map<String, Object> ret = new HashMap<>();
|
|
268
278
|
ret.put("version", getCurrentBundle().getVersionName());
|
|
269
279
|
if ("low_mem_fail".equals(error)) {
|
|
@@ -381,6 +391,7 @@ public class CapgoUpdater {
|
|
|
381
391
|
downloaded.delete();
|
|
382
392
|
}
|
|
383
393
|
this.notifyDownload(id, 100);
|
|
394
|
+
// Remove old bundle info and set new one
|
|
384
395
|
this.saveBundleInfo(id, null);
|
|
385
396
|
BundleInfo next = new BundleInfo(id, version, BundleStatus.PENDING, new Date(System.currentTimeMillis()), checksum);
|
|
386
397
|
this.saveBundleInfo(id, next);
|
|
@@ -440,7 +451,7 @@ public class CapgoUpdater {
|
|
|
440
451
|
final String id = this.randomString();
|
|
441
452
|
|
|
442
453
|
// Check if version is already downloading, but allow retry if previous download failed
|
|
443
|
-
if (this.activity != null && DownloadWorkerManager.isVersionDownloading(version)) {
|
|
454
|
+
if (this.activity != null && DownloadWorkerManager.isVersionDownloading(this.activity, version)) {
|
|
444
455
|
// Check if there's an existing bundle with error status that we can retry
|
|
445
456
|
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
446
457
|
if (existingBundle != null && existingBundle.isErrorStatus()) {
|
|
@@ -453,7 +464,7 @@ public class CapgoUpdater {
|
|
|
453
464
|
}
|
|
454
465
|
}
|
|
455
466
|
|
|
456
|
-
|
|
467
|
+
saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), ""));
|
|
457
468
|
this.notifyDownload(id, 0);
|
|
458
469
|
this.notifyDownload(id, 5);
|
|
459
470
|
|
|
@@ -469,7 +480,7 @@ public class CapgoUpdater {
|
|
|
469
480
|
}
|
|
470
481
|
|
|
471
482
|
final String id = this.randomString();
|
|
472
|
-
|
|
483
|
+
saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), ""));
|
|
473
484
|
this.notifyDownload(id, 0);
|
|
474
485
|
this.notifyDownload(id, 5);
|
|
475
486
|
final String dest = this.randomString();
|
|
@@ -16,6 +16,7 @@ import java.net.HttpURLConnection;
|
|
|
16
16
|
import java.net.URL;
|
|
17
17
|
import java.nio.channels.FileChannel;
|
|
18
18
|
import java.nio.file.Files;
|
|
19
|
+
import java.nio.file.StandardCopyOption;
|
|
19
20
|
import java.security.MessageDigest;
|
|
20
21
|
import java.util.ArrayList;
|
|
21
22
|
import java.util.Arrays;
|
|
@@ -33,6 +34,11 @@ import okhttp3.Protocol;
|
|
|
33
34
|
import okhttp3.Request;
|
|
34
35
|
import okhttp3.Response;
|
|
35
36
|
import okhttp3.ResponseBody;
|
|
37
|
+
import okio.Buffer;
|
|
38
|
+
import okio.BufferedSink;
|
|
39
|
+
import okio.BufferedSource;
|
|
40
|
+
import okio.Okio;
|
|
41
|
+
import okio.Source;
|
|
36
42
|
import org.brotli.dec.BrotliInputStream;
|
|
37
43
|
import org.json.JSONArray;
|
|
38
44
|
import org.json.JSONObject;
|
|
@@ -60,29 +66,44 @@ public class DownloadService extends Worker {
|
|
|
60
66
|
public static final String PLUGIN_VERSION = "plugin_version";
|
|
61
67
|
private static final String UPDATE_FILE = "update.dat";
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
// Shared OkHttpClient to prevent resource leaks
|
|
70
|
+
private static OkHttpClient sharedClient;
|
|
71
|
+
private static String currentAppId = "unknown";
|
|
72
|
+
private static String currentPluginVersion = "unknown";
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
String appId = getInputData().getString(APP_ID);
|
|
69
|
-
String pluginVersion = getInputData().getString(PLUGIN_VERSION);
|
|
70
|
-
|
|
71
|
-
// Build user agent with appId and plugin version
|
|
72
|
-
String userAgent =
|
|
73
|
-
"CapacitorUpdater/" + (pluginVersion != null ? pluginVersion : "unknown") + " (" + (appId != null ? appId : "unknown") + ")";
|
|
74
|
-
|
|
75
|
-
// Create OkHttpClient with custom user agent
|
|
76
|
-
this.client = new OkHttpClient.Builder()
|
|
74
|
+
// Initialize shared client with User-Agent interceptor
|
|
75
|
+
static {
|
|
76
|
+
sharedClient = new OkHttpClient.Builder()
|
|
77
77
|
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
|
|
78
78
|
.addInterceptor(chain -> {
|
|
79
79
|
Request originalRequest = chain.request();
|
|
80
|
+
String userAgent =
|
|
81
|
+
"CapacitorUpdater/" +
|
|
82
|
+
(currentPluginVersion != null ? currentPluginVersion : "unknown") +
|
|
83
|
+
" (" +
|
|
84
|
+
(currentAppId != null ? currentAppId : "unknown") +
|
|
85
|
+
")";
|
|
80
86
|
Request requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build();
|
|
81
87
|
return chain.proceed(requestWithUserAgent);
|
|
82
88
|
})
|
|
83
89
|
.build();
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
// Method to update User-Agent values
|
|
93
|
+
public static void updateUserAgent(String appId, String pluginVersion) {
|
|
94
|
+
currentAppId = appId != null ? appId : "unknown";
|
|
95
|
+
currentPluginVersion = pluginVersion != null ? pluginVersion : "unknown";
|
|
96
|
+
logger.debug("Updated User-Agent: CapacitorUpdater/" + currentPluginVersion + " (" + currentAppId + ")");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public DownloadService(@NonNull Context context, @NonNull WorkerParameters params) {
|
|
100
|
+
super(context, params);
|
|
101
|
+
// Use shared client - no need to create new instances
|
|
102
|
+
|
|
103
|
+
// Clean up old temporary files on service initialization
|
|
104
|
+
cleanupOldTempFiles(getApplicationContext().getCacheDir());
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
private void setProgress(int percent) {
|
|
87
108
|
Data progress = new Data.Builder().putInt(PERCENT, percent).build();
|
|
88
109
|
setProgressAsync(progress);
|
|
@@ -272,93 +293,156 @@ public class DownloadService extends Worker {
|
|
|
272
293
|
String checksum
|
|
273
294
|
) {
|
|
274
295
|
File target = new File(documentsDir, dest);
|
|
275
|
-
File infoFile = new File(documentsDir, UPDATE_FILE);
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
File infoFile = new File(documentsDir, UPDATE_FILE);
|
|
297
|
+
File tempFile = new File(documentsDir, "temp" + ".tmp");
|
|
298
|
+
|
|
299
|
+
// Check available disk space before starting
|
|
300
|
+
long availableSpace = target.getParentFile().getUsableSpace();
|
|
301
|
+
long estimatedSize = 50 * 1024 * 1024; // 50MB default estimate
|
|
302
|
+
if (availableSpace < estimatedSize * 2) {
|
|
303
|
+
throw new RuntimeException("insufficient_disk_space");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
HttpURLConnection httpConn = null;
|
|
307
|
+
InputStream inputStream = null;
|
|
308
|
+
FileOutputStream outputStream = null;
|
|
309
|
+
BufferedReader reader = null;
|
|
310
|
+
BufferedWriter writer = null;
|
|
311
|
+
|
|
278
312
|
try {
|
|
279
313
|
URL u = new URL(url);
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
httpConn = (HttpURLConnection) u.openConnection();
|
|
314
|
+
httpConn = (HttpURLConnection) u.openConnection();
|
|
283
315
|
|
|
284
|
-
|
|
285
|
-
|
|
316
|
+
// Set reasonable timeouts
|
|
317
|
+
httpConn.setConnectTimeout(30000); // 30 seconds
|
|
318
|
+
httpConn.setReadTimeout(60000); // 60 seconds
|
|
286
319
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
320
|
+
// Reading progress file (if exist)
|
|
321
|
+
long downloadedBytes = 0;
|
|
322
|
+
|
|
323
|
+
if (infoFile.exists() && tempFile.exists()) {
|
|
324
|
+
try {
|
|
325
|
+
reader = new BufferedReader(new FileReader(infoFile));
|
|
326
|
+
String updateVersion = reader.readLine();
|
|
327
|
+
if (updateVersion != null && !updateVersion.equals(version)) {
|
|
328
|
+
clearDownloadData(documentsDir);
|
|
329
|
+
} else {
|
|
330
|
+
downloadedBytes = tempFile.length();
|
|
331
|
+
}
|
|
332
|
+
} finally {
|
|
333
|
+
if (reader != null) {
|
|
334
|
+
try {
|
|
335
|
+
reader.close();
|
|
336
|
+
} catch (Exception ignored) {}
|
|
295
337
|
}
|
|
296
|
-
} else {
|
|
297
|
-
clearDownloadData(documentsDir);
|
|
298
338
|
}
|
|
339
|
+
} else {
|
|
340
|
+
clearDownloadData(documentsDir);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (downloadedBytes > 0) {
|
|
344
|
+
httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
|
|
345
|
+
}
|
|
299
346
|
|
|
300
|
-
|
|
301
|
-
|
|
347
|
+
int responseCode = httpConn.getResponseCode();
|
|
348
|
+
|
|
349
|
+
if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
|
|
350
|
+
long contentLength = httpConn.getContentLength() + downloadedBytes;
|
|
351
|
+
|
|
352
|
+
// Check if we have enough space for the actual file
|
|
353
|
+
if (contentLength > 0 && availableSpace < contentLength * 2) {
|
|
354
|
+
throw new RuntimeException("insufficient_disk_space");
|
|
302
355
|
}
|
|
303
356
|
|
|
304
|
-
|
|
357
|
+
try {
|
|
358
|
+
inputStream = httpConn.getInputStream();
|
|
359
|
+
outputStream = new FileOutputStream(tempFile, downloadedBytes > 0);
|
|
360
|
+
|
|
361
|
+
if (downloadedBytes == 0) {
|
|
362
|
+
writer = new BufferedWriter(new FileWriter(infoFile));
|
|
363
|
+
writer.write(String.valueOf(version));
|
|
364
|
+
writer.close();
|
|
365
|
+
writer = null;
|
|
366
|
+
}
|
|
305
367
|
|
|
306
|
-
|
|
307
|
-
|
|
368
|
+
byte[] buffer = new byte[8192]; // Larger buffer for better performance
|
|
369
|
+
int lastNotifiedPercent = 0;
|
|
370
|
+
int bytesRead;
|
|
308
371
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// Updating the info file
|
|
319
|
-
try (BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))) {
|
|
320
|
-
writer.write(String.valueOf(version));
|
|
372
|
+
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
373
|
+
outputStream.write(buffer, 0, bytesRead);
|
|
374
|
+
downloadedBytes += bytesRead;
|
|
375
|
+
|
|
376
|
+
// Flush every 1MB to ensure progress is saved
|
|
377
|
+
if (downloadedBytes % (1024 * 1024) == 0) {
|
|
378
|
+
outputStream.flush();
|
|
321
379
|
}
|
|
322
380
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
downloadedBytes += bytesRead;
|
|
329
|
-
// Saving progress (flushing every 100 Ko)
|
|
330
|
-
if (downloadedBytes % 102400 == 0) {
|
|
331
|
-
outputStream.flush();
|
|
332
|
-
}
|
|
333
|
-
// Computing percentage
|
|
334
|
-
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
335
|
-
while (lastNotifiedPercent + 10 <= percent) {
|
|
336
|
-
lastNotifiedPercent += 10;
|
|
337
|
-
// Artificial delay using CPU-bound calculation to take ~5 seconds
|
|
338
|
-
double result = 0;
|
|
339
|
-
setProgress(lastNotifiedPercent);
|
|
340
|
-
}
|
|
381
|
+
// Computing percentage
|
|
382
|
+
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
383
|
+
if (percent >= lastNotifiedPercent + 10) {
|
|
384
|
+
lastNotifiedPercent = (percent / 10) * 10;
|
|
385
|
+
setProgress(lastNotifiedPercent);
|
|
341
386
|
}
|
|
387
|
+
}
|
|
342
388
|
|
|
343
|
-
|
|
344
|
-
|
|
389
|
+
// Final flush
|
|
390
|
+
outputStream.flush();
|
|
391
|
+
outputStream.close();
|
|
392
|
+
outputStream = null;
|
|
345
393
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
394
|
+
inputStream.close();
|
|
395
|
+
inputStream = null;
|
|
396
|
+
|
|
397
|
+
// Rename the temp file with the final name (dest)
|
|
398
|
+
if (!tempFile.renameTo(new File(documentsDir, dest))) {
|
|
399
|
+
throw new RuntimeException("Failed to rename temp file to final destination");
|
|
349
400
|
}
|
|
350
|
-
} else {
|
|
351
401
|
infoFile.delete();
|
|
402
|
+
} catch (OutOfMemoryError e) {
|
|
403
|
+
logger.error("Out of memory during download: " + e.getMessage());
|
|
404
|
+
// Try to free some memory
|
|
405
|
+
System.gc();
|
|
406
|
+
throw new RuntimeException("low_mem_fail");
|
|
407
|
+
} finally {
|
|
408
|
+
// Ensure all resources are closed
|
|
409
|
+
if (outputStream != null) {
|
|
410
|
+
try {
|
|
411
|
+
outputStream.close();
|
|
412
|
+
} catch (Exception ignored) {}
|
|
413
|
+
}
|
|
414
|
+
if (inputStream != null) {
|
|
415
|
+
try {
|
|
416
|
+
inputStream.close();
|
|
417
|
+
} catch (Exception ignored) {}
|
|
418
|
+
}
|
|
419
|
+
if (writer != null) {
|
|
420
|
+
try {
|
|
421
|
+
writer.close();
|
|
422
|
+
} catch (Exception ignored) {}
|
|
423
|
+
}
|
|
352
424
|
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
425
|
+
} else {
|
|
426
|
+
infoFile.delete();
|
|
427
|
+
throw new RuntimeException("HTTP error: " + responseCode);
|
|
357
428
|
}
|
|
358
429
|
} catch (OutOfMemoryError e) {
|
|
430
|
+
logger.error("Critical memory error: " + e.getMessage());
|
|
431
|
+
System.gc(); // Suggest garbage collection
|
|
359
432
|
throw new RuntimeException("low_mem_fail");
|
|
433
|
+
} catch (SecurityException e) {
|
|
434
|
+
logger.error("Security error during download: " + e.getMessage());
|
|
435
|
+
throw new RuntimeException("security_error: " + e.getMessage());
|
|
360
436
|
} catch (Exception e) {
|
|
361
|
-
|
|
437
|
+
logger.error("Download error: " + e.getMessage());
|
|
438
|
+
throw new RuntimeException(e.getMessage());
|
|
439
|
+
} finally {
|
|
440
|
+
// Ensure connection is closed
|
|
441
|
+
if (httpConn != null) {
|
|
442
|
+
try {
|
|
443
|
+
httpConn.disconnect();
|
|
444
|
+
} catch (Exception ignored) {}
|
|
445
|
+
}
|
|
362
446
|
}
|
|
363
447
|
}
|
|
364
448
|
|
|
@@ -412,26 +496,20 @@ public class DownloadService extends Worker {
|
|
|
412
496
|
// Create a temporary file for the compressed data
|
|
413
497
|
File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".tmp");
|
|
414
498
|
|
|
415
|
-
try (Response response =
|
|
499
|
+
try (Response response = sharedClient.newCall(request).execute()) {
|
|
416
500
|
if (!response.isSuccessful()) {
|
|
417
501
|
throw new IOException("Unexpected response code: " + response.code());
|
|
418
502
|
}
|
|
419
503
|
|
|
420
|
-
// Download compressed file
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
byte[] buffer = new byte[8192];
|
|
427
|
-
int bytesRead;
|
|
428
|
-
try (InputStream inputStream = responseBody.byteStream()) {
|
|
429
|
-
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
430
|
-
compressedFos.write(buffer, 0, bytesRead);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
504
|
+
// Download compressed file atomically
|
|
505
|
+
ResponseBody responseBody = response.body();
|
|
506
|
+
if (responseBody == null) {
|
|
507
|
+
throw new IOException("Response body is null");
|
|
433
508
|
}
|
|
434
509
|
|
|
510
|
+
// Use OkIO for atomic write
|
|
511
|
+
writeFileAtomic(compressedFile, responseBody.byteStream(), null);
|
|
512
|
+
|
|
435
513
|
if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
436
514
|
logger.debug("Decrypting file " + targetFile.getName());
|
|
437
515
|
CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
|
|
@@ -439,13 +517,22 @@ public class DownloadService extends Worker {
|
|
|
439
517
|
|
|
440
518
|
// Only decompress if file has .br extension
|
|
441
519
|
if (isBrotli) {
|
|
442
|
-
// Use new decompression method
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
520
|
+
// Use new decompression method with atomic write
|
|
521
|
+
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
522
|
+
byte[] compressedData = new byte[(int) compressedFile.length()];
|
|
523
|
+
fis.read(compressedData);
|
|
524
|
+
byte[] decompressedData = decompressBrotli(compressedData, targetFile.getName());
|
|
525
|
+
|
|
526
|
+
// Write decompressed data atomically
|
|
527
|
+
try (java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(decompressedData)) {
|
|
528
|
+
writeFileAtomic(finalTargetFile, bais, null);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
446
531
|
} else {
|
|
447
|
-
// Just copy the file without decompression
|
|
448
|
-
|
|
532
|
+
// Just copy the file without decompression using atomic operation
|
|
533
|
+
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
534
|
+
writeFileAtomic(finalTargetFile, fis, null);
|
|
535
|
+
}
|
|
449
536
|
}
|
|
450
537
|
|
|
451
538
|
// Delete the compressed file
|
|
@@ -454,8 +541,10 @@ public class DownloadService extends Worker {
|
|
|
454
541
|
|
|
455
542
|
// Verify checksum
|
|
456
543
|
if (calculatedHash.equals(expectedHash)) {
|
|
457
|
-
// Only cache if checksum is correct
|
|
458
|
-
|
|
544
|
+
// Only cache if checksum is correct - use atomic copy
|
|
545
|
+
try (FileInputStream fis = new FileInputStream(finalTargetFile)) {
|
|
546
|
+
writeFileAtomic(cacheFile, fis, expectedHash);
|
|
547
|
+
}
|
|
459
548
|
} else {
|
|
460
549
|
finalTargetFile.delete();
|
|
461
550
|
throw new IOException(
|
|
@@ -486,14 +575,14 @@ public class DownloadService extends Worker {
|
|
|
486
575
|
|
|
487
576
|
private String calculateFileHash(File file) throws Exception {
|
|
488
577
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
489
|
-
FileInputStream fis = new FileInputStream(file);
|
|
490
578
|
byte[] byteArray = new byte[1024];
|
|
491
579
|
int bytesCount = 0;
|
|
492
580
|
|
|
493
|
-
|
|
494
|
-
|
|
581
|
+
try (FileInputStream fis = new FileInputStream(file)) {
|
|
582
|
+
while ((bytesCount = fis.read(byteArray)) != -1) {
|
|
583
|
+
digest.update(byteArray, 0, bytesCount);
|
|
584
|
+
}
|
|
495
585
|
}
|
|
496
|
-
fis.close();
|
|
497
586
|
|
|
498
587
|
byte[] bytes = digest.digest();
|
|
499
588
|
StringBuilder sb = new StringBuilder();
|
|
@@ -561,4 +650,75 @@ public class DownloadService extends Worker {
|
|
|
561
650
|
throw e;
|
|
562
651
|
}
|
|
563
652
|
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Atomically write data to a file using OkIO
|
|
656
|
+
*/
|
|
657
|
+
private void writeFileAtomic(File targetFile, InputStream inputStream, String expectedChecksum) throws IOException {
|
|
658
|
+
File tempFile = new File(targetFile.getParent(), targetFile.getName() + ".tmp");
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
// Write to temp file first using OkIO
|
|
662
|
+
try (BufferedSink sink = Okio.buffer(Okio.sink(tempFile)); BufferedSource source = Okio.buffer(Okio.source(inputStream))) {
|
|
663
|
+
sink.writeAll(source);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Verify checksum if provided
|
|
667
|
+
if (expectedChecksum != null && !expectedChecksum.isEmpty()) {
|
|
668
|
+
String actualChecksum = calculateFileChecksum(tempFile);
|
|
669
|
+
if (!expectedChecksum.equalsIgnoreCase(actualChecksum)) {
|
|
670
|
+
tempFile.delete();
|
|
671
|
+
throw new IOException("Checksum verification failed");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Atomic rename (on same filesystem)
|
|
676
|
+
Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
|
677
|
+
} catch (Exception e) {
|
|
678
|
+
// Clean up temp file on error
|
|
679
|
+
if (tempFile.exists()) {
|
|
680
|
+
tempFile.delete();
|
|
681
|
+
}
|
|
682
|
+
throw new IOException("Failed to write file atomically: " + e.getMessage(), e);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Calculate MD5 checksum of a file
|
|
688
|
+
*/
|
|
689
|
+
private String calculateFileChecksum(File file) throws IOException {
|
|
690
|
+
try (FileInputStream fis = new FileInputStream(file)) {
|
|
691
|
+
MessageDigest md = MessageDigest.getInstance("MD5");
|
|
692
|
+
byte[] buffer = new byte[8192];
|
|
693
|
+
int bytesRead;
|
|
694
|
+
while ((bytesRead = fis.read(buffer)) != -1) {
|
|
695
|
+
md.update(buffer, 0, bytesRead);
|
|
696
|
+
}
|
|
697
|
+
byte[] digest = md.digest();
|
|
698
|
+
StringBuilder sb = new StringBuilder();
|
|
699
|
+
for (byte b : digest) {
|
|
700
|
+
sb.append(String.format("%02x", b));
|
|
701
|
+
}
|
|
702
|
+
return sb.toString();
|
|
703
|
+
} catch (Exception e) {
|
|
704
|
+
throw new IOException("Failed to calculate checksum: " + e.getMessage(), e);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Clean up old temporary files
|
|
710
|
+
*/
|
|
711
|
+
private void cleanupOldTempFiles(File directory) {
|
|
712
|
+
if (directory == null || !directory.exists()) return;
|
|
713
|
+
|
|
714
|
+
File[] tempFiles = directory.listFiles((dir, name) -> name.endsWith(".tmp"));
|
|
715
|
+
if (tempFiles != null) {
|
|
716
|
+
long oneHourAgo = System.currentTimeMillis() - 3600000;
|
|
717
|
+
for (File tempFile : tempFiles) {
|
|
718
|
+
if (tempFile.lastModified() < oneHourAgo) {
|
|
719
|
+
tempFile.delete();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
564
724
|
}
|
|
@@ -5,12 +5,11 @@ import androidx.work.BackoffPolicy;
|
|
|
5
5
|
import androidx.work.Configuration;
|
|
6
6
|
import androidx.work.Constraints;
|
|
7
7
|
import androidx.work.Data;
|
|
8
|
+
import androidx.work.ExistingWorkPolicy;
|
|
8
9
|
import androidx.work.NetworkType;
|
|
9
10
|
import androidx.work.OneTimeWorkRequest;
|
|
10
11
|
import androidx.work.WorkManager;
|
|
11
12
|
import androidx.work.WorkRequest;
|
|
12
|
-
import java.util.HashSet;
|
|
13
|
-
import java.util.Set;
|
|
14
13
|
import java.util.concurrent.TimeUnit;
|
|
15
14
|
|
|
16
15
|
public class DownloadWorkerManager {
|
|
@@ -22,7 +21,6 @@ public class DownloadWorkerManager {
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
private static volatile boolean isInitialized = false;
|
|
25
|
-
private static final Set<String> activeVersions = new HashSet<>();
|
|
26
24
|
|
|
27
25
|
private static synchronized void initializeIfNeeded(Context context) {
|
|
28
26
|
if (!isInitialized) {
|
|
@@ -36,8 +34,18 @@ public class DownloadWorkerManager {
|
|
|
36
34
|
}
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
public static
|
|
40
|
-
|
|
37
|
+
public static boolean isVersionDownloading(Context context, String version) {
|
|
38
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
39
|
+
try {
|
|
40
|
+
return WorkManager.getInstance(context)
|
|
41
|
+
.getWorkInfosByTag(version)
|
|
42
|
+
.get()
|
|
43
|
+
.stream()
|
|
44
|
+
.anyMatch(workInfo -> !workInfo.getState().isFinished());
|
|
45
|
+
} catch (Exception e) {
|
|
46
|
+
logger.error("Error checking download status: " + e.getMessage());
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
public static void enqueueDownload(
|
|
@@ -57,12 +65,8 @@ public class DownloadWorkerManager {
|
|
|
57
65
|
) {
|
|
58
66
|
initializeIfNeeded(context.getApplicationContext());
|
|
59
67
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
logger.info("Version " + version + " is already downloading");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
activeVersions.add(version);
|
|
68
|
+
// Use unique work name for this bundle to prevent duplicates
|
|
69
|
+
String uniqueWorkName = "bundle_" + id + "_" + version;
|
|
66
70
|
|
|
67
71
|
// Create input data
|
|
68
72
|
Data inputData = new Data.Builder()
|
|
@@ -95,7 +99,7 @@ public class DownloadWorkerManager {
|
|
|
95
99
|
.setConstraints(constraints)
|
|
96
100
|
.setInputData(inputData)
|
|
97
101
|
.addTag(id)
|
|
98
|
-
.addTag(version)
|
|
102
|
+
.addTag(version)
|
|
99
103
|
.addTag("capacitor_updater_download");
|
|
100
104
|
|
|
101
105
|
// More aggressive retry policy for emulators
|
|
@@ -107,19 +111,29 @@ public class DownloadWorkerManager {
|
|
|
107
111
|
|
|
108
112
|
OneTimeWorkRequest workRequest = workRequestBuilder.build();
|
|
109
113
|
|
|
110
|
-
//
|
|
111
|
-
WorkManager.getInstance(context)
|
|
114
|
+
// Use beginUniqueWork to prevent duplicate downloads
|
|
115
|
+
WorkManager.getInstance(context)
|
|
116
|
+
.beginUniqueWork(
|
|
117
|
+
uniqueWorkName,
|
|
118
|
+
ExistingWorkPolicy.KEEP, // Don't start if already running
|
|
119
|
+
workRequest
|
|
120
|
+
)
|
|
121
|
+
.enqueue();
|
|
112
122
|
}
|
|
113
123
|
|
|
114
124
|
public static void cancelVersionDownload(Context context, String version) {
|
|
115
125
|
initializeIfNeeded(context.getApplicationContext());
|
|
116
126
|
WorkManager.getInstance(context).cancelAllWorkByTag(version);
|
|
117
|
-
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public static void cancelBundleDownload(Context context, String id, String version) {
|
|
130
|
+
String uniqueWorkName = "bundle_" + id + "_" + version;
|
|
131
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
132
|
+
WorkManager.getInstance(context).cancelUniqueWork(uniqueWorkName);
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
public static void cancelAllDownloads(Context context) {
|
|
121
136
|
initializeIfNeeded(context.getApplicationContext());
|
|
122
137
|
WorkManager.getInstance(context).cancelAllWorkByTag("capacitor_updater_download");
|
|
123
|
-
activeVersions.clear();
|
|
124
138
|
}
|
|
125
139
|
}
|
|
@@ -86,7 +86,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
86
86
|
CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise)
|
|
87
87
|
]
|
|
88
88
|
public var implementation = CapgoUpdater()
|
|
89
|
-
private let PLUGIN_VERSION: String = "7.9.
|
|
89
|
+
private let PLUGIN_VERSION: String = "7.9.2"
|
|
90
90
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
91
91
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
92
92
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -117,8 +117,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
117
117
|
override public func load() {
|
|
118
118
|
let disableJSLogging = getConfig().getBoolean("disableJSLogging", false)
|
|
119
119
|
// Set webView for logging to JavaScript console
|
|
120
|
-
if
|
|
121
|
-
logger.setWebView(webView:
|
|
120
|
+
if let webView = self.bridge?.webView, !disableJSLogging {
|
|
121
|
+
logger.setWebView(webView: webView)
|
|
122
122
|
logger.info("WebView set successfully for logging")
|
|
123
123
|
} else {
|
|
124
124
|
logger.error("Failed to get webView for logging")
|
|
@@ -135,7 +135,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
135
135
|
logger.info("init for device \(self.implementation.deviceID)")
|
|
136
136
|
guard let versionName = getConfig().getString("version", Bundle.main.versionName) else {
|
|
137
137
|
logger.error("Cannot get version name")
|
|
138
|
-
// crash the app
|
|
138
|
+
// crash the app on purpose
|
|
139
139
|
fatalError("Cannot get version name")
|
|
140
140
|
}
|
|
141
141
|
do {
|
|
@@ -195,6 +195,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
195
195
|
implementation.appId = config?["appId"] as? String ?? implementation.appId
|
|
196
196
|
implementation.appId = getConfig().getString("appId", implementation.appId)!
|
|
197
197
|
if implementation.appId == "" {
|
|
198
|
+
// crash the app on purpose it should not happen
|
|
198
199
|
fatalError("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")
|
|
199
200
|
}
|
|
200
201
|
logger.info("appId \(implementation.appId)")
|
|
@@ -252,8 +253,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
private func semaphoreWait(waitTime: Int) {
|
|
255
|
-
// print("
|
|
256
|
-
|
|
256
|
+
// print("\\(CapgoUpdater.TAG) semaphoreWait \\(waitTime)")
|
|
257
|
+
let result = semaphoreReady.wait(timeout: .now() + .milliseconds(waitTime))
|
|
258
|
+
if result == .timedOut {
|
|
259
|
+
logger.error("Semaphore wait timed out after \(waitTime)ms")
|
|
260
|
+
}
|
|
257
261
|
}
|
|
258
262
|
|
|
259
263
|
private func semaphoreUp() {
|
|
@@ -1087,6 +1091,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1087
1091
|
@objc func appKilled() {
|
|
1088
1092
|
logger.info("onActivityDestroyed: all activity destroyed")
|
|
1089
1093
|
self.delayUpdateUtils.checkCancelDelay(source: .killed)
|
|
1094
|
+
|
|
1095
|
+
// Clean up resources
|
|
1096
|
+
periodicUpdateTimer?.invalidate()
|
|
1097
|
+
periodicUpdateTimer = nil
|
|
1098
|
+
backgroundWork?.cancel()
|
|
1099
|
+
backgroundWork = nil
|
|
1100
|
+
|
|
1101
|
+
// Signal any waiting semaphores to prevent deadlocks
|
|
1102
|
+
semaphoreReady.signal()
|
|
1090
1103
|
}
|
|
1091
1104
|
|
|
1092
1105
|
private func installNext() {
|
|
@@ -1150,6 +1163,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1150
1163
|
self.checkAppReady()
|
|
1151
1164
|
}
|
|
1152
1165
|
|
|
1166
|
+
private var periodicUpdateTimer: Timer?
|
|
1167
|
+
|
|
1153
1168
|
@objc func checkForUpdateAfterDelay() {
|
|
1154
1169
|
if periodCheckDelay == 0 || !self._isAutoUpdateEnabled() {
|
|
1155
1170
|
return
|
|
@@ -1158,7 +1173,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1158
1173
|
logger.error("Error no url or wrong format")
|
|
1159
1174
|
return
|
|
1160
1175
|
}
|
|
1161
|
-
|
|
1176
|
+
|
|
1177
|
+
// Clean up any existing timer
|
|
1178
|
+
periodicUpdateTimer?.invalidate()
|
|
1179
|
+
|
|
1180
|
+
periodicUpdateTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(periodCheckDelay), repeats: true) { [weak self] timer in
|
|
1181
|
+
guard let self = self else {
|
|
1182
|
+
timer.invalidate()
|
|
1183
|
+
return
|
|
1184
|
+
}
|
|
1162
1185
|
DispatchQueue.global(qos: .background).async {
|
|
1163
1186
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
1164
1187
|
let current = self.implementation.getCurrentBundle()
|
|
@@ -1169,7 +1192,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1169
1192
|
}
|
|
1170
1193
|
}
|
|
1171
1194
|
}
|
|
1172
|
-
RunLoop.current.add(
|
|
1195
|
+
RunLoop.current.add(periodicUpdateTimer!, forMode: .default)
|
|
1173
1196
|
}
|
|
1174
1197
|
|
|
1175
1198
|
@objc func appMovedToBackground() {
|
|
@@ -43,7 +43,9 @@ import UIKit
|
|
|
43
43
|
public var publicKey: String = ""
|
|
44
44
|
|
|
45
45
|
private var userAgent: String {
|
|
46
|
-
|
|
46
|
+
let safePluginVersion = PLUGIN_VERSION.isEmpty ? "unknown" : PLUGIN_VERSION
|
|
47
|
+
let safeAppId = appId.isEmpty ? "unknown" : appId
|
|
48
|
+
return "CapacitorUpdater/\(safePluginVersion) (\(safeAppId))"
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
private lazy var alamofireSession: Session = {
|