@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
|
@@ -26,10 +26,10 @@ import javax.crypto.spec.GCMParameterSpec;
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Helper class to manage device ID persistence across app installations.
|
|
29
|
-
* Uses Android Keystore
|
|
29
|
+
* Uses Android Keystore-backed storage and backup-restorable SharedPreferences.
|
|
30
30
|
*
|
|
31
|
-
* The device ID is a random UUID stored in
|
|
32
|
-
*
|
|
31
|
+
* The device ID is a random UUID stored in encrypted preferences and mirrored
|
|
32
|
+
* to appUUID so Android backup/restore can keep it across reinstalls.
|
|
33
33
|
*/
|
|
34
34
|
public class DeviceIdHelper {
|
|
35
35
|
|
|
@@ -45,10 +45,10 @@ public class DeviceIdHelper {
|
|
|
45
45
|
* Gets or creates a device ID that persists across reinstalls.
|
|
46
46
|
*
|
|
47
47
|
* This method:
|
|
48
|
-
* 1. First checks for an existing ID in Keystore-encrypted storage
|
|
49
|
-
* 2. Falls back to legacy SharedPreferences
|
|
48
|
+
* 1. First checks for an existing ID in Keystore-encrypted storage
|
|
49
|
+
* 2. Falls back to legacy SharedPreferences restored by Android backup
|
|
50
50
|
* 3. Generates a new UUID if neither exists
|
|
51
|
-
* 4. Stores the ID in
|
|
51
|
+
* 4. Stores the ID synchronously in both stores for future use
|
|
52
52
|
*
|
|
53
53
|
* @param context Application context
|
|
54
54
|
* @param legacyPrefs Legacy SharedPreferences (for migration)
|
|
@@ -60,7 +60,9 @@ public class DeviceIdHelper {
|
|
|
60
60
|
String deviceId = getDeviceIdFromKeystore(context);
|
|
61
61
|
|
|
62
62
|
if (deviceId != null && !deviceId.isEmpty()) {
|
|
63
|
-
|
|
63
|
+
deviceId = deviceId.toLowerCase();
|
|
64
|
+
saveLegacyDeviceId(legacyPrefs, deviceId);
|
|
65
|
+
return deviceId;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
// Migration: Check legacy SharedPreferences for existing device ID
|
|
@@ -74,7 +76,8 @@ public class DeviceIdHelper {
|
|
|
74
76
|
// Ensure lowercase for consistency
|
|
75
77
|
deviceId = deviceId.toLowerCase();
|
|
76
78
|
|
|
77
|
-
// Save to Keystore storage
|
|
79
|
+
// Save to backup-restorable preferences and Keystore storage
|
|
80
|
+
saveLegacyDeviceId(legacyPrefs, deviceId);
|
|
78
81
|
saveDeviceIdToKeystore(context, deviceId);
|
|
79
82
|
|
|
80
83
|
return deviceId;
|
|
@@ -90,31 +93,35 @@ public class DeviceIdHelper {
|
|
|
90
93
|
* @param context Application context
|
|
91
94
|
* @return Device ID string or null if not found
|
|
92
95
|
*/
|
|
93
|
-
private static String getDeviceIdFromKeystore(Context context)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
private static String getDeviceIdFromKeystore(Context context) {
|
|
97
|
+
try {
|
|
98
|
+
SharedPreferences prefs = context.getSharedPreferences(DEVICE_ID_PREFS, Context.MODE_PRIVATE);
|
|
99
|
+
String encryptedDeviceId = prefs.getString(DEVICE_ID_KEY, null);
|
|
100
|
+
String ivString = prefs.getString(IV_KEY, null);
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
if (encryptedDeviceId == null || ivString == null) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
// Get the encryption key from Keystore
|
|
107
|
+
SecretKey key = getOrCreateKey();
|
|
108
|
+
if (key == null) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
107
111
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
// Decrypt the device ID
|
|
113
|
+
byte[] encryptedBytes = android.util.Base64.decode(encryptedDeviceId, android.util.Base64.DEFAULT);
|
|
114
|
+
byte[] iv = android.util.Base64.decode(ivString, android.util.Base64.DEFAULT);
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
117
|
+
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
|
118
|
+
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
|
|
121
|
+
return new String(decryptedBytes, StandardCharsets.UTF_8);
|
|
122
|
+
} catch (Exception e) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
/**
|
|
@@ -146,7 +153,7 @@ public class DeviceIdHelper {
|
|
|
146
153
|
.edit()
|
|
147
154
|
.putString(DEVICE_ID_KEY, android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT))
|
|
148
155
|
.putString(IV_KEY, android.util.Base64.encodeToString(iv, android.util.Base64.DEFAULT))
|
|
149
|
-
.
|
|
156
|
+
.commit();
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
/**
|
|
@@ -207,9 +214,17 @@ public class DeviceIdHelper {
|
|
|
207
214
|
|
|
208
215
|
if (deviceId == null || deviceId.isEmpty()) {
|
|
209
216
|
deviceId = UUID.randomUUID().toString();
|
|
210
|
-
legacyPrefs
|
|
217
|
+
saveLegacyDeviceId(legacyPrefs, deviceId);
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
return deviceId.toLowerCase();
|
|
214
221
|
}
|
|
222
|
+
|
|
223
|
+
private static void saveLegacyDeviceId(SharedPreferences legacyPrefs, String deviceId) {
|
|
224
|
+
if (legacyPrefs == null || deviceId == null || deviceId.isEmpty()) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
legacyPrefs.edit().putString(LEGACY_PREFS_KEY, deviceId).commit();
|
|
229
|
+
}
|
|
215
230
|
}
|
|
@@ -68,6 +68,7 @@ public class DownloadService extends Worker {
|
|
|
68
68
|
public static final String IS_MANIFEST = "is_manifest";
|
|
69
69
|
public static final String APP_ID = "app_id";
|
|
70
70
|
public static final String pluginVersion = "plugin_version";
|
|
71
|
+
public static final String INSTALL_SOURCE = "install_source";
|
|
71
72
|
public static final String STATS_URL = "stats_url";
|
|
72
73
|
public static final String DEVICE_ID = "device_id";
|
|
73
74
|
public static final String CUSTOM_ID = "custom_id";
|
|
@@ -115,15 +116,13 @@ public class DownloadService extends Worker {
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
StringBuilder sanitized = new StringBuilder();
|
|
118
|
-
value
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
});
|
|
119
|
+
value.codePoints().forEach((cp) -> {
|
|
120
|
+
boolean isVisibleAscii = cp >= 0x20 && cp <= 0x7E;
|
|
121
|
+
boolean isIso88591 = cp >= 0xA0 && cp <= 0xFF;
|
|
122
|
+
if (isVisibleAscii || isIso88591) {
|
|
123
|
+
sanitized.appendCodePoint(cp);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
127
126
|
|
|
128
127
|
String result = sanitized.toString().trim();
|
|
129
128
|
return result.isEmpty() ? "unknown" : result;
|
|
@@ -168,6 +167,16 @@ public class DownloadService extends Worker {
|
|
|
168
167
|
return Result.success(output);
|
|
169
168
|
}
|
|
170
169
|
|
|
170
|
+
static File resolveManifestTargetFile(final File destFolder, final String fileName) throws IOException {
|
|
171
|
+
final boolean isBrotli = fileName.endsWith(".br");
|
|
172
|
+
final String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
|
|
173
|
+
return CapgoUpdater.resolvePathInsideDirectory(destFolder, targetFileName);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static File resolveManifestBuiltinFile(final File builtinFolder, final String fileName) throws IOException {
|
|
177
|
+
return CapgoUpdater.resolvePathInsideDirectory(builtinFolder, fileName);
|
|
178
|
+
}
|
|
179
|
+
|
|
171
180
|
private String getInputString(String key, String fallback) {
|
|
172
181
|
String value = getInputData().getString(key);
|
|
173
182
|
return value != null ? value : fallback;
|
|
@@ -228,6 +237,7 @@ public class DownloadService extends Worker {
|
|
|
228
237
|
json.put("platform", "android");
|
|
229
238
|
json.put("app_id", getInputString(APP_ID, "unknown"));
|
|
230
239
|
json.put("plugin_version", getInputString(pluginVersion, "unknown"));
|
|
240
|
+
json.put("install_source", getInputString(INSTALL_SOURCE, ""));
|
|
231
241
|
json.put("version_name", version != null ? version : "");
|
|
232
242
|
json.put("old_version_name", "");
|
|
233
243
|
json.put("action", action);
|
|
@@ -245,27 +255,26 @@ public class DownloadService extends Worker {
|
|
|
245
255
|
.post(RequestBody.create(json.toString(), MediaType.get("application/json")))
|
|
246
256
|
.build();
|
|
247
257
|
|
|
248
|
-
sharedClient
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (logger != null) {
|
|
255
|
-
logger.error("Failed to send stats: " + e.getMessage());
|
|
256
|
-
}
|
|
258
|
+
sharedClient.newCall(request).enqueue(
|
|
259
|
+
new Callback() {
|
|
260
|
+
@Override
|
|
261
|
+
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
|
262
|
+
if (logger != null) {
|
|
263
|
+
logger.error("Failed to send stats: " + e.getMessage());
|
|
257
264
|
}
|
|
265
|
+
}
|
|
258
266
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
@Override
|
|
268
|
+
public void onResponse(@NonNull Call call, @NonNull Response response) {
|
|
269
|
+
try (ResponseBody body = response.body()) {
|
|
270
|
+
// nothing else to do, just closing body
|
|
271
|
+
} catch (Exception ignored) {
|
|
272
|
+
} finally {
|
|
273
|
+
response.close();
|
|
266
274
|
}
|
|
267
275
|
}
|
|
268
|
-
|
|
276
|
+
}
|
|
277
|
+
);
|
|
269
278
|
} catch (Exception e) {
|
|
270
279
|
if (logger != null) {
|
|
271
280
|
logger.error("sendStatsAsync error: " + e.getMessage());
|
|
@@ -338,11 +347,20 @@ public class DownloadService extends Worker {
|
|
|
338
347
|
boolean isBrotli = fileName.endsWith(".br");
|
|
339
348
|
String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
|
|
340
349
|
|
|
341
|
-
File targetFile
|
|
350
|
+
File targetFile;
|
|
351
|
+
File builtinFile;
|
|
352
|
+
try {
|
|
353
|
+
targetFile = resolveManifestTargetFile(destFolder, fileName);
|
|
354
|
+
builtinFile = resolveManifestBuiltinFile(builtinFolder, fileName);
|
|
355
|
+
} catch (IOException e) {
|
|
356
|
+
logger.error("Invalid manifest file path: " + fileName);
|
|
357
|
+
sendStatsAsync("manifest_path_fail", version + ":" + fileName);
|
|
358
|
+
hasError.set(true);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
342
361
|
String cacheBaseName = new File(isBrotli ? targetFileName : fileName).getName();
|
|
343
362
|
File cacheFile = new File(cacheFolder, finalFileHash + "_" + cacheBaseName);
|
|
344
363
|
final File legacyCacheFile = isBrotli ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
|
|
345
|
-
File builtinFile = new File(builtinFolder, fileName);
|
|
346
364
|
|
|
347
365
|
// Ensure parent directories of the target file exist
|
|
348
366
|
if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
|
|
@@ -63,6 +63,7 @@ public class DownloadWorkerManager {
|
|
|
63
63
|
String appId,
|
|
64
64
|
String pluginVersion,
|
|
65
65
|
boolean isProd,
|
|
66
|
+
String installSource,
|
|
66
67
|
String statsUrl,
|
|
67
68
|
String deviceId,
|
|
68
69
|
String versionBuild,
|
|
@@ -89,6 +90,7 @@ public class DownloadWorkerManager {
|
|
|
89
90
|
.putString(DownloadService.PUBLIC_KEY, publicKey)
|
|
90
91
|
.putString(DownloadService.APP_ID, appId)
|
|
91
92
|
.putString(DownloadService.pluginVersion, pluginVersion)
|
|
93
|
+
.putString(DownloadService.INSTALL_SOURCE, installSource)
|
|
92
94
|
.putString(DownloadService.STATS_URL, statsUrl)
|
|
93
95
|
.putString(DownloadService.DEVICE_ID, deviceId)
|
|
94
96
|
.putString(DownloadService.VERSION_BUILD, versionBuild)
|
|
@@ -17,8 +17,8 @@ public class ShakeDetector implements SensorEventListener {
|
|
|
17
17
|
void onShakeDetected();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
private static final float SHAKE_THRESHOLD =
|
|
21
|
-
private static final int SHAKE_TIMEOUT =
|
|
20
|
+
private static final float SHAKE_THRESHOLD = 16.0f; // Acceleration threshold for shake detection
|
|
21
|
+
private static final int SHAKE_TIMEOUT = 1000; // Minimum time between shake events (ms)
|
|
22
22
|
|
|
23
23
|
private Listener listener;
|
|
24
24
|
private SensorManager sensorManager;
|
|
@@ -34,7 +34,7 @@ public class ShakeDetector implements SensorEventListener {
|
|
|
34
34
|
this.accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
|
35
35
|
|
|
36
36
|
if (accelerometer != null) {
|
|
37
|
-
sensorManager.registerListener(this, accelerometer, SensorManager.
|
|
37
|
+
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|