@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.
Files changed (33) hide show
  1. package/Package.swift +1 -1
  2. package/README.md +510 -92
  3. package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
  4. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3192 -777
  5. package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +798 -299
  6. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
  7. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +46 -28
  8. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +2 -0
  9. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +3 -3
  10. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +359 -25
  11. package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
  12. package/dist/docs.json +1283 -169
  13. package/dist/esm/definitions.d.ts +621 -44
  14. package/dist/esm/definitions.js.map +1 -1
  15. package/dist/esm/web.d.ts +11 -1
  16. package/dist/esm/web.js +59 -1
  17. package/dist/esm/web.js.map +1 -1
  18. package/dist/plugin.cjs.js +59 -1
  19. package/dist/plugin.cjs.js.map +1 -1
  20. package/dist/plugin.js +59 -1
  21. package/dist/plugin.js.map +1 -1
  22. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +0 -1
  23. package/ios/Sources/CapacitorUpdaterPlugin/AppHealthTracker.swift +82 -0
  24. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -16
  25. package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
  26. package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
  27. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +2116 -223
  28. package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +997 -332
  29. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
  30. package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +78 -1
  31. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +418 -36
  32. package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
  33. 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 to persist the device ID across reinstalls.
29
+ * Uses Android Keystore-backed storage and backup-restorable SharedPreferences.
30
30
  *
31
- * The device ID is a random UUID stored in the Android Keystore, which persists
32
- * even after app uninstall/reinstall on Android 6.0+ (API 23+).
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 (persists across reinstalls)
49
- * 2. Falls back to legacy SharedPreferences (for migration)
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 Keystore-encrypted storage for future use
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
- return deviceId.toLowerCase();
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) throws Exception {
94
- SharedPreferences prefs = context.getSharedPreferences(DEVICE_ID_PREFS, Context.MODE_PRIVATE);
95
- String encryptedDeviceId = prefs.getString(DEVICE_ID_KEY, null);
96
- String ivString = prefs.getString(IV_KEY, null);
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
- if (encryptedDeviceId == null || ivString == null) {
99
- return null;
100
- }
102
+ if (encryptedDeviceId == null || ivString == null) {
103
+ return null;
104
+ }
101
105
 
102
- // Get the encryption key from Keystore
103
- SecretKey key = getOrCreateKey();
104
- if (key == null) {
105
- return null;
106
- }
106
+ // Get the encryption key from Keystore
107
+ SecretKey key = getOrCreateKey();
108
+ if (key == null) {
109
+ return null;
110
+ }
107
111
 
108
- // Decrypt the device ID
109
- byte[] encryptedBytes = android.util.Base64.decode(encryptedDeviceId, android.util.Base64.DEFAULT);
110
- byte[] iv = android.util.Base64.decode(ivString, android.util.Base64.DEFAULT);
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
- Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
113
- GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
114
- cipher.init(Cipher.DECRYPT_MODE, key, spec);
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
- byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
117
- return new String(decryptedBytes, StandardCharsets.UTF_8);
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
- .apply();
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.edit().putString(LEGACY_PREFS_KEY, deviceId).apply();
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
- .codePoints()
120
- .forEach((cp) -> {
121
- boolean isVisibleAscii = cp >= 0x20 && cp <= 0x7E;
122
- boolean isIso88591 = cp >= 0xA0 && cp <= 0xFF;
123
- if (isVisibleAscii || isIso88591) {
124
- sanitized.appendCodePoint(cp);
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
- .newCall(request)
250
- .enqueue(
251
- new Callback() {
252
- @Override
253
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
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
- @Override
260
- public void onResponse(@NonNull Call call, @NonNull Response response) {
261
- try (ResponseBody body = response.body()) {
262
- // nothing else to do, just closing body
263
- } catch (Exception ignored) {} finally {
264
- response.close();
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 = new File(destFolder, targetFileName);
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 = 12.0f; // Acceleration threshold for shake detection
21
- private static final int SHAKE_TIMEOUT = 500; // Minimum time between shake events (ms)
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.SENSOR_DELAY_GAME);
37
+ sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI);
38
38
  }
39
39
  }
40
40