@capgo/capacitor-updater 6.14.25 → 6.14.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapgoCapacitorUpdater.podspec +3 -2
- package/Package.swift +2 -2
- package/README.md +341 -74
- package/android/build.gradle +20 -8
- package/android/proguard-rules.pro +22 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +52 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1196 -514
- package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +522 -154
- package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipher.java → CryptoCipherV1.java} +17 -9
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +15 -26
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +300 -119
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +63 -25
- package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
- package/dist/docs.json +652 -63
- package/dist/esm/definitions.d.ts +265 -15
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/history.d.ts +1 -0
- package/dist/esm/history.js +283 -0
- package/dist/esm/history.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +12 -1
- package/dist/esm/web.js +29 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +311 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +311 -2
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/AES.swift +6 -3
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1575 -0
- package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +365 -139
- package/ios/{Plugin/CryptoCipher.swift → Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift} +13 -6
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/CryptoCipherV2.swift +33 -27
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +47 -0
- package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/RSA.swift +1 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
- package/package.json +20 -16
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -1031
- /package/{LICENCE → LICENSE} +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BigInt.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java}
RENAMED
|
@@ -10,14 +10,11 @@ import android.app.Activity;
|
|
|
10
10
|
import android.content.Context;
|
|
11
11
|
import android.content.SharedPreferences;
|
|
12
12
|
import android.os.Build;
|
|
13
|
-
import android.util.Log;
|
|
14
13
|
import androidx.annotation.NonNull;
|
|
15
14
|
import androidx.lifecycle.LifecycleOwner;
|
|
16
15
|
import androidx.work.Data;
|
|
17
16
|
import androidx.work.WorkInfo;
|
|
18
17
|
import androidx.work.WorkManager;
|
|
19
|
-
import com.getcapacitor.JSObject;
|
|
20
|
-
import com.getcapacitor.plugin.WebView;
|
|
21
18
|
import com.google.common.util.concurrent.Futures;
|
|
22
19
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
23
20
|
import java.io.BufferedInputStream;
|
|
@@ -30,17 +27,28 @@ import java.io.IOException;
|
|
|
30
27
|
import java.security.SecureRandom;
|
|
31
28
|
import java.util.ArrayList;
|
|
32
29
|
import java.util.Date;
|
|
30
|
+
import java.util.HashMap;
|
|
33
31
|
import java.util.Iterator;
|
|
34
32
|
import java.util.List;
|
|
33
|
+
import java.util.Map;
|
|
35
34
|
import java.util.Objects;
|
|
35
|
+
import java.util.Set;
|
|
36
|
+
import java.util.concurrent.CompletableFuture;
|
|
37
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
38
|
+
import java.util.concurrent.ExecutorService;
|
|
39
|
+
import java.util.concurrent.Executors;
|
|
40
|
+
import java.util.concurrent.TimeUnit;
|
|
36
41
|
import java.util.zip.ZipEntry;
|
|
37
42
|
import java.util.zip.ZipInputStream;
|
|
38
43
|
import okhttp3.*;
|
|
44
|
+
import okhttp3.HttpUrl;
|
|
39
45
|
import org.json.JSONArray;
|
|
40
46
|
import org.json.JSONException;
|
|
41
47
|
import org.json.JSONObject;
|
|
42
48
|
|
|
43
|
-
public class
|
|
49
|
+
public class CapgoUpdater {
|
|
50
|
+
|
|
51
|
+
private final Logger logger;
|
|
44
52
|
|
|
45
53
|
private static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
46
54
|
private static final SecureRandom rnd = new SecureRandom();
|
|
@@ -55,8 +63,6 @@ public class CapacitorUpdater {
|
|
|
55
63
|
public SharedPreferences.Editor editor;
|
|
56
64
|
public SharedPreferences prefs;
|
|
57
65
|
|
|
58
|
-
public OkHttpClient client;
|
|
59
|
-
|
|
60
66
|
public File documentsDir;
|
|
61
67
|
public Boolean directUpdate = false;
|
|
62
68
|
public Activity activity;
|
|
@@ -64,6 +70,7 @@ public class CapacitorUpdater {
|
|
|
64
70
|
public String versionBuild = "";
|
|
65
71
|
public String versionCode = "";
|
|
66
72
|
public String versionOs = "";
|
|
73
|
+
public String CAP_SERVER_PATH = "";
|
|
67
74
|
|
|
68
75
|
public String customId = "";
|
|
69
76
|
public String statsUrl = "";
|
|
@@ -76,6 +83,16 @@ public class CapacitorUpdater {
|
|
|
76
83
|
public String deviceID = "";
|
|
77
84
|
public int timeout = 20000;
|
|
78
85
|
|
|
86
|
+
// Flag to track if we received a 429 response - stops requests until app restart
|
|
87
|
+
private static volatile boolean rateLimitExceeded = false;
|
|
88
|
+
|
|
89
|
+
private final Map<String, CompletableFuture<BundleInfo>> downloadFutures = new ConcurrentHashMap<>();
|
|
90
|
+
private final ExecutorService io = Executors.newSingleThreadExecutor();
|
|
91
|
+
|
|
92
|
+
public CapgoUpdater(Logger logger) {
|
|
93
|
+
this.logger = logger;
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
private final FilenameFilter filter = (f, name) -> {
|
|
80
97
|
// ignore directories generated by mac os x
|
|
81
98
|
return (!name.startsWith("__MACOSX") && !name.startsWith(".") && !name.startsWith(".DS_Store"));
|
|
@@ -119,9 +136,9 @@ public class CapacitorUpdater {
|
|
|
119
136
|
|
|
120
137
|
void directUpdateFinish(final BundleInfo latest) {}
|
|
121
138
|
|
|
122
|
-
void notifyListeners(final String id, final
|
|
139
|
+
void notifyListeners(final String id, final Map<String, Object> res) {}
|
|
123
140
|
|
|
124
|
-
|
|
141
|
+
public String randomString() {
|
|
125
142
|
final StringBuilder sb = new StringBuilder(10);
|
|
126
143
|
for (int i = 0; i < 10; i++) sb.append(AB.charAt(rnd.nextInt(AB.length())));
|
|
127
144
|
return sb.toString();
|
|
@@ -144,7 +161,7 @@ public class CapacitorUpdater {
|
|
|
144
161
|
ZipEntry entry;
|
|
145
162
|
while ((entry = zis.getNextEntry()) != null) {
|
|
146
163
|
if (entry.getName().contains("\\")) {
|
|
147
|
-
|
|
164
|
+
logger.error("unzip: Windows path is not supported, please use unix path as require by zip RFC: " + entry.getName());
|
|
148
165
|
this.sendStats("windows_path_fail");
|
|
149
166
|
}
|
|
150
167
|
final File file = new File(targetDirectory, entry.getName());
|
|
@@ -209,14 +226,14 @@ public class CapacitorUpdater {
|
|
|
209
226
|
|
|
210
227
|
private void observeWorkProgress(Context context, String id) {
|
|
211
228
|
if (!(context instanceof LifecycleOwner)) {
|
|
212
|
-
|
|
229
|
+
logger.error("Context is not a LifecycleOwner, cannot observe work progress");
|
|
213
230
|
return;
|
|
214
231
|
}
|
|
215
232
|
|
|
216
233
|
activity.runOnUiThread(() -> {
|
|
217
234
|
WorkManager.getInstance(context)
|
|
218
235
|
.getWorkInfosByTagLiveData(id)
|
|
219
|
-
.observe((LifecycleOwner) context, workInfos -> {
|
|
236
|
+
.observe((LifecycleOwner) context, (workInfos) -> {
|
|
220
237
|
if (workInfos == null || workInfos.isEmpty()) return;
|
|
221
238
|
|
|
222
239
|
WorkInfo workInfo = workInfos.get(0);
|
|
@@ -228,6 +245,7 @@ public class CapacitorUpdater {
|
|
|
228
245
|
notifyDownload(id, percent);
|
|
229
246
|
break;
|
|
230
247
|
case SUCCEEDED:
|
|
248
|
+
logger.info("Download succeeded: " + workInfo.getState());
|
|
231
249
|
Data outputData = workInfo.getOutputData();
|
|
232
250
|
String dest = outputData.getString(DownloadService.FILEDEST);
|
|
233
251
|
String version = outputData.getString(DownloadService.VERSION);
|
|
@@ -235,37 +253,71 @@ public class CapacitorUpdater {
|
|
|
235
253
|
String checksum = outputData.getString(DownloadService.CHECKSUM);
|
|
236
254
|
boolean isManifest = outputData.getBoolean(DownloadService.IS_MANIFEST, false);
|
|
237
255
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
256
|
+
io.execute(() -> {
|
|
257
|
+
boolean success = finishDownload(id, dest, version, sessionKey, checksum, true, isManifest);
|
|
258
|
+
BundleInfo resultBundle;
|
|
259
|
+
if (!success) {
|
|
260
|
+
logger.error("Finish download failed: " + version);
|
|
261
|
+
resultBundle = new BundleInfo(
|
|
262
|
+
id,
|
|
263
|
+
version,
|
|
264
|
+
BundleStatus.ERROR,
|
|
265
|
+
new Date(System.currentTimeMillis()),
|
|
266
|
+
""
|
|
267
|
+
);
|
|
268
|
+
saveBundleInfo(id, resultBundle);
|
|
269
|
+
// Cleanup download tracking
|
|
270
|
+
DownloadWorkerManager.cancelBundleDownload(activity, id, version);
|
|
271
|
+
Map<String, Object> ret = new HashMap<>();
|
|
272
|
+
ret.put("version", version);
|
|
273
|
+
ret.put("error", "finish_download_fail");
|
|
274
|
+
sendStats("finish_download_fail", version);
|
|
275
|
+
notifyListeners("downloadFailed", ret);
|
|
276
|
+
} else {
|
|
277
|
+
// Successful download - cleanup tracking
|
|
278
|
+
DownloadWorkerManager.cancelBundleDownload(activity, id, version);
|
|
279
|
+
resultBundle = getBundleInfo(id);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Complete the future if it exists
|
|
283
|
+
CompletableFuture<BundleInfo> future = downloadFutures.remove(id);
|
|
284
|
+
if (future != null) {
|
|
285
|
+
future.complete(resultBundle);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
251
288
|
break;
|
|
252
289
|
case FAILED:
|
|
253
290
|
Data failedData = workInfo.getOutputData();
|
|
254
291
|
String error = failedData.getString(DownloadService.ERROR);
|
|
255
|
-
|
|
292
|
+
logger.error("Download failed: " + error + " " + workInfo.getState());
|
|
256
293
|
String failedVersion = failedData.getString(DownloadService.VERSION);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
294
|
+
|
|
295
|
+
io.execute(() -> {
|
|
296
|
+
BundleInfo failedBundle = new BundleInfo(
|
|
297
|
+
id,
|
|
298
|
+
failedVersion,
|
|
299
|
+
BundleStatus.ERROR,
|
|
300
|
+
new Date(System.currentTimeMillis()),
|
|
301
|
+
""
|
|
302
|
+
);
|
|
303
|
+
saveBundleInfo(id, failedBundle);
|
|
304
|
+
// Cleanup download tracking for failed downloads
|
|
305
|
+
DownloadWorkerManager.cancelBundleDownload(activity, id, failedVersion);
|
|
306
|
+
Map<String, Object> ret = new HashMap<>();
|
|
307
|
+
ret.put("version", failedVersion);
|
|
308
|
+
if ("low_mem_fail".equals(error)) {
|
|
309
|
+
sendStats("low_mem_fail", failedVersion);
|
|
310
|
+
}
|
|
311
|
+
ret.put("error", error != null ? error : "download_fail");
|
|
312
|
+
sendStats("download_fail", failedVersion);
|
|
313
|
+
notifyListeners("downloadFailed", ret);
|
|
314
|
+
|
|
315
|
+
// Complete the future with error status
|
|
316
|
+
CompletableFuture<BundleInfo> failedFuture = downloadFutures.remove(id);
|
|
317
|
+
if (failedFuture != null) {
|
|
318
|
+
failedFuture.complete(failedBundle);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
269
321
|
break;
|
|
270
322
|
}
|
|
271
323
|
});
|
|
@@ -282,7 +334,7 @@ public class CapacitorUpdater {
|
|
|
282
334
|
final JSONArray manifest
|
|
283
335
|
) {
|
|
284
336
|
if (this.activity == null) {
|
|
285
|
-
|
|
337
|
+
logger.error("Activity is null, cannot observe work progress");
|
|
286
338
|
return;
|
|
287
339
|
}
|
|
288
340
|
observeWorkProgress(this.activity, id);
|
|
@@ -297,7 +349,10 @@ public class CapacitorUpdater {
|
|
|
297
349
|
sessionKey,
|
|
298
350
|
checksum,
|
|
299
351
|
this.publicKey,
|
|
300
|
-
manifest != null
|
|
352
|
+
manifest != null,
|
|
353
|
+
this.isEmulator(),
|
|
354
|
+
this.appId,
|
|
355
|
+
this.PLUGIN_VERSION
|
|
301
356
|
);
|
|
302
357
|
|
|
303
358
|
if (manifest != null) {
|
|
@@ -324,16 +379,28 @@ public class CapacitorUpdater {
|
|
|
324
379
|
|
|
325
380
|
if (!isManifest) {
|
|
326
381
|
String checksumDecrypted = Objects.requireNonNullElse(checksumRes, "");
|
|
382
|
+
|
|
383
|
+
// If public key is present but no checksum provided, refuse installation
|
|
384
|
+
if (!this.publicKey.isEmpty() && checksumDecrypted.isEmpty()) {
|
|
385
|
+
logger.error("Public key present but no checksum provided");
|
|
386
|
+
this.sendStats("checksum_required");
|
|
387
|
+
throw new IOException("Checksum required when public key is present: " + id);
|
|
388
|
+
}
|
|
389
|
+
|
|
327
390
|
if (!this.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty()) {
|
|
391
|
+
// V2 Encryption (publicKey)
|
|
328
392
|
CryptoCipherV2.decryptFile(downloaded, publicKey, sessionKey);
|
|
329
393
|
checksumDecrypted = CryptoCipherV2.decryptChecksum(checksumRes, publicKey);
|
|
330
394
|
checksum = CryptoCipherV2.calcChecksum(downloaded);
|
|
395
|
+
} else if (this.hasOldPrivateKeyPropertyInConfig) {
|
|
396
|
+
// V1 Encryption (privateKey) - deprecated but supported
|
|
397
|
+
CryptoCipherV1.decryptFile(downloaded, privateKey, sessionKey, version);
|
|
398
|
+
checksum = CryptoCipherV1.calcChecksum(downloaded);
|
|
331
399
|
} else {
|
|
332
|
-
|
|
333
|
-
checksum = CryptoCipher.calcChecksum(downloaded);
|
|
400
|
+
checksum = CryptoCipherV2.calcChecksum(downloaded);
|
|
334
401
|
}
|
|
335
402
|
if ((!checksumDecrypted.isEmpty() || !this.publicKey.isEmpty()) && !checksumDecrypted.equals(checksum)) {
|
|
336
|
-
|
|
403
|
+
logger.error("Error checksum '" + checksumDecrypted + "' '" + checksum + "' '");
|
|
337
404
|
this.sendStats("checksum_fail");
|
|
338
405
|
throw new IOException("Checksum failed: " + id);
|
|
339
406
|
}
|
|
@@ -345,14 +412,14 @@ public class CapacitorUpdater {
|
|
|
345
412
|
}
|
|
346
413
|
final Boolean res = this.delete(id);
|
|
347
414
|
if (!res) {
|
|
348
|
-
|
|
415
|
+
logger.info("Double error, cannot cleanup: " + version);
|
|
349
416
|
}
|
|
350
417
|
|
|
351
|
-
final
|
|
352
|
-
ret.put("version",
|
|
418
|
+
final Map<String, Object> ret = new HashMap<>();
|
|
419
|
+
ret.put("version", version);
|
|
353
420
|
|
|
354
|
-
|
|
355
|
-
|
|
421
|
+
CapgoUpdater.this.notifyListeners("downloadFailed", ret);
|
|
422
|
+
CapgoUpdater.this.sendStats("download_fail");
|
|
356
423
|
return false;
|
|
357
424
|
}
|
|
358
425
|
|
|
@@ -369,16 +436,20 @@ public class CapacitorUpdater {
|
|
|
369
436
|
downloaded.delete();
|
|
370
437
|
}
|
|
371
438
|
this.notifyDownload(id, 100);
|
|
439
|
+
// Remove old bundle info and set new one
|
|
372
440
|
this.saveBundleInfo(id, null);
|
|
373
441
|
BundleInfo next = new BundleInfo(id, version, BundleStatus.PENDING, new Date(System.currentTimeMillis()), checksum);
|
|
374
442
|
this.saveBundleInfo(id, next);
|
|
375
443
|
|
|
376
|
-
final
|
|
377
|
-
ret.put("bundle", next.
|
|
378
|
-
|
|
444
|
+
final Map<String, Object> ret = new HashMap<>();
|
|
445
|
+
ret.put("bundle", next.toJSONMap());
|
|
446
|
+
logger.info("updateAvailable: " + ret);
|
|
447
|
+
CapgoUpdater.this.notifyListeners("updateAvailable", ret);
|
|
448
|
+
logger.info("setNext: " + setNext);
|
|
379
449
|
if (setNext) {
|
|
450
|
+
logger.info("directUpdate: " + this.directUpdate);
|
|
380
451
|
if (this.directUpdate) {
|
|
381
|
-
|
|
452
|
+
CapgoUpdater.this.directUpdateFinish(next);
|
|
382
453
|
this.directUpdate = false;
|
|
383
454
|
} else {
|
|
384
455
|
this.setNextBundle(next.getId());
|
|
@@ -390,10 +461,10 @@ public class CapacitorUpdater {
|
|
|
390
461
|
safeDelete(downloaded);
|
|
391
462
|
}
|
|
392
463
|
e.printStackTrace();
|
|
393
|
-
final
|
|
394
|
-
ret.put("version",
|
|
395
|
-
|
|
396
|
-
|
|
464
|
+
final Map<String, Object> ret = new HashMap<>();
|
|
465
|
+
ret.put("version", version);
|
|
466
|
+
CapgoUpdater.this.notifyListeners("downloadFailed", ret);
|
|
467
|
+
CapgoUpdater.this.sendStats("download_fail");
|
|
397
468
|
return false;
|
|
398
469
|
}
|
|
399
470
|
if (!isManifest) {
|
|
@@ -416,6 +487,41 @@ public class CapacitorUpdater {
|
|
|
416
487
|
}
|
|
417
488
|
}
|
|
418
489
|
|
|
490
|
+
public void cleanupDownloadDirectories(final Set<String> allowedIds) {
|
|
491
|
+
if (this.documentsDir == null) {
|
|
492
|
+
logger.warn("Documents directory is null, skipping download cleanup");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
final File bundleRoot = new File(this.documentsDir, bundleDirectory);
|
|
497
|
+
if (!bundleRoot.exists() || !bundleRoot.isDirectory()) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
final File[] entries = bundleRoot.listFiles();
|
|
502
|
+
if (entries != null) {
|
|
503
|
+
for (final File entry : entries) {
|
|
504
|
+
if (!entry.isDirectory()) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
final String id = entry.getName();
|
|
509
|
+
|
|
510
|
+
if (allowedIds != null && allowedIds.contains(id)) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
this.deleteDirectory(entry);
|
|
516
|
+
this.removeBundleInfo(id);
|
|
517
|
+
logger.info("Deleted orphan bundle directory: " + id);
|
|
518
|
+
} catch (IOException e) {
|
|
519
|
+
logger.error("Failed to delete orphan bundle directory: " + id + " " + e.getMessage());
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
419
525
|
private void safeDelete(final File target) {
|
|
420
526
|
if (target == null || !target.exists()) {
|
|
421
527
|
return;
|
|
@@ -424,16 +530,16 @@ public class CapacitorUpdater {
|
|
|
424
530
|
if (target.isDirectory()) {
|
|
425
531
|
this.deleteDirectory(target);
|
|
426
532
|
} else if (!target.delete()) {
|
|
427
|
-
|
|
533
|
+
logger.warn("Failed to delete file: " + target.getAbsolutePath());
|
|
428
534
|
}
|
|
429
535
|
} catch (IOException cleanupError) {
|
|
430
|
-
|
|
536
|
+
logger.warn("Cleanup failed for " + target.getAbsolutePath() + ": " + cleanupError.getMessage());
|
|
431
537
|
}
|
|
432
538
|
}
|
|
433
539
|
|
|
434
540
|
private void setCurrentBundle(final File bundle) {
|
|
435
|
-
this.editor.putString(
|
|
436
|
-
|
|
541
|
+
this.editor.putString(this.CAP_SERVER_PATH, bundle.getPath());
|
|
542
|
+
logger.info("Current bundle set to: " + bundle);
|
|
437
543
|
this.editor.commit();
|
|
438
544
|
}
|
|
439
545
|
|
|
@@ -446,13 +552,21 @@ public class CapacitorUpdater {
|
|
|
446
552
|
) {
|
|
447
553
|
final String id = this.randomString();
|
|
448
554
|
|
|
449
|
-
// Check if version is already downloading
|
|
450
|
-
if (this.activity != null && DownloadWorkerManager.isVersionDownloading(version)) {
|
|
451
|
-
|
|
452
|
-
|
|
555
|
+
// Check if version is already downloading, but allow retry if previous download failed
|
|
556
|
+
if (this.activity != null && DownloadWorkerManager.isVersionDownloading(this.activity, version)) {
|
|
557
|
+
// Check if there's an existing bundle with error status that we can retry
|
|
558
|
+
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
559
|
+
if (existingBundle != null && existingBundle.isErrorStatus()) {
|
|
560
|
+
// Cancel the failed download and allow retry
|
|
561
|
+
DownloadWorkerManager.cancelVersionDownload(this.activity, version);
|
|
562
|
+
logger.info("Retrying failed download for version: " + version);
|
|
563
|
+
} else {
|
|
564
|
+
logger.info("Version already downloading: " + version);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
453
567
|
}
|
|
454
568
|
|
|
455
|
-
|
|
569
|
+
saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), ""));
|
|
456
570
|
this.notifyDownload(id, 0);
|
|
457
571
|
this.notifyDownload(id, 5);
|
|
458
572
|
|
|
@@ -460,42 +574,43 @@ public class CapacitorUpdater {
|
|
|
460
574
|
}
|
|
461
575
|
|
|
462
576
|
public BundleInfo download(final String url, final String version, final String sessionKey, final String checksum) throws IOException {
|
|
577
|
+
// Check for existing bundle with same version and clean up if in error state
|
|
578
|
+
BundleInfo existingBundle = this.getBundleInfoByName(version);
|
|
579
|
+
if (existingBundle != null && (existingBundle.isErrorStatus() || existingBundle.isDeleted())) {
|
|
580
|
+
logger.info("Found existing failed bundle for version " + version + ", deleting before retry");
|
|
581
|
+
this.delete(existingBundle.getId(), true);
|
|
582
|
+
}
|
|
583
|
+
|
|
463
584
|
final String id = this.randomString();
|
|
464
|
-
|
|
585
|
+
saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), ""));
|
|
465
586
|
this.notifyDownload(id, 0);
|
|
466
587
|
this.notifyDownload(id, 5);
|
|
467
588
|
final String dest = this.randomString();
|
|
468
589
|
|
|
469
|
-
//
|
|
590
|
+
// Create a CompletableFuture to track download completion
|
|
591
|
+
CompletableFuture<BundleInfo> downloadFuture = new CompletableFuture<>();
|
|
592
|
+
downloadFutures.put(id, downloadFuture);
|
|
593
|
+
|
|
594
|
+
// Start the download
|
|
470
595
|
this.download(id, url, dest, version, sessionKey, checksum, null);
|
|
471
596
|
|
|
472
|
-
// Wait for completion
|
|
597
|
+
// Wait for completion without timeout
|
|
473
598
|
try {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (workInfos != null && !workInfos.isEmpty()) {
|
|
479
|
-
WorkInfo workInfo = workInfos.get(0);
|
|
480
|
-
while (!workInfo.getState().isFinished()) {
|
|
481
|
-
Thread.sleep(100);
|
|
482
|
-
workInfos = Futures.getChecked(WorkManager.getInstance(activity).getWorkInfosByTag(id), IOException.class);
|
|
483
|
-
if (workInfos != null && !workInfos.isEmpty()) {
|
|
484
|
-
workInfo = workInfos.get(0);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (workInfo.getState() != WorkInfo.State.SUCCEEDED) {
|
|
489
|
-
Data outputData = workInfo.getOutputData();
|
|
490
|
-
String error = outputData.getString(DownloadService.ERROR);
|
|
491
|
-
throw new IOException(error != null ? error : "Download failed: " + workInfo.getState());
|
|
492
|
-
}
|
|
599
|
+
BundleInfo result = downloadFuture.get();
|
|
600
|
+
if (result.isErrorStatus()) {
|
|
601
|
+
throw new IOException("Download failed with status: " + result.getStatus());
|
|
493
602
|
}
|
|
494
|
-
return
|
|
603
|
+
return result;
|
|
495
604
|
} catch (Exception e) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
605
|
+
// Clean up on failure
|
|
606
|
+
downloadFutures.remove(id);
|
|
607
|
+
logger.error("Error waiting for download: " + e.getMessage());
|
|
608
|
+
BundleInfo errorBundle = new BundleInfo(id, version, BundleStatus.ERROR, new Date(System.currentTimeMillis()), "");
|
|
609
|
+
saveBundleInfo(id, errorBundle);
|
|
610
|
+
if (e instanceof IOException) {
|
|
611
|
+
throw (IOException) e;
|
|
612
|
+
}
|
|
613
|
+
throw new IOException("Error waiting for download: " + e.getMessage(), e);
|
|
499
614
|
}
|
|
500
615
|
}
|
|
501
616
|
|
|
@@ -503,14 +618,14 @@ public class CapacitorUpdater {
|
|
|
503
618
|
if (!rawList) {
|
|
504
619
|
final List<BundleInfo> res = new ArrayList<>();
|
|
505
620
|
final File destHot = new File(this.documentsDir, bundleDirectory);
|
|
506
|
-
|
|
621
|
+
logger.debug("list File : " + destHot.getPath());
|
|
507
622
|
if (destHot.exists()) {
|
|
508
623
|
for (final File i : Objects.requireNonNull(destHot.listFiles())) {
|
|
509
624
|
final String id = i.getName();
|
|
510
625
|
res.add(this.getBundleInfo(id));
|
|
511
626
|
}
|
|
512
627
|
} else {
|
|
513
|
-
|
|
628
|
+
logger.info("No versions available to list" + destHot);
|
|
514
629
|
}
|
|
515
630
|
return res;
|
|
516
631
|
} else {
|
|
@@ -529,12 +644,12 @@ public class CapacitorUpdater {
|
|
|
529
644
|
public Boolean delete(final String id, final Boolean removeInfo) throws IOException {
|
|
530
645
|
final BundleInfo deleted = this.getBundleInfo(id);
|
|
531
646
|
if (deleted.isBuiltin() || this.getCurrentBundleId().equals(id)) {
|
|
532
|
-
|
|
647
|
+
logger.error("Cannot delete " + id);
|
|
533
648
|
return false;
|
|
534
649
|
}
|
|
535
650
|
final BundleInfo next = this.getNextBundle();
|
|
536
651
|
if (next != null && !next.isDeleted() && !next.isErrorStatus() && next.getId().equals(id)) {
|
|
537
|
-
|
|
652
|
+
logger.error("Cannot delete the next bundle" + id);
|
|
538
653
|
return false;
|
|
539
654
|
}
|
|
540
655
|
// Cancel download for this version if active
|
|
@@ -551,7 +666,7 @@ public class CapacitorUpdater {
|
|
|
551
666
|
}
|
|
552
667
|
return true;
|
|
553
668
|
}
|
|
554
|
-
|
|
669
|
+
logger.error("bundle removed: " + deleted.getVersionName());
|
|
555
670
|
// perhaps we did not find the bundle in the files, but if the user requested a delete, we delete
|
|
556
671
|
if (removeInfo) {
|
|
557
672
|
this.removeBundleInfo(id);
|
|
@@ -565,7 +680,7 @@ public class CapacitorUpdater {
|
|
|
565
680
|
return this.delete(id, true);
|
|
566
681
|
} catch (IOException e) {
|
|
567
682
|
e.printStackTrace();
|
|
568
|
-
|
|
683
|
+
logger.info("Failed to delete bundle (" + id + ")" + "\nError:\n" + e.toString());
|
|
569
684
|
return false;
|
|
570
685
|
}
|
|
571
686
|
}
|
|
@@ -591,7 +706,7 @@ public class CapacitorUpdater {
|
|
|
591
706
|
return true;
|
|
592
707
|
}
|
|
593
708
|
final File bundle = this.getBundleDirectory(id);
|
|
594
|
-
|
|
709
|
+
logger.info("Setting next active bundle: " + id);
|
|
595
710
|
if (this.bundleExists(id)) {
|
|
596
711
|
var currentBundleName = this.getCurrentBundle().getVersionName();
|
|
597
712
|
this.setCurrentBundle(bundle);
|
|
@@ -607,7 +722,7 @@ public class CapacitorUpdater {
|
|
|
607
722
|
public void autoReset() {
|
|
608
723
|
final BundleInfo currentBundle = this.getCurrentBundle();
|
|
609
724
|
if (!currentBundle.isBuiltin() && !this.bundleExists(currentBundle.getId())) {
|
|
610
|
-
|
|
725
|
+
logger.info("Folder at bundle path does not exist. Triggering reset.");
|
|
611
726
|
this.reset();
|
|
612
727
|
}
|
|
613
728
|
}
|
|
@@ -619,12 +734,17 @@ public class CapacitorUpdater {
|
|
|
619
734
|
public void setSuccess(final BundleInfo bundle, Boolean autoDeletePrevious) {
|
|
620
735
|
this.setBundleStatus(bundle.getId(), BundleStatus.SUCCESS);
|
|
621
736
|
final BundleInfo fallback = this.getFallbackBundle();
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
737
|
+
logger.debug("Fallback bundle is: " + fallback);
|
|
738
|
+
logger.info("Version successfully loaded: " + bundle.getVersionName());
|
|
739
|
+
// Only attempt to delete when the fallback is a different bundle than the
|
|
740
|
+
// currently loaded one. Otherwise we spam logs with "Cannot delete <id>"
|
|
741
|
+
// because delete() protects the current bundle from removal.
|
|
742
|
+
if (autoDeletePrevious && !fallback.isBuiltin() && fallback.getId() != null && !fallback.getId().equals(bundle.getId())) {
|
|
625
743
|
final Boolean res = this.delete(fallback.getId());
|
|
626
744
|
if (res) {
|
|
627
|
-
|
|
745
|
+
logger.info("Deleted previous bundle: " + fallback.getVersionName());
|
|
746
|
+
} else {
|
|
747
|
+
logger.debug("Skip deleting previous bundle (same as current or protected): " + fallback.getId());
|
|
628
748
|
}
|
|
629
749
|
}
|
|
630
750
|
this.setFallbackBundle(bundle);
|
|
@@ -635,7 +755,7 @@ public class CapacitorUpdater {
|
|
|
635
755
|
}
|
|
636
756
|
|
|
637
757
|
public void reset(final boolean internal) {
|
|
638
|
-
|
|
758
|
+
logger.debug("reset: " + internal);
|
|
639
759
|
var currentBundleName = this.getCurrentBundle().getVersionName();
|
|
640
760
|
this.setCurrentBundle(new File("public"));
|
|
641
761
|
this.setFallbackBundle(null);
|
|
@@ -666,19 +786,31 @@ public class CapacitorUpdater {
|
|
|
666
786
|
return json;
|
|
667
787
|
}
|
|
668
788
|
|
|
789
|
+
/**
|
|
790
|
+
* Check if a 429 (Too Many Requests) response was received and set the flag
|
|
791
|
+
*/
|
|
792
|
+
private boolean checkAndHandleRateLimitResponse(Response response) {
|
|
793
|
+
if (response.code() == 429) {
|
|
794
|
+
rateLimitExceeded = true;
|
|
795
|
+
logger.warn("Rate limit exceeded (429). Stopping all stats and channel requests until app restart.");
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
|
|
669
801
|
private void makeJsonRequest(String url, JSONObject jsonBody, Callback callback) {
|
|
670
802
|
MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
|
671
803
|
RequestBody body = RequestBody.create(jsonBody.toString(), JSON);
|
|
672
804
|
|
|
673
805
|
Request request = new Request.Builder().url(url).post(body).build();
|
|
674
806
|
|
|
675
|
-
|
|
807
|
+
DownloadService.sharedClient
|
|
676
808
|
.newCall(request)
|
|
677
809
|
.enqueue(
|
|
678
810
|
new okhttp3.Callback() {
|
|
679
811
|
@Override
|
|
680
812
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
681
|
-
|
|
813
|
+
Map<String, Object> retError = new HashMap<>();
|
|
682
814
|
retError.put("message", "Request failed: " + e.getMessage());
|
|
683
815
|
retError.put("error", "network_error");
|
|
684
816
|
callback.callback(retError);
|
|
@@ -687,8 +819,17 @@ public class CapacitorUpdater {
|
|
|
687
819
|
@Override
|
|
688
820
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
689
821
|
try (ResponseBody responseBody = response.body()) {
|
|
822
|
+
// Check for 429 rate limit
|
|
823
|
+
if (checkAndHandleRateLimitResponse(response)) {
|
|
824
|
+
Map<String, Object> retError = new HashMap<>();
|
|
825
|
+
retError.put("message", "Rate limit exceeded");
|
|
826
|
+
retError.put("error", "rate_limit_exceeded");
|
|
827
|
+
callback.callback(retError);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
690
831
|
if (!response.isSuccessful()) {
|
|
691
|
-
|
|
832
|
+
Map<String, Object> retError = new HashMap<>();
|
|
692
833
|
retError.put("message", "Server error: " + response.code());
|
|
693
834
|
retError.put("error", "response_error");
|
|
694
835
|
callback.callback(retError);
|
|
@@ -698,7 +839,17 @@ public class CapacitorUpdater {
|
|
|
698
839
|
assert responseBody != null;
|
|
699
840
|
String responseData = responseBody.string();
|
|
700
841
|
JSONObject jsonResponse = new JSONObject(responseData);
|
|
701
|
-
|
|
842
|
+
|
|
843
|
+
// Check for server-side errors first
|
|
844
|
+
if (jsonResponse.has("error")) {
|
|
845
|
+
Map<String, Object> retError = new HashMap<>();
|
|
846
|
+
retError.put("message", jsonResponse.getString("error"));
|
|
847
|
+
retError.put("error", "server_error");
|
|
848
|
+
callback.callback(retError);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
Map<String, Object> ret = new HashMap<>();
|
|
702
853
|
|
|
703
854
|
Iterator<String> keys = jsonResponse.keys();
|
|
704
855
|
while (keys.hasNext()) {
|
|
@@ -713,7 +864,7 @@ public class CapacitorUpdater {
|
|
|
713
864
|
}
|
|
714
865
|
callback.callback(ret);
|
|
715
866
|
} catch (JSONException e) {
|
|
716
|
-
|
|
867
|
+
Map<String, Object> retError = new HashMap<>();
|
|
717
868
|
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
718
869
|
retError.put("error", "parse_error");
|
|
719
870
|
callback.callback(retError);
|
|
@@ -731,24 +882,34 @@ public class CapacitorUpdater {
|
|
|
731
882
|
json.put("defaultChannel", channel);
|
|
732
883
|
}
|
|
733
884
|
} catch (JSONException e) {
|
|
734
|
-
|
|
735
|
-
final
|
|
885
|
+
logger.error("Error getLatest JSONException " + e.getMessage());
|
|
886
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
736
887
|
retError.put("message", "Cannot get info: " + e);
|
|
737
888
|
retError.put("error", "json_error");
|
|
738
889
|
callback.callback(retError);
|
|
739
890
|
return;
|
|
740
891
|
}
|
|
741
892
|
|
|
742
|
-
|
|
893
|
+
logger.info("Auto-update parameters: " + json);
|
|
743
894
|
|
|
744
895
|
makeJsonRequest(updateUrl, json, callback);
|
|
745
896
|
}
|
|
746
897
|
|
|
747
898
|
public void unsetChannel(final Callback callback) {
|
|
899
|
+
// Check if rate limit was exceeded
|
|
900
|
+
if (rateLimitExceeded) {
|
|
901
|
+
logger.debug("Skipping unsetChannel due to rate limit (429). Requests will resume after app restart.");
|
|
902
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
903
|
+
retError.put("message", "Rate limit exceeded");
|
|
904
|
+
retError.put("error", "rate_limit_exceeded");
|
|
905
|
+
callback.callback(retError);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
748
909
|
String channelUrl = this.channelUrl;
|
|
749
910
|
if (channelUrl == null || channelUrl.isEmpty()) {
|
|
750
|
-
|
|
751
|
-
final
|
|
911
|
+
logger.error("Channel URL is not set");
|
|
912
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
752
913
|
retError.put("message", "channelUrl missing");
|
|
753
914
|
retError.put("error", "missing_config");
|
|
754
915
|
callback.callback(retError);
|
|
@@ -758,8 +919,8 @@ public class CapacitorUpdater {
|
|
|
758
919
|
try {
|
|
759
920
|
json = this.createInfoObject();
|
|
760
921
|
} catch (JSONException e) {
|
|
761
|
-
|
|
762
|
-
final
|
|
922
|
+
logger.error("Error unsetChannel JSONException " + e.getMessage());
|
|
923
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
763
924
|
retError.put("message", "Cannot get info: " + e);
|
|
764
925
|
retError.put("error", "json_error");
|
|
765
926
|
callback.callback(retError);
|
|
@@ -771,13 +932,13 @@ public class CapacitorUpdater {
|
|
|
771
932
|
.delete(RequestBody.create(json.toString(), MediaType.get("application/json")))
|
|
772
933
|
.build();
|
|
773
934
|
|
|
774
|
-
|
|
935
|
+
DownloadService.sharedClient
|
|
775
936
|
.newCall(request)
|
|
776
937
|
.enqueue(
|
|
777
938
|
new okhttp3.Callback() {
|
|
778
939
|
@Override
|
|
779
940
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
780
|
-
|
|
941
|
+
Map<String, Object> retError = new HashMap<>();
|
|
781
942
|
retError.put("message", "Request failed: " + e.getMessage());
|
|
782
943
|
retError.put("error", "network_error");
|
|
783
944
|
callback.callback(retError);
|
|
@@ -786,8 +947,17 @@ public class CapacitorUpdater {
|
|
|
786
947
|
@Override
|
|
787
948
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
788
949
|
try (ResponseBody responseBody = response.body()) {
|
|
950
|
+
// Check for 429 rate limit
|
|
951
|
+
if (checkAndHandleRateLimitResponse(response)) {
|
|
952
|
+
Map<String, Object> retError = new HashMap<>();
|
|
953
|
+
retError.put("message", "Rate limit exceeded");
|
|
954
|
+
retError.put("error", "rate_limit_exceeded");
|
|
955
|
+
callback.callback(retError);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
789
959
|
if (!response.isSuccessful()) {
|
|
790
|
-
|
|
960
|
+
Map<String, Object> retError = new HashMap<>();
|
|
791
961
|
retError.put("message", "Server error: " + response.code());
|
|
792
962
|
retError.put("error", "response_error");
|
|
793
963
|
callback.callback(retError);
|
|
@@ -797,7 +967,17 @@ public class CapacitorUpdater {
|
|
|
797
967
|
assert responseBody != null;
|
|
798
968
|
String responseData = responseBody.string();
|
|
799
969
|
JSONObject jsonResponse = new JSONObject(responseData);
|
|
800
|
-
|
|
970
|
+
|
|
971
|
+
// Check for server-side errors first
|
|
972
|
+
if (jsonResponse.has("error")) {
|
|
973
|
+
Map<String, Object> retError = new HashMap<>();
|
|
974
|
+
retError.put("message", jsonResponse.getString("error"));
|
|
975
|
+
retError.put("error", "server_error");
|
|
976
|
+
callback.callback(retError);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
Map<String, Object> ret = new HashMap<>();
|
|
801
981
|
|
|
802
982
|
Iterator<String> keys = jsonResponse.keys();
|
|
803
983
|
while (keys.hasNext()) {
|
|
@@ -806,10 +986,10 @@ public class CapacitorUpdater {
|
|
|
806
986
|
ret.put(key, jsonResponse.get(key));
|
|
807
987
|
}
|
|
808
988
|
}
|
|
809
|
-
|
|
989
|
+
logger.info("Channel unset");
|
|
810
990
|
callback.callback(ret);
|
|
811
991
|
} catch (JSONException e) {
|
|
812
|
-
|
|
992
|
+
Map<String, Object> retError = new HashMap<>();
|
|
813
993
|
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
814
994
|
retError.put("error", "parse_error");
|
|
815
995
|
callback.callback(retError);
|
|
@@ -820,10 +1000,20 @@ public class CapacitorUpdater {
|
|
|
820
1000
|
}
|
|
821
1001
|
|
|
822
1002
|
public void setChannel(final String channel, final Callback callback) {
|
|
1003
|
+
// Check if rate limit was exceeded
|
|
1004
|
+
if (rateLimitExceeded) {
|
|
1005
|
+
logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.");
|
|
1006
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
1007
|
+
retError.put("message", "Rate limit exceeded");
|
|
1008
|
+
retError.put("error", "rate_limit_exceeded");
|
|
1009
|
+
callback.callback(retError);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
823
1013
|
String channelUrl = this.channelUrl;
|
|
824
1014
|
if (channelUrl == null || channelUrl.isEmpty()) {
|
|
825
|
-
|
|
826
|
-
final
|
|
1015
|
+
logger.error("Channel URL is not set");
|
|
1016
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
827
1017
|
retError.put("message", "channelUrl missing");
|
|
828
1018
|
retError.put("error", "missing_config");
|
|
829
1019
|
callback.callback(retError);
|
|
@@ -834,8 +1024,8 @@ public class CapacitorUpdater {
|
|
|
834
1024
|
json = this.createInfoObject();
|
|
835
1025
|
json.put("channel", channel);
|
|
836
1026
|
} catch (JSONException e) {
|
|
837
|
-
|
|
838
|
-
final
|
|
1027
|
+
logger.error("Error setChannel JSONException " + e.getMessage());
|
|
1028
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
839
1029
|
retError.put("message", "Cannot get info: " + e);
|
|
840
1030
|
retError.put("error", "json_error");
|
|
841
1031
|
callback.callback(retError);
|
|
@@ -846,10 +1036,20 @@ public class CapacitorUpdater {
|
|
|
846
1036
|
}
|
|
847
1037
|
|
|
848
1038
|
public void getChannel(final Callback callback) {
|
|
1039
|
+
// Check if rate limit was exceeded
|
|
1040
|
+
if (rateLimitExceeded) {
|
|
1041
|
+
logger.debug("Skipping getChannel due to rate limit (429). Requests will resume after app restart.");
|
|
1042
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
1043
|
+
retError.put("message", "Rate limit exceeded");
|
|
1044
|
+
retError.put("error", "rate_limit_exceeded");
|
|
1045
|
+
callback.callback(retError);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
849
1049
|
String channelUrl = this.channelUrl;
|
|
850
1050
|
if (channelUrl == null || channelUrl.isEmpty()) {
|
|
851
|
-
|
|
852
|
-
final
|
|
1051
|
+
logger.error("Channel URL is not set");
|
|
1052
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
853
1053
|
retError.put("message", "Channel URL is not set");
|
|
854
1054
|
retError.put("error", "missing_config");
|
|
855
1055
|
callback.callback(retError);
|
|
@@ -859,8 +1059,8 @@ public class CapacitorUpdater {
|
|
|
859
1059
|
try {
|
|
860
1060
|
json = this.createInfoObject();
|
|
861
1061
|
} catch (JSONException e) {
|
|
862
|
-
|
|
863
|
-
final
|
|
1062
|
+
logger.error("Error getChannel JSONException " + e.getMessage());
|
|
1063
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
864
1064
|
retError.put("message", "Cannot get info: " + e);
|
|
865
1065
|
retError.put("error", "json_error");
|
|
866
1066
|
callback.callback(retError);
|
|
@@ -872,13 +1072,13 @@ public class CapacitorUpdater {
|
|
|
872
1072
|
.put(RequestBody.create(json.toString(), MediaType.get("application/json")))
|
|
873
1073
|
.build();
|
|
874
1074
|
|
|
875
|
-
|
|
1075
|
+
DownloadService.sharedClient
|
|
876
1076
|
.newCall(request)
|
|
877
1077
|
.enqueue(
|
|
878
1078
|
new okhttp3.Callback() {
|
|
879
1079
|
@Override
|
|
880
1080
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
881
|
-
|
|
1081
|
+
Map<String, Object> retError = new HashMap<>();
|
|
882
1082
|
retError.put("message", "Request failed: " + e.getMessage());
|
|
883
1083
|
retError.put("error", "network_error");
|
|
884
1084
|
callback.callback(retError);
|
|
@@ -887,21 +1087,30 @@ public class CapacitorUpdater {
|
|
|
887
1087
|
@Override
|
|
888
1088
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
889
1089
|
try (ResponseBody responseBody = response.body()) {
|
|
1090
|
+
// Check for 429 rate limit
|
|
1091
|
+
if (checkAndHandleRateLimitResponse(response)) {
|
|
1092
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1093
|
+
retError.put("message", "Rate limit exceeded");
|
|
1094
|
+
retError.put("error", "rate_limit_exceeded");
|
|
1095
|
+
callback.callback(retError);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
890
1099
|
if (response.code() == 400) {
|
|
891
1100
|
assert responseBody != null;
|
|
892
1101
|
String data = responseBody.string();
|
|
893
1102
|
if (data.contains("channel_not_found") && !defaultChannel.isEmpty()) {
|
|
894
|
-
|
|
1103
|
+
Map<String, Object> ret = new HashMap<>();
|
|
895
1104
|
ret.put("channel", defaultChannel);
|
|
896
1105
|
ret.put("status", "default");
|
|
897
|
-
|
|
1106
|
+
logger.info("Channel get to \"" + ret);
|
|
898
1107
|
callback.callback(ret);
|
|
899
1108
|
return;
|
|
900
1109
|
}
|
|
901
1110
|
}
|
|
902
1111
|
|
|
903
1112
|
if (!response.isSuccessful()) {
|
|
904
|
-
|
|
1113
|
+
Map<String, Object> retError = new HashMap<>();
|
|
905
1114
|
retError.put("message", "Server error: " + response.code());
|
|
906
1115
|
retError.put("error", "response_error");
|
|
907
1116
|
callback.callback(retError);
|
|
@@ -911,7 +1120,17 @@ public class CapacitorUpdater {
|
|
|
911
1120
|
assert responseBody != null;
|
|
912
1121
|
String responseData = responseBody.string();
|
|
913
1122
|
JSONObject jsonResponse = new JSONObject(responseData);
|
|
914
|
-
|
|
1123
|
+
|
|
1124
|
+
// Check for server-side errors first
|
|
1125
|
+
if (jsonResponse.has("error")) {
|
|
1126
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1127
|
+
retError.put("message", jsonResponse.getString("error"));
|
|
1128
|
+
retError.put("error", "server_error");
|
|
1129
|
+
callback.callback(retError);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
Map<String, Object> ret = new HashMap<>();
|
|
915
1134
|
|
|
916
1135
|
Iterator<String> keys = jsonResponse.keys();
|
|
917
1136
|
while (keys.hasNext()) {
|
|
@@ -920,10 +1139,10 @@ public class CapacitorUpdater {
|
|
|
920
1139
|
ret.put(key, jsonResponse.get(key));
|
|
921
1140
|
}
|
|
922
1141
|
}
|
|
923
|
-
|
|
1142
|
+
logger.info("Channel get to \"" + ret);
|
|
924
1143
|
callback.callback(ret);
|
|
925
1144
|
} catch (JSONException e) {
|
|
926
|
-
|
|
1145
|
+
Map<String, Object> retError = new HashMap<>();
|
|
927
1146
|
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
928
1147
|
retError.put("error", "parse_error");
|
|
929
1148
|
callback.callback(retError);
|
|
@@ -933,6 +1152,128 @@ public class CapacitorUpdater {
|
|
|
933
1152
|
);
|
|
934
1153
|
}
|
|
935
1154
|
|
|
1155
|
+
public void listChannels(final Callback callback) {
|
|
1156
|
+
// Check if rate limit was exceeded
|
|
1157
|
+
if (rateLimitExceeded) {
|
|
1158
|
+
logger.debug("Skipping listChannels due to rate limit (429). Requests will resume after app restart.");
|
|
1159
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
1160
|
+
retError.put("message", "Rate limit exceeded");
|
|
1161
|
+
retError.put("error", "rate_limit_exceeded");
|
|
1162
|
+
callback.callback(retError);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
String channelUrl = this.channelUrl;
|
|
1167
|
+
if (channelUrl == null || channelUrl.isEmpty()) {
|
|
1168
|
+
logger.error("Channel URL is not set");
|
|
1169
|
+
final Map<String, Object> retError = new HashMap<>();
|
|
1170
|
+
retError.put("message", "Channel URL is not set");
|
|
1171
|
+
retError.put("error", "missing_config");
|
|
1172
|
+
callback.callback(retError);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Auto-detect values
|
|
1177
|
+
String appId = this.appId;
|
|
1178
|
+
String platform = "android";
|
|
1179
|
+
boolean isEmulator = this.isEmulator();
|
|
1180
|
+
boolean isProd = this.isProd();
|
|
1181
|
+
|
|
1182
|
+
// Build URL with query parameters
|
|
1183
|
+
HttpUrl.Builder urlBuilder = HttpUrl.parse(channelUrl).newBuilder();
|
|
1184
|
+
urlBuilder.addQueryParameter("app_id", appId);
|
|
1185
|
+
urlBuilder.addQueryParameter("platform", platform);
|
|
1186
|
+
urlBuilder.addQueryParameter("is_emulator", String.valueOf(isEmulator));
|
|
1187
|
+
urlBuilder.addQueryParameter("is_prod", String.valueOf(isProd));
|
|
1188
|
+
|
|
1189
|
+
Request request = new Request.Builder().url(urlBuilder.build()).get().build();
|
|
1190
|
+
|
|
1191
|
+
DownloadService.sharedClient
|
|
1192
|
+
.newCall(request)
|
|
1193
|
+
.enqueue(
|
|
1194
|
+
new okhttp3.Callback() {
|
|
1195
|
+
@Override
|
|
1196
|
+
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
1197
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1198
|
+
retError.put("message", "Request failed: " + e.getMessage());
|
|
1199
|
+
retError.put("error", "network_error");
|
|
1200
|
+
callback.callback(retError);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
@Override
|
|
1204
|
+
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
1205
|
+
try (ResponseBody responseBody = response.body()) {
|
|
1206
|
+
// Check for 429 rate limit
|
|
1207
|
+
if (checkAndHandleRateLimitResponse(response)) {
|
|
1208
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1209
|
+
retError.put("message", "Rate limit exceeded");
|
|
1210
|
+
retError.put("error", "rate_limit_exceeded");
|
|
1211
|
+
callback.callback(retError);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (!response.isSuccessful()) {
|
|
1216
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1217
|
+
retError.put("message", "Server error: " + response.code());
|
|
1218
|
+
retError.put("error", "response_error");
|
|
1219
|
+
callback.callback(retError);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
assert responseBody != null;
|
|
1224
|
+
String data = responseBody.string();
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
Map<String, Object> ret = new HashMap<>();
|
|
1228
|
+
|
|
1229
|
+
try {
|
|
1230
|
+
// Try to parse as direct array first
|
|
1231
|
+
JSONArray channelsJson = new JSONArray(data);
|
|
1232
|
+
List<Map<String, Object>> channelsList = new ArrayList<>();
|
|
1233
|
+
|
|
1234
|
+
for (int i = 0; i < channelsJson.length(); i++) {
|
|
1235
|
+
JSONObject channelJson = channelsJson.getJSONObject(i);
|
|
1236
|
+
Map<String, Object> channel = new HashMap<>();
|
|
1237
|
+
channel.put("id", channelJson.optString("id", ""));
|
|
1238
|
+
channel.put("name", channelJson.optString("name", ""));
|
|
1239
|
+
channel.put("public", channelJson.optBoolean("public", false));
|
|
1240
|
+
channel.put("allow_self_set", channelJson.optBoolean("allow_self_set", false));
|
|
1241
|
+
channelsList.add(channel);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Wrap in channels object for JS API
|
|
1245
|
+
ret.put("channels", channelsList);
|
|
1246
|
+
|
|
1247
|
+
logger.info("Channels listed successfully");
|
|
1248
|
+
callback.callback(ret);
|
|
1249
|
+
} catch (JSONException arrayException) {
|
|
1250
|
+
// If not an array, try to parse as error object
|
|
1251
|
+
try {
|
|
1252
|
+
JSONObject json = new JSONObject(data);
|
|
1253
|
+
if (json.has("error")) {
|
|
1254
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1255
|
+
retError.put("message", json.getString("error"));
|
|
1256
|
+
retError.put("error", "server_error");
|
|
1257
|
+
callback.callback(retError);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
} catch (JSONException objException) {
|
|
1261
|
+
// If neither array nor object, throw parse error
|
|
1262
|
+
throw arrayException;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
} catch (JSONException e) {
|
|
1266
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1267
|
+
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
1268
|
+
retError.put("error", "parse_error");
|
|
1269
|
+
callback.callback(retError);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
936
1277
|
public void sendStats(final String action) {
|
|
937
1278
|
this.sendStats(action, this.getCurrentBundle().getVersionName());
|
|
938
1279
|
}
|
|
@@ -942,6 +1283,12 @@ public class CapacitorUpdater {
|
|
|
942
1283
|
}
|
|
943
1284
|
|
|
944
1285
|
public void sendStats(final String action, final String versionName, final String oldVersionName) {
|
|
1286
|
+
// Check if rate limit was exceeded
|
|
1287
|
+
if (rateLimitExceeded) {
|
|
1288
|
+
logger.debug("Skipping sendStats due to rate limit (429). Stats will resume after app restart.");
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
945
1292
|
String statsUrl = this.statsUrl;
|
|
946
1293
|
if (statsUrl == null || statsUrl.isEmpty()) {
|
|
947
1294
|
return;
|
|
@@ -953,7 +1300,7 @@ public class CapacitorUpdater {
|
|
|
953
1300
|
json.put("old_version_name", oldVersionName);
|
|
954
1301
|
json.put("action", action);
|
|
955
1302
|
} catch (JSONException e) {
|
|
956
|
-
|
|
1303
|
+
logger.error("Error sendStats JSONException " + e.getMessage());
|
|
957
1304
|
return;
|
|
958
1305
|
}
|
|
959
1306
|
|
|
@@ -962,21 +1309,28 @@ public class CapacitorUpdater {
|
|
|
962
1309
|
.post(RequestBody.create(json.toString(), MediaType.get("application/json")))
|
|
963
1310
|
.build();
|
|
964
1311
|
|
|
965
|
-
|
|
1312
|
+
DownloadService.sharedClient
|
|
966
1313
|
.newCall(request)
|
|
967
1314
|
.enqueue(
|
|
968
1315
|
new okhttp3.Callback() {
|
|
969
1316
|
@Override
|
|
970
1317
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
971
|
-
|
|
1318
|
+
logger.error("Failed to send stats: " + e.getMessage());
|
|
972
1319
|
}
|
|
973
1320
|
|
|
974
1321
|
@Override
|
|
975
1322
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1323
|
+
try (ResponseBody responseBody = response.body()) {
|
|
1324
|
+
// Check for 429 rate limit
|
|
1325
|
+
if (checkAndHandleRateLimitResponse(response)) {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (response.isSuccessful()) {
|
|
1330
|
+
logger.info("Stats send for \"" + action + "\", version " + versionName);
|
|
1331
|
+
} else {
|
|
1332
|
+
logger.error("Error sending stats: " + response.code());
|
|
1333
|
+
}
|
|
980
1334
|
}
|
|
981
1335
|
}
|
|
982
1336
|
}
|
|
@@ -996,13 +1350,26 @@ public class CapacitorUpdater {
|
|
|
996
1350
|
} else {
|
|
997
1351
|
try {
|
|
998
1352
|
String stored = this.prefs.getString(trueId + INFO_SUFFIX, "");
|
|
999
|
-
|
|
1353
|
+
if (stored.isEmpty()) {
|
|
1354
|
+
result = new BundleInfo(trueId, null, BundleStatus.PENDING, "", "");
|
|
1355
|
+
} else {
|
|
1356
|
+
result = BundleInfo.fromJSON(stored);
|
|
1357
|
+
}
|
|
1000
1358
|
} catch (JSONException e) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1359
|
+
logger.error(
|
|
1360
|
+
"Failed to parse info for bundle [" +
|
|
1361
|
+
trueId +
|
|
1362
|
+
"] stored value: '" +
|
|
1363
|
+
this.prefs.getString(trueId + INFO_SUFFIX, "") +
|
|
1364
|
+
"' error: " +
|
|
1365
|
+
e.getMessage()
|
|
1366
|
+
);
|
|
1367
|
+
// Clear corrupted data
|
|
1368
|
+
this.editor.remove(trueId + INFO_SUFFIX);
|
|
1369
|
+
this.editor.commit();
|
|
1370
|
+
result = new BundleInfo(trueId, null, BundleStatus.ERROR, "", "");
|
|
1003
1371
|
}
|
|
1004
1372
|
}
|
|
1005
|
-
// Log.d(TAG, "Returning info [" + trueId + "] " + result);
|
|
1006
1373
|
return result;
|
|
1007
1374
|
}
|
|
1008
1375
|
|
|
@@ -1022,17 +1389,18 @@ public class CapacitorUpdater {
|
|
|
1022
1389
|
|
|
1023
1390
|
public void saveBundleInfo(final String id, final BundleInfo info) {
|
|
1024
1391
|
if (id == null || (info != null && (info.isBuiltin() || info.isUnknown()))) {
|
|
1025
|
-
|
|
1392
|
+
logger.debug("Not saving info for bundle: [" + id + "] " + info);
|
|
1026
1393
|
return;
|
|
1027
1394
|
}
|
|
1028
1395
|
|
|
1029
1396
|
if (info == null) {
|
|
1030
|
-
|
|
1397
|
+
logger.debug("Removing info for bundle [" + id + "]");
|
|
1031
1398
|
this.editor.remove(id + INFO_SUFFIX);
|
|
1032
1399
|
} else {
|
|
1033
1400
|
final BundleInfo update = info.setId(id);
|
|
1034
|
-
|
|
1035
|
-
|
|
1401
|
+
String jsonString = update.toString();
|
|
1402
|
+
logger.debug("Storing info for bundle [" + id + "] " + update.getClass().getName() + " -> " + jsonString);
|
|
1403
|
+
this.editor.putString(id + INFO_SUFFIX, jsonString);
|
|
1036
1404
|
}
|
|
1037
1405
|
this.editor.commit();
|
|
1038
1406
|
}
|
|
@@ -1040,7 +1408,7 @@ public class CapacitorUpdater {
|
|
|
1040
1408
|
private void setBundleStatus(final String id, final BundleStatus status) {
|
|
1041
1409
|
if (id != null && status != null) {
|
|
1042
1410
|
BundleInfo info = this.getBundleInfo(id);
|
|
1043
|
-
|
|
1411
|
+
logger.debug("Setting status for bundle [" + id + "] to " + status);
|
|
1044
1412
|
this.saveBundleInfo(id, info.setStatus(status));
|
|
1045
1413
|
}
|
|
1046
1414
|
}
|
|
@@ -1059,7 +1427,7 @@ public class CapacitorUpdater {
|
|
|
1059
1427
|
}
|
|
1060
1428
|
|
|
1061
1429
|
public String getCurrentBundlePath() {
|
|
1062
|
-
String path = this.prefs.getString(
|
|
1430
|
+
String path = this.prefs.getString(this.CAP_SERVER_PATH, "public");
|
|
1063
1431
|
if (path.trim().isEmpty()) {
|
|
1064
1432
|
return "public";
|
|
1065
1433
|
}
|