@capgo/capacitor-updater 7.43.3 → 7.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +5 -2
- package/README.md +643 -115
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3121 -353
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +924 -290
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +49 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +75 -32
- 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 +401 -27
- package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
- package/dist/docs.json +1590 -196
- package/dist/esm/definitions.d.ts +755 -66
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +11 -2
- package/dist/esm/web.js +59 -5
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +59 -5
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +59 -5
- 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 +2732 -417
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1191 -363
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +37 -16
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +80 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +438 -39
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +24 -9
|
@@ -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,7 +439,138 @@ public class CapgoUpdater {
|
|
|
345
439
|
}
|
|
346
440
|
}
|
|
347
441
|
|
|
348
|
-
private
|
|
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
|
+
|
|
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");
|
|
351
576
|
return;
|
|
@@ -375,7 +600,7 @@ public class CapgoUpdater {
|
|
|
375
600
|
boolean isManifest = outputData.getBoolean(DownloadService.IS_MANIFEST, false);
|
|
376
601
|
|
|
377
602
|
io.execute(() -> {
|
|
378
|
-
boolean success = finishDownload(id, dest, version, sessionKey, checksum,
|
|
603
|
+
boolean success = finishDownload(id, dest, version, sessionKey, checksum, setNext, isManifest);
|
|
379
604
|
BundleInfo resultBundle;
|
|
380
605
|
if (!success) {
|
|
381
606
|
logger.error("Finish download failed");
|
|
@@ -454,13 +679,14 @@ public class CapgoUpdater {
|
|
|
454
679
|
final String version,
|
|
455
680
|
final String sessionKey,
|
|
456
681
|
final String checksum,
|
|
457
|
-
final JSONArray manifest
|
|
682
|
+
final JSONArray manifest,
|
|
683
|
+
final boolean setNext
|
|
458
684
|
) {
|
|
459
685
|
if (this.activity == null) {
|
|
460
686
|
logger.error("Activity is null, cannot observe work progress");
|
|
461
687
|
return;
|
|
462
688
|
}
|
|
463
|
-
observeWorkProgress(this.activity, id);
|
|
689
|
+
observeWorkProgress(this.activity, id, setNext);
|
|
464
690
|
|
|
465
691
|
DownloadWorkerManager.enqueueDownload(
|
|
466
692
|
this.activity,
|
|
@@ -477,6 +703,7 @@ public class CapgoUpdater {
|
|
|
477
703
|
this.appId,
|
|
478
704
|
this.pluginVersion,
|
|
479
705
|
this.isProd(),
|
|
706
|
+
this.getInstallSource(),
|
|
480
707
|
this.statsUrl,
|
|
481
708
|
this.deviceID,
|
|
482
709
|
this.versionBuild,
|
|
@@ -578,11 +805,15 @@ public class CapgoUpdater {
|
|
|
578
805
|
CapgoUpdater.this.notifyListeners("updateAvailable", ret);
|
|
579
806
|
logger.info("setNext: " + setNext);
|
|
580
807
|
if (setNext) {
|
|
581
|
-
|
|
582
|
-
|
|
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);
|
|
583
813
|
CapgoUpdater.this.directUpdateFinish(next);
|
|
584
814
|
this.directUpdate = false;
|
|
585
815
|
} else {
|
|
816
|
+
logger.info("directUpdate: " + this.directUpdate);
|
|
586
817
|
this.setNextBundle(next.getId());
|
|
587
818
|
}
|
|
588
819
|
}
|
|
@@ -752,17 +983,110 @@ public class CapgoUpdater {
|
|
|
752
983
|
}
|
|
753
984
|
|
|
754
985
|
private void setCurrentBundle(final File bundle) {
|
|
986
|
+
this.cancelBackgroundRunnerWorkBeforeBundleSwitch();
|
|
755
987
|
this.editor.putString(this.CAP_SERVER_PATH, bundle.getPath());
|
|
756
988
|
logger.info("Current bundle set to: " + bundle);
|
|
757
989
|
this.editor.commit();
|
|
758
990
|
}
|
|
759
991
|
|
|
992
|
+
static boolean shouldResetForForeignBundle(final String bundlePath, final boolean isBuiltin, final boolean hasStoredBundleInfo) {
|
|
993
|
+
return bundlePath != null && !bundlePath.trim().isEmpty() && !isBuiltin && !hasStoredBundleInfo;
|
|
994
|
+
}
|
|
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
|
+
|
|
1063
|
+
private boolean hasStoredBundleInfo(final String id) {
|
|
1064
|
+
return (
|
|
1065
|
+
id != null &&
|
|
1066
|
+
!id.isEmpty() &&
|
|
1067
|
+
!BundleInfo.ID_BUILTIN.equals(id) &&
|
|
1068
|
+
!BundleInfo.VERSION_UNKNOWN.equals(id) &&
|
|
1069
|
+
this.prefs.contains(id + INFO_SUFFIX)
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
760
1073
|
public void downloadBackground(
|
|
761
1074
|
final String url,
|
|
762
1075
|
final String version,
|
|
763
1076
|
final String sessionKey,
|
|
764
1077
|
final String checksum,
|
|
765
1078
|
final JSONArray manifest
|
|
1079
|
+
) {
|
|
1080
|
+
downloadBackground(url, version, sessionKey, checksum, manifest, true);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
public void downloadBackground(
|
|
1084
|
+
final String url,
|
|
1085
|
+
final String version,
|
|
1086
|
+
final String sessionKey,
|
|
1087
|
+
final String checksum,
|
|
1088
|
+
final JSONArray manifest,
|
|
1089
|
+
final boolean setNext
|
|
766
1090
|
) {
|
|
767
1091
|
final String id = this.randomString();
|
|
768
1092
|
|
|
@@ -784,7 +1108,7 @@ public class CapgoUpdater {
|
|
|
784
1108
|
this.notifyDownload(id, 0);
|
|
785
1109
|
this.notifyDownload(id, 5);
|
|
786
1110
|
|
|
787
|
-
this.download(id, url, this.randomString(), version, sessionKey, checksum, manifest);
|
|
1111
|
+
this.download(id, url, this.randomString(), version, sessionKey, checksum, manifest, setNext);
|
|
788
1112
|
}
|
|
789
1113
|
|
|
790
1114
|
public BundleInfo download(final String url, final String version, final String sessionKey, final String checksum) throws IOException {
|
|
@@ -806,7 +1130,7 @@ public class CapgoUpdater {
|
|
|
806
1130
|
downloadFutures.put(id, downloadFuture);
|
|
807
1131
|
|
|
808
1132
|
// Start the download
|
|
809
|
-
this.download(id, url, dest, version, sessionKey, checksum, null);
|
|
1133
|
+
this.download(id, url, dest, version, sessionKey, checksum, null, false);
|
|
810
1134
|
|
|
811
1135
|
// Wait for completion without timeout
|
|
812
1136
|
try {
|
|
@@ -858,7 +1182,7 @@ public class CapgoUpdater {
|
|
|
858
1182
|
downloadFutures.put(id, downloadFuture);
|
|
859
1183
|
|
|
860
1184
|
// Start the download
|
|
861
|
-
this.download(id, url, dest, version, sessionKey, checksum, manifest);
|
|
1185
|
+
this.download(id, url, dest, version, sessionKey, checksum, manifest, false);
|
|
862
1186
|
|
|
863
1187
|
// Wait for completion without timeout
|
|
864
1188
|
try {
|
|
@@ -915,6 +1239,17 @@ public class CapgoUpdater {
|
|
|
915
1239
|
logger.debug("Bundle ID: " + id);
|
|
916
1240
|
return false;
|
|
917
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
|
+
}
|
|
918
1253
|
final BundleInfo next = this.getNextBundle();
|
|
919
1254
|
if (next != null && !next.isDeleted() && !next.isErrorStatus() && next.getId().equals(id)) {
|
|
920
1255
|
logger.error("Cannot delete the next bundle");
|
|
@@ -965,6 +1300,62 @@ public class CapgoUpdater {
|
|
|
965
1300
|
return (bundle.isDirectory() && bundle.exists() && new File(bundle.getPath(), "/index.html").exists() && !bundleInfo.isDeleted());
|
|
966
1301
|
}
|
|
967
1302
|
|
|
1303
|
+
static final class ResetState {
|
|
1304
|
+
|
|
1305
|
+
final String currentBundlePath;
|
|
1306
|
+
final String fallbackBundleId;
|
|
1307
|
+
final String nextBundleId;
|
|
1308
|
+
|
|
1309
|
+
ResetState(final String currentBundlePath, final String fallbackBundleId, final String nextBundleId) {
|
|
1310
|
+
this.currentBundlePath = currentBundlePath;
|
|
1311
|
+
this.fallbackBundleId = fallbackBundleId;
|
|
1312
|
+
this.nextBundleId = nextBundleId;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
ResetState captureResetState() {
|
|
1317
|
+
return new ResetState(
|
|
1318
|
+
this.getCurrentBundlePath(),
|
|
1319
|
+
this.prefs.getString(FALLBACK_VERSION, BundleInfo.ID_BUILTIN),
|
|
1320
|
+
this.prefs.getString(NEXT_VERSION, null)
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
void restoreResetState(final ResetState state) {
|
|
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;
|
|
1329
|
+
|
|
1330
|
+
this.editor.putString(this.CAP_SERVER_PATH, currentBundlePath);
|
|
1331
|
+
this.editor.putString(FALLBACK_VERSION, fallbackBundleId);
|
|
1332
|
+
if (state.nextBundleId == null || state.nextBundleId.isEmpty()) {
|
|
1333
|
+
this.editor.remove(NEXT_VERSION);
|
|
1334
|
+
} else {
|
|
1335
|
+
this.editor.putString(NEXT_VERSION, state.nextBundleId);
|
|
1336
|
+
}
|
|
1337
|
+
this.editor.commit();
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
void prepareResetStateForTransition() {
|
|
1341
|
+
this.setCurrentBundle(new File("public"));
|
|
1342
|
+
this.setFallbackBundle(null);
|
|
1343
|
+
this.setNextBundle(null);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
void finalizeResetTransition(final String previousBundleName, final boolean internal) {
|
|
1347
|
+
if (this.activity != null) {
|
|
1348
|
+
DownloadWorkerManager.cancelAllDownloads(this.activity);
|
|
1349
|
+
}
|
|
1350
|
+
if (!internal) {
|
|
1351
|
+
this.sendStats("reset", this.getCurrentBundle().getVersionName(), previousBundleName);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
boolean canSet(final BundleInfo bundle) {
|
|
1356
|
+
return bundle != null && (bundle.isBuiltin() || this.bundleExists(bundle.getId()));
|
|
1357
|
+
}
|
|
1358
|
+
|
|
968
1359
|
public Boolean set(final BundleInfo bundle) {
|
|
969
1360
|
return this.set(bundle.getId());
|
|
970
1361
|
}
|
|
@@ -989,14 +1380,88 @@ public class CapgoUpdater {
|
|
|
989
1380
|
return false;
|
|
990
1381
|
}
|
|
991
1382
|
|
|
1383
|
+
boolean stagePendingReload(final BundleInfo bundle) {
|
|
1384
|
+
if (bundle == null || bundle.isBuiltin() || !this.bundleExists(bundle.getId())) {
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
this.setCurrentBundle(this.getBundleDirectory(bundle.getId()));
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
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
|
+
|
|
1406
|
+
void finalizePendingReload(final BundleInfo bundle, final String previousBundleName) {
|
|
1407
|
+
if (bundle == null || bundle.isBuiltin()) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
this.sendStats("set", bundle.getVersionName(), previousBundleName);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
@Deprecated
|
|
992
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) {
|
|
993
1423
|
final BundleInfo currentBundle = this.getCurrentBundle();
|
|
994
1424
|
if (!currentBundle.isBuiltin() && !this.bundleExists(currentBundle.getId())) {
|
|
995
1425
|
logger.info("Folder at bundle path does not exist. Triggering reset.");
|
|
996
1426
|
this.reset();
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
String bundlePath = this.prefs.getString(this.CAP_SERVER_PATH, null);
|
|
1430
|
+
if (shouldResetForForeignBundle(bundlePath, currentBundle.isBuiltin(), this.hasStoredBundleInfo(currentBundle.getId()))) {
|
|
1431
|
+
logger.info("Current bundle id is not one of the bundle ids stored by this plugin. Triggering reset.");
|
|
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();
|
|
997
1451
|
}
|
|
998
1452
|
}
|
|
999
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", "");
|
|
1461
|
+
}
|
|
1462
|
+
return previousNativeBuildVersion == null ? "" : previousNativeBuildVersion;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1000
1465
|
public void reset() {
|
|
1001
1466
|
this.reset(false);
|
|
1002
1467
|
}
|
|
@@ -1004,12 +1469,20 @@ public class CapgoUpdater {
|
|
|
1004
1469
|
public void setSuccess(final BundleInfo bundle, Boolean autoDeletePrevious) {
|
|
1005
1470
|
this.setBundleStatus(bundle.getId(), BundleStatus.SUCCESS);
|
|
1006
1471
|
final BundleInfo fallback = this.getFallbackBundle();
|
|
1472
|
+
final BundleInfo previewFallback = this.getPreviewFallbackBundle();
|
|
1473
|
+
final boolean fallbackIsPreviewFallback = previewFallback != null && previewFallback.getId().equals(fallback.getId());
|
|
1007
1474
|
logger.debug("Fallback bundle is: " + fallback);
|
|
1008
1475
|
logger.info("Version successfully loaded: " + bundle.getVersionName());
|
|
1009
1476
|
// Only attempt to delete when the fallback is a different bundle than the
|
|
1010
1477
|
// currently loaded one. Otherwise we spam logs with "Cannot delete <id>"
|
|
1011
1478
|
// because delete() protects the current bundle from removal.
|
|
1012
|
-
if (
|
|
1479
|
+
if (
|
|
1480
|
+
autoDeletePrevious &&
|
|
1481
|
+
!fallback.isBuiltin() &&
|
|
1482
|
+
fallback.getId() != null &&
|
|
1483
|
+
!fallback.getId().equals(bundle.getId()) &&
|
|
1484
|
+
!fallbackIsPreviewFallback
|
|
1485
|
+
) {
|
|
1013
1486
|
final Boolean res = this.delete(fallback.getId());
|
|
1014
1487
|
if (res) {
|
|
1015
1488
|
logger.info("Deleted previous bundle: " + fallback.getVersionName());
|
|
@@ -1026,24 +1499,20 @@ public class CapgoUpdater {
|
|
|
1026
1499
|
|
|
1027
1500
|
public void reset(final boolean internal) {
|
|
1028
1501
|
logger.debug("reset: " + internal);
|
|
1029
|
-
|
|
1030
|
-
this.
|
|
1031
|
-
this.
|
|
1032
|
-
this.setNextBundle(null);
|
|
1033
|
-
// Cancel any ongoing downloads
|
|
1034
|
-
if (this.activity != null) {
|
|
1035
|
-
DownloadWorkerManager.cancelAllDownloads(this.activity);
|
|
1036
|
-
}
|
|
1037
|
-
if (!internal) {
|
|
1038
|
-
this.sendStats("reset", this.getCurrentBundle().getVersionName(), currentBundleName);
|
|
1039
|
-
}
|
|
1502
|
+
final String currentBundleName = this.getCurrentBundle().getVersionName();
|
|
1503
|
+
this.prepareResetStateForTransition();
|
|
1504
|
+
this.finalizeResetTransition(currentBundleName, internal);
|
|
1040
1505
|
}
|
|
1041
1506
|
|
|
1042
1507
|
private JSONObject createInfoObject() throws JSONException {
|
|
1508
|
+
return this.createInfoObject(null);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
private JSONObject createInfoObject(final String appIdOverride) throws JSONException {
|
|
1043
1512
|
JSONObject json = new JSONObject();
|
|
1044
1513
|
json.put("platform", "android");
|
|
1045
1514
|
json.put("device_id", this.deviceID);
|
|
1046
|
-
json.put("app_id", this.appId);
|
|
1515
|
+
json.put("app_id", appIdOverride == null || appIdOverride.trim().isEmpty() ? this.appId : appIdOverride);
|
|
1047
1516
|
json.put("custom_id", this.customId);
|
|
1048
1517
|
json.put("version_build", this.versionBuild);
|
|
1049
1518
|
json.put("version_code", this.versionCode);
|
|
@@ -1052,6 +1521,7 @@ public class CapgoUpdater {
|
|
|
1052
1521
|
json.put("plugin_version", this.pluginVersion);
|
|
1053
1522
|
json.put("is_emulator", this.isEmulator());
|
|
1054
1523
|
json.put("is_prod", this.isProd());
|
|
1524
|
+
json.put("install_source", this.getInstallSource());
|
|
1055
1525
|
json.put("defaultChannel", this.defaultChannel);
|
|
1056
1526
|
|
|
1057
1527
|
// Add encryption key ID if encryption is enabled (use cached value)
|
|
@@ -1069,7 +1539,7 @@ public class CapgoUpdater {
|
|
|
1069
1539
|
if (response.code() == 429) {
|
|
1070
1540
|
// Send a statistic about the rate limit BEFORE setting the flag
|
|
1071
1541
|
// Only send once to prevent infinite loop if the stat request itself gets rate limited
|
|
1072
|
-
if (!rateLimitExceeded && !rateLimitStatisticSent) {
|
|
1542
|
+
if (!this.previewSession && !rateLimitExceeded && !rateLimitStatisticSent) {
|
|
1073
1543
|
rateLimitStatisticSent = true;
|
|
1074
1544
|
sendRateLimitStatistic();
|
|
1075
1545
|
}
|
|
@@ -1123,89 +1593,115 @@ public class CapgoUpdater {
|
|
|
1123
1593
|
|
|
1124
1594
|
Request request = new Request.Builder().url(url).post(body).build();
|
|
1125
1595
|
|
|
1126
|
-
DownloadService.sharedClient
|
|
1127
|
-
.
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
}
|
|
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
|
+
}
|
|
1137
1606
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
return;
|
|
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.
|
|
1150
1618
|
}
|
|
1619
|
+
}
|
|
1151
1620
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
retError.put("error", "response_error");
|
|
1156
|
-
retError.put("statusCode", statusCode);
|
|
1157
|
-
callback.callback(retError);
|
|
1158
|
-
return;
|
|
1621
|
+
if (jsonResponse != null && (jsonResponse.has("error") || jsonResponse.has("kind"))) {
|
|
1622
|
+
if (statusCode == 429) {
|
|
1623
|
+
checkAndHandleRateLimitResponse(response);
|
|
1159
1624
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
String responseData = responseBody.string();
|
|
1163
|
-
JSONObject jsonResponse = new JSONObject(responseData);
|
|
1164
|
-
|
|
1165
|
-
// Check for server-side errors first
|
|
1166
|
-
if (jsonResponse.has("error")) {
|
|
1167
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1625
|
+
Map<String, Object> retError = new HashMap<>();
|
|
1626
|
+
if (jsonResponse.has("error") && !jsonResponse.isNull("error")) {
|
|
1168
1627
|
retError.put("error", jsonResponse.getString("error"));
|
|
1169
|
-
if (jsonResponse.has("message")) {
|
|
1170
|
-
retError.put("message", jsonResponse.getString("message"));
|
|
1171
|
-
} else {
|
|
1172
|
-
retError.put("message", "server did not provide a message");
|
|
1173
|
-
}
|
|
1174
|
-
retError.put("statusCode", statusCode);
|
|
1175
|
-
callback.callback(retError);
|
|
1176
|
-
return;
|
|
1177
1628
|
}
|
|
1629
|
+
if (jsonResponse.has("kind") && !jsonResponse.isNull("kind")) {
|
|
1630
|
+
retError.put("kind", jsonResponse.getString("kind"));
|
|
1631
|
+
}
|
|
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"));
|
|
1639
|
+
}
|
|
1640
|
+
retError.put("statusCode", statusCode);
|
|
1641
|
+
callback.callback(retError);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1178
1644
|
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
+
}
|
|
1181
1655
|
|
|
1182
|
-
|
|
1183
|
-
while (keys.hasNext()) {
|
|
1184
|
-
String key = keys.next();
|
|
1185
|
-
if (jsonResponse.has(key)) {
|
|
1186
|
-
if ("session_key".equals(key)) {
|
|
1187
|
-
ret.put("sessionKey", jsonResponse.get(key));
|
|
1188
|
-
} else {
|
|
1189
|
-
ret.put(key, jsonResponse.get(key));
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
callback.callback(ret);
|
|
1194
|
-
} catch (JSONException e) {
|
|
1656
|
+
if (!response.isSuccessful()) {
|
|
1195
1657
|
Map<String, Object> retError = new HashMap<>();
|
|
1196
|
-
retError.put("message", "
|
|
1197
|
-
retError.put("error", "
|
|
1658
|
+
retError.put("message", "Server error: " + response.code());
|
|
1659
|
+
retError.put("error", "response_error");
|
|
1660
|
+
retError.put("kind", "failed");
|
|
1661
|
+
retError.put("statusCode", statusCode);
|
|
1198
1662
|
callback.callback(retError);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (jsonResponse == null) {
|
|
1667
|
+
throw new JSONException("Response is not a JSON object");
|
|
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
|
+
}
|
|
1199
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);
|
|
1200
1691
|
}
|
|
1201
1692
|
}
|
|
1202
|
-
|
|
1693
|
+
}
|
|
1694
|
+
);
|
|
1203
1695
|
}
|
|
1204
1696
|
|
|
1205
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) {
|
|
1206
1702
|
JSONObject json;
|
|
1207
1703
|
try {
|
|
1208
|
-
json = this.createInfoObject();
|
|
1704
|
+
json = this.createInfoObject(appIdOverride);
|
|
1209
1705
|
if (channel != null && json != null) {
|
|
1210
1706
|
json.put("defaultChannel", channel);
|
|
1211
1707
|
}
|
|
@@ -1219,7 +1715,9 @@ public class CapgoUpdater {
|
|
|
1219
1715
|
return;
|
|
1220
1716
|
}
|
|
1221
1717
|
|
|
1222
|
-
logger
|
|
1718
|
+
if (logger != null) {
|
|
1719
|
+
logger.info("Auto-update parameters: " + json);
|
|
1720
|
+
}
|
|
1223
1721
|
|
|
1224
1722
|
makeJsonRequest(updateUrl, json, callback);
|
|
1225
1723
|
}
|
|
@@ -1248,6 +1746,17 @@ public class CapgoUpdater {
|
|
|
1248
1746
|
final String defaultChannelKey,
|
|
1249
1747
|
final boolean allowSetDefaultChannel,
|
|
1250
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
|
|
1251
1760
|
) {
|
|
1252
1761
|
// Check if setting defaultChannel is allowed
|
|
1253
1762
|
if (!allowSetDefaultChannel) {
|
|
@@ -1300,6 +1809,7 @@ public class CapgoUpdater {
|
|
|
1300
1809
|
// Clear persisted defaultChannel and revert to config value
|
|
1301
1810
|
editor.remove(defaultChannelKey);
|
|
1302
1811
|
editor.apply();
|
|
1812
|
+
this.defaultChannel = configDefaultChannel;
|
|
1303
1813
|
logger.info("Public channel requested, channel override removed");
|
|
1304
1814
|
callback.callback(res);
|
|
1305
1815
|
} else {
|
|
@@ -1314,6 +1824,10 @@ public class CapgoUpdater {
|
|
|
1314
1824
|
}
|
|
1315
1825
|
|
|
1316
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) {
|
|
1317
1831
|
// Check if rate limit was exceeded
|
|
1318
1832
|
if (rateLimitExceeded) {
|
|
1319
1833
|
logger.debug("Skipping getChannel due to rate limit (429). Requests will resume after app restart.");
|
|
@@ -1351,88 +1865,117 @@ public class CapgoUpdater {
|
|
|
1351
1865
|
.put(RequestBody.create(json.toString(), MediaType.get("application/json")))
|
|
1352
1866
|
.build();
|
|
1353
1867
|
|
|
1354
|
-
DownloadService.sharedClient
|
|
1355
|
-
.
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
callback.callback(retError);
|
|
1364
|
-
}
|
|
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
|
+
}
|
|
1365
1877
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
+
}
|
|
1889
|
+
|
|
1890
|
+
if (response.code() == 400) {
|
|
1891
|
+
if (responseBody == null) {
|
|
1371
1892
|
Map<String, Object> retError = new HashMap<>();
|
|
1372
|
-
retError.put("message", "
|
|
1373
|
-
retError.put("error", "
|
|
1893
|
+
retError.put("message", "Empty response body");
|
|
1894
|
+
retError.put("error", "no_response_body");
|
|
1374
1895
|
callback.callback(retError);
|
|
1375
1896
|
return;
|
|
1376
1897
|
}
|
|
1377
|
-
|
|
1378
|
-
if (
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
ret.put("status", "default");
|
|
1385
|
-
logger.info("Channel get to \"" + ret);
|
|
1386
|
-
callback.callback(ret);
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
if (!response.isSuccessful()) {
|
|
1392
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1393
|
-
retError.put("message", "Server error: " + response.code());
|
|
1394
|
-
retError.put("error", "response_error");
|
|
1395
|
-
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);
|
|
1396
1905
|
return;
|
|
1397
1906
|
}
|
|
1907
|
+
}
|
|
1398
1908
|
|
|
1399
|
-
|
|
1400
|
-
String
|
|
1401
|
-
|
|
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
|
+
}
|
|
1402
1916
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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");
|
|
1414
1935
|
}
|
|
1936
|
+
callback.callback(retError);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1415
1939
|
|
|
1416
|
-
|
|
1940
|
+
Map<String, Object> ret = new HashMap<>();
|
|
1417
1941
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
}
|
|
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));
|
|
1424
1947
|
}
|
|
1425
|
-
logger.info("Channel get to \"" + ret);
|
|
1426
|
-
callback.callback(ret);
|
|
1427
|
-
} catch (JSONException e) {
|
|
1428
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1429
|
-
retError.put("message", "JSON parse error: " + e.getMessage());
|
|
1430
|
-
retError.put("error", "parse_error");
|
|
1431
|
-
callback.callback(retError);
|
|
1432
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);
|
|
1433
1957
|
}
|
|
1434
1958
|
}
|
|
1435
|
-
|
|
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);
|
|
1436
1979
|
}
|
|
1437
1980
|
|
|
1438
1981
|
public void listChannels(final Callback callback) {
|
|
@@ -1487,94 +2030,106 @@ public class CapgoUpdater {
|
|
|
1487
2030
|
|
|
1488
2031
|
Request request = new Request.Builder().url(urlBuilder.build()).get().build();
|
|
1489
2032
|
|
|
1490
|
-
DownloadService.sharedClient
|
|
1491
|
-
.
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
callback.callback(retError);
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
@Override
|
|
1503
|
-
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
|
1504
|
-
try (ResponseBody responseBody = response.body()) {
|
|
1505
|
-
// Check for 429 rate limit
|
|
1506
|
-
if (checkAndHandleRateLimitResponse(response)) {
|
|
1507
|
-
Map<String, Object> retError = new HashMap<>();
|
|
1508
|
-
retError.put("message", "Rate limit exceeded");
|
|
1509
|
-
retError.put("error", "rate_limit_exceeded");
|
|
1510
|
-
callback.callback(retError);
|
|
1511
|
-
return;
|
|
1512
|
-
}
|
|
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
|
+
}
|
|
1513
2042
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
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
|
+
}
|
|
1521
2054
|
|
|
1522
|
-
|
|
1523
|
-
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
|
+
}
|
|
1524
2062
|
|
|
1525
|
-
|
|
1526
|
-
|
|
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();
|
|
1527
2071
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
JSONArray channelsJson = new JSONArray(data);
|
|
1531
|
-
List<Map<String, Object>> channelsList = new ArrayList<>();
|
|
1532
|
-
|
|
1533
|
-
for (int i = 0; i < channelsJson.length(); i++) {
|
|
1534
|
-
JSONObject channelJson = channelsJson.getJSONObject(i);
|
|
1535
|
-
Map<String, Object> channel = new HashMap<>();
|
|
1536
|
-
channel.put("id", channelJson.optString("id", ""));
|
|
1537
|
-
channel.put("name", channelJson.optString("name", ""));
|
|
1538
|
-
channel.put("public", channelJson.optBoolean("public", false));
|
|
1539
|
-
channel.put("allow_self_set", channelJson.optBoolean("allow_self_set", false));
|
|
1540
|
-
channelsList.add(channel);
|
|
1541
|
-
}
|
|
2072
|
+
try {
|
|
2073
|
+
Map<String, Object> ret = parseListChannelsResponse(data);
|
|
1542
2074
|
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
retError.put("message", json.getString("message"));
|
|
1557
|
-
} else {
|
|
1558
|
-
retError.put("message", "server did not provide a message");
|
|
1559
|
-
}
|
|
1560
|
-
callback.callback(retError);
|
|
1561
|
-
return;
|
|
1562
|
-
}
|
|
1563
|
-
} catch (JSONException objException) {
|
|
1564
|
-
// If neither array nor object, throw parse error
|
|
1565
|
-
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");
|
|
1566
2088
|
}
|
|
2089
|
+
callback.callback(retError);
|
|
2090
|
+
return;
|
|
1567
2091
|
}
|
|
1568
|
-
} catch (JSONException e) {
|
|
1569
2092
|
Map<String, Object> retError = new HashMap<>();
|
|
1570
|
-
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());
|
|
1571
2102
|
retError.put("error", "parse_error");
|
|
1572
2103
|
callback.callback(retError);
|
|
1573
2104
|
}
|
|
1574
2105
|
}
|
|
1575
2106
|
}
|
|
1576
2107
|
}
|
|
1577
|
-
|
|
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;
|
|
1578
2133
|
}
|
|
1579
2134
|
|
|
1580
2135
|
public void sendStats(final String action) {
|
|
@@ -1586,6 +2141,27 @@ public class CapgoUpdater {
|
|
|
1586
2141
|
}
|
|
1587
2142
|
|
|
1588
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
|
+
|
|
1589
2165
|
// Check if rate limit was exceeded
|
|
1590
2166
|
if (rateLimitExceeded) {
|
|
1591
2167
|
logger.debug("Skipping sendStats due to rate limit (429). Stats will resume after app restart.");
|
|
@@ -1604,13 +2180,16 @@ public class CapgoUpdater {
|
|
|
1604
2180
|
json.put("old_version_name", oldVersionName);
|
|
1605
2181
|
json.put("action", action);
|
|
1606
2182
|
json.put("timestamp", System.currentTimeMillis());
|
|
2183
|
+
if (metadata != null && !metadata.isEmpty()) {
|
|
2184
|
+
json.put("metadata", new JSONObject(metadata));
|
|
2185
|
+
}
|
|
1607
2186
|
} catch (JSONException e) {
|
|
1608
2187
|
logger.error("Error preparing stats");
|
|
1609
2188
|
logger.debug("JSONException: " + e.getMessage());
|
|
1610
2189
|
return;
|
|
1611
2190
|
}
|
|
1612
2191
|
|
|
1613
|
-
statsQueue.add(json);
|
|
2192
|
+
statsQueue.add(new QueuedStatsEvent(json, onSent));
|
|
1614
2193
|
ensureStatsTimerStarted();
|
|
1615
2194
|
}
|
|
1616
2195
|
|
|
@@ -1637,7 +2216,7 @@ public class CapgoUpdater {
|
|
|
1637
2216
|
}
|
|
1638
2217
|
|
|
1639
2218
|
// Copy and clear the queue atomically using synchronized block
|
|
1640
|
-
List<
|
|
2219
|
+
List<QueuedStatsEvent> eventsToSend;
|
|
1641
2220
|
synchronized (statsQueue) {
|
|
1642
2221
|
if (statsQueue.isEmpty()) {
|
|
1643
2222
|
return;
|
|
@@ -1647,8 +2226,8 @@ public class CapgoUpdater {
|
|
|
1647
2226
|
}
|
|
1648
2227
|
|
|
1649
2228
|
JSONArray jsonArray = new JSONArray();
|
|
1650
|
-
for (
|
|
1651
|
-
jsonArray.put(event);
|
|
2229
|
+
for (QueuedStatsEvent queuedEvent : eventsToSend) {
|
|
2230
|
+
jsonArray.put(queuedEvent.event);
|
|
1652
2231
|
}
|
|
1653
2232
|
|
|
1654
2233
|
Request request = new Request.Builder()
|
|
@@ -1657,35 +2236,51 @@ public class CapgoUpdater {
|
|
|
1657
2236
|
.build();
|
|
1658
2237
|
|
|
1659
2238
|
final int eventCount = eventsToSend.size();
|
|
1660
|
-
DownloadService.sharedClient
|
|
1661
|
-
.
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
logger.debug("Error: " + e.getMessage());
|
|
1668
|
-
}
|
|
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
|
+
}
|
|
1669
2246
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
+
}
|
|
1677
2254
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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());
|
|
1685
2262
|
}
|
|
1686
2263
|
}
|
|
1687
2264
|
}
|
|
1688
|
-
|
|
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
|
+
}
|
|
1689
2284
|
}
|
|
1690
2285
|
|
|
1691
2286
|
public BundleInfo getBundleInfo(final String id) {
|
|
@@ -1695,7 +2290,13 @@ public class CapgoUpdater {
|
|
|
1695
2290
|
}
|
|
1696
2291
|
BundleInfo result;
|
|
1697
2292
|
if (BundleInfo.ID_BUILTIN.equals(trueId)) {
|
|
1698
|
-
result = new BundleInfo(
|
|
2293
|
+
result = new BundleInfo(
|
|
2294
|
+
trueId,
|
|
2295
|
+
this.versionBuild == null || this.versionBuild.isEmpty() ? null : this.versionBuild,
|
|
2296
|
+
BundleStatus.SUCCESS,
|
|
2297
|
+
"",
|
|
2298
|
+
""
|
|
2299
|
+
);
|
|
1699
2300
|
} else if (BundleInfo.VERSION_UNKNOWN.equals(trueId)) {
|
|
1700
2301
|
result = new BundleInfo(trueId, null, BundleStatus.ERROR, "", "");
|
|
1701
2302
|
} else {
|
|
@@ -1799,7 +2400,33 @@ public class CapgoUpdater {
|
|
|
1799
2400
|
return this.getBundleInfo(id);
|
|
1800
2401
|
}
|
|
1801
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
|
+
|
|
1802
2428
|
public boolean setNextBundle(final String next) {
|
|
2429
|
+
BundleInfo bundleToNotify = null;
|
|
1803
2430
|
if (next == null) {
|
|
1804
2431
|
this.editor.remove(NEXT_VERSION);
|
|
1805
2432
|
} else {
|
|
@@ -1809,8 +2436,15 @@ public class CapgoUpdater {
|
|
|
1809
2436
|
}
|
|
1810
2437
|
this.editor.putString(NEXT_VERSION, next);
|
|
1811
2438
|
this.setBundleStatus(next, BundleStatus.PENDING);
|
|
2439
|
+
bundleToNotify = newBundle;
|
|
1812
2440
|
}
|
|
1813
2441
|
this.editor.commit();
|
|
2442
|
+
if (bundleToNotify != null) {
|
|
2443
|
+
this.sendStats("set_next", bundleToNotify.getVersionName(), this.getCurrentBundle().getVersionName());
|
|
2444
|
+
final Map<String, Object> payload = new HashMap<>();
|
|
2445
|
+
payload.put("bundle", bundleToNotify.toJSONMap());
|
|
2446
|
+
this.notifyListeners("setNext", payload);
|
|
2447
|
+
}
|
|
1814
2448
|
return true;
|
|
1815
2449
|
}
|
|
1816
2450
|
|