@capgo/capacitor-updater 6.14.0 → 6.14.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.
Files changed (30) hide show
  1. package/README.md +11 -11
  2. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +134 -194
  3. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +23 -23
  4. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +1 -1
  5. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +960 -1165
  6. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1259 -1629
  7. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +161 -197
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +202 -256
  9. package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +16 -16
  10. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +44 -48
  11. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +378 -467
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +71 -83
  14. package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +19 -28
  15. package/dist/docs.json +17 -17
  16. package/dist/esm/definitions.d.ts +13 -13
  17. package/dist/esm/definitions.js.map +1 -1
  18. package/dist/esm/index.d.ts +2 -2
  19. package/dist/esm/index.js +4 -4
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/web.d.ts +2 -2
  22. package/dist/esm/web.js +43 -43
  23. package/dist/esm/web.js.map +1 -1
  24. package/dist/plugin.cjs.js +43 -43
  25. package/dist/plugin.cjs.js.map +1 -1
  26. package/dist/plugin.js +43 -43
  27. package/dist/plugin.js.map +1 -1
  28. package/ios/Plugin/CapacitorUpdater.swift +8 -8
  29. package/ios/Plugin/CapacitorUpdaterPlugin.swift +1 -1
  30. package/package.json +5 -7
@@ -38,505 +38,416 @@ import org.json.JSONObject;
38
38
 
39
39
  public class DownloadService extends Worker {
40
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()
56
- .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
57
- .build();
58
-
59
- public DownloadService(
60
- @NonNull Context context,
61
- @NonNull WorkerParameters params
62
- ) {
63
- super(context, params);
64
- }
65
-
66
- private void setProgress(int percent) {
67
- Data progress = new Data.Builder().putInt(PERCENT, percent).build();
68
- setProgressAsync(progress);
69
- }
70
-
71
- private Result createFailureResult(String error) {
72
- Data output = new Data.Builder().putString(ERROR, error).build();
73
- return Result.failure(output);
74
- }
75
-
76
- private Result createSuccessResult(
77
- String dest,
78
- String version,
79
- String sessionKey,
80
- String checksum,
81
- boolean isManifest
82
- ) {
83
- Data output = new Data.Builder()
84
- .putString(FILEDEST, dest)
85
- .putString(VERSION, version)
86
- .putString(SESSIONKEY, sessionKey)
87
- .putString(CHECKSUM, checksum)
88
- .putBoolean(IS_MANIFEST, isManifest)
89
- .build();
90
- return Result.success(output);
91
- }
92
-
93
- @NonNull
94
- @Override
95
- public Result doWork() {
96
- try {
97
- String url = getInputData().getString(URL);
98
- String id = getInputData().getString(ID);
99
- String documentsDir = getInputData().getString(DOCDIR);
100
- String dest = getInputData().getString(FILEDEST);
101
- String version = getInputData().getString(VERSION);
102
- String sessionKey = getInputData().getString(SESSIONKEY);
103
- String checksum = getInputData().getString(CHECKSUM);
104
- String publicKey = getInputData().getString(PUBLIC_KEY);
105
- boolean isManifest = getInputData().getBoolean(IS_MANIFEST, false);
106
-
107
- Log.d(TAG, "doWork isManifest: " + isManifest);
108
-
109
- if (isManifest) {
110
- JSONArray manifest = DataManager.getInstance().getAndClearManifest();
111
- if (manifest != null) {
112
- handleManifestDownload(
113
- id,
114
- documentsDir,
115
- dest,
116
- version,
117
- sessionKey,
118
- publicKey,
119
- manifest.toString()
120
- );
121
- return createSuccessResult(dest, version, sessionKey, checksum, true);
122
- } else {
123
- Log.e(TAG, "Manifest is null");
124
- return createFailureResult("Manifest is null");
125
- }
126
- } else {
127
- handleSingleFileDownload(
128
- url,
129
- id,
130
- documentsDir,
131
- dest,
132
- version,
133
- sessionKey,
134
- checksum
135
- );
136
- return createSuccessResult(dest, version, sessionKey, checksum, false);
137
- }
138
- } catch (Exception e) {
139
- Log.e(TAG, "Error in doWork", e);
140
- return createFailureResult(e.getMessage());
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);
141
59
  }
142
- }
143
60
 
