@capgo/capacitor-updater 7.3.3 → 7.5.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/README.md CHANGED
@@ -252,6 +252,7 @@ CapacitorUpdater can be configured with these options:
252
252
  | **`appId`** | <code>string</code> | Configure the app id for the app in the config. | <code>undefined</code> | 6.0.0 |
253
253
  | **`keepUrlPathAfterReload`** | <code>boolean</code> | Configure the plugin to keep the URL path after a reload. WARNING: When a reload is triggered, 'window.history' will be cleared. | <code>false</code> | 6.8.0 |
254
254
  | **`disableJSLogging`** | <code>boolean</code> | Disable the JavaScript logging of the plugin. if true, the plugin will not log to the JavaScript console. only the native log will be done | <code>false</code> | 7.3.0 |
255
+ | **`shakeMenu`** | <code>boolean</code> | Enable shake gesture to show update menu for debugging/testing purposes | <code>false</code> | 7.5.0 |
255
256
 
256
257
  ### Examples
257
258
 
@@ -285,7 +286,8 @@ In `capacitor.config.json`:
285
286
  "defaultChannel": undefined,
286
287
  "appId": undefined,
287
288
  "keepUrlPathAfterReload": undefined,
288
- "disableJSLogging": undefined
289
+ "disableJSLogging": undefined,
290
+ "shakeMenu": undefined
289
291
  }
290
292
  }
291
293
  }
@@ -326,6 +328,7 @@ const config: CapacitorConfig = {
326
328
  appId: undefined,
327
329
  keepUrlPathAfterReload: undefined,
328
330
  disableJSLogging: undefined,
331
+ shakeMenu: undefined,
329
332
  },
330
333
  },
331
334
  };
