@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.
Files changed (36) hide show
  1. package/Package.swift +5 -2
  2. package/README.md +643 -115
  3. package/android/build.gradle +3 -3
  4. package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
  5. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3121 -353
  6. package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +924 -290
  7. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +49 -13
  8. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
  9. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +75 -32
  10. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +2 -0
  11. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +3 -3
  12. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +401 -27
  13. package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
  14. package/dist/docs.json +1590 -196
  15. package/dist/esm/definitions.d.ts +755 -66
  16. package/dist/esm/definitions.js.map +1 -1
  17. package/dist/esm/web.d.ts +11 -2
  18. package/dist/esm/web.js +59 -5
  19. package/dist/esm/web.js.map +1 -1
  20. package/dist/plugin.cjs.js +59 -5
  21. package/dist/plugin.cjs.js.map +1 -1
  22. package/dist/plugin.js +59 -5
  23. package/dist/plugin.js.map +1 -1
  24. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +0 -1
  25. package/ios/Sources/CapacitorUpdaterPlugin/AppHealthTracker.swift +82 -0
  26. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -16
  27. package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
  28. package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
  29. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +2732 -417
  30. package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1191 -363
  31. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
  32. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +37 -16
  33. package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +80 -1
  34. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +438 -39
  35. package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
  36. package/package.json +24 -9
@@ -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
+ }
82
+
83
+ private void onMenuGestureDetected(String gestureName) {
84
+ logger.info(gestureName + " detected");
54
85
 
55
- // Check if shake menu is enabled
56
- if (!plugin.shakeMenuEnabled) {
57
- logger.info("Shake menu is disabled");
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) {
@@ -249,12 +582,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
249
582
  }
250
583
 
251
584
  List<?> channelsRaw = (List<?>) channelsObj;
252
- List<Map<String, Object>> channels = new ArrayList<>();
253
- for (Object item : channelsRaw) {
254
- if (item instanceof Map) {
255
- channels.add((Map<String, Object>) item);
256
- }
257
- }
585
+ List<Map<String, Object>> channels = toChannelList(channelsRaw);
258
586
 
259
587
  if (channels.isEmpty()) {
260
588
  showError("No channels available for self-assignment");
@@ -264,8 +592,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
264
592
  presentChannelPicker(channels);
265
593
  });
266
594
  });
267
- })
268
- .start();
595
+ }).start();
269
596
  } catch (Exception e) {
270
597
  logger.error("Error showing channel selector: " + e.getMessage());
271
598
  isShowing = false;
@@ -273,6 +600,27 @@ public class ShakeMenu implements ShakeDetector.Listener {
273
600
  });
274
601
  }
275
602
 
603
+ private List<Map<String, Object>> toChannelList(List<?> channelsRaw) {
604
+ List<Map<String, Object>> channels = new ArrayList<>();
605
+ for (Object item : channelsRaw) {
606
+ if (!(item instanceof Map<?, ?> rawMap)) {
607
+ continue;
608
+ }
609
+
610
+ Map<String, Object> channel = new java.util.HashMap<>();
611
+ for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
612
+ if (entry.getKey() instanceof String key) {
613
+ channel.put(key, entry.getValue());
614
+ }
615
+ }
616
+
617
+ if (!channel.isEmpty()) {
618
+ channels.add(channel);
619
+ }
620
+ }
621
+ return channels;
622
+ }
623
+
276
624
  private void presentChannelPicker(List<Map<String, Object>> channels) {
277
625
  try {
278
626
  AlertDialog.Builder builder = new AlertDialog.Builder(activity);
@@ -385,6 +733,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
385
733
  new Thread(() -> {
386
734
  final CapgoUpdater updater = plugin.implementation;
387
735
  final Bridge bridge = activity.getBridge();
736
+ final String configDefaultChannel = plugin.getConfig().getString("defaultChannel", "");
388
737
 
389
738
  // Set the channel - respect plugin's allowSetDefaultChannel config
390
739
  updater.setChannel(
@@ -392,6 +741,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
392
741
  updater.editor,
393
742
  "CapacitorUpdater.defaultChannel",
394
743
  plugin.allowSetDefaultChannel,
744
+ configDefaultChannel,
395
745
  (setRes) -> {
396
746
  if (setRes == null) {
397
747
  activity.runOnUiThread(() -> {
@@ -432,12 +782,37 @@ public class ShakeMenu implements ShakeDetector.Listener {
432
782
  }
433
783
 
434
784
  String latestError = getString(latestRes, "error");
785
+ String latestKind = getString(latestRes, "kind");
786
+ String latestMessage = getString(latestRes, "message");
787
+
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";
435
796
 
436
797
  // Handle update errors first (before "no new version" check)
437
- if (latestError != null && !latestError.isEmpty() && !"no_new_version_available".equals(latestError)) {
798
+ if (
799
+ "failed".equals(latestKind) ||
800
+ (latestError != null &&
801
+ !latestError.isEmpty() &&
802
+ !"up_to_date".equals(latestKind) &&
803
+ !"blocked".equals(latestKind))
804
+ ) {
805
+ activity.runOnUiThread(() -> {
806
+ progressDialog.dismiss();
807
+ showError("Channel set to " + channelName + ". Update check failed: " + detail);
808
+ });
809
+ return;
810
+ }
811
+
812
+ if ("blocked".equals(latestKind)) {
438
813
  activity.runOnUiThread(() -> {
439
814
  progressDialog.dismiss();
440
- showError("Channel set to " + channelName + ". Update check failed: " + latestError);
815
+ showError("Channel set to " + channelName + ". Update check blocked: " + detail);
441
816
  });
442
817
  return;
443
818
  }
@@ -445,7 +820,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
445
820
  String latestUrl = getString(latestRes, "url");
446
821
 
447
822
  // Check if there's an actual update available
448
- if ("no_new_version_available".equals(latestError) || latestUrl == null || latestUrl.isEmpty()) {
823
+ if ("up_to_date".equals(latestKind) || latestUrl == null || latestUrl.isEmpty()) {
449
824
  activity.runOnUiThread(() -> {
450
825
  progressDialog.dismiss();
451
826
  showSuccess("Channel set to " + channelName + ". Already on latest version.");
@@ -536,8 +911,7 @@ public class ShakeMenu implements ShakeDetector.Listener {
536
911
  });
537
912
  }
538
913
  );
539
- })
540
- .start();
914
+ }).start();
541
915
  } catch (Exception e) {
542
916
  logger.error("Error selecting channel: " + e.getMessage());
543
917
  isShowing = false;