144
- private int calcTotalPercent(long downloadedBytes, long contentLength) {
145
- if (contentLength <= 0) {
146
- return 0;
61
+ private void setProgress(int percent) {
62
+ Data progress = new Data.Builder().putInt(PERCENT, percent).build();
63
+ setProgressAsync(progress);
147
64
  }
148
- int percent = (int) (((double) downloadedBytes / contentLength) * 100);
149
- percent = Math.max(10, percent);
150
- percent = Math.min(70, percent);
151
- return percent;
152
- }
153
-
154
- private void handleManifestDownload(
155
- String id,
156
- String documentsDir,
157
- String dest,
158
- String version,
159
- String sessionKey,
160
- String publicKey,
161
- String manifestString
162
- ) {
163
- try {
164
- Log.d(TAG, "handleManifestDownload");
165
- JSONArray manifest = new JSONArray(manifestString);
166
- File destFolder = new File(documentsDir, dest);
167
- File cacheFolder = new File(
168
- getApplicationContext().getCacheDir(),
169
- "capgo_downloads"
170
- );
171
- File builtinFolder = new File(
172
- getApplicationContext().getFilesDir(),
173
- "public"
174
- );
175
-
176
- // Ensure directories are created
177
- if (!destFolder.exists() && !destFolder.mkdirs()) {
178
- throw new IOException(
179
- "Failed to create destination directory: " +
180
- destFolder.getAbsolutePath()
181
- );
182
- }
183
- if (!cacheFolder.exists() && !cacheFolder.mkdirs()) {
184
- throw new IOException(
185
- "Failed to create cache directory: " + cacheFolder.getAbsolutePath()
186
- );
187
- }
188
-
189
- int totalFiles = manifest.length();
190
- final AtomicLong completedFiles = new AtomicLong(0);
191
- final AtomicBoolean hasError = new AtomicBoolean(false);
192
-
193
- // Use more threads for I/O-bound operations
194
- int threadCount = Math.min(64, Math.max(32, totalFiles));
195
- ExecutorService executor = Executors.newFixedThreadPool(threadCount);
196
- List<Future<?>> futures = new ArrayList<>();
197
-
198
- for (int i = 0; i < totalFiles; i++) {
199
- JSONObject entry = manifest.getJSONObject(i);
200
- String fileName = entry.getString("file_name");
201
- String fileHash = entry.getString("file_hash");
202
- String downloadUrl = entry.getString("download_url");
203
-
204
- File targetFile = new File(destFolder, fileName);
205
- File cacheFile = new File(
206
- cacheFolder,
207
- fileHash + "_" + new File(fileName).getName()
208
- );
209
- File builtinFile = new File(builtinFolder, fileName);
210
-
211
- // Ensure parent directories of the target file exist
212
- if (
213
- !Objects.requireNonNull(targetFile.getParentFile()).exists() &&
214
- !targetFile.getParentFile().mkdirs()
215
- ) {
216
- throw new IOException(
217
- "Failed to create parent directory for: " +
218
- targetFile.getAbsolutePath()
219
- );
220
- }
221
65
 
222
- Future<?> future = executor.submit(() -> {
223
- try {
224
- if (builtinFile.exists() && verifyChecksum(builtinFile, fileHash)) {
225
- copyFile(builtinFile, targetFile);
226
- Log.d(TAG, "using builtin file " + fileName);
227
- } else if (
228
- cacheFile.exists() && verifyChecksum(cacheFile, fileHash)
229
- ) {
230
- copyFile(cacheFile, targetFile);
231
- Log.d(TAG, "already cached " + fileName);
232
- } else {
233
- downloadAndVerify(
234
- downloadUrl,
235
- targetFile,
236
- cacheFile,
237
- fileHash,
238
- sessionKey,
239
- publicKey
240
- );
241
- }
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
+ }
242
81
 
243
- long completed = completedFiles.incrementAndGet();
244
- int percent = calcTotalPercent(completed, totalFiles);
245
- setProgress(percent);
246
- } catch (Exception e) {
247
- Log.e(TAG, "Error processing file: " + fileName, e);
248
- hasError.set(true);
249
- }
250
- });
251
- futures.add(future);
252
- }
253
-
254
- // Wait for all downloads to complete
255
- for (Future<?> future : futures) {
82
+ @NonNull
83
+ @Override
84
+ public Result doWork() {
256
85
  try {
257
- future.get();
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
+ }
258
111
  } catch (Exception e) {
259
- Log.e(TAG, "Error waiting for download", e);
260
- hasError.set(true);
112
+ Log.e(TAG, "Error in doWork", e);
113
+ return createFailureResult(e.getMessage());
261
114
  }
262
- }
115
+ }
263
116
 
264
- executor.shutdown();
265
- try {
266
- if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
267
- executor.shutdownNow();
117
+ private int calcTotalPercent(long downloadedBytes, long contentLength) {
118
+ if (contentLength <= 0) {
119
+ return 0;
268
120
  }
269
- } catch (InterruptedException e) {
270
- executor.shutdownNow();
271
- Thread.currentThread().interrupt();
272
- }
273
-
274
- if (hasError.get()) {
275
- throw new IOException("One or more files failed to download");
276
- }
277
- } catch (Exception e) {
278
- Log.e(TAG, "Error in handleManifestDownload", e);
121
+ int percent = (int) (((double) downloadedBytes / contentLength) * 100);
122
+ percent = Math.max(10, percent);
123
+ percent = Math.min(70, percent);
124
+ return percent;
279
125
  }
280
- }
281
-
282
- private void handleSingleFileDownload(
283
- String url,
284
- String id,
285
- String documentsDir,
286
- String dest,
287
- String version,
288
- String sessionKey,
289
- String checksum
290
- ) {
291
- File target = new File(documentsDir, dest);
292
- File infoFile = new File(documentsDir, UPDATE_FILE); // The file where the download progress (how much byte
293
- // downloaded) is stored
294
- File tempFile = new File(documentsDir, "temp" + ".tmp"); // Temp file, where the downloaded data is stored
295
- try {
296
- URL u = new URL(url);
297
- HttpURLConnection httpConn = null;
298
- try {
299
- httpConn = (HttpURLConnection) u.openConnection();
300
-
301
- // Reading progress file (if exist)
302
- long downloadedBytes = 0;
303
-
304
- if (infoFile.exists() && tempFile.exists()) {
305
- try (
306
- BufferedReader reader = new BufferedReader(new FileReader(infoFile))
307
- ) {
308
- String updateVersion = reader.readLine();
309
- if (!updateVersion.equals(version)) {
310
- clearDownloadData(documentsDir);
311
- } else {
312
- downloadedBytes = tempFile.length();
313
- }
314
- }
315
- } else {
316
- clearDownloadData(documentsDir);
317
- }
318
126
 
319
- if (downloadedBytes > 0) {
320
- httpConn.setRequestProperty(
321
- "Range",
322
- "bytes=" + downloadedBytes + "-"
323
- );
324
- }
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
+ }
325
150
 
326
- int responseCode = httpConn.getResponseCode();
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
+ }
327
197
 
328
- if (
329
- responseCode == HttpURLConnection.HTTP_OK ||
330
- responseCode == HttpURLConnection.HTTP_PARTIAL
331
- ) {
332
- long contentLength = httpConn.getContentLength() + downloadedBytes;
333
-
334
- try (
335
- InputStream inputStream = httpConn.getInputStream();
336
- FileOutputStream outputStream = new FileOutputStream(
337
- tempFile,
338
- downloadedBytes > 0
339
- )
340
- ) {
341
- if (downloadedBytes == 0) {
342
- try (
343
- BufferedWriter writer = new BufferedWriter(
344
- new FileWriter(infoFile)
345
- )
346
- ) {
347
- writer.write(String.valueOf(version));
348
- }
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
+ }
349
206
  }
350
- // Updating the info file
351
- try (
352
- BufferedWriter writer = new BufferedWriter(
353
- new FileWriter(infoFile)
354
- )
355
- ) {
356
- writer.write(String.valueOf(version));
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();
357
216
  }
358
217
 
359
- int bytesRead = -1;
360
- byte[] buffer = new byte[4096];
361
- int lastNotifiedPercent = 0;
362
- while ((bytesRead = inputStream.read(buffer)) != -1) {
363
- outputStream.write(buffer, 0, bytesRead);
364
- downloadedBytes += bytesRead;
365
- // Saving progress (flushing every 100 Ko)
366
- if (downloadedBytes % 102400 == 0) {
367
- outputStream.flush();
368
- }
369
- // Computing percentage
370
- int percent = calcTotalPercent(downloadedBytes, contentLength);
371
- while (lastNotifiedPercent + 10 <= percent) {
372
- lastNotifiedPercent += 10;
373
- // Artificial delay using CPU-bound calculation to take ~5 seconds
374
- double result = 0;
375
- setProgress(lastNotifiedPercent);
376
- }
218
+ if (hasError.get()) {
219
+ throw new IOException("One or more files failed to download");
377
220
  }
221
+ } catch (Exception e) {
222
+ Log.e(TAG, "Error in handleManifestDownload", e);
223
+ }
224
+ }
378
225
 
379
- outputStream.close();
380
- inputStream.close();
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
+ }
381
327
 