@@ -357,6 +360,7 @@ export default config;
357
360
  * [`setChannel(...)`](#setchannel)
358
361
  * [`unsetChannel(...)`](#unsetchannel)
359
362
  * [`getChannel()`](#getchannel)
363
+ * [`listChannels()`](#listchannels)
360
364
  * [`setCustomId(...)`](#setcustomid)
361
365
  * [`getBuiltinVersion()`](#getbuiltinversion)
362
366
  * [`getDeviceId()`](#getdeviceid)
@@ -374,6 +378,8 @@ export default config;
374
378
  * [`addListener('appReady', ...)`](#addlistenerappready-)
375
379
  * [`isAutoUpdateAvailable()`](#isautoupdateavailable)
376
380
  * [`getNextBundle()`](#getnextbundle)
381
+ * [`setShakeMenu(...)`](#setshakemenu)
382
+ * [`isShakeMenuEnabled()`](#isshakemenuenabled)
377
383
  * [Interfaces](#interfaces)
378
384
  * [Type Aliases](#type-aliases)
379
385
 
@@ -678,6 +684,21 @@ Get the channel for this device
678
684
  --------------------
679
685
 
680
686
 
687
+ ### listChannels()
688
+
689
+ ```typescript
690
+ listChannels() => Promise<ListChannelsResult>
691
+ ```
692
+
693
+ List all channels available for this device that allow self-assignment
694
+
695
+ **Returns:** <code>Promise&lt;<a href="#listchannelsresult">ListChannelsResult</a>&gt;</code>
696
+
697
+ **Since:** 7.5.0
698
+
699
+ --------------------
700
+
701
+
681
702
  ### setCustomId(...)
682
703
 
683
704
  ```typescript
@@ -972,6 +993,38 @@ Returns null if no next bundle is set.
972
993
  --------------------
973
994
 
974
995
 
996
+ ### setShakeMenu(...)
997
+
998
+ ```typescript
999
+ setShakeMenu(options: SetShakeMenuOptions) => Promise<void>
1000
+ ```
1001
+
1002
+ Enable or disable the shake menu for debugging/testing purposes
1003
+
1004
+ | Param | Type | Description |
1005
+ | ------------- | ------------------------------------------------------------------- | -------------------------------------------------------- |
1006
+ | **`options`** | <code><a href="#setshakemenuoptions">SetShakeMenuOptions</a></code> | Contains enabled boolean to enable or disable shake menu |
1007
+
1008
+ **Since:** 7.5.0
1009
+
1010
+ --------------------
1011
+
1012
+
1013
+ ### isShakeMenuEnabled()
1014
+
1015
+ ```typescript
1016
+ isShakeMenuEnabled() => Promise<ShakeMenuEnabled>
1017
+ ```
1018
+
1019
+ Get the current state of the shake menu
1020
+
1021
+ **Returns:** <code>Promise&lt;<a href="#shakemenuenabled">ShakeMenuEnabled</a>&gt;</code>
1022
+
1023
+ **Since:** 7.5.0
1024
+
1025
+ --------------------
1026
+
1027
+
975
1028
  ### Interfaces
976
1029
 
977
1030
 
@@ -1019,12 +1072,22 @@ Returns null if no next bundle is set.
1019
1072
  This URL and versions are used to download the bundle from the server, If you use backend all information will be gived by the method getLatest.
1020
1073
  If you don't use backend, you need to provide the URL and version of the bundle. Checksum and sessionKey are required if you encrypted the bundle with the CLI command encrypt, you should receive them as result of the command.
1021
1074
 
1022
- | Prop | Type | Description | Default | Since |
1023
- | ---------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----- |
1024
- | **`url`** | <code>string</code> | The URL of the bundle zip file (e.g: dist.zip) to be downloaded. (This can be any URL. E.g: Amazon S3, a GitHub tag, any other place you've hosted your bundle.) | | |
1025
- | **`version`** | <code>string</code> | The version code/name of this bundle/version | | |
1026
- | **`sessionKey`** | <code>string</code> | The session key for the update, when the bundle is encrypted with a session key | <code>undefined</code> | 4.0.0 |
1027
- | **`checksum`** | <code>string</code> | The checksum for the update, it should be in sha256 and encrypted with private key if the bundle is encrypted | <code>undefined</code> | 4.0.0 |
1075
+ | Prop | Type | Description | Default | Since |
1076
+ | ---------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----- |
1077
+ | **`url`** | <code>string</code> | The URL of the bundle zip file (e.g: dist.zip) to be downloaded. (This can be any URL. E.g: Amazon S3, a GitHub tag, any other place you've hosted your bundle.) | | |
1078
+ | **`version`** | <code>string</code> | The version code/name of this bundle/version | | |
1079
+ | **`sessionKey`** | <code>string</code> | The session key for the update, when the bundle is encrypted with a session key | <code>undefined</code> | 4.0.0 |
1080
+ | **`checksum`** | <code>string</code> | The checksum for the update, it should be in sha256 and encrypted with private key if the bundle is encrypted | <code>undefined</code> | 4.0.0 |
1081
+ | **`manifest`** | <code>ManifestEntry[]</code> | The manifest for multi-file downloads | <code>undefined</code> | 6.1.0 |
1082
+
1083
+
1084
+ #### ManifestEntry
1085
+
1086
+ | Prop | Type |
1087
+ | ------------------ | --------------------------- |
1088
+ | **`file_name`** | <code>string \| null</code> |
1089
+ | **`file_hash`** | <code>string \| null</code> |
1090
+ | **`download_url`** | <code>string \| null</code> |
1028
1091
 
1029
1092
 
1030
1093
  #### BundleId
@@ -1093,15 +1156,6 @@ If you don't use backend, you need to provide the URL and version of the bundle.
1093
1156
  | **`manifest`** | <code>ManifestEntry[]</code> | | 6.1 |
1094
1157
 
1095
1158
 
1096
- #### ManifestEntry
1097
-
1098
- | Prop | Type |
1099
- | ------------------ | --------------------------- |
1100
- | **`file_name`** | <code>string \| null</code> |
1101
- | **`file_hash`** | <code>string \| null</code> |
1102
- | **`download_url`** | <code>string \| null</code> |
1103
-
1104
-
1105
1159
  #### GetLatestOptions
1106
1160
 
1107
1161
  | Prop | Type | Description | Default | Since |
@@ -1144,6 +1198,23 @@ If you don't use backend, you need to provide the URL and version of the bundle.
1144
1198
  | **`allowSet`** | <code>boolean</code> | | |
1145
1199
 
1146
1200
 
1201
+ #### ListChannelsResult
1202
+
1203
+ | Prop | Type | Description | Since |
1204
+ | -------------- | -------------------------- | -------------------------- | ----- |
1205
+ | **`channels`** | <code>ChannelInfo[]</code> | List of available channels | 7.5.0 |
1206
+
1207
+
1208
+ #### ChannelInfo
1209
+
1210
+ | Prop | Type | Description | Since |
1211
+ | -------------------- | -------------------- | ----------------------------------------------- | ----- |
1212
+ | **`id`** | <code>string</code> | The channel ID | 7.5.0 |
1213
+ | **`name`** | <code>string</code> | The channel name | 7.5.0 |
1214
+ | **`public`** | <code>boolean</code> | Whether this is a public channel | 7.5.0 |
1215
+ | **`allow_self_set`** | <code>boolean</code> | Whether devices can self-assign to this channel | 7.5.0 |
1216
+
1217
+
1147
1218
  #### SetCustomIdOptions
1148
1219
 
1149
1220
  | Prop | Type |
@@ -1251,6 +1322,20 @@ If you don't use backend, you need to provide the URL and version of the bundle.
1251
1322
  | **`available`** | <code>boolean</code> |
1252
1323
 
1253
1324
 
1325
+ #### SetShakeMenuOptions
1326
+
1327
+ | Prop | Type |
1328
+ | ------------- | -------------------- |
1329
+ | **`enabled`** | <code>boolean</code> |
1330
+
1331
+
1332
+ #### ShakeMenuEnabled
1333
+
1334
+ | Prop | Type |
1335
+ | ------------- | -------------------- |
1336
+ | **`enabled`** | <code>boolean</code> |
1337
+
1338
+
1254
1339
  ### Type Aliases
1255
1340
 
1256
1341
 
@@ -60,7 +60,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
60
60
  private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
61
61
  private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
62
62
 
63
- private final String PLUGIN_VERSION = "7.3.3";
63
+ private final String PLUGIN_VERSION = "7.5.1";
64
64
  private static final String DELAY_CONDITION_PREFERENCES = "";
65
65
 
66
66
  private SharedPreferences.Editor editor;
@@ -78,6 +78,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
78
78
  private Thread backgroundTask;
79
79
  private Boolean taskRunning = false;
80
80
  private Boolean keepUrlPathAfterReload = false;
81
+ Boolean shakeMenuEnabled = false;
81
82
 
82
83
  private Boolean isPreviousMainActivity = true;
83
84
 
@@ -91,6 +92,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
91
92
 
92
93
  private DelayUpdateUtils delayUpdateUtils;
93
94
 
95
+ private ShakeMenu shakeMenu;
96
+
94
97
  private JSObject mapToJSObject(Map<String, Object> map) {
95
98
  JSObject jsObject = new JSObject();
96
99
  for (Map.Entry<String, Object> entry : map.entrySet()) {
@@ -224,6 +227,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
224
227
  this.appReadyTimeout = this.getConfig().getInt("appReadyTimeout", 10000);
225
228
  this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
226
229
  this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
230
+ this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
227
231
  boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
228
232
 
229
233
  this.implementation.autoReset();
@@ -438,7 +442,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
438
442
  CapacitorUpdaterPlugin.this.implementation.unsetChannel(res -> {
439
443
  JSObject jsRes = mapToJSObject(res);
440
444
  if (jsRes.has("error")) {
441
- call.reject(jsRes.getString("error"));
445
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
446
+ String errorCode = jsRes.getString("error");
447
+
448
+ JSObject errorObj = new JSObject();
449
+ errorObj.put("message", errorMessage);
450
+ errorObj.put("error", errorCode);
451
+
452
+ call.reject(errorMessage, "UNSETCHANNEL_FAILED", null, errorObj);
442
453
  } else {
443
454
  if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
444
455
  logger.info("Calling autoupdater after channel change!");
@@ -461,7 +472,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
461
472
 
462
473
  if (channel == null) {
463
474
  logger.error("setChannel called without channel");
464
- call.reject("setChannel called without channel");
475
+ JSObject errorObj = new JSObject();
476
+ errorObj.put("message", "setChannel called without channel");
477
+ errorObj.put("error", "missing_parameter");
478
+ call.reject("setChannel called without channel", "SETCHANNEL_INVALID_PARAMS", null, errorObj);
465
479
  return;
466
480
  }
467
481
  try {
@@ -470,7 +484,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
470
484
  CapacitorUpdaterPlugin.this.implementation.setChannel(channel, res -> {
471
485
  JSObject jsRes = mapToJSObject(res);
472
486
  if (jsRes.has("error")) {
473
- call.reject(jsRes.getString("error"));
487
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
488
+ String errorCode = jsRes.getString("error");
489
+
490
+ JSObject errorObj = new JSObject();
491
+ errorObj.put("message", errorMessage);
492
+ errorObj.put("error", errorCode);
493
+
494
+ call.reject(errorMessage, "SETCHANNEL_FAILED", null, errorObj);
474
495
  } else {
475
496
  if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
476
497
  logger.info("Calling autoupdater after channel change!");
@@ -494,7 +515,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
494
515
  CapacitorUpdaterPlugin.this.implementation.getChannel(res -> {
495
516
  JSObject jsRes = mapToJSObject(res);
496
517
  if (jsRes.has("error")) {
497
- call.reject(jsRes.getString("error"));
518
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
519
+ String errorCode = jsRes.getString("error");
520
+
521
+ JSObject errorObj = new JSObject();
522
+ errorObj.put("message", errorMessage);
523
+ errorObj.put("error", errorCode);
524
+
525
+ call.reject(errorMessage, "GETCHANNEL_FAILED", null, errorObj);
498
526
  } else {
499
527
  call.resolve(jsRes);
500
528
  }
@@ -506,12 +534,40 @@ public class CapacitorUpdaterPlugin extends Plugin {
506
534
  }
507
535
  }
508
536
 
537
+ @PluginMethod
538
+ public void listChannels(final PluginCall call) {
539
+ try {
540
+ logger.info("listChannels");
541
+ startNewThread(() ->
542
+ CapacitorUpdaterPlugin.this.implementation.listChannels(res -> {
543
+ JSObject jsRes = mapToJSObject(res);
544
+ if (jsRes.has("error")) {
545
+ String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
546
+ String errorCode = jsRes.getString("error");
547
+
548
+ JSObject errorObj = new JSObject();
549
+ errorObj.put("message", errorMessage);
550
+ errorObj.put("error", errorCode);
551
+
552
+ call.reject(errorMessage, "LISTCHANNELS_FAILED", null, errorObj);
553
+ } else {
554
+ call.resolve(jsRes);
555
+ }
556
+ })
557
+ );
558
+ } catch (final Exception e) {
559
+ logger.error("Failed to listChannels: " + e.getMessage());
560
+ call.reject("Failed to listChannels", e);
561
+ }
562
+ }
563
+
509
564
  @PluginMethod
510
565
  public void download(final PluginCall call) {
511
566
  final String url = call.getString("url");
512
567
  final String version = call.getString("version");
513
568
  final String sessionKey = call.getString("sessionKey", "");
514
569
  final String checksum = call.getString("checksum", "");
570
+ final JSONArray manifest = call.getData().optJSONArray("manifest");
515
571
  if (url == null) {
516
572
  logger.error("Download called without url");
517
573
  call.reject("Download called without url");
@@ -526,7 +582,19 @@ public class CapacitorUpdaterPlugin extends Plugin {
526
582
  logger.info("Downloading " + url);
527
583
  startNewThread(() -> {
528
584
  try {
529
- final BundleInfo downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
585
+ final BundleInfo downloaded;
586
+ if (manifest != null) {
587
+ // For manifest downloads, we need to handle this asynchronously
588
+ // since there's no synchronous downloadManifest method in Java
589
+ CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest);
590
+ // Return immediately with a pending status - the actual result will come via listeners
591
+ final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
592
+ downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
593
+ call.resolve(mapToJSObject(downloaded.toJSONMap()));
594
+ return;
595
+ } else {
596
+ downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
597
+ }
530
598
  if (downloaded.isErrorStatus()) {
531
599
  throw new RuntimeException("Download failed: " + downloaded.getStatus());
532
600
  } else {
@@ -1315,6 +1383,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
1315
1383
  }
1316
1384
  logger.info("onActivityStarted " + getActivity().getClass().getName());
1317
1385
  isPreviousMainActivity = true;
1386
+
1387
+ // Initialize shake menu if enabled and activity is BridgeActivity
1388
+ if (shakeMenuEnabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
1389
+ try {
1390
+ shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
1391
+ logger.info("Shake menu initialized");
1392
+ } catch (Exception e) {
1393
+ logger.error("Failed to initialize shake menu: " + e.getMessage());
1394
+ }
1395
+ }
1318
1396
  }
1319
1397
 
1320
1398
  @Override
@@ -1346,5 +1424,61 @@ public class CapacitorUpdaterPlugin extends Plugin {
1346
1424
  if (counterActivityCreate == 0) {
1347
1425
  this.appKilled();
1348
1426
  }
1427
+
1428
+ // Clean up shake menu
1429
+ if (shakeMenu != null) {
1430
+ try {
1431
+ shakeMenu.stop();
1432
+ shakeMenu = null;
1433
+ logger.info("Shake menu cleaned up");
1434
+ } catch (Exception e) {
1435
+ logger.error("Failed to clean up shake menu: " + e.getMessage());
1436
+ }
1437
+ }
1438
+ }
1439
+
1440
+ @PluginMethod
1441
+ public void setShakeMenu(final PluginCall call) {
1442
+ final Boolean enabled = call.getBoolean("enabled");
1443
+ if (enabled == null) {
1444
+ logger.error("setShakeMenu called without enabled parameter");
1445
+ call.reject("setShakeMenu called without enabled parameter");
1446
+ return;
1447
+ }
1448
+
1449
+ this.shakeMenuEnabled = enabled;
1450
+ logger.info("Shake menu " + (enabled ? "enabled" : "disabled"));
1451
+
1452
+ // Manage shake menu instance based on enabled state
1453
+ if (enabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
1454
+ try {
1455
+ shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
1456
+ logger.info("Shake menu initialized");
1457
+ } catch (Exception e) {
1458
+ logger.error("Failed to initialize shake menu: " + e.getMessage());
1459
+ }
1460
+ } else if (!enabled && shakeMenu != null) {
1461
+ try {
1462
+ shakeMenu.stop();
1463
+ shakeMenu = null;
1464
+ logger.info("Shake menu stopped");
1465
+ } catch (Exception e) {
1466
+ logger.error("Failed to stop shake menu: " + e.getMessage());
1467
+ }
1468
+ }
1469
+
1470
+ call.resolve();
1471
+ }
1472
+
1473
+ @PluginMethod
1474
+ public void isShakeMenuEnabled(final PluginCall call) {
1475
+ try {
1476
+ final JSObject ret = new JSObject();
1477
+ ret.put("enabled", this.shakeMenuEnabled);
1478
+ call.resolve(ret);
1479
+ } catch (final Exception e) {
1480
+ logger.error("Could not get shake menu status " + e.getMessage());
1481
+ call.reject("Could not get shake menu status", e);
1482
+ }
1349
1483
  }
1350
1484
  }
@@ -35,6 +35,7 @@ import java.util.Objects;
35
35
  import java.util.zip.ZipEntry;
36
36
  import java.util.zip.ZipInputStream;
37
37
  import okhttp3.*;
38
+ import okhttp3.HttpUrl;
38
39
  import org.json.JSONArray;
39
40
  import org.json.JSONException;
40
41
  import org.json.JSONObject;
@@ -125,7 +126,7 @@ public class CapgoUpdater {
125
126
 
126
127
  void notifyListeners(final String id, final Map<String, Object> res) {}
127
128
 
128
- private String randomString() {
129
+ public String randomString() {
129
130
  final StringBuilder sb = new StringBuilder(10);
130
131
  for (int i = 0; i < 10; i++) sb.append(AB.charAt(rnd.nextInt(AB.length())));
131
132
  return sb.toString();
@@ -699,6 +700,16 @@ public class CapgoUpdater {
699
700
  assert responseBody != null;
700
701
  String responseData = responseBody.string();
701
702
  JSONObject jsonResponse = new JSONObject(responseData);
703
+
704
+ // Check for server-side errors first
705
+ if (jsonResponse.has("error")) {
706
+ Map<String, Object> retError = new HashMap<>();
707
+ retError.put("message", jsonResponse.getString("error"));
708
+ retError.put("error", "server_error");
709
+ callback.callback(retError);
710
+ return;
711
+ }
712
+
702
713
  Map<String, Object> ret = new HashMap<>();
703
714
 
704
715
  Iterator<String> keys = jsonResponse.keys();
@@ -798,6 +809,16 @@ public class CapgoUpdater {
798
809
  assert responseBody != null;
799
810
  String responseData = responseBody.string();
800
811
  JSONObject jsonResponse = new JSONObject(responseData);
812
+
813
+ // Check for server-side errors first
814
+ if (jsonResponse.has("error")) {
815
+ Map<String, Object> retError = new HashMap<>();
816
+ retError.put("message", jsonResponse.getString("error"));
817
+ retError.put("error", "server_error");
818
+ callback.callback(retError);
819
+ return;
820
+ }
821
+
801
822
  Map<String, Object> ret = new HashMap<>();
802
823
 
803
824
  Iterator<String> keys = jsonResponse.keys();
@@ -912,6 +933,16 @@ public class CapgoUpdater {
912
933
  assert responseBody != null;
913
934
  String responseData = responseBody.string();
914
935
  JSONObject jsonResponse = new JSONObject(responseData);
936
+
937
+ // Check for server-side errors first
938
+ if (jsonResponse.has("error")) {
939
+ Map<String, Object> retError = new HashMap<>();
940
+ retError.put("message", jsonResponse.getString("error"));
941
+ retError.put("error", "server_error");
942
+ callback.callback(retError);
943
+ return;
944
+ }
945
+
915
946
  Map<String, Object> ret = new HashMap<>();
916
947
 
917
948
  Iterator<String> keys = jsonResponse.keys();
@@ -934,6 +965,109 @@ public class CapgoUpdater {
934
965
  );
935
966
  }
936
967
 
968
+ public void listChannels(final Callback callback) {
969
+ String channelUrl = this.channelUrl;
970
+ if (channelUrl == null || channelUrl.isEmpty()) {
971
+ logger.error("Channel URL is not set");
972
+ final Map<String, Object> retError = new HashMap<>();
973
+ retError.put("message", "Channel URL is not set");
974
+ retError.put("error", "missing_config");
975
+ callback.callback(retError);
976
+ return;
977
+ }
978
+
979
+ // Auto-detect values
980
+ String appId = this.appId;
981
+ String platform = "android";
982
+ boolean isEmulator = this.isEmulator();
983
+ boolean isProd = this.isProd();
984
+
985
+ // Build URL with query parameters
986
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(channelUrl).newBuilder();
987
+ urlBuilder.addQueryParameter("app_id", appId);
988
+ urlBuilder.addQueryParameter("platform", platform);
989
+ urlBuilder.addQueryParameter("is_emulator", String.valueOf(isEmulator));
990
+ urlBuilder.addQueryParameter("is_prod", String.valueOf(isProd));
991
+
992
+ Request request = new Request.Builder().url(urlBuilder.build()).get().build();
993
+
994
+ client
995
+ .newCall(request)
996
+ .enqueue(
997
+ new okhttp3.Callback() {
998
+ @Override
999
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
1000
+ Map<String, Object> retError = new HashMap<>();
1001
+ retError.put("message", "Request failed: " + e.getMessage());
1002
+ retError.put("error", "network_error");
1003
+ callback.callback(retError);
1004
+ }
1005
+
1006
+ @Override
1007
+ public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
1008
+ try (ResponseBody responseBody = response.body()) {
1009
+ if (!response.isSuccessful()) {
1010
+ Map<String, Object> retError = new HashMap<>();
1011
+ retError.put("message", "Server error: " + response.code());
1012
+ retError.put("error", "response_error");
1013
+ callback.callback(retError);
1014
+ return;
1015
+ }
1016
+
1017
+ assert responseBody != null;
1018
+ String data = responseBody.string();
1019
+
1020
+ try {
1021
+ Map<String, Object> ret = new HashMap<>();
1022
+
1023
+ try {
1024
+ // Try to parse as direct array first
1025
+ JSONArray channelsJson = new JSONArray(data);
1026
+ List<Map<String, Object>> channelsList = new ArrayList<>();
1027
+
1028
+ for (int i = 0; i < channelsJson.length(); i++) {
1029
+ JSONObject channelJson = channelsJson.getJSONObject(i);
1030
+ Map<String, Object> channel = new HashMap<>();
1031
+ channel.put("id", channelJson.optString("id", ""));
1032
+ channel.put("name", channelJson.optString("name", ""));
1033
+ channel.put("public", channelJson.optBoolean("public", false));
1034
+ channel.put("allow_self_set", channelJson.optBoolean("allow_self_set", false));
1035
+ channelsList.add(channel);
1036
+ }
1037
+
1038
+ // Wrap in channels object for JS API
1039
+ ret.put("channels", channelsList);
1040
+
1041
+ logger.info("Channels listed successfully");
1042
+ callback.callback(ret);
1043
+ } catch (JSONException arrayException) {
1044
+ // If not an array, try to parse as error object
1045
+ try {
1046
+ JSONObject json = new JSONObject(data);
1047
+ if (json.has("error")) {
1048
+ Map<String, Object> retError = new HashMap<>();
1049
+ retError.put("message", json.getString("error"));
1050
+ retError.put("error", "server_error");
1051
+ callback.callback(retError);
1052
+ return;
1053
+ }
1054
+ } catch (JSONException objException) {
1055
+ // If neither array nor object, throw parse error
1056
+ throw arrayException;
1057
+ }
1058
+ }
1059
+ } catch (JSONException e) {
1060
+ Map<String, Object> retError = new HashMap<>();
1061
+ retError.put("message", "JSON parse error: " + e.getMessage());
1062
+ retError.put("error", "parse_error");
1063
+ callback.callback(retError);
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+ );
1069
+ }
1070
+
937
1071
  public void sendStats(final String action) {
938
1072
  this.sendStats(action, this.getCurrentBundle().getVersionName());
939
1073
  }
@@ -0,0 +1,72 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ package ee.forgr.capacitor_updater;
8
+
9
+ import android.hardware.Sensor;
10
+ import android.hardware.SensorEvent;
11
+ import android.hardware.SensorEventListener;
12
+ import android.hardware.SensorManager;
13
+
14
+ public class ShakeDetector implements SensorEventListener {
15
+
16
+ public interface Listener {
17
+ void onShakeDetected();
18
+ }
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)
22
+
23
+ private Listener listener;
24
+ private SensorManager sensorManager;
25
+ private Sensor accelerometer;
26
+ private long lastShakeTime = 0;
27
+
28
+ public ShakeDetector(Listener listener) {
29
+ this.listener = listener;
30
+ }
31
+
32
+ public void start(SensorManager sensorManager) {
33
+ this.sensorManager = sensorManager;
34
+ this.accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
35
+
36
+ if (accelerometer != null) {
37
+ sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
38
+ }
39
+ }
40
+
41
+ public void stop() {
42
+ if (sensorManager != null) {
43
+ sensorManager.unregisterListener(this);
44
+ }
45
+ }
46
+
47
+ @Override
48
+ public void onSensorChanged(SensorEvent event) {
49
+ if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
50
+ float x = event.values[0];
51
+ float y = event.values[1];
52
+ float z = event.values[2];
53
+
54
+ // Calculate the acceleration magnitude (excluding gravity)
55
+ float acceleration = (float) Math.sqrt(x * x + y * y + z * z) - SensorManager.GRAVITY_EARTH;
56
+
57
+ // Check if acceleration exceeds threshold and enough time has passed
58
+ long currentTime = System.currentTimeMillis();
59
+ if (Math.abs(acceleration) > SHAKE_THRESHOLD && currentTime - lastShakeTime > SHAKE_TIMEOUT) {
60
+ lastShakeTime = currentTime;
61
+ if (listener != null) {
62
+ listener.onShakeDetected();
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ @Override
69
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
70
+ // Not needed for shake detection
71
+ }
72
+ }