@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
@@ -19,42 +19,78 @@ import android.widget.ListView;
19
19
  import android.widget.ProgressBar;
20
20
  import com.getcapacitor.Bridge;
21
21
  import com.getcapacitor.BridgeActivity;
22
+ import com.getcapacitor.JSArray;
23
+ import com.getcapacitor.JSObject;
22
24
  import java.util.ArrayList;
23
25
  import java.util.List;
24
26
  import java.util.Map;
25
27
  import org.json.JSONArray;
28
+ import org.json.JSONObject;
26
29
 
27
- public class ShakeMenu implements ShakeDetector.Listener {
30
+ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetector.Listener {
31
+
32
+ private interface PreviewMenuAction {
33
+ boolean run();
34
+ }
28
35
 
29
36
  private CapacitorUpdaterPlugin plugin;
30
37
  private BridgeActivity activity;
31
38
  private ShakeDetector shakeDetector;
39
+ private ThreeFingerPinchDetector pinchDetector;
32
40
  private boolean isShowing = false;
33
41
  private Logger logger;
42
+ private String gesture;
34
43
 
35
- public ShakeMenu(CapacitorUpdaterPlugin plugin, BridgeActivity activity, Logger logger) {
44
+ public ShakeMenu(CapacitorUpdaterPlugin plugin, BridgeActivity activity, Logger logger, String gesture) {
36
45
  this.plugin = plugin;
37
46
  this.activity = activity;
38
47
  this.logger = logger;
48
+ this.gesture = gesture;
49
+
50
+ if (CapacitorUpdaterPlugin.SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(gesture)) {
51
+ this.pinchDetector = new ThreeFingerPinchDetector(this, logger);
52
+ this.pinchDetector.start(activity);
53
+ } else {
54
+ SensorManager sensorManager = (SensorManager) activity.getSystemService(Activity.SENSOR_SERVICE);
55
+ this.shakeDetector = new ShakeDetector(this);
56
+ this.shakeDetector.start(sensorManager);
57
+ }
58
+ }
39
59
 
40
- SensorManager sensorManager = (SensorManager) activity.getSystemService(Activity.SENSOR_SERVICE);
41
- this.shakeDetector = new ShakeDetector(this);
42
- this.shakeDetector.start(sensorManager);
60
+ public boolean usesGesture(String gesture) {
61
+ return this.gesture != null && this.gesture.equals(gesture);
43
62
  }
44
63
 
45
64
  public void stop() {
46
65
  if (shakeDetector != null) {
47
66
  shakeDetector.stop();
48
67
  }
68
+ if (pinchDetector != null) {
69
+ pinchDetector.stop();
70
+ }
49
71
  }
50
72
 
51
73
  @Override
52
74
  public void onShakeDetected() {
53
- logger.info("Shake detected");
75
+ onMenuGestureDetected("Shake");
76
+ }
77
+
78
+ @Override
79
+ public void onThreeFingerPinchDetected() {
80
+ onMenuGestureDetected("Three finger pinch");
81
+ }
54
82
 
55
- // Check if shake menu is enabled
56
- if (!plugin.shakeMenuEnabled) {
57
- logger.info("Shake menu is disabled");
83
+ private void onMenuGestureDetected(String gestureName) {
84
+ logger.info(gestureName + " detected");
85
+
86
+ boolean canShowPreviewMenu = Boolean.TRUE.equals(plugin.shakeMenuEnabled) && plugin.hasActivePreviewSession();
87
+ boolean canShowChannelSelector = Boolean.TRUE.equals(plugin.shakeChannelSelectorEnabled);
88
+ if (!canShowPreviewMenu && !canShowChannelSelector) {
89
+ if (Boolean.TRUE.equals(plugin.shakeMenuEnabled)) {
90
+ logger.info("Shake preview menu ignored because no preview session is active");
91
+ } else {
92
+ logger.info("Shake menu is disabled");
93
+ }
58
94
  return;
59
95
  }
60
96
 
@@ -66,15 +102,313 @@ public class ShakeMenu implements ShakeDetector.Listener {
66
102
 
67
103
  isShowing = true;
68
104
 
69
- // Check if channel selector mode is enabled
70
- if (plugin.shakeChannelSelectorEnabled) {
71
- showChannelSelector();
72
- } else {
105
+ if (canShowPreviewMenu) {
73
106
  showDefaultMenu();
107
+ } else {
108
+ showChannelSelector();
74
109
  }
75
110
  }
76
111
 
77
112
  private void showDefaultMenu() {
113
+ activity.runOnUiThread(() -> {
114
+ try {
115
+ if (!plugin.hasActivePreviewSession()) {
116
+ logger.info("Shake preview menu ignored because no preview session is active");
117
+ isShowing = false;
118
+ return;
119
+ }
120
+ if (Boolean.TRUE.equals(plugin.shakeChannelSelectorEnabled)) {
121
+ showCombinedPreviewMenu();
122
+ return;
123
+ }
124
+ String appName = activity.getPackageManager().getApplicationLabel(activity.getApplicationInfo()).toString();
125
+ String title = "Preview " + appName + " Menu";
126
+ String message = "Reload, switch, or leave the current preview.";
127
+ List<String> actions = new ArrayList<>();
128
+ actions.add("Reload preview");
129
+ if (plugin.previewMenuPreviews().length() > 0) {
130
+ actions.add("Switch preview");
131
+ }
132
+ actions.add("Leave test app");
133
+ final boolean[] openingNestedSelector = { false };
134
+ final boolean[] previewActionRunning = { false };
135
+
136
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
137
+ builder.setTitle(title);
138
+ builder.setMessage(message);
139
+ builder.setItems(actions.toArray(new String[0]), (dialogInterface, which) -> {
140
+ AlertDialog dialog = (AlertDialog) dialogInterface;
141
+ String action = actions.get(which);
142
+ if ("Reload preview".equals(action)) {
143
+ previewActionRunning[0] = true;
144
+ logger.info("Reloading webview");
145
+ runPreviewMenuAction(dialog, "Could not reload the test app.", "Error reloading test app: ", () ->
146
+ plugin.reloadPreviewSessionFromShakeMenu()
147
+ );
148
+ } else if ("Switch preview".equals(action)) {
149
+ openingNestedSelector[0] = true;
150
+ dialog.dismiss();
151
+ showPreviewSelector();
152
+ } else {
153
+ previewActionRunning[0] = true;
154
+ runPreviewMenuAction(dialog, "Could not leave the test app.", "Error leaving test app: ", () ->
155
+ plugin.leavePreviewSessionFromShakeMenu()
156
+ );
157
+ }
158
+ });
159
+
160
+ // Cancel button
161
+ builder.setNegativeButton(
162
+ "Close menu",
163
+ new DialogInterface.OnClickListener() {
164
+ public void onClick(DialogInterface dialog, int id) {
165
+ logger.info("Shake menu cancelled");
166
+ dialog.dismiss();
167
+ isShowing = false;
168
+ }
169
+ }
170
+ );
171
+
172
+ AlertDialog dialog = builder.create();
173
+ dialog.setOnDismissListener((dialogInterface) -> {
174
+ if (!openingNestedSelector[0] && !previewActionRunning[0]) {
175
+ isShowing = false;
176
+ }
177
+ });
178
+ dialog.show();
179
+ } catch (Exception e) {
180
+ logger.error("Error showing shake menu: " + e.getMessage());
181
+ isShowing = false;
182
+ }
183
+ });
184
+ }
185
+
186
+ private void showCombinedPreviewMenu() {
187
+ try {
188
+ String appName = activity.getPackageManager().getApplicationLabel(activity.getApplicationInfo()).toString();
189
+ String title = "Preview " + appName + " Menu";
190
+ String message = "Reload, switch, or leave the current preview.";
191
+ List<String> actions = new ArrayList<>();
192
+ actions.add("Reload preview");
193
+ if (plugin.previewMenuPreviews().length() > 0) {
194
+ actions.add("Switch preview");
195
+ }
196
+ actions.add("Leave test app");
197
+ actions.add("Switch channel");
198
+ final boolean[] openingNestedSelector = { false };
199
+ final boolean[] previewActionRunning = { false };
200
+
201
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
202
+ builder.setTitle(title);
203
+ builder.setMessage(message);
204
+ builder.setItems(actions.toArray(new String[0]), (dialogInterface, which) -> {
205
+ AlertDialog dialog = (AlertDialog) dialogInterface;
206
+ String action = actions.get(which);
207
+ if ("Reload preview".equals(action)) {
208
+ previewActionRunning[0] = true;
209
+ logger.info("Reloading webview");
210
+ runPreviewMenuAction(dialog, "Could not reload the test app.", "Error reloading test app: ", () ->
211
+ plugin.reloadPreviewSessionFromShakeMenu()
212
+ );
213
+ } else if ("Leave test app".equals(action)) {
214
+ previewActionRunning[0] = true;
215
+ runPreviewMenuAction(dialog, "Could not leave the test app.", "Error leaving test app: ", () ->
216
+ plugin.leavePreviewSessionFromShakeMenu()
217
+ );
218
+ } else if ("Switch preview".equals(action)) {
219
+ openingNestedSelector[0] = true;
220
+ dialog.dismiss();
221
+ showPreviewSelector();
222
+ } else {
223
+ openingNestedSelector[0] = true;
224
+ dialog.dismiss();
225
+ showChannelSelector();
226
+ }
227
+ });
228
+ builder.setNegativeButton("Close menu", (dialog, id) -> {
229
+ logger.info("Shake menu cancelled");
230
+ dialog.dismiss();
231
+ isShowing = false;
232
+ });
233
+
234
+ AlertDialog dialog = builder.create();
235
+ dialog.setOnDismissListener((dialogInterface) -> {
236
+ if (!openingNestedSelector[0] && !previewActionRunning[0]) {
237
+ isShowing = false;
238
+ }
239
+ });
240
+ dialog.show();
241
+ } catch (Exception e) {
242
+ logger.error("Error showing combined shake menu: " + e.getMessage());
243
+ isShowing = false;
244
+ }
245
+ }
246
+
247
+ private void runPreviewMenuAction(AlertDialog dialog, String failureMessage, String errorPrefix, PreviewMenuAction action) {
248
+ new Thread(() -> {
249
+ try {
250
+ if (!action.run()) {
251
+ activity.runOnUiThread(() -> showError(failureMessage));
252
+ }
253
+ } catch (Exception e) {
254
+ logger.error(errorPrefix + e.getMessage());
255
+ activity.runOnUiThread(() -> showError(errorPrefix + e.getMessage()));
256
+ } finally {
257
+ activity.runOnUiThread(() -> {
258
+ dialog.dismiss();
259
+ isShowing = false;
260
+ });
261
+ }
262
+ }).start();
263
+ }
264
+
265
+ private void setPreviewMenuButtonsEnabled(AlertDialog dialog, boolean enabled) {
266
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
267
+ dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setEnabled(enabled);
268
+ dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(enabled);
269
+ }
270
+
271
+ private void showPreviewSelector() {
272
+ activity.runOnUiThread(() -> {
273
+ try {
274
+ JSArray previewsRaw = plugin.previewMenuPreviews();
275
+ List<JSObject> previews = new ArrayList<>();
276
+ for (int i = 0; i < previewsRaw.length(); i++) {
277
+ Object raw = previewsRaw.opt(i);
278
+ if (raw instanceof JSObject preview) {
279
+ previews.add(preview);
280
+ } else if (raw instanceof JSONObject json) {
281
+ previews.add(JSObject.fromJSONObject(json));
282
+ }
283
+ }
284
+
285
+ if (previews.isEmpty()) {
286
+ showError("No saved previews available on this device.");
287
+ return;
288
+ }
289
+
290
+ presentPreviewPicker(previews);
291
+ } catch (Exception e) {
292
+ logger.error("Error showing preview selector: " + e.getMessage());
293
+ isShowing = false;
294
+ }
295
+ });
296
+ }
297
+
298
+ private String previewLabel(JSObject preview) {
299
+ String name = preview.optString("name", "");
300
+ JSObject bundle = preview.getJSObject("bundle");
301
+ String version = bundle == null ? "" : bundle.optString("version", "");
302
+ String label = !name.isEmpty() ? name : (!version.isEmpty() ? version : preview.optString("id", "Preview"));
303
+ if (preview.optBoolean("isActive", false)) {
304
+ label += " (current)";
305
+ }
306
+ return label;
307
+ }
308
+
309
+ private void presentPreviewPicker(List<JSObject> previews) {
310
+ try {
311
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
312
+ builder.setTitle("Select Preview");
313
+
314
+ LinearLayout layout = new LinearLayout(activity);
315
+ layout.setOrientation(LinearLayout.VERTICAL);
316
+ int padding = dpToPx(16);
317
+ layout.setPadding(padding, padding, padding, padding);
318
+
319
+ EditText searchField = new EditText(activity);
320
+ searchField.setHint("Search previews...");
321
+ searchField.setSingleLine(true);
322
+ layout.addView(searchField);
323
+
324
+ final List<JSObject> displayedPreviews = new ArrayList<>();
325
+ displayedPreviews.addAll(previews.subList(0, Math.min(5, previews.size())));
326
+ final ArrayAdapter<String> adapter = new ArrayAdapter<>(
327
+ activity,
328
+ android.R.layout.simple_list_item_1,
329
+ previewLabels(displayedPreviews)
330
+ );
331
+
332
+ ListView listView = new ListView(activity);
333
+ listView.setAdapter(adapter);
334
+ LinearLayout.LayoutParams listParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, dpToPx(250));
335
+ listView.setLayoutParams(listParams);
336
+ layout.addView(listView);
337
+
338
+ builder.setView(layout);
339
+ builder.setNegativeButton("Cancel", (dialog, which) -> {
340
+ dialog.dismiss();
341
+ isShowing = false;
342
+ });
343
+
344
+ AlertDialog dialog = builder.create();
345
+ dialog.setOnDismissListener((d) -> isShowing = false);
346
+
347
+ searchField.addTextChangedListener(
348
+ new TextWatcher() {
349
+ @Override
350
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
351
+
352
+ @Override
353
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
354
+
355
+ @Override
356
+ public void afterTextChanged(Editable s) {
357
+ String query = s.toString().toLowerCase();
358
+ displayedPreviews.clear();
359
+
360
+ for (JSObject preview : previews) {
361
+ if (previewLabel(preview).toLowerCase().contains(query)) {
362
+ displayedPreviews.add(preview);
363
+ if (displayedPreviews.size() >= 5) break;
364
+ }
365
+ }
366
+
367
+ adapter.clear();
368
+ adapter.addAll(previewLabels(displayedPreviews));
369
+ adapter.notifyDataSetChanged();
370
+ }
371
+ }
372
+ );
373
+
374
+ listView.setOnItemClickListener((parent, view, position, id) -> {
375
+ JSObject selectedPreview = displayedPreviews.get(position);
376
+ String previewId = selectedPreview.optString("id", "");
377
+ dialog.dismiss();
378
+ selectPreview(previewId);
379
+ });
380
+
381
+ dialog.show();
382
+ } catch (Exception e) {
383
+ logger.error("Error presenting preview picker: " + e.getMessage());
384
+ isShowing = false;
385
+ }
386
+ }
387
+
388
+ private List<String> previewLabels(List<JSObject> previews) {
389
+ List<String> labels = new ArrayList<>();
390
+ for (JSObject preview : previews) {
391
+ labels.add(previewLabel(preview));
392
+ }
393
+ return labels;
394
+ }
395
+
396
+ private void selectPreview(String previewId) {
397
+ new Thread(() -> {
398
+ try {
399
+ if (!plugin.setPreviewFromShakeMenu(previewId)) {
400
+ activity.runOnUiThread(() -> showError("Could not switch preview."));
401
+ }
402
+ } catch (Exception e) {
403
+ logger.error("Error switching preview: " + e.getMessage());
404
+ activity.runOnUiThread(() -> showError("Error switching preview: " + e.getMessage()));
405
+ } finally {
406
+ isShowing = false;
407
+ }
408
+ }).start();
409
+ }
410
+
411
+ private void showConfiguredDefaultMenu() {
78
412
  activity.runOnUiThread(() -> {
79
413
  try {
80
414
  String appName = activity.getPackageManager().getApplicationLabel(activity.getApplicationInfo()).toString();
@@ -120,7 +454,6 @@ public class ShakeMenu implements ShakeDetector.Listener {
120
454
  bridge.setServerAssetPath(path);
121
455
  }
122
456
 
123
- // Try to delete the current bundle
124
457
  try {
125
458
  updater.delete(current.getId());
126
459
  } catch (Exception err) {
@@ -259,8 +592,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
259
592
  presentChannelPicker(channels);
260
593
  });
261
594
  });
262
- })
263
- .start();
595
+ }).start();
264
596
  } catch (Exception e) {
265
597
  logger.error("Error showing channel selector: " + e.getMessage());
266
598
  isShowing = false;
@@ -401,6 +733,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
401
733
  new Thread(() -> {
402
734
  final CapgoUpdater updater = plugin.implementation;
403
735
  final Bridge bridge = activity.getBridge();
736
+ final String configDefaultChannel = plugin.getConfig().getString("defaultChannel", "");
404
737
 
405
738
  // Set the channel - respect plugin's allowSetDefaultChannel config
406
739
  updater.setChannel(
@@ -408,6 +741,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
408
741
  updater.editor,
409
742
  "CapacitorUpdater.defaultChannel",
410
743
  plugin.allowSetDefaultChannel,
744
+ configDefaultChannel,
411
745
  (setRes) -> {
412
746
  if (setRes == null) {
413
747
  activity.runOnUiThread(() -> {
@@ -451,13 +785,14 @@ public class ShakeMenu implements ShakeDetector.Listener {
451
785
  String latestKind = getString(latestRes, "kind");
452
786
  String latestMessage = getString(latestRes, "message");
453
787
 
454
- String detail = latestMessage != null && !latestMessage.isEmpty()
455
- ? latestMessage
456
- : latestError != null && !latestError.isEmpty()
457
- ? latestError
458
- : latestKind != null && !latestKind.isEmpty()
459
- ? latestKind
460
- : "server did not provide a message";
788
+ String detail =
789
+ latestMessage != null && !latestMessage.isEmpty()
790
+ ? latestMessage
791
+ : latestError != null && !latestError.isEmpty()
792
+ ? latestError
793
+ : latestKind != null && !latestKind.isEmpty()
794
+ ? latestKind
795
+ : "server did not provide a message";
461
796
 
462
797
  // Handle update errors first (before "no new version" check)
463
798
  if (
@@ -576,8 +911,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
576
911
  });
577
912
  }
578
913
  );
579
- })
580
- .start();
914
+ }).start();
581
915
  } catch (Exception e) {
582
916
  logger.error("Error selecting channel: " + e.getMessage());
583
917
  isShowing = false;