382
- // Rename the temp file with the final name (dest)
383
- tempFile.renameTo(new File(documentsDir, dest));
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();
384
333
  infoFile.delete();
385
- }
386
- } else {
387
- infoFile.delete();
388
- }
389
- } finally {
390
- if (httpConn != null) {
391
- httpConn.disconnect();
334
+ infoFile.createNewFile();
335
+ tempFile.createNewFile();
336
+ } catch (IOException e) {
337
+ e.printStackTrace();
392
338
  }
393
- }
394
- } catch (OutOfMemoryError e) {
395
- e.printStackTrace();
396
- throw new RuntimeException("low_mem_fail");
397
- } catch (Exception e) {
398
- e.printStackTrace();
399
- throw new RuntimeException(e.getLocalizedMessage());
400
339
  }
401
- }
402
-
403
- private void clearDownloadData(String docDir) {
404
- File tempFile = new File(docDir, "temp" + ".tmp");
405
- File infoFile = new File(docDir, UPDATE_FILE);
406
- try {
407
- tempFile.delete();
408
- infoFile.delete();
409
- infoFile.createNewFile();
410
- tempFile.createNewFile();
411
- } catch (IOException e) {
412
- e.printStackTrace();
413
- }
414
- }
415
340
 
416
- // Helper methods
341
+ // Helper methods
417
342
 
