@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
@@ -3,124 +3,451 @@
3
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
4
4
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
5
  */
6
-
7
6
  package ee.forgr.capacitor_updater;
8
7
 
9
- import android.app.IntentService;
10
- import android.content.Intent;
11
- import java.io.DataInputStream;
12
- import java.io.File;
13
- import java.io.FileOutputStream;
14
- import java.io.InputStream;
8
+ import android.content.Context;
9
+ import android.util.Log;
10
+ import androidx.annotation.NonNull;
11
+ import androidx.work.Data;
12
+ import androidx.work.Worker;
13
+ import androidx.work.WorkerParameters;
14
+ import java.io.*;
15
+ import java.io.FileInputStream;
16
+ import java.net.HttpURLConnection;
15
17
  import java.net.URL;
16
- import java.net.URLConnection;
17
-
18
- public class DownloadService extends IntentService {
19
-
20
- public static final String URL = "URL";
21
- public static final String ID = "id";
22
- public static final String PERCENT = "percent";
23
- public static final String FILEDEST = "filendest";
24
- public static final String DOCDIR = "docdir";
25
- public static final String ERROR = "error";
26
- public static final String VERSION = "version";
27
- public static final String SESSIONKEY = "sessionkey";
28
- public static final String CHECKSUM = "checksum";
29
- public static final String NOTIFICATION = "service receiver";
30
- public static final String PERCENTDOWNLOAD = "percent receiver";
31
-
32
- public DownloadService() {
33
- super("Background DownloadService");
34
- }
35
-
36
- private int calcTotalPercent(
37
- final int percent,
38
- final int min,
39
- final int max
40
- ) {
41
- return (percent * (max - min)) / 100 + min;
42
- }
43
-
44
- // Will be called asynchronously by OS.
45
- @Override
46
- protected void onHandleIntent(Intent intent) {
47
- String url = intent.getStringExtra(URL);
48
- String id = intent.getStringExtra(ID);
49
- String documentsDir = intent.getStringExtra(DOCDIR);
50
- String dest = intent.getStringExtra(FILEDEST);
51
- String version = intent.getStringExtra(VERSION);
52
- String sessionKey = intent.getStringExtra(SESSIONKEY);
53
- String checksum = intent.getStringExtra(CHECKSUM);
54
-
55
- try {
56
- final URL u = new URL(url);
57
- final URLConnection connection = u.openConnection();
58
- final InputStream is = u.openStream();
59
- final DataInputStream dis = new DataInputStream(is);
60
-
61
- final File target = new File(documentsDir, dest);
62
- target.getParentFile().mkdirs();
63
- target.createNewFile();
64
- final FileOutputStream fos = new FileOutputStream(target);
65
-
66
- final long totalLength = connection.getContentLength();
67
- final int bufferSize = 1024;
68
- final byte[] buffer = new byte[bufferSize];
69
- int length;
70
-
71
- int bytesRead = bufferSize;
72
- int percent = 0;
73
- this.notifyDownload(id, 10);
74
- while ((length = dis.read(buffer)) > 0) {
75
- fos.write(buffer, 0, length);
76
- final int newPercent = (int) ((bytesRead * 100) / totalLength);
77
- if (totalLength > 1 && newPercent != percent) {
78
- percent = newPercent;
79
- this.notifyDownload(id, this.calcTotalPercent(percent, 10, 70));
18
+ import java.nio.channels.FileChannel;
19
+ import java.security.MessageDigest;
20
+ import java.util.ArrayList;
21
+ import java.util.Arrays;
22
+ import java.util.List;
23
+ import java.util.Objects;
24
+ import java.util.concurrent.ExecutorService;
25
+ import java.util.concurrent.Executors;
26
+ import java.util.concurrent.Future;
27
+ import java.util.concurrent.TimeUnit;
28
+ import java.util.concurrent.atomic.AtomicBoolean;
29
+ import java.util.concurrent.atomic.AtomicLong;
30
+ import okhttp3.OkHttpClient;
31
+ import okhttp3.Protocol;
32
+ import okhttp3.Request;
33
+ import okhttp3.Response;
34
+ import okhttp3.ResponseBody;
35
+ import org.brotli.dec.BrotliInputStream;
36
+ import org.json.JSONArray;
37
+ import org.json.JSONObject;
38
+
39
+ public class DownloadService extends Worker {
40
+
41
+ public static final String TAG = "Capacitor-updater";
42
+ public static final String URL = "URL";
43
+ public static final String ID = "id";
44
+ public static final String PERCENT = "percent";
45
+ public static final String FILEDEST = "filendest";
46
+ public static final String DOCDIR = "docdir";
47
+ public static final String ERROR = "error";
48
+ public static final String VERSION = "version";
49
+ public static final String SESSIONKEY = "sessionkey";
50
+ public static final String CHECKSUM = "checksum";
51
+ public static final String PUBLIC_KEY = "publickey";
52
+ public static final String IS_MANIFEST = "is_manifest";
53
+ private static final String UPDATE_FILE = "update.dat";
54
+
55
+ private final OkHttpClient client = new OkHttpClient.Builder().protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)).build();
56
+
57
+ public DownloadService(@NonNull Context context, @NonNull WorkerParameters params) {
58
+ super(context, params);
59
+ }
60
+
61
+ private void setProgress(int percent) {
62
+ Data progress = new Data.Builder().putInt(PERCENT, percent).build();
63
+ setProgressAsync(progress);
64
+ }
65
+
66
+ private Result createFailureResult(String error) {
67
+ Data output = new Data.Builder().putString(ERROR, error).build();
68
+ return Result.failure(output);
69
+ }
70
+
71
+ private Result createSuccessResult(String dest, String version, String sessionKey, String checksum, boolean isManifest) {
72
+ Data output = new Data.Builder()
73
+ .putString(FILEDEST, dest)
74
+ .putString(VERSION, version)
75
+ .putString(SESSIONKEY, sessionKey)
76
+ .putString(CHECKSUM, checksum)
77
+ .putBoolean(IS_MANIFEST, isManifest)
78
+ .build();
79
+ return Result.success(output);
80
+ }
81
+
82
+ @NonNull
83
+ @Override
84
+ public Result doWork() {
85
+ try {
86
+ String url = getInputData().getString(URL);
87
+ String id = getInputData().getString(ID);
88
+ String documentsDir = getInputData().getString(DOCDIR);
89
+ String dest = getInputData().getString(FILEDEST);
90
+ String version = getInputData().getString(VERSION);
91
+ String sessionKey = getInputData().getString(SESSIONKEY);
92
+ String checksum = getInputData().getString(CHECKSUM);
93
+ String publicKey = getInputData().getString(PUBLIC_KEY);
94
+ boolean isManifest = getInputData().getBoolean(IS_MANIFEST, false);
95
+
96
+ Log.d(TAG, "doWork isManifest: " + isManifest);
97
+
98
+ if (isManifest) {
99
+ JSONArray manifest = DataManager.getInstance().getAndClearManifest();
100
+ if (manifest != null) {
101
+ handleManifestDownload(id, documentsDir, dest, version, sessionKey, publicKey, manifest.toString());
102
+ return createSuccessResult(dest, version, sessionKey, checksum, true);
103
+ } else {
104
+ Log.e(TAG, "Manifest is null");
105
+ return createFailureResult("Manifest is null");
106
+ }
107
+ } else {
108
+ handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum);
109
+ return createSuccessResult(dest, version, sessionKey, checksum, false);
110
+ }
111
+ } catch (Exception e) {
112
+ Log.e(TAG, "Error in doWork", e);
113
+ return createFailureResult(e.getMessage());
114
+ }
115
+ }
116
+
117
+ private int calcTotalPercent(long downloadedBytes, long contentLength) {
118
+ if (contentLength <= 0) {
119
+ return 0;
80
120
  }
81
- bytesRead += length;
82
- }
83
- publishResults(dest, id, version, checksum, sessionKey, "");
84
- } catch (Exception e) {
85
- e.printStackTrace();
86
- publishResults(
87
- "",
88
- id,
89
- version,
90
- checksum,
91
- sessionKey,
92
- e.getLocalizedMessage()
93
- );
121
+ int percent = (int) (((double) downloadedBytes / contentLength) * 100);
122
+ percent = Math.max(10, percent);
123
+ percent = Math.min(70, percent);
124
+ return percent;
94
125
  }
95
- }
96
-
97
- private void notifyDownload(String id, int percent) {
98
- Intent intent = new Intent(PERCENTDOWNLOAD);
99
- intent.putExtra(ID, id);
100
- intent.putExtra(PERCENT, percent);
101
- sendBroadcast(intent);
102
- }
103
-
104
- private void publishResults(
105
- String dest,
106
- String id,
107
- String version,
108
- String checksum,
109
- String sessionKey,
110
- String error
111
- ) {
112
- Intent intent = new Intent(NOTIFICATION);
113
- if (dest != null && !dest.isEmpty()) {
114
- intent.putExtra(FILEDEST, dest);
126
+
127
+ private void handleManifestDownload(
128
+ String id,
129
+ String documentsDir,
130
+ String dest,
131
+ String version,
132
+ String sessionKey,
133
+ String publicKey,
134
+ String manifestString
135
+ ) {
136
+ try {
137
+ Log.d(TAG, "handleManifestDownload");
138
+ JSONArray manifest = new JSONArray(manifestString);
139
+ File destFolder = new File(documentsDir, dest);
140
+ File cacheFolder = new File(getApplicationContext().getCacheDir(), "capgo_downloads");
141
+ File builtinFolder = new File(getApplicationContext().getFilesDir(), "public");
142
+
143
+ // Ensure directories are created
144
+ if (!destFolder.exists() && !destFolder.mkdirs()) {
145
+ throw new IOException("Failed to create destination directory: " + destFolder.getAbsolutePath());
146
+ }
147
+ if (!cacheFolder.exists() && !cacheFolder.mkdirs()) {
148
+ throw new IOException("Failed to create cache directory: " + cacheFolder.getAbsolutePath());
149
+ }
150
+
151
+ int totalFiles = manifest.length();
152
+ final AtomicLong completedFiles = new AtomicLong(0);
153
+ final AtomicBoolean hasError = new AtomicBoolean(false);
154
+
155
+ // Use more threads for I/O-bound operations
156
+ int threadCount = Math.min(64, Math.max(32, totalFiles));
157
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
158
+ List<Future<?>> futures = new ArrayList<>();
159
+
160
+ for (int i = 0; i < totalFiles; i++) {
161
+ JSONObject entry = manifest.getJSONObject(i);
162
+ String fileName = entry.getString("file_name");
163
+ String fileHash = entry.getString("file_hash");
164
+ String downloadUrl = entry.getString("download_url");
165
+
166
+ File targetFile = new File(destFolder, fileName);
167
+ File cacheFile = new File(cacheFolder, fileHash + "_" + new File(fileName).getName());
168
+ File builtinFile = new File(builtinFolder, fileName);
169
+
170
+ // Ensure parent directories of the target file exist
171
+ if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
172
+ throw new IOException("Failed to create parent directory for: " + targetFile.getAbsolutePath());
173
+ }
174
+
175
+ Future<?> future = executor.submit(() -> {
176
+ try {
177
+ if (builtinFile.exists() && verifyChecksum(builtinFile, fileHash)) {
178
+ copyFile(builtinFile, targetFile);
179
+ Log.d(TAG, "using builtin file " + fileName);
180
+ } else if (cacheFile.exists() && verifyChecksum(cacheFile, fileHash)) {
181
+ copyFile(cacheFile, targetFile);
182
+ Log.d(TAG, "already cached " + fileName);
183
+ } else {
184
+ downloadAndVerify(downloadUrl, targetFile, cacheFile, fileHash, sessionKey, publicKey);
185
+ }
186
+
187
+ long completed = completedFiles.incrementAndGet();
188
+ int percent = calcTotalPercent(completed, totalFiles);
189
+ setProgress(percent);
190
+ } catch (Exception e) {
191
+ Log.e(TAG, "Error processing file: " + fileName, e);
192
+ hasError.set(true);
193
+ }
194
+ });
195
+ futures.add(future);
196
+ }
197
+
198
+ // Wait for all downloads to complete
199
+ for (Future<?> future : futures) {
200
+ try {
201
+ future.get();
202
+ } catch (Exception e) {
203
+ Log.e(TAG, "Error waiting for download", e);
204
+ hasError.set(true);
205
+ }
206
+ }
207
+
208
+ executor.shutdown();
209
+ try {
210
+ if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
211
+ executor.shutdownNow();
212
+ }
213
+ } catch (InterruptedException e) {
214
+ executor.shutdownNow();
215
+ Thread.currentThread().interrupt();
216
+ }
217
+
218
+ if (hasError.get()) {
219
+ throw new IOException("One or more files failed to download");
220
+ }
221
+ } catch (Exception e) {
222
+ Log.e(TAG, "Error in handleManifestDownload", e);
223
+ }
115
224
  }
116
- if (error != null && !error.isEmpty()) {
117
- intent.putExtra(ERROR, error);
225
+
226
+ private void handleSingleFileDownload(
227
+ String url,
228
+ String id,
229
+ String documentsDir,
230
+ String dest,
231
+ String version,
232
+ String sessionKey,
233
+ String checksum
234
+ ) {
235
+ File target = new File(documentsDir, dest);
236
+ File infoFile = new File(documentsDir, UPDATE_FILE); // The file where the download progress (how much byte
237
+ // downloaded) is stored
238
+ File tempFile = new File(documentsDir, "temp" + ".tmp"); // Temp file, where the downloaded data is stored
239
+ try {
240
+ URL u = new URL(url);
241
+ HttpURLConnection httpConn = null;
242
+ try {
243
+ httpConn = (HttpURLConnection) u.openConnection();
244
+
245
+ // Reading progress file (if exist)
246
+ long downloadedBytes = 0;
247
+
248
+ if (infoFile.exists() && tempFile.exists()) {
249
+ try (BufferedReader reader = new BufferedReader(new FileReader(infoFile))) {
250
+ String updateVersion = reader.readLine();
251
+ if (!updateVersion.equals(version)) {
252
+ clearDownloadData(documentsDir);
253
+ } else {
254
+ downloadedBytes = tempFile.length();
255
+ }
256
+ }
257
+ } else {
258
+ clearDownloadData(documentsDir);
259
+ }
260
+
261
+ if (downloadedBytes > 0) {
262
+ httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
263
+ }
264
+
265
+ int responseCode = httpConn.getResponseCode();
266
+
267
+ if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
268
+ long contentLength = httpConn.getContentLength() + downloadedBytes;
269
+
270
+ try (
271
+ InputStream inputStream = httpConn.getInputStream();
272
+ FileOutputStream outputStream = new FileOutputStream(tempFile, downloadedBytes > 0)
273
+ ) {
274
+ if (downloadedBytes == 0) {
275
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))) {
276
+ writer.write(String.valueOf(version));
277
+ }
278
+ }
279
+ // Updating the info file
280
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))) {
281
+ writer.write(String.valueOf(version));
282
+ }
283
+
284
+ int bytesRead = -1;
285
+ byte[] buffer = new byte[4096];
286
+ int lastNotifiedPercent = 0;
287
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
288
+ outputStream.write(buffer, 0, bytesRead);
289
+ downloadedBytes += bytesRead;
290
+ // Saving progress (flushing every 100 Ko)
291
+ if (downloadedBytes % 102400 == 0) {
292
+ outputStream.flush();
293
+ }
294
+ // Computing percentage
295
+ int percent = calcTotalPercent(downloadedBytes, contentLength);
296
+ while (lastNotifiedPercent + 10 <= percent) {
297
+ lastNotifiedPercent += 10;
298
+ // Artificial delay using CPU-bound calculation to take ~5 seconds
299
+ double result = 0;
300
+ setProgress(lastNotifiedPercent);
301
+ }
302
+ }
303
+
304
+ outputStream.close();
305
+ inputStream.close();
306
+
307
+ // Rename the temp file with the final name (dest)
308
+ tempFile.renameTo(new File(documentsDir, dest));
309
+ infoFile.delete();
310
+ }
311
+ } else {
312
+ infoFile.delete();
313
+ }
314
+ } finally {
315
+ if (httpConn != null) {
316
+ httpConn.disconnect();
317
+ }
318
+ }
319
+ } catch (OutOfMemoryError e) {
320
+ e.printStackTrace();
321
+ throw new RuntimeException("low_mem_fail");
322
+ } catch (Exception e) {
323
+ e.printStackTrace();
324
+ throw new RuntimeException(e.getLocalizedMessage());
325
+ }
326
+ }
327
+
328
+ private void clearDownloadData(String docDir) {
329
+ File tempFile = new File(docDir, "temp" + ".tmp");
330
+ File infoFile = new File(docDir, UPDATE_FILE);
331
+ try {
332
+ tempFile.delete();
333
+ infoFile.delete();
334
+ infoFile.createNewFile();
335
+ tempFile.createNewFile();
336
+ } catch (IOException e) {
337
+ e.printStackTrace();
338
+ }
339
+ }
340
+
341
+ // Helper methods
342
+
343
+ private void copyFile(File source, File dest) throws IOException {
344
+ try (
345
+ FileInputStream inStream = new FileInputStream(source);
346
+ FileOutputStream outStream = new FileOutputStream(dest);
347
+ FileChannel inChannel = inStream.getChannel();
348
+ FileChannel outChannel = outStream.getChannel()
349
+ ) {
350
+ inChannel.transferTo(0, inChannel.size(), outChannel);
351
+ }
352
+ }
353
+
354
+ private void downloadAndVerify(
355
+ String downloadUrl,
356
+ File targetFile,
357
+ File cacheFile,
358
+ String expectedHash,
359
+ String sessionKey,
360
+ String publicKey
361
+ ) throws Exception {
362
+ Log.d(TAG, "downloadAndVerify " + downloadUrl);
363
+
364
+ Request request = new Request.Builder().url(downloadUrl).build();
365
+
366
+ // Create a temporary file for the compressed data
367
+ File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".br");
368
+
369
+ try (Response response = client.newCall(request).execute()) {
370
+ if (!response.isSuccessful()) {
371
+ throw new IOException("Unexpected response code: " + response.code());
372
+ }
373
+
374
+ // Download compressed file
375
+ try (ResponseBody responseBody = response.body(); FileOutputStream compressedFos = new FileOutputStream(compressedFile)) {
376
+ if (responseBody == null) {
377
+ throw new IOException("Response body is null");
378
+ }
379
+
380
+ byte[] buffer = new byte[8192];
381
+ int bytesRead;
382
+ try (InputStream inputStream = responseBody.byteStream()) {
383
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
384
+ compressedFos.write(buffer, 0, bytesRead);
385
+ }
386
+ }
387
+ }
388
+
389
+ String decryptedExpectedHash = expectedHash;
390
+
391
+ if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
392
+ Log.d(CapacitorUpdater.TAG + " DLSrv", "Decrypting file " + targetFile.getName());
393
+ CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
394
+ decryptedExpectedHash = CryptoCipherV2.decryptChecksum(decryptedExpectedHash, publicKey);
395
+ }
396
+
397
+ // Decompress the file
398
+ try (
399
+ FileInputStream fis = new FileInputStream(compressedFile);
400
+ BrotliInputStream brotliInputStream = new BrotliInputStream(fis);
401
+ FileOutputStream fos = new FileOutputStream(targetFile)
402
+ ) {
403
+ byte[] buffer = new byte[8192];
404
+ int len;
405
+ while ((len = brotliInputStream.read(buffer)) != -1) {
406
+ fos.write(buffer, 0, len);
407
+ }
408
+ }
409
+
410
+ // Delete the compressed file
411
+ compressedFile.delete();
412
+ String calculatedHash = CryptoCipherV2.calcChecksum(targetFile);
413
+
414
+ // Verify checksum
415
+ if (calculatedHash.equals(decryptedExpectedHash)) {
416
+ // Only cache if checksum is correct
417
+ copyFile(targetFile, cacheFile);
418
+ } else {
419
+ targetFile.delete();
420
+ throw new IOException("Checksum verification failed for " + targetFile.getName());
421
+ }
422
+ }
423
+ }
424
+
425
+ private boolean verifyChecksum(File file, String expectedHash) {
426
+ try {
427
+ String actualHash = calculateFileHash(file);
428
+ return actualHash.equals(expectedHash);
429
+ } catch (Exception e) {
430
+ e.printStackTrace();
431
+ return false;
432
+ }
433
+ }
434
+
435
+ private String calculateFileHash(File file) throws Exception {
436
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
437
+ FileInputStream fis = new FileInputStream(file);
438
+ byte[] byteArray = new byte[1024];
439
+ int bytesCount = 0;
440
+
441
+ while ((bytesCount = fis.read(byteArray)) != -1) {
442
+ digest.update(byteArray, 0, bytesCount);
443
+ }
444
+ fis.close();
445
+
446
+ byte[] bytes = digest.digest();
447
+ StringBuilder sb = new StringBuilder();
448
+ for (byte aByte : bytes) {
449
+ sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
450
+ }
451
+ return sb.toString();
118
452
  }
