@wisdomgarden/capacitor-plugin-beacon 0.0.1 → 0.0.3

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 (22) hide show
  1. package/CHANGE-LOG.md +13 -0
  2. package/android/src/main/java/com/wisdomgarden/mobile/beacon/Beacon.java +100 -58
  3. package/android/src/main/java/com/wisdomgarden/mobile/beacon/BeaconUtils.java +110 -13
  4. package/ios/Plugin/BeaconUtils.swift +108 -40
  5. package/ios/Plugin/Plugin.swift +109 -128
  6. package/package.json +9 -3
  7. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  8. package/android/gradle/wrapper/gradle-wrapper.properties +0 -5
  9. package/android/gradle.properties +0 -24
  10. package/android/gradlew +0 -188
  11. package/android/gradlew.bat +0 -100
  12. package/android/proguard-rules.pro +0 -21
  13. package/android/settings.gradle +0 -2
  14. package/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +0 -26
  15. package/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +0 -18
  16. package/ios/Plugin.xcodeproj/xcuserdata/peixinliu.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  17. package/ios/Plugin.xcworkspace/xcuserdata/peixinliu.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  18. package/ios/Pods/Pods.xcodeproj/xcuserdata/peixinliu.xcuserdatad/xcschemes/Capacitor.xcscheme +0 -58
  19. package/ios/Pods/Pods.xcodeproj/xcuserdata/peixinliu.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +0 -58
  20. package/ios/Pods/Pods.xcodeproj/xcuserdata/peixinliu.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +0 -58
  21. package/ios/Pods/Pods.xcodeproj/xcuserdata/peixinliu.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +0 -58
  22. package/ios/Pods/Pods.xcodeproj/xcuserdata/peixinliu.xcuserdatad/xcschemes/xcschememanagement.plist +0 -39
package/CHANGE-LOG.md ADDED
@@ -0,0 +1,13 @@
1
+ ## 0.0.2 (2025-12-23)
2
+
3
+ ### Bug Fixes
4
+ - Fix (Android): Resolved broadcast failure `ADVERTISE_FAILED_DATA_TOO_LARGE`.
5
+ - Fix (Android/iOS): Resolved issue where stale/historical messages were received due to system caching.
6
+ - Optimization: Standardized logToJsWithTag output format and logical sequence `[SEQ-XX]` .
7
+
8
+ ## 0.0.3 (2025-12-23)
9
+
10
+ ### Optimize code
11
+ - Logic Optimization: Aligned cross-platform encoding and improved variable-length payload handling.
12
+ - Code Fixes: Resolved naming conflicts and indexing safety issues.
13
+ - Log Refinement: Simplified logs with concise method tags.
@@ -10,10 +10,12 @@ import android.bluetooth.le.BluetoothLeAdvertiser;
10
10
  import android.bluetooth.le.BluetoothLeScanner;
11
11
  import android.bluetooth.le.ScanCallback;
12
12
  import android.bluetooth.le.ScanFilter;
13
+ import android.bluetooth.le.ScanRecord;
13
14
  import android.bluetooth.le.ScanResult;
14
15
  import android.bluetooth.le.ScanSettings;
15
16
  import android.content.Context;
16
17
  import android.os.ParcelUuid;
18
+ import android.util.Log;
17
19
 
18
20
  import com.getcapacitor.JSObject;
19
21
  import com.getcapacitor.NativePlugin;
@@ -25,22 +27,33 @@ import java.util.ArrayList;
25
27
  import java.util.List;
26
28
  import java.util.Map;
27
29
 