418
- private void copyFile(File source, File dest) throws IOException {
419
- try (
420
- FileInputStream inStream = new FileInputStream(source);
421
- FileOutputStream outStream = new FileOutputStream(dest);
422
- FileChannel inChannel = inStream.getChannel();
423
- FileChannel outChannel = outStream.getChannel()
424
- ) {
425
- inChannel.transferTo(0, inChannel.size(), outChannel);
426
- }
427
- }
428
-
429
- private void downloadAndVerify(
430
- String downloadUrl,
431
- File targetFile,
432
- File cacheFile,
433
- String expectedHash,
434
- String sessionKey,
435
- String publicKey
436
- ) throws Exception {
437
- Log.d(TAG, "downloadAndVerify " + downloadUrl);
438
-
439
- Request request = new Request.Builder().url(downloadUrl).build();
440
-
441
- // Create a temporary file for the compressed data
442
- File compressedFile = new File(
443
- getApplicationContext().getCacheDir(),
444
- "temp_" + targetFile.getName() + ".br"
445
- );
446
-
447
- try (Response response = client.newCall(request).execute()) {
448
- if (!response.isSuccessful()) {
449
- throw new IOException("Unexpected response code: " + response.code());
450
- }
451
-
452
- // Download compressed file
453
- try (
454
- ResponseBody responseBody = response.body();
455
- FileOutputStream compressedFos = new FileOutputStream(compressedFile)
456
- ) {
457
- if (responseBody == null) {
458
- throw new IOException("Response body is null");
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);
459
351
  }
352
+ }
460
353
 
461
- byte[] buffer = new byte[8192];
462
- int bytesRead;
463
- try (InputStream inputStream = responseBody.byteStream()) {
464
- while ((bytesRead = inputStream.read(buffer)) != -1) {
465
- compressedFos.write(buffer, 0, bytesRead);
466
- }
467
- }
468
- }
469
-
470
- String decryptedExpectedHash = expectedHash;
471
-
472
- if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
473
- Log.d(
474
- CapacitorUpdater.TAG + " DLSrv",
475
- "Decrypting file " + targetFile.getName()
476
- );
477
- CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
478
- decryptedExpectedHash = CryptoCipherV2.decryptChecksum(
479
- decryptedExpectedHash,
480
- publicKey
481
- );
482
- }
483
-
484
- // Decompress the file
485
- try (
486
- FileInputStream fis = new FileInputStream(compressedFile);
487
- BrotliInputStream brotliInputStream = new BrotliInputStream(fis);
488
- FileOutputStream fos = new FileOutputStream(targetFile)
489
- ) {
490
- byte[] buffer = new byte[8192];
491
- int len;
492
- while ((len = brotliInputStream.read(buffer)) != -1) {
493
- fos.write(buffer, 0, len);
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
+ }
494
422
  }