119
- intent.putExtra(ID, id);
120
- intent.putExtra(VERSION, version);
121
- intent.putExtra(SESSIONKEY, sessionKey);
122
- intent.putExtra(CHECKSUM, checksum);
123
- intent.putExtra(ERROR, error);
124
- sendBroadcast(intent);
125
- }
126
453
  }
@@ -0,0 +1,101 @@
1
+ package ee.forgr.capacitor_updater;
2
+
3
+ import android.content.Context;
4
+ import android.util.Log;
5
+ import androidx.work.BackoffPolicy;
6
+ import androidx.work.Configuration;
7
+ import androidx.work.Constraints;
8
+ import androidx.work.Data;
9
+ import androidx.work.NetworkType;
10
+ import androidx.work.OneTimeWorkRequest;
11
+ import androidx.work.WorkManager;
12
+ import androidx.work.WorkRequest;
13
+ import java.util.HashSet;
14
+ import java.util.Set;
15
+ import java.util.concurrent.TimeUnit;
16
+
17
+ public class DownloadWorkerManager {
18
+
19
+ private static final String TAG = "DownloadWorkerManager";
20
+ private static volatile boolean isInitialized = false;
21
+ private static final Set<String> activeVersions = new HashSet<>();
22
+
23
+ private static synchronized void initializeIfNeeded(Context context) {
24
+ if (!isInitialized) {
25
+ try {
26
+ Configuration config = new Configuration.Builder().setMinimumLoggingLevel(android.util.Log.INFO).build();
27
+ WorkManager.initialize(context, config);
28
+ isInitialized = true;
29
+ } catch (IllegalStateException e) {
30
+ // WorkManager was already initialized, ignore
31
+ }
32
+ }
33
+ }
34
+
35
+ public static synchronized boolean isVersionDownloading(String version) {
36
+ return activeVersions.contains(version);
37
+ }
38
+
39
+ public static void enqueueDownload(
40
+ Context context,
41
+ String url,
42
+ String id,
43
+ String documentsDir,
44
+ String dest,
45
+ String version,
46
+ String sessionKey,
47
+ String checksum,
48
+ String publicKey,
49
+ boolean isManifest
50
+ ) {
51
+ initializeIfNeeded(context.getApplicationContext());
52
+
53
+ // If version is already downloading, don't start another one
54
+ if (isVersionDownloading(version)) {
55
+ Log.i(TAG, "Version " + version + " is already downloading");
56
+ return;
57
+ }
58
+ activeVersions.add(version);
59
+
60
+ // Create input data
61
+ Data inputData = new Data.Builder()
62
+ .putString(DownloadService.URL, url)
63
+ .putString(DownloadService.ID, id)
64
+ .putString(DownloadService.DOCDIR, documentsDir)
65
+ .putString(DownloadService.FILEDEST, dest)
66
+ .putString(DownloadService.VERSION, version)
67
+ .putString(DownloadService.SESSIONKEY, sessionKey)
68
+ .putString(DownloadService.CHECKSUM, checksum)
69
+ .putBoolean(DownloadService.IS_MANIFEST, isManifest)
70
+ .putString(DownloadService.PUBLIC_KEY, publicKey)
71
+ .build();
72
+
73
+ // Create network constraints
74
+ Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
75
+
76
+ // Create work request with tags for tracking
77
+ OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DownloadService.class)
78
+ .setConstraints(constraints)
79
+ .setInputData(inputData)
80
+ .addTag(id)
81
+ .addTag(version) // Add version tag for tracking
82
+ .addTag("capacitor_updater_download")
83
+ .setBackoffCriteria(BackoffPolicy.LINEAR, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
84
+ .build();
85
+
86
+ // Enqueue work
87
+ WorkManager.getInstance(context).enqueue(workRequest);
88
+ }
89
+
90
+ public static void cancelVersionDownload(Context context, String version) {
91
+ initializeIfNeeded(context.getApplicationContext());
92
+ WorkManager.getInstance(context).cancelAllWorkByTag(version);
93
+ activeVersions.remove(version);
94
+ }
95
+
96
+ public static void cancelAllDownloads(Context context) {
97
+ initializeIfNeeded(context.getApplicationContext());
98
+ WorkManager.getInstance(context).cancelAllWorkByTag("capacitor_updater_download");
99
+ activeVersions.clear();
100
+ }
101
+ }