28
- @NativePlugin()
30
+ @NativePlugin
29
31
  public class Beacon extends Plugin {
30
- private static final String TAG = "Beacon";
31
- private static final String WG_UUID = "1ed18c59-a7cc-4752-96b0-51d77a456584";
32
+
33
+ private static final String TAG = "WisdomGardenBeacon";
34
+
35
+ private static final int MANUFACTURER_ID = 0xFFFF;
32
36
 
33
37
  private BluetoothAdapter bluetoothAdapter;
34
38
  private BluetoothLeAdvertiser advertiser;
35
39
  private BluetoothLeScanner scanner;
36
40
  private boolean isBroadcasting = false;
37
41
  private boolean isMonitoring = false;
38
- private int rollcallId = 0;
39
- private String nonce = "";
40
42
  private String message = "";
41
43
 
44
+ /**
45
+ * Helper to send tagged logs to JS bridge with sequence prefix.
46
+ * Consolidates bridge communication and system logging.
47
+ */
48
+ private void logToJsWithTag(String seq, String msg) {
49
+ String formattedMessage = (seq != null ? " " + seq : "") + ": " + msg;
50
+ bridge.logToJs("[" + TAG + "]" + formattedMessage);
51
+ Log.i(TAG, formattedMessage);
52
+ }
53
+
42
54
  @PluginMethod
43
55
  public void initialize(PluginCall call) {
56
+ Log.i(TAG, "[SEQ-0] Plugin Initialized");
44
57
  call.resolve();
45
58
  }
46
59
 
@@ -51,11 +64,13 @@ public class Beacon extends Plugin {
51
64
  }
52
65
 
53
66
  if (bluetoothAdapter == null) {
67
+ Log.e(TAG, "Bluetooth hardware not supported on this device");
54
68
  call.reject("Bluetooth is not supported on this device");
55
69
  return false;
56
70
  }
57
71
 
58
72
  if (!bluetoothAdapter.isEnabled()) {
73
+ Log.w(TAG, "Bluetooth is currently disabled");
59
74
  call.reject("Bluetooth is not enabled");
60
75
  return false;
61
76
  }
@@ -65,40 +80,41 @@ public class Beacon extends Plugin {
65
80
 
66
81
  @PluginMethod
67
82
  public void startBroadcasting(PluginCall call) {
68
- bridge.logToJs("[Debug] startBroadcasting 11");
83
+ logToJsWithTag("[SEQ-10]", "startBroadcasting invoked");
69
84
 
70
- if (call.getString("message") != null && call.getString("message").length() == BeaconUtils.getMessageMaxLength()) {
71
- message = call.getString("message");
85
+ String inputMsg = call.getString("message");
86
+ if (inputMsg != null && inputMsg.length() == BeaconUtils.getMessageMaxLength()) {
87
+ message = inputMsg;
72
88
  } else {
73
- if (call.getInt("rollcallId") == null) {
89
+ Integer rId = call.getInt("rollcallId");
90
+ String nonce = call.getString("nonce");
91
+ if (rId == null) {
92
+ Log.e(TAG, "Broadcasting failed: Missing rollcallId");
74
93
  call.reject("rollcallId is required");
75
94
  return;
76
95
  }
77
- if (call.getString("nonce") == null) {
96
+ if (nonce == null) {
97
+ Log.e(TAG, "Broadcasting failed: Missing nonce");
78
98
  call.reject("nonce is required");
79
99
  return;
80
100
  }
81
101
 
82
- rollcallId = call.getInt("rollcallId");
83
- nonce = call.getString("nonce");
84
-
85
- message = BeaconUtils.buildMessage(rollcallId, nonce);
102
+ message = BeaconUtils.buildMessage(rId, nonce);
86
103
  }
87
-
88
- bridge.logToJs("[Debug] startBroadcasting 11.1");
104
+ logToJsWithTag("[SEQ-11]", "Payload prepared: " + message);
89
105
 
90
106
  if (isBroadcasting) {
107
+ logToJsWithTag("[SEQ-12]", "Already broadcasting, skipping");
91
108
  call.resolve();
92
109
  return;
93
110
  }
94
- bridge.logToJs("[Debug] startBroadcasting 11.2");
95
111
 
96
112
  if (!checkBluetoothEnabled(call)) {
97
113
  return;
98
114
  }
99
115
 
100
116
  startAdvertising();
101
- bridge.logToJs("[Debug] startBroadcasting 11.3");
117
+ logToJsWithTag("[SEQ-16]", "startBroadcasting logic completed");
102
118
 
103
119
  call.resolve();
104
120
  }
@@ -106,6 +122,7 @@ public class Beacon extends Plugin {
106
122
  @PluginMethod
107
123
  @SuppressLint("MissingPermission")
108
124
  public void stopBroadcasting(PluginCall call) {
125
+ logToJsWithTag("[SEQ-20]", "stopBroadcasting called");
109
126
  if (advertiser != null) {
110
127
  advertiser.stopAdvertising(advertiseCallback);
111
128
  advertiser = null;
@@ -118,19 +135,19 @@ public class Beacon extends Plugin {
118
135
 
119
136
  @PluginMethod
120
137
  public void startMonitoring(PluginCall call) {
121
- bridge.logToJs("[Debug] startMonitoring 21");
138
+ logToJsWithTag("[SEQ-30]", "startMonitoring invoked");
122
139
  if (isMonitoring) {
140
+ logToJsWithTag("[SEQ-31]", "Already monitoring, skipping");
123
141
  call.resolve();
124
142
  return;
125
143
  }
126
- bridge.logToJs("[Debug] startMonitoring 21.1");
127
144
 
128
145
  if (!checkBluetoothEnabled(call)) {
129
146
  return;
130
147
  }
131
148
 
132
149
  scanForPeripherals();
133
- bridge.logToJs("[Debug] startMonitoring 21.3");
150
+ logToJsWithTag("[SEQ-36]", "startMonitoring logic completed");
134
151
 
135
152
  call.resolve();
136
153
  }
@@ -138,6 +155,7 @@ public class Beacon extends Plugin {
138
155
  @PluginMethod
139
156
  @SuppressLint("MissingPermission")
140
157
  public void stopMonitoring(PluginCall call) {
158
+ logToJsWithTag("[SEQ-40]", "stopMonitoring called");
141
159
  if (scanner != null) {
142
160
  scanner.stopScan(scanCallback);
143
161
  scanner = null;
@@ -149,6 +167,7 @@ public class Beacon extends Plugin {
149
167
  @PluginMethod
150
168
  @SuppressLint("MissingPermission")
151
169
  public void cleanup(PluginCall call) {
170
+ Log.i(TAG, "[SEQ-90] Cleanup: Resetting all states");
152
171
  if (advertiser != null) {
153
172
  advertiser.stopAdvertising(advertiseCallback);
154
173
  advertiser = null;
@@ -165,8 +184,6 @@ public class Beacon extends Plugin {
165
184
  isMonitoring = false;
166
185
 
167
186
  message = "";
168
- nonce = "";
169
- rollcallId = 0;
170
187
 
171
188
  removeAllListeners(call);
172
189
 
@@ -176,6 +193,7 @@ public class Beacon extends Plugin {
176
193
  @PluginMethod
177
194
  public void parseMessage(PluginCall call) {
178
195
  String message = call.getString("message");
196
+ Log.v(TAG, "Parsing message: " + message);
179
197
  Map<String, Object> result = BeaconUtils.parseMessage(message);
180
198
  JSObject jsResult = new JSObject();
181
199
  jsResult.put("rollcallId", result.get("rollcallId"));
@@ -185,7 +203,7 @@ public class Beacon extends Plugin {
185
203
 
186
204
  @SuppressLint("MissingPermission")
187
205
  private void startAdvertising() {
188
- bridge.logToJs("[Debug] startAdvertising 12");
206
+ logToJsWithTag("[SEQ-13]", "Initialising LE Advertiser");
189
207
  if (advertiser == null) {
190
208
  advertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
191
209
  }
@@ -193,7 +211,7 @@ public class Beacon extends Plugin {
193
211
  if (isBroadcasting) {
194
212
  return;
195
213
  }
196
- bridge.logToJs("[Debug] startAdvertising broadcasting 13");
214
+ logToJsWithTag("[SEQ-13-1]", "Preparing packet with message: " + message);
197
215
  isBroadcasting = true;
198
216
 
199
217
  AdvertiseSettings settings = new AdvertiseSettings.Builder()
@@ -203,24 +221,22 @@ public class Beacon extends Plugin {
203
221
  .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
204
222
  .build();
205
223
 
206
-
207
- bluetoothAdapter.setName(message);
208
- bridge.logToJs("[Debug] startAdvertising broadcasting 13.1 " + message + " " + message.length());
224
+ byte[] payload = BeaconUtils.addPrefixToBytes(message);
209
225
 
210
226
  AdvertiseData advertiseData = new AdvertiseData.Builder()
211
- .setIncludeDeviceName(true)
227
+ .setIncludeDeviceName(false)
212
228
  .setIncludeTxPowerLevel(false)
213
- .addServiceUuid(new ParcelUuid(java.util.UUID.fromString(WG_UUID)))
229
+ .addServiceUuid(new ParcelUuid(java.util.UUID.fromString(BeaconUtils.WG_UUID)))
230
+ .addManufacturerData(MANUFACTURER_ID, payload)
214
231
  .build();
215
232
 
216
- bridge.logToJs("[Debug] startAdvertising broadcasting 14");
233
+ logToJsWithTag("[SEQ-14]", "Calling advertiser.startAdvertising");
217
234
  advertiser.startAdvertising(settings, advertiseData, advertiseCallback);
218
- bridge.logToJs("[Debug] startAdvertising broadcasting 15");
219
235
  }
220
236
 
221
237
  @SuppressLint("MissingPermission")
222
238
  private void scanForPeripherals() {
223
- bridge.logToJs("[Debug] scanForPeripherals 22");
239
+ logToJsWithTag("[SEQ-32]", "Initialising LE Scanner");
224
240
  if (scanner == null) {
225
241
  scanner = bluetoothAdapter.getBluetoothLeScanner();
226
242
  }
@@ -228,30 +244,24 @@ public class Beacon extends Plugin {
228
244
  if (isMonitoring) {
229
245
  return;
230
246
  }
231
- bridge.logToJs("[Debug] scanForPeripherals monitoring 23");
247
+
232
248
  isMonitoring = true;
233
249
 
234
- ScanSettings settings = new ScanSettings.Builder()
235
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
236
- .setReportDelay(0)
237
- .build();
250
+ ScanSettings settings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).setReportDelay(0).build();
238
251
 
239
252
  List<ScanFilter> filters = new ArrayList<>();
240
- ScanFilter filter = new ScanFilter.Builder()
241
- .setServiceUuid(new ParcelUuid(java.util.UUID.fromString(WG_UUID)))
242
- .build();
253
+ ScanFilter filter = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(java.util.UUID.fromString(BeaconUtils.WG_UUID))).build();
243
254
  filters.add(filter);
244
255
 
245
- bridge.logToJs("[Debug] scanForPeripherals monitoring 24");
256
+ logToJsWithTag("[SEQ-33]", "Calling scanner.startScan");
246
257
  scanner.startScan(filters, settings, scanCallback);
247
- bridge.logToJs("[Debug] scanForPeripherals monitoring 25");
248
258
  }
249
259
 
250
260
  private final AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
251
261
  @Override
252
262
  public void onStartSuccess(AdvertiseSettings settingsInEffect) {
253
263
  super.onStartSuccess(settingsInEffect);
254
- bridge.logToJs("[Beacon] Peripheral manager is powered on");
264
+ logToJsWithTag("[SEQ-15]", "AdvertiseCallback: Success (Peripheral Active)");
255
265
  JSObject data = new JSObject();
256
266
  data.put("type", "peripheral");
257
267
  data.put("state", "poweredOn");
@@ -261,7 +271,7 @@ public class Beacon extends Plugin {
261
271
  @Override
262
272
  public void onStartFailure(int errorCode) {
263
273
  super.onStartFailure(errorCode);
264
- bridge.logToJs("[Beacon] Peripheral manager failed to start advertising: " + errorCode);
274
+ logToJsWithTag("[SEQ-15-F]", "AdvertiseCallback: Failure (Error: " + errorCode + ")");
265
275
  JSObject data = new JSObject();
266
276
  data.put("type", "peripheral");
267
277
  data.put("state", errorCode);
@@ -275,20 +285,52 @@ public class Beacon extends Plugin {
275
285
  @Override
276
286
  public void onScanResult(int callbackType, ScanResult result) {
277
287
  super.onScanResult(callbackType, result);
278
- bridge.logToJs("[Debug] onScanResult 31");
279
- if (result.getScanRecord() != null) {
280
- bridge.logToJs("[Debug] onScanResult 32");
281
- String deviceName = result.getScanRecord().getDeviceName();
282
- if (deviceName != null && deviceName.length() == BeaconUtils.getMessageMaxLength()) {
283
- bridge.logToJs("[Debug] onScanResult 33" + deviceName);
284
- JSObject jsResult = new JSObject();
285
- jsResult.put("message", deviceName);
286
- jsResult.put("rssi", result.getRssi());
287
- jsResult.put("peripheralId", result.getDevice().getAddress());
288
- jsResult.put("timestamp", System.currentTimeMillis());
289
- notifyListeners("beaconReceived", jsResult);
288
+ ScanRecord record = result.getScanRecord();
289
+ Log.d(TAG, "start scan result received");
290
+ if (record == null) {
291
+ Log.w(TAG, "Scan result is null");
292
+ return;
293
+ }
294
+
295
+ String finalMsg;
296
+ /*
297
+ * [SEQ-50]
298
+ * Strategy 1: Attempt to extract message from Manufacturer Specific Data.
299
+ * This is the primary channel, typically used by Android broadcasters.
300
+ * The data is retrieved using the 0xFFFF Company ID.
301
+ */
302
+ byte[] vendorData = record.getManufacturerSpecificData(MANUFACTURER_ID);
303
+ finalMsg = BeaconUtils.extractMessageFromBytes(vendorData);
304
+
305
+ /*
306
+ * [SEQ-51]
307
+ * Strategy 2: Fallback to Device Name if Manufacturer Data is invalid or missing.
308
+ * This channel is used to support iOS broadcasters, which advertise the
309
+ * message within the Scan Response's Local Name field.
310
+ */
311
+ if (finalMsg == null) {
312
+ String deviceName = record.getDeviceName();
313
+ Log.d(TAG, "Valid message found in Device Name: " + deviceName);
314
+ String deviceMessage = BeaconUtils.extractMessageFromDeviceName(deviceName);
315
+ if (deviceMessage != null) {
316
+ finalMsg = deviceMessage;
290
317
  }
291
318
  }
319
+ /*
320
+ * Post-Processing: If a valid protocol message is identified, package the
321
+ * metadata (RSSI, MAC address, and timestamp) and notify the JavaScript layer.
322
+ */
323
+ if (finalMsg != null) {
324
+ logToJsWithTag("[SEQ-55]", "Beacon Resolved: " + finalMsg + " | RSSI: " + result.getRssi());
325
+ JSObject jsResult = new JSObject();
326
+ jsResult.put("message", finalMsg);
327
+ jsResult.put("rssi", result.getRssi());
328
+ jsResult.put("peripheralId", result.getDevice().getAddress());
329
+ jsResult.put("timestamp", System.currentTimeMillis());
330
+ notifyListeners("beaconReceived", jsResult);
331
+ } else {
332
+ logToJsWithTag("[SEQ-55-F]", "No valid message found in scan result");
333
+ }
292
334
  }
293
335
  };
294
- }
336
+ }
@@ -1,10 +1,41 @@
1
1
  package com.wisdomgarden.mobile.beacon;
2
2
 
3
+ import android.util.Log;
4
+ import java.nio.charset.StandardCharsets;
3
5
  import java.util.HashMap;
4
6
  import java.util.Map;
5
- import java.util.Random;
6
7
 
7
8
  public class BeaconUtils {
9
+
10
+ private static final String TAG = "BeaconUtils";
11
+
12
+ /**
13
+ * 16-bit Bluetooth Short UUID (0x5747)
14
+ * Logic & Rationale:
15
+ * 1. 'W' hex value is 0x57, 'G' hex value is 0x47.
16
+ * 2. Formatted according to the Bluetooth SIG Base UUID: 0000XXXX-0000-1000-8000-00805F9B34FB.
17
+ * 3. Replacing XXXX with 5747 results in this full string.
18
+ * 4. Using a 16-bit UUID reduces the UUID overhead in the broadcast packet from 18 bytes
19
+ * to 4 bytes, effectively fixing the 'ADVERTISE_FAILED_DATA_TOO_LARGE' error by
20
+ * staying well within the 31-byte BLE Legacy Advertising limit.
21
+ */
22
+ public static final String WG_UUID = "00005747-0000-1000-8000-00805F9B34FB";
23
+
24
+ /**
25
+ * Identification prefix for string-based payloads (Device Name).
26
+ * This prefix is used by iOS when advertising the message via the Device Name field.
27
+ * Both platforms use this string to identify and validate Wisdom Garden
28
+ * peripherals during the scanning process.
29
+ */
30
+ public static final String WG_PREFIX = "WG";
31
+ /**
32
+ * Hexadecimal representation of "WG" (0x57, 0x47).
33
+ * This prefix is specifically designed for Android's ManufacturerData field.
34
+ * It serves as a protocol identifier within the raw byte payload to
35
+ * distinguish our packets from other 0xFFFF manufacturer data.
36
+ */
37
+ private static final byte[] WG_PREFIX_BYTES = new byte[] { 0x57, 0x47 };
38
+
8
39
  private static final String CHARS = "2O9AuFNPDx4gtJwS3ye7l1Mq0dEB5HsaKInikRmLhjpG6b8fzQrCcvo";
9
40
  private static final int RADIX = CHARS.length();
10
41
  private static final int MESSAGE_MAX_LENGTH = 11;
@@ -16,10 +47,9 @@ public class BeaconUtils {
16
47
  * @return 编码后的字符串
17
48
  */
18
49
  public static String encode(int num) {
19
- if (num == 0) return String.valueOf(CHARS.charAt(0));
50
+ if (num <= 0) return String.valueOf(CHARS.charAt(0));
20
51
  StringBuilder result = new StringBuilder();
21
- boolean isNegative = num < 0;
22
- int n = Math.abs(num);
52
+ int n = num;
23
53
 
24
54
  while (n > 0) {
25
55
  int remainder = n % RADIX;
@@ -27,7 +57,7 @@ public class BeaconUtils {
27
57
  n /= RADIX;
28
58
  }
29
59
 
30
- return isNegative ? "-" + result.toString() : result.toString();
60
+ return result.toString();
31
61
  }
32
62
 
33
63
  /**
@@ -56,8 +86,7 @@ public class BeaconUtils {
56
86
  * @return 如果是 nonce 字母返回 true,否则返回 false
57
87
  */
58
88
  public static boolean isNonceLetter(char c) {
59
- int code = (int) c;
60
- return code >= 84 && code <= 90;
89
+ return c >= 'T' && c <= 'Z';
61
90
  }
62
91
 
63
92
  /**
@@ -89,18 +118,16 @@ public class BeaconUtils {
89
118
  payload.put("rollcallId", 0);
90
119
  payload.put("nonce", "");
91
120
 
92
- if (message == null) {
121
+ if (message == null || message.isEmpty()) {
93
122
  return payload;
94
123
  }
95
124
 
96
-
97
125
  for (int i = 0; i < message.length(); i++) {
98
- char c = message.charAt(i);
99
- if (isNonceLetter(c)) {
126
+ if (isNonceLetter(message.charAt(i))) {
100
127
  String rollcallIdStr = message.substring(0, i);
101
128
  payload.put("rollcallId", decode(rollcallIdStr));
102
129
  payload.put("nonce", message.substring(i));
103
- break;
130
+ return payload;
104
131
  }
105
132
  }
106
133
 
@@ -115,4 +142,74 @@ public class BeaconUtils {
115
142
  public static int getMessageMaxLength() {
116
143
  return MESSAGE_MAX_LENGTH;
117
144
  }
118
- }
145
+
146
+ /**
147
+ * Constructs the raw byte array for Android's Manufacturer Data.
148
+ * It prepends the "WG" hexadecimal prefix (0x57, 0x47) to the ASCII-encoded message.
149
+ * This format ensures that the data is recognizable as a "Wisdom Garden" packet
150
+ * when transmitted within a raw byte stream.
151
+ */
152
+ public static byte[] addPrefixToBytes(String message) {
153
+ Log.d(TAG, "[addPrefixToBytes]Adding hex prefix to message: " + message);
154
+
155
+ byte[] msgBytes = message.getBytes(StandardCharsets.US_ASCII);
156
+ byte[] data = new byte[WG_PREFIX_BYTES.length + msgBytes.length];
157
+ System.arraycopy(WG_PREFIX_BYTES, 0, data, 0, WG_PREFIX_BYTES.length);
158
+ System.arraycopy(msgBytes, 0, data, WG_PREFIX_BYTES.length, msgBytes.length);
159
+
160
+ Log.v(TAG, "[addPrefixToBytes]Resulting byte array length: " + data.length);
161
+ return data;
162
+ }
163
+
164
+ /**
165
+ * Extracts and validates the message from raw Manufacturer Data bytes.
166
+ * The method verifies the "WG" hexadecimal prefix and ensures the total
167
+ * length matches the protocol specification before returning the decoded string.
168
+ */
169
+ public static String extractMessageFromBytes(byte[] data) {
170
+ if (data == null) {
171
+ Log.w(TAG, "[extractBytes] data is null");
172
+ return null;
173
+ }
174
+
175
+ // Total length must be: length of WG_PREFIX_BYTES + length of message
176
+ int expectedLength = WG_PREFIX_BYTES.length + MESSAGE_MAX_LENGTH;
177
+
178
+ if (data.length < WG_PREFIX_BYTES.length || data.length > expectedLength) {
179
+ Log.w(TAG, "[extractBytes] Length out of bounds. Max: " + expectedLength + ", Got: " + data.length);
180
+ return null;
181
+ }
182
+ for (int i = 0; i < WG_PREFIX_BYTES.length; i++) {
183
+ if (data[i] != WG_PREFIX_BYTES[i]) {
184
+ return null;
185
+ }
186
+ }
187
+ int payloadLength = data.length - WG_PREFIX_BYTES.length;
188
+ String extracted = new String(data, WG_PREFIX_BYTES.length, payloadLength, StandardCharsets.US_ASCII);
189
+ Log.i(TAG, "[extractBytes] Successfully extracted message from bytes: " + extracted);
190
+ return extracted;
191
+ }
192
+
193
+ /**
194
+ * Extracts the message payload from a scanned Device Name string.
195
+ * Specifically designed for iOS-to-Android or iOS-to-iOS communication,
196
+ * where the message is prefixed with "WG" and advertised as the device's name.
197
+ */
198
+ public static String extractMessageFromDeviceName(String message) {
199
+ if (message == null || message.isEmpty()) {
200
+ Log.w(TAG, "[extractName] message is null");
201
+ return null;
202
+ }
203
+ if (!message.startsWith(WG_PREFIX)) {
204
+ return null;
205
+ }
206
+ int expectedLength = WG_PREFIX.length() + MESSAGE_MAX_LENGTH;
207
+ if (message.length() > expectedLength) {
208
+ Log.w(TAG, "[extractName] Length out of bounds. Max: " + expectedLength + ", Got: " + message.length());
209
+ return null;
210
+ }
211
+ String extracted = message.substring(WG_PREFIX.length());
212
+ Log.i(TAG, "[extractName] Successfully. " + extracted);
213
+ return extracted;
214
+ }
215
+ }