495
- }
496
-
497
- // Delete the compressed file
498
- compressedFile.delete();
499
- String calculatedHash = CryptoCipherV2.calcChecksum(targetFile);
500
-
501
- // Verify checksum
502
- if (calculatedHash.equals(decryptedExpectedHash)) {
503
- // Only cache if checksum is correct
504
- copyFile(targetFile, cacheFile);
505
- } else {
506
- targetFile.delete();
507
- throw new IOException(
508
- "Checksum verification failed for " + targetFile.getName()
509
- );
510
- }
511
423
  }
512
- }
513
-
514
- private boolean verifyChecksum(File file, String expectedHash) {
515
- try {
516
- String actualHash = calculateFileHash(file);
517
- return actualHash.equals(expectedHash);
518
- } catch (Exception e) {
519
- e.printStackTrace();
520
- return false;
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
+ }
521
433
  }
522
- }
523
434
 
524
- private String calculateFileHash(File file) throws Exception {
525
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
526
- FileInputStream fis = new FileInputStream(file);
527
- byte[] byteArray = new byte[1024];
528
- int bytesCount = 0;
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;
529
440
 
530
- while ((bytesCount = fis.read(byteArray)) != -1) {
531
- digest.update(byteArray, 0, bytesCount);
532
- }
533
- fis.close();
441
+ while ((bytesCount = fis.read(byteArray)) != -1) {
442
+ digest.update(byteArray, 0, bytesCount);
443
+ }
444
+ fis.close();
534
445
 
535
- byte[] bytes = digest.digest();
536
- StringBuilder sb = new StringBuilder();
537
- for (byte aByte : bytes) {
538
- sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
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();
539
452
  }
540
- return sb.toString();
541
- }
542
453
  }