@capgo/capacitor-updater 6.7.7-alpha.2 → 6.7.7

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.
@@ -49,10 +49,13 @@ repositories {
49
49
 
50
50
 
51
51
  dependencies {
52
+ def work_version = "2.9.1"
53
+ implementation "androidx.work:work-runtime:$work_version"
54
+ implementation "com.google.android.gms:play-services-tasks:18.2.0"
55
+ implementation "com.google.guava:guava:32.1.3-android"
52
56
  implementation fileTree(dir: 'libs', include: ['*.jar'])
53
57
  implementation project(':capacitor-android')
54
58
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
55
- // implementation 'com.android.volley:volley:1.2.1'
56
59
  implementation 'io.github.g00fy2:versioncompare:1.5.0'
57
60
  implementation 'com.google.code.gson:gson:2.11.0'
58
61
  testImplementation "junit:junit:$junitVersion"
@@ -1,9 +1,4 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
- <uses-permission android:name="android.permission.WAKE_LOCK" />
3
-
4
2
  <application>
5
- <service
6
- android:name="ee.forgr.capacitor_updater.DownloadService"
7
- android:exported="false" />
8
3
  </application>
9
4
  </manifest>
@@ -6,21 +6,21 @@
6
6
 
7
7
  package ee.forgr.capacitor_updater;
8
8
 
9
- import static android.content.Context.RECEIVER_NOT_EXPORTED;
10
-
11
- import android.annotation.SuppressLint;
12
9
  import android.app.Activity;
13
- import android.content.BroadcastReceiver;
14
10
  import android.content.Context;
15
- import android.content.Intent;
16
- import android.content.IntentFilter;
17
11
  import android.content.SharedPreferences;
18
12
  import android.os.Build;
19
- import android.os.Bundle;
20
13
  import android.util.Base64;
21
14
  import android.util.Log;
15
+ import androidx.annotation.NonNull;
16
+ import androidx.lifecycle.LifecycleOwner;
17
+ import androidx.work.Data;
18
+ import androidx.work.WorkInfo;
19
+ import androidx.work.WorkManager;
22
20
  import com.getcapacitor.JSObject;
23
21
  import com.getcapacitor.plugin.WebView;
22
+ import com.google.common.util.concurrent.Futures;
23
+ import com.google.common.util.concurrent.ListenableFuture;
24
24
  import java.io.BufferedInputStream;
25
25
  import java.io.DataInputStream;
26
26
  import java.io.File;
@@ -29,9 +29,6 @@ import java.io.FileNotFoundException;
29
29
  import java.io.FileOutputStream;
30
30
  import java.io.FilenameFilter;
31
31
  import java.io.IOException;
32
- import java.io.InputStream;
33
- import java.net.URL;
34
- import java.net.URLConnection;
35
32
  import java.security.GeneralSecurityException;
36
33
  import java.security.MessageDigest;
37
34
  import java.security.PrivateKey;
@@ -251,87 +248,130 @@ public class CapacitorUpdater {
251
248
  sourceFile.delete();
252
249
  }
253
250
 
254
- @SuppressLint("UnspecifiedRegisterReceiverFlag")
255
- public void onResume() {
256
- IntentFilter filter = new IntentFilter();
257
- filter.addAction(DownloadService.NOTIFICATION);
258
- filter.addAction(DownloadService.PERCENTDOWNLOAD);
259
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
260
- this.activity.registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED);
261
- } else {
262
- this.activity.registerReceiver(receiver, filter);
251
+ private void observeWorkProgress(Context context, String id) {
252
+ if (!(context instanceof LifecycleOwner)) {
253
+ Log.e(
254
+ TAG,
255
+ "Context is not a LifecycleOwner, cannot observe work progress"
256
+ );
257
+ return;
263
258
  }
264
- }
265
259
 
266
- public void onPause() {
267
- this.activity.unregisterReceiver(receiver);
268
- }
260
+ activity.runOnUiThread(() -> {
261
+ WorkManager.getInstance(context)
262
+ .getWorkInfosByTagLiveData(id)
263
+ .observe((LifecycleOwner) context, workInfos -> {
264
+ if (workInfos == null || workInfos.isEmpty()) return;
265
+
266
+ WorkInfo workInfo = workInfos.get(0);
267
+ Data progress = workInfo.getProgress();
268
+
269
+ switch (workInfo.getState()) {
270
+ case RUNNING:
271
+ int percent = progress.getInt(DownloadService.PERCENT, 0);
272
+ notifyDownload(id, percent);
273
+ break;
274
+ case SUCCEEDED:
275
+ Data outputData = workInfo.getOutputData();
276
+ String dest = outputData.getString(DownloadService.FILEDEST);
277
+ String version = outputData.getString(DownloadService.VERSION);
278
+ String sessionKey = outputData.getString(
279
+ DownloadService.SESSIONKEY
280
+ );
281
+ String checksum = outputData.getString(DownloadService.CHECKSUM);
282
+ boolean isManifest = outputData.getBoolean(
283
+ DownloadService.IS_MANIFEST,
284
+ false
285
+ );
269
286
 
270
- private final BroadcastReceiver receiver = new BroadcastReceiver() {
271
- @Override
272
- public void onReceive(Context context, Intent intent) {
273
- String action = intent.getAction();
274
- Bundle bundle = intent.getExtras();
275
- if (bundle != null) {
276
- if (Objects.equals(action, DownloadService.PERCENTDOWNLOAD)) {
277
- String id = bundle.getString(DownloadService.ID);
278
- int percent = bundle.getInt(DownloadService.PERCENT);
279
- CapacitorUpdater.this.notifyDownload(id, percent);
280
- } else if (Objects.equals(action, DownloadService.NOTIFICATION)) {
281
- String id = bundle.getString(DownloadService.ID);
282
- String dest = bundle.getString(DownloadService.FILEDEST);
283
- String version = bundle.getString(DownloadService.VERSION);
284
- String sessionKey = bundle.getString(DownloadService.SESSIONKEY);
285
- String checksum = bundle.getString(DownloadService.CHECKSUM);
286
- String error = bundle.getString(DownloadService.ERROR);
287
- boolean isManifest = bundle.getBoolean(
288
- DownloadService.IS_MANIFEST,
289
- false
290
- );
291
- Log.i(
292
- CapacitorUpdater.TAG,
293
- "res " +
294
- id +
295
- " " +
296
- dest +
297
- " " +
298
- version +
299
- " " +
300
- sessionKey +
301
- " " +
302
- checksum +
303
- " " +
304
- error
305
- );
306
- if (dest == null) {
307
- final JSObject ret = new JSObject();
308
- ret.put(
309
- "version",
310
- CapacitorUpdater.this.getCurrentBundle().getVersionName()
311
- );
312
- if ("low_mem_fail".equals(error)) {
313
- CapacitorUpdater.this.sendStats("low_mem_fail", version);
314
- }
315
- ret.put("error", "download_fail");
316
- CapacitorUpdater.this.sendStats("download_fail", version);
317
- CapacitorUpdater.this.notifyListeners("downloadFailed", ret);
318
- return;
287
+ boolean success = finishDownload(
288
+ id,
289
+ dest,
290
+ version,
291
+ sessionKey,
292
+ checksum,
293
+ true,
294
+ isManifest
295
+ );
296
+ if (!success) {
297
+ saveBundleInfo(
298
+ id,
299
+ new BundleInfo(
300
+ id,
301
+ version,
302
+ BundleStatus.ERROR,
303
+ new Date(System.currentTimeMillis()),
304
+ ""
305
+ )
306
+ );
307
+ JSObject ret = new JSObject();
308
+ ret.put("version", getCurrentBundle().getVersionName());
309
+ ret.put("error", "finish_download_fail");
310
+ sendStats("finish_download_fail", version);
311
+ notifyListeners("downloadFailed", ret);
312
+ }
313
+ break;
314
+ case FAILED:
315
+ Data failedData = workInfo.getOutputData();
316
+ String error = failedData.getString(DownloadService.ERROR);
317
+ String failedVersion = failedData.getString(
318
+ DownloadService.VERSION
319
+ );
320
+ saveBundleInfo(
321
+ id,
322
+ new BundleInfo(
323
+ id,
324
+ failedVersion,
325
+ BundleStatus.ERROR,
326
+ new Date(System.currentTimeMillis()),
327
+ ""
328
+ )
329
+ );
330
+ JSObject ret = new JSObject();
331
+ ret.put("version", getCurrentBundle().getVersionName());
332
+ if ("low_mem_fail".equals(error)) {
333
+ sendStats("low_mem_fail", failedVersion);
334
+ }
335
+ ret.put("error", error != null ? error : "download_fail");
336
+ sendStats("download_fail", failedVersion);
337
+ notifyListeners("downloadFailed", ret);
338
+ break;
319
339
  }
320
- CapacitorUpdater.this.finishDownload(
321
- id,
322
- dest,
323
- version,
324
- sessionKey,
325
- checksum,
326
- true,
327
- isManifest
328
- );
329
- } else {
330
- Log.i(TAG, "Unknown action " + action);
331
- }
332
- }
340
+ });
341
+ });
342
+ }
343
+
344
+ private void download(
345
+ final String id,
346
+ final String url,
347
+ final String dest,
348
+ final String version,
349
+ final String sessionKey,
350
+ final String checksum,
351
+ final JSONArray manifest
352
+ ) {
353
+ if (this.activity == null) {
354
+ Log.e(TAG, "Activity is null, cannot observe work progress");
355
+ return;
333
356
  }
334
- };
357
+ observeWorkProgress(this.activity, id);
358
+
359
+ DownloadWorkerManager.enqueueDownload(
360
+ this.activity,
361
+ url,
362
+ id,
363
+ this.documentsDir.getAbsolutePath(),
364
+ dest,
365
+ version,
366
+ sessionKey,
367
+ checksum,
368
+ manifest != null
369
+ );
370
+
371
+ if (manifest != null) {
372
+ DataManager.getInstance().setManifest(manifest);
373
+ }
374
+ }
335
375
 
336
376
  private String decryptChecksum(String checksum, String version)
337
377
  throws IOException {
@@ -457,77 +497,6 @@ public class CapacitorUpdater {
457
497
  return true;
458
498
  }
459
499
 
460
- private void downloadFileBackground(
461
- final String id,
462
- final String url,
463
- final String version,
464
- final String sessionKey,
465
- final String checksum,
466
- final String dest,
467
- final JSONArray manifest
468
- ) {
469
- Intent intent = new Intent(this.activity, DownloadService.class);
470
- intent.putExtra(DownloadService.URL, url);
471
- intent.putExtra(DownloadService.FILEDEST, dest);
472
- intent.putExtra(
473
- DownloadService.DOCDIR,
474
- this.documentsDir.getAbsolutePath()
475
- );
476
- intent.putExtra(DownloadService.ID, id);
477
- intent.putExtra(DownloadService.VERSION, version);
478
- intent.putExtra(DownloadService.SESSIONKEY, sessionKey);
479
- intent.putExtra(DownloadService.CHECKSUM, checksum);
480
- if (manifest != null) {
481
- DataManager.getInstance().setManifest(manifest);
482
- intent.putExtra(DownloadService.IS_MANIFEST, true);
483
- }
484
-
485
- this.activity.startService(intent);
486
- }
487
-
488
- private void downloadFile(
489
- final String id,
490
- final String url,
491
- final String dest
492
- ) throws IOException {
493
- final URL u = new URL(url);
494
- final URLConnection connection = u.openConnection();
495
-
496
- final File target = new File(this.documentsDir, dest);
497
- Objects.requireNonNull(target.getParentFile()).mkdirs();
498
- target.createNewFile();
499
-
500
- final long totalLength = connection.getContentLength();
501
- int bytesRead = 0;
502
- int percent = 0;
503
- this.notifyDownload(id, 10);
504
-
505
- try (
506
- InputStream is = connection.getInputStream();
507
- DataInputStream dis = new DataInputStream(is);
508
- FileOutputStream fos = new FileOutputStream(target)
509
- ) {
510
- byte[] buffer = new byte[8192];
511
- int length;
512
- while ((length = dis.read(buffer)) > 0) {
513
- fos.write(buffer, 0, length);
514
- bytesRead += length;
515
- final int newPercent = (int) ((bytesRead / (float) totalLength) * 100);
516
- if (totalLength > 1 && newPercent != percent) {
517
- percent = newPercent;
518
- this.notifyDownload(id, this.calcTotalPercent(percent, 10, 70));
519
- }
520
- }
521
- if (bytesRead == 0) {
522
- throw new IOException("Failed to download: No data read from URL");
523
- }
524
- } catch (OutOfMemoryError e) {
525
- Log.e(TAG, "OutOfMemoryError while downloading file", e);
526
- this.sendStats("low_mem_fail");
527
- throw new IOException("OutOfMemoryError while downloading file");
528
- }
529
- }
530
-
531
500
  private void deleteDirectory(final File file) throws IOException {
532
501
  if (file.isDirectory()) {
533
502
  final File[] entries = file.listFiles();
@@ -727,6 +696,16 @@ public class CapacitorUpdater {
727
696
  final JSONArray manifest
728
697
  ) {
729
698
  final String id = this.randomString();
699
+
700
+ // Check if version is already downloading
701
+ if (
702
+ this.activity != null &&
703
+ DownloadWorkerManager.isVersionDownloading(version)
704
+ ) {
705
+ Log.i(TAG, "Version already downloading: " + version);
706
+ return;
707
+ }
708
+
730
709
  this.saveBundleInfo(
731
710
  id,
732
711
  new BundleInfo(
@@ -740,13 +719,13 @@ public class CapacitorUpdater {
740
719
  this.notifyDownload(id, 0);
741
720
  this.notifyDownload(id, 5);
742
721
 
743
- this.downloadFileBackground(
722
+ this.download(
744
723
  id,
745
724
  url,
725
+ this.randomString(),
746
726
  version,
747
727
  sessionKey,
748
728
  checksum,
749
- this.randomString(),
750
729
  manifest
751
730
  );
752
731
  }
@@ -771,29 +750,68 @@ public class CapacitorUpdater {
771
750
  this.notifyDownload(id, 0);
772
751
  this.notifyDownload(id, 5);
773
752
  final String dest = this.randomString();
774
- this.downloadFile(id, url, dest);
775
- final Boolean finished =
776
- this.finishDownload(
753
+
754
+ // Use the new WorkManager-based download
755
+ this.download(id, url, dest, version, sessionKey, checksum, null);
756
+
757
+ // Wait for completion
758
+ try {
759
+ ListenableFuture<List<WorkInfo>> future = WorkManager.getInstance(
760
+ activity
761
+ ).getWorkInfosByTag(id);
762
+
763
+ List<WorkInfo> workInfos = Futures.getChecked(future, IOException.class);
764
+
765
+ if (workInfos != null && !workInfos.isEmpty()) {
766
+ WorkInfo workInfo = workInfos.get(0);
767
+ while (!workInfo.getState().isFinished()) {
768
+ Thread.sleep(100);
769
+ workInfos = Futures.getChecked(
770
+ WorkManager.getInstance(activity).getWorkInfosByTag(id),
771
+ IOException.class
772
+ );
773
+ if (workInfos != null && !workInfos.isEmpty()) {
774
+ workInfo = workInfos.get(0);
775
+ }
776
+ }
777
+
778
+ if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
779
+ Data outputData = workInfo.getOutputData();
780
+ boolean success = finishDownload(
781
+ id,
782
+ outputData.getString(DownloadService.FILEDEST),
783
+ version,
784
+ sessionKey,
785
+ checksum,
786
+ true,
787
+ false
788
+ );
789
+ if (!success) {
790
+ throw new IOException("Failed to finish download");
791
+ }
792
+ } else {
793
+ Data outputData = workInfo.getOutputData();
794
+ String error = outputData.getString(DownloadService.ERROR);
795
+ throw new IOException(
796
+ error != null ? error : "Download failed: " + workInfo.getState()
797
+ );
798
+ }
799
+ }
800
+ return getBundleInfo(id);
801
+ } catch (Exception e) {
802
+ Log.e(TAG, "Error waiting for download", e);
803
+ saveBundleInfo(
804
+ id,
805
+ new BundleInfo(
777
806
  id,
778
- dest,
779
807
  version,
780
- sessionKey,
781
- checksum,
782
- false,
783
- false
784
- );
785
- final BundleStatus status = finished
786
- ? BundleStatus.PENDING
787
- : BundleStatus.ERROR;
788
- BundleInfo info = new BundleInfo(
789
- id,
790
- version,
791
- status,
792
- new Date(System.currentTimeMillis()),
793
- checksum
794
- );
795
- this.saveBundleInfo(id, info);
796
- return info;
808
+ BundleStatus.ERROR,
809
+ new Date(System.currentTimeMillis()),
810
+ ""
811
+ )
812
+ );
813
+ throw new IOException("Error waiting for download: " + e.getMessage());
814
+ }
797
815
  }
798
816
 
799
817
  public List<BundleInfo> list() {
@@ -818,6 +836,13 @@ public class CapacitorUpdater {
818
836
  Log.e(TAG, "Cannot delete " + id);
819
837
  return false;
820
838
  }
839
+ // Cancel download for this version if active
840
+ if (this.activity != null) {
841
+ DownloadWorkerManager.cancelVersionDownload(
842
+ this.activity,
843
+ deleted.getVersionName()
844
+ );
845
+ }
821
846
  final File bundle = new File(this.documentsDir, bundleDirectory + "/" + id);
822
847
  if (bundle.exists()) {
823
848
  this.deleteDirectory(bundle);
@@ -929,6 +954,10 @@ public class CapacitorUpdater {
929
954
  this.setCurrentBundle(new File("public"));
930
955
  this.setFallbackBundle(null);
931
956
  this.setNextBundle(null);
957
+ // Cancel any ongoing downloads
958
+ if (this.activity != null) {
959
+ DownloadWorkerManager.cancelAllDownloads(this.activity);
960
+ }
932
961
  if (!internal) {
933
962
  this.sendStats(
934
963
  "reset",
@@ -970,7 +999,7 @@ public class CapacitorUpdater {
970
999
  .enqueue(
971
1000
  new okhttp3.Callback() {
972
1001
  @Override
973
- public void onFailure(Call call, IOException e) {
1002
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
974
1003
  JSObject retError = new JSObject();
975
1004
  retError.put("message", "Request failed: " + e.getMessage());
976
1005
  retError.put("error", "network_error");
@@ -978,8 +1007,10 @@ public class CapacitorUpdater {
978
1007
  }
979
1008
 
980
1009
  @Override
981
- public void onResponse(Call call, Response response)
982
- throws IOException {
1010
+ public void onResponse(
1011
+ @NonNull Call call,
1012
+ @NonNull Response response
1013
+ ) throws IOException {
983
1014
  try (ResponseBody responseBody = response.body()) {
984
1015
  if (!response.isSuccessful()) {
985
1016
  JSObject retError = new JSObject();
@@ -989,6 +1020,7 @@ public class CapacitorUpdater {
989
1020
  return;
990
1021
  }
991
1022
 
1023
+ assert responseBody != null;
992
1024
  String responseData = responseBody.string();
993
1025
  JSONObject jsonResponse = new JSONObject(responseData);
994
1026
  JSObject ret = new JSObject();
@@ -1068,7 +1100,7 @@ public class CapacitorUpdater {
1068
1100
  .enqueue(
1069
1101
  new okhttp3.Callback() {
1070
1102
  @Override
1071
- public void onFailure(Call call, IOException e) {
1103
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
1072
1104
  JSObject retError = new JSObject();
1073
1105
  retError.put("message", "Request failed: " + e.getMessage());
1074
1106
  retError.put("error", "network_error");
@@ -1076,8 +1108,10 @@ public class CapacitorUpdater {
1076
1108
  }
1077
1109
 
1078
1110
  @Override
1079
- public void onResponse(Call call, Response response)
1080
- throws IOException {
1111
+ public void onResponse(
1112
+ @NonNull Call call,
1113
+ @NonNull Response response
1114
+ ) throws IOException {
1081
1115
  try (ResponseBody responseBody = response.body()) {
1082
1116
  if (!response.isSuccessful()) {
1083
1117
  JSObject retError = new JSObject();
@@ -1087,6 +1121,7 @@ public class CapacitorUpdater {
1087
1121
  return;
1088
1122
  }
1089
1123
 
1124
+ assert responseBody != null;
1090
1125
  String responseData = responseBody.string();
1091
1126
  JSONObject jsonResponse = new JSONObject(responseData);
1092
1127
  JSObject ret = new JSObject();
@@ -1171,7 +1206,7 @@ public class CapacitorUpdater {
1171
1206
  .enqueue(
1172
1207
  new okhttp3.Callback() {
1173
1208
  @Override
1174
- public void onFailure(Call call, IOException e) {
1209
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
1175
1210
  JSObject retError = new JSObject();
1176
1211
  retError.put("message", "Request failed: " + e.getMessage());
1177
1212
  retError.put("error", "network_error");
@@ -1179,10 +1214,13 @@ public class CapacitorUpdater {
1179
1214
  }
1180
1215
 
1181
1216
  @Override
1182
- public void onResponse(Call call, Response response)
1183
- throws IOException {
1217
+ public void onResponse(
1218
+ @NonNull Call call,
1219
+ @NonNull Response response
1220
+ ) throws IOException {
1184
1221
  try (ResponseBody responseBody = response.body()) {
1185
1222
  if (response.code() == 400) {
1223
+ assert responseBody != null;
1186
1224
  String data = responseBody.string();
1187
1225
  if (
1188
1226
  data.contains("channel_not_found") &&
@@ -1205,6 +1243,7 @@ public class CapacitorUpdater {
1205
1243
  return;
1206
1244
  }
1207
1245
 
1246
+ assert responseBody != null;
1208
1247
  String responseData = responseBody.string();
1209
1248
  JSONObject jsonResponse = new JSONObject(responseData);
1210
1249
  JSObject ret = new JSObject();
@@ -1269,13 +1308,15 @@ public class CapacitorUpdater {
1269
1308
  .enqueue(
1270
1309
  new okhttp3.Callback() {
1271
1310
  @Override
1272
- public void onFailure(Call call, IOException e) {
1311
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
1273
1312
  Log.e(TAG, "Failed to send stats: " + e.getMessage());
1274
1313
  }
1275
1314
 
1276
1315
  @Override
1277
- public void onResponse(Call call, Response response)
1278
- throws IOException {
1316
+ public void onResponse(
1317
+ @NonNull Call call,
1318
+ @NonNull Response response
1319
+ ) throws IOException {
1279
1320
  if (response.isSuccessful()) {
1280
1321
  Log.i(
1281
1322
  TAG,
@@ -57,7 +57,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
57
57
  private static final String channelUrlDefault =
58
58
  "https://plugin.capgo.app/channel_self";
59
59
 
60
- private final String PLUGIN_VERSION = "6.7.7-alpha.2";
60
+ private final String PLUGIN_VERSION = "6.7.7";
61
61
  private static final String DELAY_CONDITION_PREFERENCES = "";
62
62
 
63
63
  private SharedPreferences.Editor editor;
@@ -1658,13 +1658,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
1658
1658
  backgroundTask.interrupt();
1659
1659
  }
1660
1660
  this.implementation.activity = getActivity();
1661
- this.implementation.onResume();
1662
1661
  }
1663
1662
 
1664
1663
  @Override
1665
1664
  public void handleOnPause() {
1666
1665
  this.implementation.activity = getActivity();
1667
- this.implementation.onPause();
1668
1666
  }
1669
1667
 
1670
1668
  @Override
@@ -5,10 +5,12 @@
5
5
  */
6
6
  package ee.forgr.capacitor_updater;
7
7
 
8
- import android.app.IntentService;
9
- import android.content.Intent;
10
- import android.os.PowerManager;
8
+ import android.content.Context;
11
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;
12
14
  import java.io.*;
13
15
  import java.io.FileInputStream;
14
16
  import java.net.HttpURLConnection;
@@ -18,6 +20,7 @@ import java.security.MessageDigest;
18
20
  import java.util.ArrayList;
19
21
  import java.util.Arrays;
20
22
  import java.util.List;
23
+ import java.util.Objects;
21
24
  import java.util.concurrent.ExecutorService;
22
25
  import java.util.concurrent.Executors;
23
26
  import java.util.concurrent.Future;
@@ -33,7 +36,7 @@ import org.brotli.dec.BrotliInputStream;
33
36
  import org.json.JSONArray;
34
37
  import org.json.JSONObject;
35
38
 
36
- public class DownloadService extends IntentService {
39
+ public class DownloadService extends Worker {
37
40
 
38
41
  public static final String TAG = "Capacitor-updater";
39
42
  public static final String URL = "URL";
@@ -45,68 +48,62 @@ public class DownloadService extends IntentService {
45
48
  public static final String VERSION = "version";
46
49
  public static final String SESSIONKEY = "sessionkey";
47
50
  public static final String CHECKSUM = "checksum";
48
- public static final String NOTIFICATION = "service receiver";
49
- public static final String PERCENTDOWNLOAD = "percent receiver";
50
51
  public static final String IS_MANIFEST = "is_manifest";
51
- public static final String MANIFEST = "manifest";
52
52
  private static final String UPDATE_FILE = "update.dat";
53
53
 
54
54
  private final OkHttpClient client = new OkHttpClient.Builder()
55
55
  .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
56
56
  .build();
57
- private PowerManager.WakeLock wakeLock;
58
57
 
59
- public DownloadService() {
60
- super("Background DownloadService");
58
+ public DownloadService(
59
+ @NonNull Context context,
60
+ @NonNull WorkerParameters params
61
+ ) {
62
+ super(context, params);
61
63
  }
62
64
 
63
- @Override
64
- public void onCreate() {
65
- super.onCreate();
66
- PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
67
- wakeLock = powerManager.newWakeLock(
68
- PowerManager.PARTIAL_WAKE_LOCK,
69
- "CapacitorUpdater::DownloadWakeLock"
70
- );
65
+ private void setProgress(int percent) {
66
+ Data progress = new Data.Builder().putInt(PERCENT, percent).build();
67
+ setProgressAsync(progress);
71
68
  }
72
69
 
73
- @Override
74
- public void onDestroy() {
75
- super.onDestroy();
76
- Log.w(TAG + " DownloadService", "DownloadService killed/destroyed");
77
- if (wakeLock != null && wakeLock.isHeld()) {
78
- wakeLock.release();
79
- }
70
+ private Result createFailureResult(String error) {
71
+ Data output = new Data.Builder().putString(ERROR, error).build();
72
+ return Result.failure(output);
80
73
  }
81
74
 
82
- private int calcTotalPercent(long downloadedBytes, long contentLength) {
83
- if (contentLength <= 0) {
84
- return 0;
85
- }
86
- int percent = (int) (((double) downloadedBytes / contentLength) * 100);
87
- percent = Math.max(10, percent);
88
- percent = Math.min(70, percent);
89
- return percent;
75
+ private Result createSuccessResult(
76
+ String dest,
77
+ String version,
78
+ String sessionKey,
79
+ String checksum,
80
+ boolean isManifest
81
+ ) {
82
+ Data output = new Data.Builder()
83
+ .putString(FILEDEST, dest)
84
+ .putString(VERSION, version)
85
+ .putString(SESSIONKEY, sessionKey)
86
+ .putString(CHECKSUM, checksum)
87
+ .putBoolean(IS_MANIFEST, isManifest)
88
+ .build();
89
+ return Result.success(output);
90
90
  }
91
91
 
92
+ @NonNull
92
93
  @Override
93
- protected void onHandleIntent(Intent intent) {
94
+ public Result doWork() {
94
95
  try {
95
- wakeLock.acquire(3 * 60 * 1000L);
96
- assert intent != null;
97
- String url = intent.getStringExtra(URL);
98
- String id = intent.getStringExtra(ID);
99
- String documentsDir = intent.getStringExtra(DOCDIR);
100
- String dest = intent.getStringExtra(FILEDEST);
101
- String version = intent.getStringExtra(VERSION);
102
- String sessionKey = intent.getStringExtra(SESSIONKEY);
103
- String checksum = intent.getStringExtra(CHECKSUM);
104
- boolean isManifest = intent.getBooleanExtra(IS_MANIFEST, false);
105
-
106
- Log.d(
107
- TAG + " DownloadService",
108
- "onHandleIntent isManifest: " + isManifest
109
- );
96
+ String url = getInputData().getString(URL);
97
+ String id = getInputData().getString(ID);
98
+ String documentsDir = getInputData().getString(DOCDIR);
99
+ String dest = getInputData().getString(FILEDEST);
100
+ String version = getInputData().getString(VERSION);
101
+ String sessionKey = getInputData().getString(SESSIONKEY);
102
+ String checksum = getInputData().getString(CHECKSUM);
103
+ boolean isManifest = getInputData().getBoolean(IS_MANIFEST, false);
104
+
105
+ Log.d(TAG, "doWork isManifest: " + isManifest);
106
+
110
107
  if (isManifest) {
111
108
  JSONArray manifest = DataManager.getInstance().getAndClearManifest();
112
109
  if (manifest != null) {
@@ -118,17 +115,10 @@ public class DownloadService extends IntentService {
118
115
  sessionKey,
119
116
  manifest.toString()
120
117
  );
118
+ return createSuccessResult(dest, version, sessionKey, checksum, true);
121
119
  } else {
122
- Log.e(TAG + " DownloadService", "Manifest is null");
123
- publishResults(
124
- "",
125
- id,
126
- version,
127
- checksum,
128
- sessionKey,
129
- "Manifest is null",
130
- false
131
- );
120
+ Log.e(TAG, "Manifest is null");
121
+ return createFailureResult("Manifest is null");
132
122
  }
133
123
  } else {
134
124
  handleSingleFileDownload(
@@ -140,12 +130,24 @@ public class DownloadService extends IntentService {
140
130
  sessionKey,
141
131
  checksum
142
132
  );
133
+ return createSuccessResult(dest, version, sessionKey, checksum, false);
143
134
  }
144
- } finally {
145
- wakeLock.release();
135
+ } catch (Exception e) {
136
+ Log.e(TAG, "Error in doWork", e);
137
+ return createFailureResult(e.getMessage());
146
138
  }
147
139
  }
148
140
 
141
+ private int calcTotalPercent(long downloadedBytes, long contentLength) {
142
+ if (contentLength <= 0) {
143
+ return 0;
144
+ }
145
+ int percent = (int) (((double) downloadedBytes / contentLength) * 100);
146
+ percent = Math.max(10, percent);
147
+ percent = Math.min(70, percent);
148
+ return percent;
149
+ }
150
+
149
151
  private void handleManifestDownload(
150
152
  String id,
151
153
  String documentsDir,
@@ -155,7 +157,7 @@ public class DownloadService extends IntentService {
155
157
  String manifestString
156
158
  ) {
157
159
  try {
158
- Log.d(TAG + " DownloadService", "handleManifestDownload");
160
+ Log.d(TAG, "handleManifestDownload");
159
161
  JSONArray manifest = new JSONArray(manifestString);
160
162
  File destFolder = new File(documentsDir, dest);
161
163
  File cacheFolder = new File(
@@ -204,7 +206,7 @@ public class DownloadService extends IntentService {
204
206
 
205
207
  // Ensure parent directories of the target file exist
206
208
  if (
207
- !targetFile.getParentFile().exists() &&
209
+ !Objects.requireNonNull(targetFile.getParentFile()).exists() &&
208
210
  !targetFile.getParentFile().mkdirs()
209
211
  ) {
210
212
  throw new IOException(
@@ -217,12 +219,12 @@ public class DownloadService extends IntentService {
217
219
  try {
218
220
  if (builtinFile.exists() && verifyChecksum(builtinFile, fileHash)) {
219
221
  copyFile(builtinFile, targetFile);
220
- Log.d(TAG + " DownloadService", "using builtin file " + fileName);
222
+ Log.d(TAG, "using builtin file " + fileName);
221
223
  } else if (
222
224
  cacheFile.exists() && verifyChecksum(cacheFile, fileHash)
223
225
  ) {
224
226
  copyFile(cacheFile, targetFile);
225
- Log.d(TAG + " DownloadService", "already cached " + fileName);
227
+ Log.d(TAG, "already cached " + fileName);
226
228
  } else {
227
229
  downloadAndVerify(
228
230
  downloadUrl,
@@ -235,13 +237,9 @@ public class DownloadService extends IntentService {
235
237
 
236
238
  long completed = completedFiles.incrementAndGet();
237
239
  int percent = calcTotalPercent(completed, totalFiles);
238
- notifyDownload(id, percent);
240
+ setProgress(percent);
239
241
  } catch (Exception e) {
240
- Log.e(
241
- TAG + " DownloadService",
242
- "Error processing file: " + fileName,
243
- e
244
- );
242
+ Log.e(TAG, "Error processing file: " + fileName, e);
245
243
  hasError.set(true);
246
244
  }
247
245
  });
@@ -253,7 +251,7 @@ public class DownloadService extends IntentService {
253
251
  try {
254
252
  future.get();
255
253
  } catch (Exception e) {
256
- Log.e(TAG + " DownloadService", "Error waiting for download", e);
254
+ Log.e(TAG, "Error waiting for download", e);
257
255
  hasError.set(true);
258
256
  }
259
257
  }
@@ -271,11 +269,8 @@ public class DownloadService extends IntentService {
271
269
  if (hasError.get()) {
272
270
  throw new IOException("One or more files failed to download");
273
271
  }
274
-
275
- publishResults(dest, id, version, "", sessionKey, "", true);
276
272
  } catch (Exception e) {
277
- Log.e(TAG + " DownloadService", "Error in handleManifestDownload", e);
278
- publishResults("", id, version, "", sessionKey, e.getMessage(), true);
273
+ Log.e(TAG, "Error in handleManifestDownload", e);
279
274
  }
280
275
  }
281
276
 
@@ -308,14 +303,12 @@ public class DownloadService extends IntentService {
308
303
  String updateVersion = reader.readLine();
309
304
  if (!updateVersion.equals(version)) {
310
305
  clearDownloadData(documentsDir);
311
- downloadedBytes = 0;
312
306
  } else {
313
307
  downloadedBytes = tempFile.length();
314
308
  }
315
309
  }
316
310
  } else {
317
311
  clearDownloadData(documentsDir);
318
- downloadedBytes = 0;
319
312
  }
320
313
 
321
314
  if (downloadedBytes > 0) {
@@ -331,7 +324,6 @@ public class DownloadService extends IntentService {
331
324
  responseCode == HttpURLConnection.HTTP_OK ||
332
325
  responseCode == HttpURLConnection.HTTP_PARTIAL
333
326
  ) {
334
- String contentType = httpConn.getContentType();
335
327
  long contentLength = httpConn.getContentLength() + downloadedBytes;
336
328
 
337
329
  try (
@@ -375,7 +367,7 @@ public class DownloadService extends IntentService {
375
367
  lastNotifiedPercent += 10;
376
368
  // Artificial delay using CPU-bound calculation to take ~5 seconds
377
369
  double result = 0;
378
- notifyDownload(id, lastNotifiedPercent);
370
+ setProgress(lastNotifiedPercent);
379
371
  }
380
372
  }
381
373
 
@@ -385,7 +377,6 @@ public class DownloadService extends IntentService {
385
377
  // Rename the temp file with the final name (dest)
386
378
  tempFile.renameTo(new File(documentsDir, dest));
387
379
  infoFile.delete();
388
- publishResults(dest, id, version, checksum, sessionKey, "", false);
389
380
  }
390
381
  } else {
391
382
  infoFile.delete();
@@ -397,26 +388,10 @@ public class DownloadService extends IntentService {
397
388
  }
398
389
  } catch (OutOfMemoryError e) {
399
390
  e.printStackTrace();
400
- publishResults(
401
- "",
402
- id,
403
- version,
404
- checksum,
405
- sessionKey,
406
- "low_mem_fail",
407
- false
408
- );
391
+ throw new RuntimeException("low_mem_fail");
409
392
  } catch (Exception e) {
410
393
  e.printStackTrace();
411
- publishResults(
412
- "",
413
- id,
414
- version,
415
- checksum,
416
- sessionKey,
417
- e.getLocalizedMessage(),
418
- false
419
- );
394
+ throw new RuntimeException(e.getLocalizedMessage());
420
395
  }
421
396
  }
422
397
 
@@ -433,37 +408,6 @@ public class DownloadService extends IntentService {
433
408
  }
434
409
  }
435
410
 
436
- private void notifyDownload(String id, int percent) {
437
- Intent intent = new Intent(PERCENTDOWNLOAD);
438
- intent.setPackage(getPackageName());
439
- intent.putExtra(ID, id);
440
- intent.putExtra(PERCENT, percent);
441
- sendBroadcast(intent);
442
- }
443
-
444
- private void publishResults(
445
- String dest,
446
- String id,
447
- String version,
448
- String checksum,
449
- String sessionKey,
450
- String error,
451
- boolean isManifest
452
- ) {
453
- Intent intent = new Intent(NOTIFICATION);
454
- intent.setPackage(getPackageName());
455
- if (dest != null && !dest.isEmpty()) {
456
- intent.putExtra(FILEDEST, dest);
457
- }
458
- intent.putExtra(ERROR, error);
459
- intent.putExtra(ID, id);
460
- intent.putExtra(VERSION, version);
461
- intent.putExtra(SESSIONKEY, sessionKey);
462
- intent.putExtra(CHECKSUM, checksum);
463
- intent.putExtra(IS_MANIFEST, isManifest);
464
- sendBroadcast(intent);
465
- }
466
-
467
411
  // Helper methods
468
412
 
469
413
  private void copyFile(File source, File dest) throws IOException {
@@ -484,7 +428,7 @@ public class DownloadService extends IntentService {
484
428
  String expectedHash,
485
429
  String id
486
430
  ) throws Exception {
487
- Log.d(TAG + " DownloadService", "downloadAndVerify " + downloadUrl);
431
+ Log.d(TAG, "downloadAndVerify " + downloadUrl);
488
432
 
489
433
  Request request = new Request.Builder().url(downloadUrl).build();
490
434
 
@@ -0,0 +1,111 @@
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()
27
+ .setMinimumLoggingLevel(android.util.Log.INFO)
28
+ .build();
29
+ WorkManager.initialize(context, config);
30
+ isInitialized = true;
31
+ } catch (IllegalStateException e) {
32
+ // WorkManager was already initialized, ignore
33
+ }
34
+ }
35
+ }
36
+
37
+ public static synchronized boolean isVersionDownloading(String version) {
38
+ return activeVersions.contains(version);
39
+ }
40
+
41
+ public static void enqueueDownload(
42
+ Context context,
43
+ String url,
44
+ String id,
45
+ String documentsDir,
46
+ String dest,
47
+ String version,
48
+ String sessionKey,
49
+ String checksum,
50
+ boolean isManifest
51
+ ) {
52
+ initializeIfNeeded(context.getApplicationContext());
53
+
54
+ // If version is already downloading, don't start another one
55
+ if (isVersionDownloading(version)) {
56
+ Log.i(TAG, "Version " + version + " is already downloading");
57
+ return;
58
+ }
59
+ activeVersions.add(version);
60
+
61
+ // Create input data
62
+ Data inputData = new Data.Builder()
63
+ .putString(DownloadService.URL, url)
64
+ .putString(DownloadService.ID, id)
65
+ .putString(DownloadService.DOCDIR, documentsDir)
66
+ .putString(DownloadService.FILEDEST, dest)
67
+ .putString(DownloadService.VERSION, version)
68
+ .putString(DownloadService.SESSIONKEY, sessionKey)
69
+ .putString(DownloadService.CHECKSUM, checksum)
70
+ .putBoolean(DownloadService.IS_MANIFEST, isManifest)
71
+ .build();
72
+
73
+ // Create network constraints
74
+ Constraints constraints = new Constraints.Builder()
75
+ .setRequiredNetworkType(NetworkType.CONNECTED)
76
+ .build();
77
+
78
+ // Create work request with tags for tracking
79
+ OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(
80
+ DownloadService.class
81
+ )
82
+ .setConstraints(constraints)
83
+ .setInputData(inputData)
84
+ .addTag(id)
85
+ .addTag(version) // Add version tag for tracking
86
+ .addTag("capacitor_updater_download")
87
+ .setBackoffCriteria(
88
+ BackoffPolicy.LINEAR,
89
+ WorkRequest.MIN_BACKOFF_MILLIS,
90
+ TimeUnit.MILLISECONDS
91
+ )
92
+ .build();
93
+
94
+ // Enqueue work
95
+ WorkManager.getInstance(context).enqueue(workRequest);
96
+ }
97
+
98
+ public static void cancelVersionDownload(Context context, String version) {
99
+ initializeIfNeeded(context.getApplicationContext());
100
+ WorkManager.getInstance(context).cancelAllWorkByTag(version);
101
+ activeVersions.remove(version);
102
+ }
103
+
104
+ public static void cancelAllDownloads(Context context) {
105
+ initializeIfNeeded(context.getApplicationContext());
106
+ WorkManager.getInstance(context).cancelAllWorkByTag(
107
+ "capacitor_updater_download"
108
+ );
109
+ activeVersions.clear();
110
+ }
111
+ }
@@ -44,7 +44,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
44
44
  CAPPluginMethod(name: "isAutoUpdateAvailable", returnType: CAPPluginReturnPromise)
45
45
  ]
46
46
  public var implementation = CapacitorUpdater()
47
- private let PLUGIN_VERSION: String = "6.7.7-alpha.2"
47
+ private let PLUGIN_VERSION: String = "6.7.7"
48
48
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
49
49
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
50
50
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "6.7.7-alpha.2",
3
+ "version": "6.7.7",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",