@capgo/capacitor-updater 7.45.10 → 7.50.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +1 -1
- package/README.md +510 -92
- package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3192 -777
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +798 -299
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +46 -28
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +2 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +359 -25
- package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
- package/dist/docs.json +1283 -169
- package/dist/esm/definitions.d.ts +621 -44
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +11 -1
- package/dist/esm/web.js +59 -1
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +59 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +59 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AppHealthTracker.swift +82 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -16
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +2116 -223
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +997 -332
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +78 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +418 -36
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +15 -3
|
@@ -9,6 +9,7 @@ package ee.forgr.capacitor_updater;
|
|
|
9
9
|
import android.app.Activity;
|
|
10
10
|
import android.content.Context;
|
|
11
11
|
import android.content.SharedPreferences;
|
|
12
|
+
import android.content.pm.PackageManager;
|
|
12
13
|
import android.os.Build;
|
|
13
14
|
import androidx.annotation.NonNull;
|
|
14
15
|
import androidx.lifecycle.LifecycleOwner;
|
|
@@ -18,12 +19,15 @@ import androidx.work.WorkManager;
|
|
|
18
19
|
import com.google.common.util.concurrent.Futures;
|
|
19
20
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
20
21
|
import java.io.BufferedInputStream;
|
|
22
|
+
import java.io.BufferedReader;
|
|
21
23
|
import java.io.File;
|
|
22
24
|
import java.io.FileInputStream;
|
|
23
25
|
import java.io.FileNotFoundException;
|
|
24
26
|
import java.io.FileOutputStream;
|
|
25
27
|
import java.io.FilenameFilter;
|
|
26
28
|
import java.io.IOException;
|
|
29
|
+
import java.io.InputStreamReader;
|
|
30
|
+
import java.nio.charset.StandardCharsets;
|
|
27
31
|
import java.security.SecureRandom;
|
|
28
32
|
import java.util.ArrayList;
|
|
29
33
|
import java.util.Date;
|
|
@@ -60,8 +64,11 @@ public class CapgoUpdater {
|
|
|
60
64
|
|
|
61
65
|
private static final String FALLBACK_VERSION = "pastVersion";
|
|
62
66
|
private static final String NEXT_VERSION = "nextVersion";
|
|
67
|
+
private static final String PREVIEW_FALLBACK_VERSION = "previewFallbackVersion";
|
|
63
68
|
private static final String bundleDirectory = "versions";
|
|
64
69
|
private static final String TEMP_UNZIP_PREFIX = "capgo_unzip_";
|
|
70
|
+
private static final String CAPACITOR_CONFIG_ASSET = "capacitor.config.json";
|
|
71
|
+
private static final String BACKGROUND_RUNNER_CONFIG_KEY = "BackgroundRunner";
|
|
65
72
|
|
|
66
73
|
public static final String TAG = "Capacitor-updater";
|
|
67
74
|
public SharedPreferences.Editor editor;
|
|
@@ -81,6 +88,7 @@ public class CapgoUpdater {
|
|
|
81
88
|
public String channelUrl = "";
|
|
82
89
|
public String defaultChannel = "";
|
|
83
90
|
public String appId = "";
|
|
91
|
+
public volatile boolean previewSession = false;
|
|
84
92
|
public String publicKey = "";
|
|
85
93
|
public String deviceID = "";
|
|
86
94
|
public int timeout = 20000;
|
|
@@ -95,11 +103,22 @@ public class CapgoUpdater {
|
|
|
95
103
|
private static volatile boolean rateLimitStatisticSent = false;
|
|
96
104
|
|
|
97
105
|
// Stats batching - queue events and send max once per second
|
|
98
|
-
private final List<
|
|
106
|
+
private final List<QueuedStatsEvent> statsQueue = new CopyOnWriteArrayList<>();
|
|
99
107
|
private final ScheduledExecutorService statsScheduler = Executors.newSingleThreadScheduledExecutor();
|
|
100
108
|
private ScheduledFuture<?> statsFlushTask = null;
|
|
101
109
|
private static final long STATS_FLUSH_INTERVAL_MS = 1000;
|
|
102
110
|
|
|
111
|
+
private static final class QueuedStatsEvent {
|
|
112
|
+
|
|
113
|
+
private final JSONObject event;
|
|
114
|
+
private final Runnable onSent;
|
|
115
|
+
|
|
116
|
+
private QueuedStatsEvent(final JSONObject event, final Runnable onSent) {
|
|
117
|
+
this.event = event;
|
|
118
|
+
this.onSent = onSent;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
103
122
|
private final Map<String, CompletableFuture<BundleInfo>> downloadFutures = new ConcurrentHashMap<>();
|
|
104
123
|
private final ExecutorService io = Executors.newSingleThreadExecutor();
|
|
105
124
|
|
|
@@ -123,25 +142,78 @@ public class CapgoUpdater {
|
|
|
123
142
|
}
|
|
124
143
|
}
|
|
125
144
|
|
|
145
|
+
static String installSourceForInstallerPackage(final String installerPackageName) {
|
|
146
|
+
if (installerPackageName == null || installerPackageName.trim().isEmpty()) {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
switch (installerPackageName) {
|
|
151
|
+
case "com.android.vending":
|
|
152
|
+
// Android exposes the Google Play installer package, but not whether the app came from production, alpha, beta, or internal testing.
|
|
153
|
+
return "google_play";
|
|
154
|
+
case "com.amazon.venezia":
|
|
155
|
+
return "amazon_appstore";
|
|
156
|
+
case "com.sec.android.app.samsungapps":
|
|
157
|
+
return "samsung_galaxy_store";
|
|
158
|
+
case "com.huawei.appmarket":
|
|
159
|
+
return "huawei_appgallery";
|
|
160
|
+
default:
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@SuppressWarnings("deprecation")
|
|
166
|
+
private String getInstallSource() {
|
|
167
|
+
if (activity == null) {
|
|
168
|
+
return "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
PackageManager packageManager = activity.getPackageManager();
|
|
173
|
+
String packageName = activity.getPackageName();
|
|
174
|
+
String installerPackageName;
|
|
175
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
176
|
+
android.content.pm.InstallSourceInfo installSourceInfo = packageManager.getInstallSourceInfo(packageName);
|
|
177
|
+
installerPackageName = installSourceInfo.getInstallingPackageName();
|
|
178
|
+
if (installerPackageName == null || installerPackageName.trim().isEmpty()) {
|
|
179
|
+
installerPackageName = installSourceInfo.getInitiatingPackageName();
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
installerPackageName = packageManager.getInstallerPackageName(packageName);
|
|
183
|
+
}
|
|
184
|
+
return installSourceForInstallerPackage(installerPackageName);
|
|
185
|
+
} catch (Exception e) {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
126
190
|
private boolean isEmulator() {
|
|
191
|
+
final String brand = String.valueOf(Build.BRAND);
|
|
192
|
+
final String device = String.valueOf(Build.DEVICE);
|
|
193
|
+
final String fingerprint = String.valueOf(Build.FINGERPRINT);
|
|
194
|
+
final String hardware = String.valueOf(Build.HARDWARE);
|
|
195
|
+
final String model = String.valueOf(Build.MODEL);
|
|
196
|
+
final String manufacturer = String.valueOf(Build.MANUFACTURER);
|
|
197
|
+
final String product = String.valueOf(Build.PRODUCT);
|
|
198
|
+
|
|
127
199
|
return (
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
200
|
+
(brand.startsWith("generic") && device.startsWith("generic")) ||
|
|
201
|
+
fingerprint.startsWith("generic") ||
|
|
202
|
+
fingerprint.startsWith("unknown") ||
|
|
203
|
+
hardware.contains("goldfish") ||
|
|
204
|
+
hardware.contains("ranchu") ||
|
|
205
|
+
model.contains("google_sdk") ||
|
|
206
|
+
model.contains("Emulator") ||
|
|
207
|
+
model.contains("Android SDK built for x86") ||
|
|
208
|
+
manufacturer.contains("Genymotion") ||
|
|
209
|
+
product.contains("sdk_google") ||
|
|
210
|
+
product.contains("google_sdk") ||
|
|
211
|
+
product.contains("sdk") ||
|
|
212
|
+
product.contains("sdk_x86") ||
|
|
213
|
+
product.contains("sdk_gphone64_arm64") ||
|
|
214
|
+
product.contains("vbox86p") ||
|
|
215
|
+
product.contains("emulator") ||
|
|
216
|
+
product.contains("simulator")
|
|
145
217
|
);
|
|
146
218
|
}
|
|
147
219
|
|
|
@@ -183,6 +255,30 @@ public class CapgoUpdater {
|
|
|
183
255
|
this.cachedKeyId = CryptoCipher.calcKeyId(publicKey);
|
|
184
256
|
}
|
|
185
257
|
|
|
258
|
+
static File resolvePathInsideDirectory(final File baseDirectory, final String relativePath) throws IOException {
|
|
259
|
+
if (relativePath == null || relativePath.isEmpty()) {
|
|
260
|
+
throw new IOException("Invalid empty path");
|
|
261
|
+
}
|
|
262
|
+
if (relativePath.contains("\\") || relativePath.indexOf('\0') >= 0) {
|
|
263
|
+
throw new IOException("Invalid path separator");
|
|
264
|
+
}
|
|
265
|
+
if (new File(relativePath).isAbsolute()) {
|
|
266
|
+
throw new IOException("Absolute paths are not allowed");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
final File canonicalBase = baseDirectory.getCanonicalFile();
|
|
270
|
+
final File canonicalTarget = new File(canonicalBase, relativePath).getCanonicalFile();
|
|
271
|
+
final String basePath = canonicalBase.getPath();
|
|
272
|
+
final String targetPath = canonicalTarget.getPath();
|
|
273
|
+
final String normalizedBasePath = basePath.endsWith(File.separator) ? basePath : basePath + File.separator;
|
|
274
|
+
|
|
275
|
+
if (!targetPath.equals(basePath) && !targetPath.startsWith(normalizedBasePath)) {
|
|
276
|
+
throw new IOException("Path escapes base directory: " + relativePath);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return canonicalTarget;
|
|
280
|
+
}
|
|
281
|
+
|
|
186
282
|
public String getKeyId() {
|
|
187
283
|
return this.cachedKeyId;
|
|
188
284
|
}
|
|
@@ -203,23 +299,21 @@ public class CapgoUpdater {
|
|
|
203
299
|
|
|
204
300
|
ZipEntry entry;
|
|
205
301
|
while ((entry = zis.getNextEntry()) != null) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
302
|
+
final File file;
|
|
303
|
+
try {
|
|
304
|
+
file = resolvePathInsideDirectory(targetDirectory, entry.getName());
|
|
305
|
+
} catch (IOException e) {
|
|
306
|
+
if (entry.getName().contains("\\")) {
|
|
307
|
+
logger.error("Unzip failed: Windows path not supported");
|
|
308
|
+
logger.debug("Invalid path: " + entry.getName());
|
|
309
|
+
this.sendStats("windows_path_fail");
|
|
310
|
+
} else {
|
|
311
|
+
this.sendStats("canonical_path_fail");
|
|
312
|
+
}
|
|
313
|
+
throw e;
|
|
210
314
|
}
|
|
211
|
-
final File file = new File(targetDirectory, entry.getName());
|
|
212
|
-
final String canonicalPath = file.getCanonicalPath();
|
|
213
|
-
final String canonicalDir = targetDirectory.getCanonicalPath();
|
|
214
315
|
final File dir = entry.isDirectory() ? file : file.getParentFile();
|
|
215
316
|
|
|
216
|
-
if (!canonicalPath.startsWith(canonicalDir)) {
|
|
217
|
-
this.sendStats("canonical_path_fail");
|
|
218
|
-
throw new FileNotFoundException(
|
|
219
|
-
"SecurityException, Failed to ensure directory is the start path : " + canonicalDir + " of " + canonicalPath
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
317
|
assert dir != null;
|
|
224
318
|
if (!dir.isDirectory() && !dir.mkdirs()) {
|
|
225
319
|
this.sendStats("directory_path_fail");
|
|
@@ -345,6 +439,137 @@ public class CapgoUpdater {
|
|
|
345
439
|
}
|
|
346
440
|
}
|
|
347
441
|
|
|
442
|
+
private boolean verifyChecksum(final File file, final String expectedHash) {
|
|
443
|
+
if (expectedHash == null || expectedHash.isEmpty() || file == null || !file.exists()) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
final String actualHash = CryptoCipher.calcChecksum(file);
|
|
447
|
+
return expectedHash.equalsIgnoreCase(actualHash);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private String resolveManifestFileHash(final JSONObject entry, final String sessionKey) {
|
|
451
|
+
String fileHash = entry.optString("file_hash", "");
|
|
452
|
+
if (fileHash.isEmpty()) {
|
|
453
|
+
return "";
|
|
454
|
+
}
|
|
455
|
+
if (this.publicKey != null && !this.publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
456
|
+
try {
|
|
457
|
+
fileHash = CryptoCipher.decryptChecksum(fileHash, this.publicKey);
|
|
458
|
+
} catch (Exception e) {
|
|
459
|
+
logger.error("Checksum decryption failed while checking missing manifest files");
|
|
460
|
+
logger.debug("File: " + entry.optString("file_name", "unknown") + ", Error: " + e.getMessage());
|
|
461
|
+
return "";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return fileHash;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private boolean isManifestEntryAvailableLocally(final JSONObject entry, final String sessionKey) {
|
|
468
|
+
final String fileName = entry.optString("file_name", "");
|
|
469
|
+
final String fileHash = resolveManifestFileHash(entry, sessionKey);
|
|
470
|
+
if (fileName.isEmpty() || fileHash.isEmpty() || this.activity == null) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
final File builtinFile = new File(this.activity.getFilesDir(), "public/" + fileName);
|
|
475
|
+
if (verifyChecksum(builtinFile, fileHash)) {
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
final boolean isBrotli = fileName.endsWith(".br");
|
|
480
|
+
final String fileNameWithoutPath = new File(fileName).getName();
|
|
481
|
+
final String cacheBaseName = isBrotli ? fileNameWithoutPath.substring(0, fileNameWithoutPath.length() - 3) : fileNameWithoutPath;
|
|
482
|
+
final File cacheFolder = new File(this.activity.getCacheDir(), "capgo_downloads");
|
|
483
|
+
final File cacheFile = new File(cacheFolder, fileHash + "_" + cacheBaseName);
|
|
484
|
+
if (verifyChecksum(cacheFile, fileHash)) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (isBrotli) {
|
|
489
|
+
final File legacyCacheFile = new File(cacheFolder, fileHash + "_" + fileNameWithoutPath);
|
|
490
|
+
return verifyChecksum(legacyCacheFile, fileHash);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
public JSONArray getMissingBundleFiles(final JSONArray manifest, final String sessionKey) throws JSONException {
|
|
497
|
+
final JSONArray missing = new JSONArray();
|
|
498
|
+
for (int i = 0; i < manifest.length(); i++) {
|
|
499
|
+
final JSONObject entry = manifest.getJSONObject(i);
|
|
500
|
+
if (!isManifestEntryAvailableLocally(entry, sessionKey)) {
|
|
501
|
+
missing.put(entry);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return missing;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
public JSONObject missingBundleFilesResult(final JSONArray manifest, final String sessionKey) throws JSONException {
|
|
508
|
+
final JSONArray missing = getMissingBundleFiles(manifest, sessionKey);
|
|
509
|
+
final JSONObject ret = new JSONObject();
|
|
510
|
+
ret.put("missing", missing);
|
|
511
|
+
ret.put("total", manifest.length());
|
|
512
|
+
ret.put("missingCount", missing.length());
|
|
513
|
+
ret.put("reusableCount", manifest.length() - missing.length());
|
|
514
|
+
return ret;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private String manifestSizeUrl(final String updateUrl) {
|
|
518
|
+
HttpUrl parsed = HttpUrl.parse(updateUrl);
|
|
519
|
+
if (parsed == null) {
|
|
520
|
+
return updateUrl;
|
|
521
|
+
}
|
|
522
|
+
return parsed.newBuilder().addPathSegment("manifest_size").query(null).build().toString();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private JSONObject unavailableBundleSizeResult(final JSONArray manifest, final String error) throws JSONException {
|
|
526
|
+
final JSONObject ret = new JSONObject();
|
|
527
|
+
final JSONArray files = new JSONArray();
|
|
528
|
+
for (int i = 0; i < manifest.length(); i++) {
|
|
529
|
+
final JSONObject entry = new JSONObject(manifest.getJSONObject(i).toString());
|
|
530
|
+
entry.put("error", error);
|
|
531
|
+
files.put(entry);
|
|
532
|
+
}
|
|
533
|
+
ret.put("totalSize", 0);
|
|
534
|
+
ret.put("knownFiles", 0);
|
|
535
|
+
ret.put("unknownFiles", manifest.length());
|
|
536
|
+
ret.put("files", files);
|
|
537
|
+
return ret;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
public JSONObject getBundleDownloadSize(final String updateUrl, final String version, final JSONArray manifest) throws JSONException {
|
|
541
|
+
if (manifest.length() == 0) {
|
|
542
|
+
final JSONObject ret = new JSONObject();
|
|
543
|
+
ret.put("totalSize", 0);
|
|
544
|
+
ret.put("knownFiles", 0);
|
|
545
|
+
ret.put("unknownFiles", 0);
|
|
546
|
+
ret.put("files", new JSONArray());
|
|
547
|
+
return ret;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
final JSONObject json = this.createInfoObject();
|
|
551
|
+
json.put("version", version != null ? version : "");
|
|
552
|
+
json.put("manifest", manifest);
|
|
553
|
+
|
|
554
|
+
Request request = new Request.Builder()
|
|
555
|
+
.url(manifestSizeUrl(updateUrl))
|
|
556
|
+
.post(RequestBody.create(json.toString(), MediaType.get("application/json; charset=utf-8")))
|
|
557
|
+
.build();
|
|
558
|
+
|
|
559
|
+
try (Response response = DownloadService.sharedClient.newCall(request).execute()) {
|
|
560
|
+
final ResponseBody responseBody = response.body();
|
|
561
|
+
final String responseData = responseBody != null ? responseBody.string() : "";
|
|
562
|
+
if (!response.isSuccessful() || responseData.isEmpty()) {
|
|
563
|
+
return unavailableBundleSizeResult(manifest, "response_error");
|
|
564
|
+
}
|
|
565
|
+
return new JSONObject(responseData);
|
|
566
|
+
} catch (IOException e) {
|
|
567
|
+
logger.error("Error getting bundle download size");
|
|
568
|
+
logger.debug("Error: " + e.getMessage());
|
|
569
|
+
return unavailableBundleSizeResult(manifest, "response_error");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
348
573
|
private void observeWorkProgress(Context context, String id, boolean setNext) {
|
|
349
574
|
if (!(context instanceof LifecycleOwner)) {
|
|
350
575
|
logger.error("Context is not a LifecycleOwner, cannot observe work progress");
|
|
@@ -478,6 +703,7 @@ public class CapgoUpdater {
|
|
|
478
703
|
this.appId,
|
|
479
704
|
this.pluginVersion,
|
|
480
705
|
this.isProd(),
|
|
706
|
+
this.getInstallSource(),
|
|
481
707
|
this.statsUrl,
|
|
482
708
|
this.deviceID,
|
|
483
709
|
this.versionBuild,
|
|
@@ -579,11 +805,15 @@ public class CapgoUpdater {
|
|
|
579
805
|
CapgoUpdater.this.notifyListeners("updateAvailable", ret);
|
|
580
806
|
logger.info("setNext: " + setNext);
|
|
581
807
|
if (setNext) {
|
|
582
|
-
|
|
583
|
-
|
|
808
|
+
if (this.previewSession) {
|
|
809
|
+
logger.info("Preview session is active, skipping automatic install of downloaded bundle");
|
|
810
|
+
this.directUpdate = false;
|
|
811
|
+
} else if (this.directUpdate) {
|
|
812
|
+
logger.info("directUpdate: " + this.directUpdate);
|
|
584
813
|
CapgoUpdater.this.directUpdateFinish(next);
|
|
585
814
|
this.directUpdate = false;
|
|
586
815
|
} else {
|
|
816
|
+
logger.info("directUpdate: " + this.directUpdate);
|
|
587
817
|
this.setNextBundle(next.getId());
|
|
588
818
|
}
|
|
589
819
|
}
|
|
@@ -753,6 +983,7 @@ public class CapgoUpdater {
|
|
|
753
983
|
}
|
|
754
984
|
|
|
755
985
|
private void setCurrentBundle(final File bundle) {
|
|
986
|
+
this.cancelBackgroundRunnerWorkBeforeBundleSwitch();
|
|
756
987
|
this.editor.putString(this.CAP_SERVER_PATH, bundle.getPath());
|
|
757
988
|
logger.info("Current bundle set to: " + bundle);
|
|
758
989
|
this.editor.commit();
|
|
@@ -762,6 +993,73 @@ public class CapgoUpdater {
|
|
|
762
993
|
return bundlePath != null && !bundlePath.trim().isEmpty() && !isBuiltin && !hasStoredBundleInfo;
|
|
763
994
|
}
|
|
764
995
|
|
|
996
|
+
static String getBackgroundRunnerLabelFromConfig(final String configJson) {
|
|
997
|
+
if (configJson == null || configJson.trim().isEmpty()) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
try {
|
|
1002
|
+
final JSONObject config = new JSONObject(configJson);
|
|
1003
|
+
final JSONObject plugins = config.optJSONObject("plugins");
|
|
1004
|
+
if (plugins == null) {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
final JSONObject backgroundRunner = plugins.optJSONObject(BACKGROUND_RUNNER_CONFIG_KEY);
|
|
1009
|
+
if (backgroundRunner == null) {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
final String label = backgroundRunner.optString("label", "").trim();
|
|
1014
|
+
return label.isEmpty() ? null : label;
|
|
1015
|
+
} catch (JSONException ignored) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private String readAssetAsString(final String assetPath) throws IOException {
|
|
1021
|
+
final StringBuilder buffer = new StringBuilder();
|
|
1022
|
+
try (
|
|
1023
|
+
final BufferedReader reader = new BufferedReader(
|
|
1024
|
+
new InputStreamReader(this.activity.getAssets().open(assetPath), StandardCharsets.UTF_8)
|
|
1025
|
+
)
|
|
1026
|
+
) {
|
|
1027
|
+
String line;
|
|
1028
|
+
while ((line = reader.readLine()) != null) {
|
|
1029
|
+
buffer.append(line).append('\n');
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return buffer.toString();
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private void cancelBackgroundRunnerWorkBeforeBundleSwitch() {
|
|
1036
|
+
if (this.activity == null) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
final String label;
|
|
1041
|
+
try {
|
|
1042
|
+
label = getBackgroundRunnerLabelFromConfig(this.readAssetAsString(CAPACITOR_CONFIG_ASSET));
|
|
1043
|
+
} catch (IOException ignored) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (label == null) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
try {
|
|
1052
|
+
final WorkManager workManager = WorkManager.getInstance(this.activity.getApplicationContext());
|
|
1053
|
+
workManager.cancelUniqueWork(label);
|
|
1054
|
+
workManager.cancelAllWorkByTag(label);
|
|
1055
|
+
logger.info("Cancelled Background Runner work before bundle switch.");
|
|
1056
|
+
logger.debug("Background Runner label: " + label);
|
|
1057
|
+
} catch (Exception e) {
|
|
1058
|
+
logger.warn("Failed to cancel Background Runner work before bundle switch.");
|
|
1059
|
+
logger.debug("Background Runner cancellation error: " + e.getMessage());
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
765
1063
|
private boolean hasStoredBundleInfo(final String id) {
|
|
766
1064
|
return (
|
|
767
1065
|
id != null &&
|
|
@@ -941,6 +1239,17 @@ public class CapgoUpdater {
|
|
|
941
1239
|
logger.debug("Bundle ID: " + id);
|
|
942
1240
|
return false;
|
|
943
1241
|
}
|
|
1242
|
+
final BundleInfo previewFallback = this.getPreviewFallbackBundle();
|
|
1243
|
+
if (
|
|
1244
|
+
previewFallback != null &&
|
|
1245
|
+
!previewFallback.isDeleted() &&
|
|
1246
|
+
!previewFallback.isErrorStatus() &&
|
|
1247
|
+
previewFallback.getId().equals(id)
|
|
1248
|
+
) {
|
|
1249
|
+
logger.error("Cannot delete the preview fallback bundle");
|
|
1250
|
+
logger.debug("Bundle ID: " + id);
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
944
1253
|
final BundleInfo next = this.getNextBundle();
|
|
945
1254
|
if (next != null && !next.isDeleted() && !next.isErrorStatus() && next.getId().equals(id)) {
|
|
946
1255
|
logger.error("Cannot delete the next bundle");
|
|
@@ -1013,12 +1322,10 @@ public class CapgoUpdater {
|
|
|
1013
1322
|
}
|
|
1014
1323
|
|
|
1015
1324
|
void restoreResetState(final ResetState state) {
|
|
1016
|
-
final String currentBundlePath =
|
|
1017
|
-
? "public"
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
? BundleInfo.ID_BUILTIN
|
|
1021
|
-
: state.fallbackBundleId;
|
|
1325
|
+
final String currentBundlePath =
|
|
1326
|
+
state.currentBundlePath == null || state.currentBundlePath.trim().isEmpty() ? "public" : state.currentBundlePath;
|
|
1327
|
+
final String fallbackBundleId =
|
|
1328
|
+
state.fallbackBundleId == null || state.fallbackBundleId.isEmpty() ? BundleInfo.ID_BUILTIN : state.fallbackBundleId;
|
|
1022
1329
|
|
|
1023
1330
|
this.editor.putString(this.CAP_SERVER_PATH, currentBundlePath);
|
|
1024
1331
|
this.editor.putString(FALLBACK_VERSION, fallbackBundleId);
|
|
@@ -1081,6 +1388,21 @@ public class CapgoUpdater {
|
|
|
1081
1388
|
return true;
|
|
1082
1389
|
}
|
|
1083
1390
|
|
|
1391
|
+
boolean stagePreviewFallbackReload(final BundleInfo bundle) {
|
|
1392
|
+
if (bundle == null || bundle.isErrorStatus()) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
if (bundle.isBuiltin()) {
|
|
1396
|
+
this.setCurrentBundle(new File("public"));
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
if (!this.bundleExists(bundle.getId())) {
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
this.setCurrentBundle(this.getBundleDirectory(bundle.getId()));
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1084
1406
|
void finalizePendingReload(final BundleInfo bundle, final String previousBundleName) {
|
|
1085
1407
|
if (bundle == null || bundle.isBuiltin()) {
|
|
1086
1408
|
return;
|
|
@@ -1088,7 +1410,16 @@ public class CapgoUpdater {
|
|
|
1088
1410
|
this.sendStats("set", bundle.getVersionName(), previousBundleName);
|
|
1089
1411
|
}
|
|
1090
1412
|
|
|
1413
|
+
@Deprecated
|
|
1091
1414
|
public void autoReset() {
|
|
1415
|
+
this.autoReset(this.versionCode == null ? "" : this.versionCode);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
public void autoReset(final String currentNativeBuildVersion) {
|
|
1419
|
+
this.autoReset(currentNativeBuildVersion, true);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
public void autoReset(final String currentNativeBuildVersion, final boolean resetWhenNativeVersionChanged) {
|
|
1092
1423
|
final BundleInfo currentBundle = this.getCurrentBundle();
|
|
1093
1424
|
if (!currentBundle.isBuiltin() && !this.bundleExists(currentBundle.getId())) {
|
|
1094
1425
|
logger.info("Folder at bundle path does not exist. Triggering reset.");
|
|
@@ -1099,7 +1430,36 @@ public class CapgoUpdater {
|
|
|
1099
1430
|
if (shouldResetForForeignBundle(bundlePath, currentBundle.isBuiltin(), this.hasStoredBundleInfo(currentBundle.getId()))) {
|
|
1100
1431
|
logger.info("Current bundle id is not one of the bundle ids stored by this plugin. Triggering reset.");
|
|
1101
1432
|
this.reset();
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
final String previousNativeBuildVersion = this.getStoredNativeBuildVersion();
|
|
1436
|
+
if (
|
|
1437
|
+
resetWhenNativeVersionChanged &&
|
|
1438
|
+
!previousNativeBuildVersion.isEmpty() &&
|
|
1439
|
+
currentNativeBuildVersion != null &&
|
|
1440
|
+
!currentNativeBuildVersion.isEmpty() &&
|
|
1441
|
+
!Objects.equals(previousNativeBuildVersion, currentNativeBuildVersion)
|
|
1442
|
+
) {
|
|
1443
|
+
logger.info(
|
|
1444
|
+
"Stored native build version " +
|
|
1445
|
+
previousNativeBuildVersion +
|
|
1446
|
+
" does not match current native build version " +
|
|
1447
|
+
currentNativeBuildVersion +
|
|
1448
|
+
". Triggering reset."
|
|
1449
|
+
);
|
|
1450
|
+
this.reset();
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private String getStoredNativeBuildVersion() {
|
|
1455
|
+
if (this.prefs == null) {
|
|
1456
|
+
return "";
|
|
1457
|
+
}
|
|
1458
|
+
String previousNativeBuildVersion = this.prefs.getString("LatestNativeBuildVersion", "");
|
|
1459
|
+
if (previousNativeBuildVersion == null || previousNativeBuildVersion.isEmpty()) {
|
|
1460
|
+
previousNativeBuildVersion = this.prefs.getString("LatestVersionNative", "");
|
|
1102
1461
|
}
|
|
1462
|
+
return previousNativeBuildVersion == null ? "" : previousNativeBuildVersion;
|
|
1103
1463
|
}
|
|
1104
1464
|
|
|
1105
1465
|
public void reset() {
|
|
@@ -1109,12 +1469,20 @@ public class CapgoUpdater {
|
|
|
1109
1469
|
public void setSuccess(final BundleInfo bundle, Boolean autoDeletePrevious) {
|
|
1110
1470
|
this.setBundleStatus(bundle.getId(), BundleStatus.SUCCESS);
|
|
1111
1471
|
final BundleInfo fallback = this.getFallbackBundle();
|
|
1472
|
+
final BundleInfo previewFallback = this.getPreviewFallbackBundle();
|
|
1473
|
+
final boolean fallbackIsPreviewFallback = previewFallback != null && previewFallback.getId().equals(fallback.getId());
|
|
1112
1474
|
logger.debug("Fallback bundle is: " + fallback);
|
|
1113
1475
|
logger.info("Version successfully loaded: " + bundle.getVersionName());
|
|
1114
1476
|
// Only attempt to delete when the fallback is a different bundle than the
|
|
1115
1477
|
// currently loaded one. Otherwise we spam logs with "Cannot delete <id>"
|
|
1116
1478
|
// because delete() protects the current bundle from removal.
|
|
1117
|
-
if (
|
|
1479
|
+
if (
|
|
1480
|
+
autoDeletePrevious &&
|
|
1481
|
+
!fallback.isBuiltin() &&
|
|
1482
|
+
fallback.getId() != null &&
|
|
1483
|
+
!fallback.getId().equals(bundle.getId()) &&
|
|
1484
|
+
!fallbackIsPreviewFallback
|
|
1485
|
+
) {
|
|
1118
1486
|
final Boolean res = this.delete(fallback.getId());
|
|
1119
1487
|
if (res) {
|
|
1120
1488
|
logger.info("Deleted previous bundle: " + fallback.getVersionName());
|
|
@@ -1137,10 +1505,14 @@ public class CapgoUpdater {
|
|
|
1137
1505
|
}
|
|
1138
1506
|
|
|
1139
1507
|
private JSONObject createInfoObject() throws JSONException {
|
|
1508
|
+
return this.createInfoObject(null);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
private JSONObject createInfoObject(final String appIdOverride) throws JSONException {
|
|
1140
1512
|
JSONObject json = new JSONObject();
|
|
1141
1513
|
json.put("platform", "android");
|
|
1142
1514
|
json.put("device_id", this.deviceID);
|
|
1143
|
-
json.put("app_id", this.appId);
|
|
1515
|
+
json.put("app_id", appIdOverride == null || appIdOverride.trim().isEmpty() ? this.appId : appIdOverride);
|
|
1144
1516
|
json.put("custom_id", this.customId);
|
|
1145
1517
|
json.put("version_build", this.versionBuild);
|
|
1146
1518
|
json.put("version_code", this.versionCode);
|
|
@@ -1149,6 +1521,7 @@ public class CapgoUpdater {
|
|
|
1149
1521
|
json.put("plugin_version", this.pluginVersion);
|
|
1150
1522
|
json.put("is_emulator", this.isEmulator());
|
|
1151
1523
|
json.put("is_prod", this.isProd());
|
|
1524
|
+
json.put("install_source", this.getInstallSource());
|
|
1152
1525
|
json.put("defaultChannel", this.defaultChannel);
|
|
1153
1526
|
|
|
1154
1527
|
// Add encryption key ID if encryption is enabled (use cached value)
|
|
@@ -1166,7 +1539,7 @@ public class CapgoUpdater {
|
|
|
1166
1539
|
if (response.code() == 429) {
|
|
1167
1540
|
// Send a statistic about the rate limit BEFORE setting the flag
|
|
1168
1541
|
// Only send once to prevent infinite loop if the stat request itself gets rate limited
|
|
1169
|
-
if (!rateLimitExceeded && !rateLimitStatisticSent) {
|
|
1542
|
+
if (!this.previewSession && !rateLimitExceeded && !rateLimitStatisticSent) {
|
|
1170
1543
|
rateLimitStatisticSent = true;
|
|
1171
1544
|
sendRateLimitStatistic();
|
|
1172
1545
|
}
|
|
@@ -1220,113 +1593,115 @@ public class CapgoUpdater {
|
|
|
1220
1593
|
|
|
1221
1594
|
Request request = new Request.Builder().url(url).post(body).build();
|
|
1222
1595
|
|
|
1223
|
-
DownloadService.sharedClient
|
|
1224
|
-
.
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
callback.callback(retError);
|
|
1234
|
-
}
|
|
1596
|
+
DownloadService.sharedClient.newCall(request).enqueue(
|
|
1597
|
+
new okhttp3.Callback() {
|
|
1598
|
+
@Override
|
|
1599
|
+
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
1600
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1601
|
+
retError.put("message", "Request failed: " + e.getMessage());
|
|
1602
|
+
retError.put("error", "network_error");
|
|
1603
|
+
retError.put("kind", "failed");
|
|
1604
|
+
callback.callback(retError);
|
|
1605
|
+
}
|
|
1235
1606
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
}
|
|
1607
|
+
@Override
|
|
1608
|
+
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
1609
|
+
try (ResponseBody responseBody = response.body()) {
|
|
1610
|
+
final int statusCode = response.code();
|
|
1611
|
+
final String responseData = responseBody != null ? responseBody.string() : "";
|
|
1612
|
+
JSONObject jsonResponse = null;
|
|
1613
|
+
if (!responseData.isEmpty()) {
|
|
1614
|
+
try {
|
|
1615
|
+
jsonResponse = new JSONObject(responseData);
|
|
1616
|
+
} catch (JSONException ignored) {
|
|
1617
|
+
// Non-JSON responses are handled as response or parse errors below.
|
|
1248
1618
|
}
|
|
1619
|
+
}
|
|
1249
1620
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1254
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1255
|
-
if (jsonResponse.has("error") && !jsonResponse.isNull("error")) {
|
|
1256
|
-
retError.put("error", jsonResponse.getString("error"));
|
|
1257
|
-
}
|
|
1258
|
-
if (jsonResponse.has("kind") && !jsonResponse.isNull("kind")) {
|
|
1259
|
-
retError.put("kind", jsonResponse.getString("kind"));
|
|
1260
|
-
}
|
|
1261
|
-
if (jsonResponse.has("message") && !jsonResponse.isNull("message")) {
|
|
1262
|
-
retError.put("message", jsonResponse.getString("message"));
|
|
1263
|
-
} else {
|
|
1264
|
-
retError.put("message", "server did not provide a message");
|
|
1265
|
-
}
|
|
1266
|
-
if (jsonResponse.has("version") && !jsonResponse.isNull("version")) {
|
|
1267
|
-
retError.put("version", jsonResponse.getString("version"));
|
|
1268
|
-
}
|
|
1269
|
-
retError.put("statusCode", statusCode);
|
|
1270
|
-
callback.callback(retError);
|
|
1271
|
-
return;
|
|
1621
|
+
if (jsonResponse != null && (jsonResponse.has("error") || jsonResponse.has("kind"))) {
|
|
1622
|
+
if (statusCode == 429) {
|
|
1623
|
+
checkAndHandleRateLimitResponse(response);
|
|
1272
1624
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1277
|
-
retError.put("message", "Rate limit exceeded");
|
|
1278
|
-
retError.put("error", "rate_limit_exceeded");
|
|
1279
|
-
retError.put("kind", "failed");
|
|
1280
|
-
retError.put("statusCode", statusCode);
|
|
1281
|
-
callback.callback(retError);
|
|
1282
|
-
return;
|
|
1625
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1626
|
+
if (jsonResponse.has("error") && !jsonResponse.isNull("error")) {
|
|
1627
|
+
retError.put("error", jsonResponse.getString("error"));
|
|
1283
1628
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1287
|
-
retError.put("message", "Server error: " + response.code());
|
|
1288
|
-
retError.put("error", "response_error");
|
|
1289
|
-
retError.put("kind", "failed");
|
|
1290
|
-
retError.put("statusCode", statusCode);
|
|
1291
|
-
callback.callback(retError);
|
|
1292
|
-
return;
|
|
1629
|
+
if (jsonResponse.has("kind") && !jsonResponse.isNull("kind")) {
|
|
1630
|
+
retError.put("kind", jsonResponse.getString("kind"));
|
|
1293
1631
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1632
|
+
if (jsonResponse.has("message") && !jsonResponse.isNull("message")) {
|
|
1633
|
+
retError.put("message", jsonResponse.getString("message"));
|
|
1634
|
+
} else {
|
|
1635
|
+
retError.put("message", "server did not provide a message");
|
|
1636
|
+
}
|
|
1637
|
+
if (jsonResponse.has("version") && !jsonResponse.isNull("version")) {
|
|
1638
|
+
retError.put("version", jsonResponse.getString("version"));
|
|
1297
1639
|
}
|
|
1640
|
+
retError.put("statusCode", statusCode);
|
|
1641
|
+
callback.callback(retError);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1298
1644
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1645
|
+
// Check for 429 rate limit
|
|
1646
|
+
if (checkAndHandleRateLimitResponse(response)) {
|
|
1647
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1648
|
+
retError.put("message", "Rate limit exceeded");
|
|
1649
|
+
retError.put("error", "rate_limit_exceeded");
|
|
1650
|
+
retError.put("kind", "failed");
|
|
1651
|
+
retError.put("statusCode", statusCode);
|
|
1652
|
+
callback.callback(retError);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1301
1655
|
|
|
1302
|
-
|
|
1303
|
-
while (keys.hasNext()) {
|
|
1304
|
-
String key = keys.next();
|
|
1305
|
-
if (jsonResponse.has(key)) {
|
|
1306
|
-
if ("session_key".equals(key)) {
|
|
1307
|
-
ret.put("sessionKey", jsonResponse.get(key));
|
|
1308
|
-
} else {
|
|
1309
|
-
ret.put(key, jsonResponse.get(key));
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
callback.callback(ret);
|
|
1314
|
-
} catch (JSONException e) {
|
|
1656
|
+
if (!response.isSuccessful()) {
|
|
1315
1657
|
Map<String, Object> retError = new HashMap<>();
|
|
1316
|
-
retError.put("message", "
|
|
1317
|
-
retError.put("error", "
|
|
1658
|
+
retError.put("message", "Server error: " + response.code());
|
|
1659
|
+
retError.put("error", "response_error");
|
|
1318
1660
|
retError.put("kind", "failed");
|
|
1661
|
+
retError.put("statusCode", statusCode);
|
|
1319
1662
|
callback.callback(retError);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (jsonResponse == null) {
|
|
1667
|
+
throw new JSONException("Response is not a JSON object");
|
|
1320
1668
|
}
|
|
1669
|
+
|
|
1670
|
+
Map<String, Object> ret = new HashMap<>();
|
|
1671
|
+
ret.put("statusCode", statusCode);
|
|
1672
|
+
|
|
1673
|
+
Iterator<String> keys = jsonResponse.keys();
|
|
1674
|
+
while (keys.hasNext()) {
|
|
1675
|
+
String key = keys.next();
|
|
1676
|
+
if (jsonResponse.has(key)) {
|
|
1677
|
+
if ("session_key".equals(key)) {
|
|
1678
|
+
ret.put("sessionKey", jsonResponse.get(key));
|
|
1679
|
+
} else {
|
|
1680
|
+
ret.put(key, jsonResponse.get(key));
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
callback.callback(ret);
|
|
1685
|
+
} catch (JSONException e) {
|
|
1686
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1687
|
+
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
1688
|
+
retError.put("error", "parse_error");
|
|
1689
|
+
retError.put("kind", "failed");
|
|
1690
|
+
callback.callback(retError);
|
|
1321
1691
|
}
|
|
1322
1692
|
}
|
|
1323
|
-
|
|
1693
|
+
}
|
|
1694
|
+
);
|
|
1324
1695
|
}
|
|
1325
1696
|
|
|
1326
1697
|
public void getLatest(final String updateUrl, final String channel, final Callback callback) {
|
|
1698
|
+
this.getLatest(updateUrl, channel, null, callback);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
public void getLatest(final String updateUrl, final String channel, final String appIdOverride, final Callback callback) {
|
|
1327
1702
|
JSONObject json;
|
|
1328
1703
|
try {
|
|
1329
|
-
json = this.createInfoObject();
|
|
1704
|
+
json = this.createInfoObject(appIdOverride);
|
|
1330
1705
|
if (channel != null && json != null) {
|
|
1331
1706
|
json.put("defaultChannel", channel);
|
|
1332
1707
|
}
|
|
@@ -1340,7 +1715,9 @@ public class CapgoUpdater {
|
|
|
1340
1715
|
return;
|
|
1341
1716
|
}
|
|
1342
1717
|
|
|
1343
|
-
logger
|
|
1718
|
+
if (logger != null) {
|
|
1719
|
+
logger.info("Auto-update parameters: " + json);
|
|
1720
|
+
}
|
|
1344
1721
|
|
|
1345
1722
|
makeJsonRequest(updateUrl, json, callback);
|
|
1346
1723
|
}
|
|
@@ -1369,6 +1746,17 @@ public class CapgoUpdater {
|
|
|
1369
1746
|
final String defaultChannelKey,
|
|
1370
1747
|
final boolean allowSetDefaultChannel,
|
|
1371
1748
|
final Callback callback
|
|
1749
|
+
) {
|
|
1750
|
+
this.setChannel(channel, editor, defaultChannelKey, allowSetDefaultChannel, "", callback);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
public void setChannel(
|
|
1754
|
+
final String channel,
|
|
1755
|
+
final SharedPreferences.Editor editor,
|
|
1756
|
+
final String defaultChannelKey,
|
|
1757
|
+
final boolean allowSetDefaultChannel,
|
|
1758
|
+
final String configDefaultChannel,
|
|
1759
|
+
final Callback callback
|
|
1372
1760
|
) {
|
|
1373
1761
|
// Check if setting defaultChannel is allowed
|
|
1374
1762
|
if (!allowSetDefaultChannel) {
|
|
@@ -1421,6 +1809,7 @@ public class CapgoUpdater {
|
|
|
1421
1809
|
// Clear persisted defaultChannel and revert to config value
|
|
1422
1810
|
editor.remove(defaultChannelKey);
|
|
1423
1811
|
editor.apply();
|
|
1812
|
+
this.defaultChannel = configDefaultChannel;
|
|
1424
1813
|
logger.info("Public channel requested, channel override removed");
|
|
1425
1814
|
callback.callback(res);
|
|
1426
1815
|
} else {
|
|
@@ -1435,6 +1824,10 @@ public class CapgoUpdater {
|
|
|
1435
1824
|
}
|
|
1436
1825
|
|
|
1437
1826
|
public void getChannel(final Callback callback) {
|
|
1827
|
+
this.getChannel(callback, null, null);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
public void getChannel(final Callback callback, final SharedPreferences.Editor editor, final String defaultChannelKey) {
|
|
1438
1831
|
// Check if rate limit was exceeded
|
|
1439
1832
|
if (rateLimitExceeded) {
|
|
1440
1833
|
logger.debug("Skipping getChannel due to rate limit (429). Requests will resume after app restart.");
|
|
@@ -1472,88 +1865,117 @@ public class CapgoUpdater {
|
|
|
1472
1865
|
.put(RequestBody.create(json.toString(), MediaType.get("application/json")))
|
|
1473
1866
|
.build();
|
|
1474
1867
|
|
|
1475
|
-
DownloadService.sharedClient
|
|
1476
|
-
.
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1868
|
+
DownloadService.sharedClient.newCall(request).enqueue(
|
|
1869
|
+
new okhttp3.Callback() {
|
|
1870
|
+
@Override
|
|
1871
|
+
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
1872
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1873
|
+
retError.put("message", "Request failed: " + e.getMessage());
|
|
1874
|
+
retError.put("error", "network_error");
|
|
1875
|
+
callback.callback(retError);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
@Override
|
|
1879
|
+
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
1880
|
+
try (ResponseBody responseBody = response.body()) {
|
|
1881
|
+
// Check for 429 rate limit
|
|
1882
|
+
if (checkAndHandleRateLimitResponse(response)) {
|
|
1883
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1884
|
+
retError.put("message", "Rate limit exceeded");
|
|
1885
|
+
retError.put("error", "rate_limit_exceeded");
|
|
1886
|
+
callback.callback(retError);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1486
1889
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
try (ResponseBody responseBody = response.body()) {
|
|
1490
|
-
// Check for 429 rate limit
|
|
1491
|
-
if (checkAndHandleRateLimitResponse(response)) {
|
|
1890
|
+
if (response.code() == 400) {
|
|
1891
|
+
if (responseBody == null) {
|
|
1492
1892
|
Map<String, Object> retError = new HashMap<>();
|
|
1493
|
-
retError.put("message", "
|
|
1494
|
-
retError.put("error", "
|
|
1893
|
+
retError.put("message", "Empty response body");
|
|
1894
|
+
retError.put("error", "no_response_body");
|
|
1495
1895
|
callback.callback(retError);
|
|
1496
1896
|
return;
|
|
1497
1897
|
}
|
|
1498
|
-
|
|
1499
|
-
if (
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
ret.put("status", "default");
|
|
1506
|
-
logger.info("Channel get to \"" + ret);
|
|
1507
|
-
callback.callback(ret);
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
if (!response.isSuccessful()) {
|
|
1513
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1514
|
-
retError.put("message", "Server error: " + response.code());
|
|
1515
|
-
retError.put("error", "response_error");
|
|
1516
|
-
callback.callback(retError);
|
|
1898
|
+
String data = responseBody.string();
|
|
1899
|
+
if (data.contains("channel_not_found") && !defaultChannel.isEmpty()) {
|
|
1900
|
+
Map<String, Object> ret = new HashMap<>();
|
|
1901
|
+
ret.put("channel", defaultChannel);
|
|
1902
|
+
ret.put("status", "default");
|
|
1903
|
+
logger.info("Channel get to \"" + ret);
|
|
1904
|
+
callback.callback(ret);
|
|
1517
1905
|
return;
|
|
1518
1906
|
}
|
|
1907
|
+
}
|
|
1519
1908
|
|
|
1520
|
-
|
|
1521
|
-
String
|
|
1522
|
-
|
|
1909
|
+
if (!response.isSuccessful()) {
|
|
1910
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1911
|
+
retError.put("message", "Server error: " + response.code());
|
|
1912
|
+
retError.put("error", "response_error");
|
|
1913
|
+
callback.callback(retError);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1523
1916
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1917
|
+
if (responseBody == null) {
|
|
1918
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1919
|
+
retError.put("message", "Empty response body");
|
|
1920
|
+
retError.put("error", "no_response_body");
|
|
1921
|
+
callback.callback(retError);
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
String responseData = responseBody.string();
|
|
1925
|
+
JSONObject jsonResponse = new JSONObject(responseData);
|
|
1926
|
+
|
|
1927
|
+
// Check for server-side errors first
|
|
1928
|
+
if (jsonResponse.has("error")) {
|
|
1929
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1930
|
+
retError.put("error", jsonResponse.getString("error"));
|
|
1931
|
+
if (jsonResponse.has("message")) {
|
|
1932
|
+
retError.put("message", jsonResponse.getString("message"));
|
|
1933
|
+
} else {
|
|
1934
|
+
retError.put("message", "server did not provide a message");
|
|
1535
1935
|
}
|
|
1936
|
+
callback.callback(retError);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1536
1939
|
|
|
1537
|
-
|
|
1940
|
+
Map<String, Object> ret = new HashMap<>();
|
|
1538
1941
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
}
|
|
1942
|
+
Iterator<String> keys = jsonResponse.keys();
|
|
1943
|
+
while (keys.hasNext()) {
|
|
1944
|
+
String key = keys.next();
|
|
1945
|
+
if (jsonResponse.has(key)) {
|
|
1946
|
+
ret.put(key, jsonResponse.get(key));
|
|
1545
1947
|
}
|
|
1546
|
-
logger.info("Channel get to \"" + ret);
|
|
1547
|
-
callback.callback(ret);
|
|
1548
|
-
} catch (JSONException e) {
|
|
1549
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1550
|
-
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
1551
|
-
retError.put("error", "parse_error");
|
|
1552
|
-
callback.callback(retError);
|
|
1553
1948
|
}
|
|
1949
|
+
persistDefaultChannelFromResponse(ret.get("channel"), editor, defaultChannelKey);
|
|
1950
|
+
logger.info("Channel get to \"" + ret);
|
|
1951
|
+
callback.callback(ret);
|
|
1952
|
+
} catch (JSONException e) {
|
|
1953
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1954
|
+
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
1955
|
+
retError.put("error", "parse_error");
|
|
1956
|
+
callback.callback(retError);
|
|
1554
1957
|
}
|
|
1555
1958
|
}
|
|
1556
|
-
|
|
1959
|
+
}
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
void persistDefaultChannelFromResponse(final Object channel, final SharedPreferences.Editor editor, final String defaultChannelKey) {
|
|
1964
|
+
if (!(channel instanceof String)) {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
final String channelName = ((String) channel).trim();
|
|
1969
|
+
if (channelName.isEmpty() || BundleInfo.ID_BUILTIN.equals(channelName)) {
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
this.defaultChannel = channelName;
|
|
1974
|
+
if (editor != null && defaultChannelKey != null && !defaultChannelKey.isEmpty()) {
|
|
1975
|
+
editor.putString(defaultChannelKey, channelName);
|
|
1976
|
+
editor.apply();
|
|
1977
|
+
}
|
|
1978
|
+
logger.info("defaultChannel synchronized from getChannel(): " + channelName);
|
|
1557
1979
|
}
|
|
1558
1980
|
|
|
1559
1981
|
public void listChannels(final Callback callback) {
|
|
@@ -1608,94 +2030,106 @@ public class CapgoUpdater {
|
|
|
1608
2030
|
|
|
1609
2031
|
Request request = new Request.Builder().url(urlBuilder.build()).get().build();
|
|
1610
2032
|
|
|
1611
|
-
DownloadService.sharedClient
|
|
1612
|
-
.
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
callback.callback(retError);
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
@Override
|
|
1624
|
-
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
1625
|
-
try (ResponseBody responseBody = response.body()) {
|
|
1626
|
-
// Check for 429 rate limit
|
|
1627
|
-
if (checkAndHandleRateLimitResponse(response)) {
|
|
1628
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1629
|
-
retError.put("message", "Rate limit exceeded");
|
|
1630
|
-
retError.put("error", "rate_limit_exceeded");
|
|
1631
|
-
callback.callback(retError);
|
|
1632
|
-
return;
|
|
1633
|
-
}
|
|
2033
|
+
DownloadService.sharedClient.newCall(request).enqueue(
|
|
2034
|
+
new okhttp3.Callback() {
|
|
2035
|
+
@Override
|
|
2036
|
+
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
2037
|
+
Map<String, Object> retError = new HashMap<>();
|
|
2038
|
+
retError.put("message", "Request failed: " + e.getMessage());
|
|
2039
|
+
retError.put("error", "network_error");
|
|
2040
|
+
callback.callback(retError);
|
|
2041
|
+
}
|
|
1634
2042
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2043
|
+
@Override
|
|
2044
|
+
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
2045
|
+
try (ResponseBody responseBody = response.body()) {
|
|
2046
|
+
// Check for 429 rate limit
|
|
2047
|
+
if (checkAndHandleRateLimitResponse(response)) {
|
|
2048
|
+
Map<String, Object> retError = new HashMap<>();
|
|
2049
|
+
retError.put("message", "Rate limit exceeded");
|
|
2050
|
+
retError.put("error", "rate_limit_exceeded");
|
|
2051
|
+
callback.callback(retError);
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
1642
2054
|
|
|
1643
|
-
|
|
1644
|
-
String
|
|
2055
|
+
if (!response.isSuccessful()) {
|
|
2056
|
+
Map<String, Object> retError = new HashMap<>();
|
|
2057
|
+
retError.put("message", "Server error: " + response.code());
|
|
2058
|
+
retError.put("error", "response_error");
|
|
2059
|
+
callback.callback(retError);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
1645
2062
|
|
|
1646
|
-
|
|
1647
|
-
|
|
2063
|
+
if (responseBody == null) {
|
|
2064
|
+
Map<String, Object> retError = new HashMap<>();
|
|
2065
|
+
retError.put("message", "Empty response body");
|
|
2066
|
+
retError.put("error", "no_response_body");
|
|
2067
|
+
callback.callback(retError);
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
String data = responseBody.string();
|
|
1648
2071
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
JSONArray channelsJson = new JSONArray(data);
|
|
1652
|
-
List<Map<String, Object>> channelsList = new ArrayList<>();
|
|
1653
|
-
|
|
1654
|
-
for (int i = 0; i < channelsJson.length(); i++) {
|
|
1655
|
-
JSONObject channelJson = channelsJson.getJSONObject(i);
|
|
1656
|
-
Map<String, Object> channel = new HashMap<>();
|
|
1657
|
-
channel.put("id", channelJson.optString("id", ""));
|
|
1658
|
-
channel.put("name", channelJson.optString("name", ""));
|
|
1659
|
-
channel.put("public", channelJson.optBoolean("public", false));
|
|
1660
|
-
channel.put("allow_self_set", channelJson.optBoolean("allow_self_set", false));
|
|
1661
|
-
channelsList.add(channel);
|
|
1662
|
-
}
|
|
2072
|
+
try {
|
|
2073
|
+
Map<String, Object> ret = parseListChannelsResponse(data);
|
|
1663
2074
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
retError.put("message", json.getString("message"));
|
|
1678
|
-
} else {
|
|
1679
|
-
retError.put("message", "server did not provide a message");
|
|
1680
|
-
}
|
|
1681
|
-
callback.callback(retError);
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
} catch (JSONException objException) {
|
|
1685
|
-
// If neither array nor object, throw parse error
|
|
1686
|
-
throw arrayException;
|
|
2075
|
+
logger.info("Channels listed successfully");
|
|
2076
|
+
callback.callback(ret);
|
|
2077
|
+
} catch (JSONException arrayException) {
|
|
2078
|
+
// If not an array, try to parse as error object
|
|
2079
|
+
try {
|
|
2080
|
+
JSONObject json = new JSONObject(data);
|
|
2081
|
+
if (json.has("error")) {
|
|
2082
|
+
Map<String, Object> retError = new HashMap<>();
|
|
2083
|
+
retError.put("error", json.getString("error"));
|
|
2084
|
+
if (json.has("message")) {
|
|
2085
|
+
retError.put("message", json.getString("message"));
|
|
2086
|
+
} else {
|
|
2087
|
+
retError.put("message", "server did not provide a message");
|
|
1687
2088
|
}
|
|
2089
|
+
callback.callback(retError);
|
|
2090
|
+
return;
|
|
1688
2091
|
}
|
|
1689
|
-
} catch (JSONException e) {
|
|
1690
2092
|
Map<String, Object> retError = new HashMap<>();
|
|
1691
|
-
retError.put("message", "
|
|
2093
|
+
retError.put("message", "Unexpected channels response format");
|
|
2094
|
+
retError.put("error", "parse_error");
|
|
2095
|
+
callback.callback(retError);
|
|
2096
|
+
return;
|
|
2097
|
+
} catch (JSONException objException) {
|
|
2098
|
+
// If neither array nor object, throw parse error
|
|
2099
|
+
arrayException.addSuppressed(objException);
|
|
2100
|
+
Map<String, Object> retError = new HashMap<>();
|
|
2101
|
+
retError.put("message", "JSON parse error: " + arrayException.getMessage());
|
|
1692
2102
|
retError.put("error", "parse_error");
|
|
1693
2103
|
callback.callback(retError);
|
|
1694
2104
|
}
|
|
1695
2105
|
}
|
|
1696
2106
|
}
|
|
1697
2107
|
}
|
|
1698
|
-
|
|
2108
|
+
}
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
static Map<String, Object> parseListChannelsResponse(final String data) throws JSONException {
|
|
2113
|
+
JSONArray channelsJson = new JSONArray(data);
|
|
2114
|
+
List<Map<String, Object>> channelsList = new ArrayList<>();
|
|
2115
|
+
|
|
2116
|
+
for (int i = 0; i < channelsJson.length(); i++) {
|
|
2117
|
+
JSONObject channelJson = channelsJson.getJSONObject(i);
|
|
2118
|
+
Object channelId = channelJson.get("id");
|
|
2119
|
+
if (!(channelId instanceof Number)) {
|
|
2120
|
+
throw new JSONException("Channel id must be a number");
|
|
2121
|
+
}
|
|
2122
|
+
Map<String, Object> channel = new HashMap<>();
|
|
2123
|
+
channel.put("id", channelId);
|
|
2124
|
+
channel.put("name", channelJson.optString("name", ""));
|
|
2125
|
+
channel.put("public", channelJson.optBoolean("public", false));
|
|
2126
|
+
channel.put("allow_self_set", channelJson.optBoolean("allow_self_set", false));
|
|
2127
|
+
channelsList.add(channel);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
Map<String, Object> ret = new HashMap<>();
|
|
2131
|
+
ret.put("channels", channelsList);
|
|
2132
|
+
return ret;
|
|
1699
2133
|
}
|
|
1700
2134
|
|
|
1701
2135
|
public void sendStats(final String action) {
|
|
@@ -1707,6 +2141,27 @@ public class CapgoUpdater {
|
|
|
1707
2141
|
}
|
|
1708
2142
|
|
|
1709
2143
|
public void sendStats(final String action, final String versionName, final String oldVersionName) {
|
|
2144
|
+
this.sendStats(action, versionName, oldVersionName, null);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
public void sendStats(final String action, final String versionName, final String oldVersionName, final Map<String, String> metadata) {
|
|
2148
|
+
this.sendStats(action, versionName, oldVersionName, metadata, null);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
public void sendStats(
|
|
2152
|
+
final String action,
|
|
2153
|
+
final String versionName,
|
|
2154
|
+
final String oldVersionName,
|
|
2155
|
+
final Map<String, String> metadata,
|
|
2156
|
+
final Runnable onSent
|
|
2157
|
+
) {
|
|
2158
|
+
if (this.previewSession) {
|
|
2159
|
+
if (logger != null) {
|
|
2160
|
+
logger.debug("Skipping sendStats during preview session.");
|
|
2161
|
+
}
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
1710
2165
|
// Check if rate limit was exceeded
|
|
1711
2166
|
if (rateLimitExceeded) {
|
|
1712
2167
|
logger.debug("Skipping sendStats due to rate limit (429). Stats will resume after app restart.");
|
|
@@ -1725,13 +2180,16 @@ public class CapgoUpdater {
|
|
|
1725
2180
|
json.put("old_version_name", oldVersionName);
|
|
1726
2181
|
json.put("action", action);
|
|
1727
2182
|
json.put("timestamp", System.currentTimeMillis());
|
|
2183
|
+
if (metadata != null && !metadata.isEmpty()) {
|
|
2184
|
+
json.put("metadata", new JSONObject(metadata));
|
|
2185
|
+
}
|
|
1728
2186
|
} catch (JSONException e) {
|
|
1729
2187
|
logger.error("Error preparing stats");
|
|
1730
2188
|
logger.debug("JSONException: " + e.getMessage());
|
|
1731
2189
|
return;
|
|
1732
2190
|
}
|
|
1733
2191
|
|
|
1734
|
-
statsQueue.add(json);
|
|
2192
|
+
statsQueue.add(new QueuedStatsEvent(json, onSent));
|
|
1735
2193
|
ensureStatsTimerStarted();
|
|
1736
2194
|
}
|
|
1737
2195
|
|
|
@@ -1758,7 +2216,7 @@ public class CapgoUpdater {
|
|
|
1758
2216
|
}
|
|
1759
2217
|
|
|
1760
2218
|
// Copy and clear the queue atomically using synchronized block
|
|
1761
|
-
List<
|
|
2219
|
+
List<QueuedStatsEvent> eventsToSend;
|
|
1762
2220
|
synchronized (statsQueue) {
|
|
1763
2221
|
if (statsQueue.isEmpty()) {
|
|
1764
2222
|
return;
|
|
@@ -1768,8 +2226,8 @@ public class CapgoUpdater {
|
|
|
1768
2226
|
}
|
|
1769
2227
|
|
|
1770
2228
|
JSONArray jsonArray = new JSONArray();
|
|
1771
|
-
for (
|
|
1772
|
-
jsonArray.put(event);
|
|
2229
|
+
for (QueuedStatsEvent queuedEvent : eventsToSend) {
|
|
2230
|
+
jsonArray.put(queuedEvent.event);
|
|
1773
2231
|
}
|
|
1774
2232
|
|
|
1775
2233
|
Request request = new Request.Builder()
|
|
@@ -1778,35 +2236,51 @@ public class CapgoUpdater {
|
|
|
1778
2236
|
.build();
|
|
1779
2237
|
|
|
1780
2238
|
final int eventCount = eventsToSend.size();
|
|
1781
|
-
DownloadService.sharedClient
|
|
1782
|
-
.
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
logger.debug("Error: " + e.getMessage());
|
|
1789
|
-
}
|
|
2239
|
+
DownloadService.sharedClient.newCall(request).enqueue(
|
|
2240
|
+
new okhttp3.Callback() {
|
|
2241
|
+
@Override
|
|
2242
|
+
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
2243
|
+
logger.error("Failed to send stats batch");
|
|
2244
|
+
logger.debug("Error: " + e.getMessage());
|
|
2245
|
+
}
|
|
1790
2246
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2247
|
+
@Override
|
|
2248
|
+
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
2249
|
+
try (ResponseBody responseBody = response.body()) {
|
|
2250
|
+
// Check for 429 rate limit
|
|
2251
|
+
if (checkAndHandleRateLimitResponse(response)) {
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
1798
2254
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2255
|
+
if (response.isSuccessful()) {
|
|
2256
|
+
logger.info("Stats batch sent successfully");
|
|
2257
|
+
logger.debug("Sent " + eventCount + " events");
|
|
2258
|
+
runStatsCallbacks(eventsToSend);
|
|
2259
|
+
} else {
|
|
2260
|
+
logger.error("Error sending stats batch");
|
|
2261
|
+
logger.debug("Response code: " + response.code());
|
|
1806
2262
|
}
|
|
1807
2263
|
}
|
|
1808
2264
|
}
|
|
1809
|
-
|
|
2265
|
+
}
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
private void runStatsCallbacks(final List<QueuedStatsEvent> sentEvents) {
|
|
2270
|
+
for (final QueuedStatsEvent sentEvent : sentEvents) {
|
|
2271
|
+
if (sentEvent.onSent == null) {
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
try {
|
|
2276
|
+
sentEvent.onSent.run();
|
|
2277
|
+
} catch (Exception e) {
|
|
2278
|
+
if (logger != null) {
|
|
2279
|
+
logger.error("Error running stats sent callback");
|
|
2280
|
+
logger.debug("Error: " + e.getMessage());
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
1810
2284
|
}
|
|
1811
2285
|
|
|
1812
2286
|
public BundleInfo getBundleInfo(final String id) {
|
|
@@ -1926,6 +2400,31 @@ public class CapgoUpdater {
|
|
|
1926
2400
|
return this.getBundleInfo(id);
|
|
1927
2401
|
}
|
|
1928
2402
|
|
|
2403
|
+
public BundleInfo getPreviewFallbackBundle() {
|
|
2404
|
+
final String id = this.prefs.getString(PREVIEW_FALLBACK_VERSION, null);
|
|
2405
|
+
if (id == null) return null;
|
|
2406
|
+
final BundleInfo bundle = this.getBundleInfo(id);
|
|
2407
|
+
if (bundle.isErrorStatus() || (!bundle.isBuiltin() && !this.bundleExists(id))) {
|
|
2408
|
+
this.setPreviewFallbackBundle(null);
|
|
2409
|
+
return null;
|
|
2410
|
+
}
|
|
2411
|
+
return bundle;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
public boolean setPreviewFallbackBundle(final String fallback) {
|
|
2415
|
+
if (fallback == null) {
|
|
2416
|
+
this.editor.remove(PREVIEW_FALLBACK_VERSION);
|
|
2417
|
+
} else {
|
|
2418
|
+
final BundleInfo newBundle = this.getBundleInfo(fallback);
|
|
2419
|
+
if (newBundle.isErrorStatus() || (!newBundle.isBuiltin() && !this.bundleExists(fallback))) {
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2422
|
+
this.editor.putString(PREVIEW_FALLBACK_VERSION, fallback);
|
|
2423
|
+
}
|
|
2424
|
+
this.editor.commit();
|
|
2425
|
+
return true;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
1929
2428
|
public boolean setNextBundle(final String next) {
|
|
1930
2429
|
BundleInfo bundleToNotify = null;
|
|
1931
2430
|
if (next == null) {
|