@mentra/bluetooth-sdk 0.1.21-beta.0 → 0.1.21-beta.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 (89) hide show
  1. package/README.md +22 -11
  2. package/android/build.gradle +3 -0
  3. package/android/src/main/AndroidManifest.xml +3 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +34 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +43 -3
  7. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +7 -2
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +82 -6
  9. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -1
  10. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +32 -3
  11. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
  12. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -4
  13. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +1 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +53 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Ar99.java +2653 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +161 -14
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +3 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
  24. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +41 -1
  25. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +7 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +8 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +45 -0
  29. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/StreamStatusEventTest.kt +29 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +30 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +20 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
  36. package/build/BluetoothSdk.types.d.ts +58 -25
  37. package/build/BluetoothSdk.types.d.ts.map +1 -1
  38. package/build/BluetoothSdk.types.js +2 -1
  39. package/build/BluetoothSdk.types.js.map +1 -1
  40. package/build/_internal.d.ts +1 -0
  41. package/build/_internal.d.ts.map +1 -1
  42. package/build/_internal.js +1 -0
  43. package/build/_internal.js.map +1 -1
  44. package/build/_private/BluetoothSdkModule.d.ts +8 -2
  45. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  46. package/build/_private/BluetoothSdkModule.js +6 -3
  47. package/build/_private/BluetoothSdkModule.js.map +1 -1
  48. package/build/_private/cameraRequestPayload.d.ts +1 -1
  49. package/build/_private/cameraRequestPayload.d.ts.map +1 -1
  50. package/build/_private/cameraRequestPayload.js +6 -0
  51. package/build/_private/cameraRequestPayload.js.map +1 -1
  52. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  53. package/build/_private/photoRequestPayload.js +10 -1
  54. package/build/_private/photoRequestPayload.js.map +1 -1
  55. package/build/_private/sdkOtaManifest.d.ts +20 -0
  56. package/build/_private/sdkOtaManifest.d.ts.map +1 -0
  57. package/build/_private/sdkOtaManifest.js +28 -0
  58. package/build/_private/sdkOtaManifest.js.map +1 -0
  59. package/build/index.d.ts +1 -1
  60. package/build/index.d.ts.map +1 -1
  61. package/build/index.js +7 -1
  62. package/build/index.js.map +1 -1
  63. package/ios/BluetoothSdkModule.swift +41 -2
  64. package/ios/Source/BluetoothSdkDefaults.swift +2 -1
  65. package/ios/Source/Bridge.swift +15 -3
  66. package/ios/Source/DeviceManager.swift +67 -17
  67. package/ios/Source/DeviceStore.swift +5 -1
  68. package/ios/Source/MentraBluetoothSDK.swift +37 -8
  69. package/ios/Source/OtaManifest.swift +47 -4
  70. package/ios/Source/camera/CameraModels.swift +20 -5
  71. package/ios/Source/internal/BleTraceLogger.swift +1 -1
  72. package/ios/Source/internal/BluetoothSdkAnalytics.swift +39 -0
  73. package/ios/Source/sgcs/Ar99.swift +2451 -0
  74. package/ios/Source/sgcs/MentraLive.swift +174 -38
  75. package/ios/Source/sgcs/SGCManager.swift +8 -0
  76. package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
  77. package/ios/Source/streaming/StreamModels.swift +71 -7
  78. package/ios/Source/types/DeviceModels.swift +13 -0
  79. package/ios/Source/utils/BleJsonCompact.swift +8 -0
  80. package/ios/Source/utils/Constants.swift +2 -0
  81. package/ios/Source/utils/PhoneAudioMonitor.swift +4 -0
  82. package/package.json +1 -1
  83. package/src/BluetoothSdk.types.ts +61 -28
  84. package/src/_internal.ts +1 -0
  85. package/src/_private/BluetoothSdkModule.ts +20 -6
  86. package/src/_private/cameraRequestPayload.ts +8 -2
  87. package/src/_private/photoRequestPayload.ts +12 -2
  88. package/src/_private/sdkOtaManifest.ts +31 -0
  89. package/src/index.ts +9 -1
@@ -70,8 +70,13 @@ internal object OtaManifestChecker {
70
70
  currentMtkVersion: String,
71
71
  currentBesVersion: String,
72
72
  manifest: JSONObject,
73
+ // Downgrade floor: a non-positive value (the default) disables downgrades entirely,
74
+ // matching ASG's fail-closed DowngradeGate and the engine. OEM SDK consumers that do
75
+ // not set a floor therefore get upgrade-only behavior and are never told a downgrade is
76
+ // available that the glasses would refuse.
77
+ downgradeFloorVersionCode: Long = 0L,
73
78
  ): Boolean =
74
- hasApkUpdate(currentBuildNumber, manifest) ||
79
+ hasApkUpdate(currentBuildNumber, manifest, downgradeFloorVersionCode) ||
75
80
  hasMtkUpdate(manifest.optJSONArray("mtk_patches"), currentMtkVersion) ||
76
81
  hasBesUpdate(manifest.optJSONObject("bes_firmware"), currentBesVersion)
77
82
 
@@ -95,15 +100,50 @@ internal object OtaManifestChecker {
95
100
  throw BluetoothSdkException("invalid_ota_manifest", "OTA manifest is missing ASG app versionCode.")
96
101
  }
97
102
 
98
- private fun hasApkUpdate(currentBuildNumber: String, manifest: JSONObject): Boolean {
103
+ private fun hasApkUpdate(
104
+ currentBuildNumber: String,
105
+ manifest: JSONObject,
106
+ downgradeFloorVersionCode: Long,
107
+ ): Boolean {
99
108
  val currentVersion =
100
109
  currentBuildNumber.toLongOrNull()
101
110
  ?: throw BluetoothSdkException(
102
111
  "invalid_glasses_version",
103
112
  "Cannot check OTA update because glasses build number is invalid.",
104
113
  )
114
+ // Only apps-shaped manifests are exact pins. A legacy top-level manifest (versionCode at
115
+ // the root, no apps entry) is NOT a pin and stays strictly upgrade-only, matching the
116
+ // engine TS checker — otherwise a build newer than such a manifest would report a false
117
+ // downgrade the JS path ignores.
118
+ val isExactPin = manifest.optJSONObject("apps")?.optJSONObject(ASG_CLIENT_PACKAGE)?.hasNumber("versionCode") == true
105
119
  val serverVersion = latestAppInfo(manifest).requiredLong("versionCode")
106
- return serverVersion > currentVersion
120
+ if (!isExactPin) {
121
+ return serverVersion > currentVersion
122
+ }
123
+ // Exact pin: any mismatch is an update in either direction (downgrades take the
124
+ // uninstall-then-reinstall detour on the glasses). A non-positive pin is only legitimate
125
+ // in the frozen legacy rescue manifests that pre-39 glasses are checked against; for
126
+ // modern glasses it means the manifest cannot verify anything, and that must surface as an
127
+ // error — never as "no update".
128
+ if (serverVersion <= 0) {
129
+ if (currentVersion >= 39) {
130
+ throw BluetoothSdkException(
131
+ "invalid_ota_manifest",
132
+ "OTA manifest ASG pin is missing or zero; cannot verify update state.",
133
+ )
134
+ }
135
+ return false
136
+ }
137
+ if (serverVersion > currentVersion) {
138
+ return true
139
+ }
140
+ // Exact match: already on the pin, no update.
141
+ if (serverVersion == currentVersion) {
142
+ return false
143
+ }
144
+ // Downgrade: only actionable at/above the enabled floor; a non-positive floor disables
145
+ // downgrades (fail closed, matching ASG and the engine).
146
+ return downgradeFloorVersionCode > 0 && serverVersion >= downgradeFloorVersionCode
107
147
  }
108
148
 
109
149
  private fun hasMtkUpdate(patches: JSONArray?, currentVersion: String): Boolean {
@@ -110,14 +110,15 @@ class CameraFov @JvmOverloads constructor(
110
110
  companion object {
111
111
  const val MIN_FOV = 62
112
112
  const val MAX_FOV = 118
113
- const val DEFAULT_FOV = 102
113
+ const val DEFAULT_FOV = 118
114
114
  const val NARROW_FOV = 82
115
+ const val STANDARD_FOV = 102
115
116
  @JvmField
116
117
  val DEFAULT_ROI_POSITION = CameraRoiPosition.CENTER
117
118
  @JvmField
118
119
  val NARROW = CameraFov(NARROW_FOV, DEFAULT_ROI_POSITION)
119
120
  @JvmField
120
- val STANDARD = CameraFov(DEFAULT_FOV, DEFAULT_ROI_POSITION)
121
+ val STANDARD = CameraFov(STANDARD_FOV, DEFAULT_ROI_POSITION)
121
122
  @JvmField
122
123
  val WIDE = CameraFov(MAX_FOV, DEFAULT_ROI_POSITION)
123
124
  }
@@ -200,10 +201,22 @@ data class PhotoRequest @JvmOverloads constructor(
200
201
  val ispAnalogGain: String? = null,
201
202
  val resetCaptureTuning: Boolean? = null,
202
203
  val mode: PhotoMode = PhotoMode.PHOTO,
203
- /** `ble` skips direct upload; `auto` tries Wi-Fi with BLE fallback. */
204
+ /** `direct` disables BLE fallback; `ble` skips direct upload; `auto` tries both. */
204
205
  val transferMethod: String = "auto",
205
206
  ) {
206
207
  companion object {
208
+ private fun transferMethodFromValue(value: Any?): String {
209
+ if (value == null) return "auto"
210
+ val raw = value as? String
211
+ ?: throw IllegalArgumentException(
212
+ "Invalid transferMethod ${value::class.java.simpleName}. Expected auto, direct, or ble."
213
+ )
214
+ return raw.takeIf { it == "auto" || it == "direct" || it == "ble" }
215
+ ?: throw IllegalArgumentException(
216
+ "Invalid transferMethod \"$raw\". Expected auto, direct, or ble."
217
+ )
218
+ }
219
+
207
220
  /** Mirrors iOS `BluetoothSdkModule` defaults for keys omitted from the JS bridge. */
208
221
  @JvmStatic
209
222
  fun fromMap(values: Map<String, Any>): PhotoRequest {
@@ -240,7 +253,7 @@ data class PhotoRequest @JvmOverloads constructor(
240
253
  save = boolValue(values, "save", "saveToGallery") ?: false,
241
254
  sound = boolValue(values, "sound") ?: true,
242
255
  mode = PhotoMode.fromValue(stringValue(values, "mode")),
243
- transferMethod = if (stringValue(values, "transferMethod") == "ble") "ble" else "auto",
256
+ transferMethod = transferMethodFromValue(values["transferMethod"]),
244
257
  exposureTimeNs = exposureTimeNs,
245
258
  iso = iso,
246
259
  aeExposureDivisor = aeDivisor,
@@ -12,7 +12,7 @@ object BleTraceLogger {
12
12
  private const val MAX_PAYLOAD_CHARS = 3000
13
13
  private const val K900_TYPE = "k900"
14
14
  private val sensitiveKeyParts =
15
- listOf("password", "pass", "token", "secret", "authorization", "auth", "email")
15
+ listOf("password", "pass", "token", "secret", "authorization", "auth", "email", "serial")
16
16
 
17
17
  @JvmStatic
18
18
  fun logJson(direction: String, layer: String, payload: JSONObject?, bytes: Int? = null) {
@@ -6,6 +6,7 @@ import android.app.Service
6
6
  import android.content.Intent
7
7
  import android.content.pm.PackageManager
8
8
  import android.content.pm.ServiceInfo
9
+ import android.location.LocationManager
9
10
  import android.os.Build
10
11
  import android.os.IBinder
11
12
  import androidx.core.app.NotificationCompat
@@ -17,8 +18,12 @@ class ForegroundService : Service() {
17
18
  companion object {
18
19
  const val CHANNEL_ID = "MentraServiceChannel"
19
20
  const val NOTIFICATION_ID = 1001
21
+ const val ACTION_REFRESH_TYPES =
22
+ "com.mentra.bluetoothsdk.services.action.REFRESH_FOREGROUND_SERVICE_TYPES"
20
23
  }
21
24
 
25
+ private var locationTypeRequested = false
26
+
22
27
  override fun onCreate() {
23
28
  super.onCreate()
24
29
  Bridge.log("ForegroundService: onCreate() called")
@@ -34,6 +39,11 @@ class ForegroundService : Service() {
34
39
  "service_start_command",
35
40
  mapOf("action" to intent?.action, "flags" to flags, "startId" to startId)
36
41
  )
42
+ if (intent?.action == ACTION_REFRESH_TYPES) {
43
+ // This action is only sent while the host Activity is foregrounded. Android 14+
44
+ // rejects adding a while-in-use location type from the background.
45
+ locationTypeRequested = true
46
+ }
37
47
  // Re-check permissions in case they changed
38
48
  startForegroundWithAutoDetectedType()
39
49
  return START_STICKY
@@ -71,6 +81,14 @@ class ForegroundService : Service() {
71
81
 
72
82
  var serviceType = 0
73
83
 
84
+ // Audio prompts can be initiated by glasses while the host Activity is
85
+ // backgrounded. mediaPlayback has no runtime prerequisite, so keep it
86
+ // active on the existing Mentra service for the entire connected
87
+ // session rather than trying to launch a second service after a wake
88
+ // phrase arrives.
89
+ serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
90
+ Bridge.log("ForegroundService: Added mediaPlayback (supports background audio)")
91
+
74
92
  // Check Bluetooth permissions
75
93
  val hasBluetoothPermission =
76
94
  when {
@@ -124,6 +142,28 @@ class ForegroundService : Service() {
124
142
  Bridge.log("ForegroundService: No microphone permission")
125
143
  }
126
144
 
145
+ val hasLocationPermission =
146
+ ContextCompat.checkSelfPermission(
147
+ this,
148
+ android.Manifest.permission.ACCESS_FINE_LOCATION
149
+ ) == PackageManager.PERMISSION_GRANTED ||
150
+ ContextCompat.checkSelfPermission(
151
+ this,
152
+ android.Manifest.permission.ACCESS_COARSE_LOCATION
153
+ ) == PackageManager.PERMISSION_GRANTED
154
+ val locationManager = getSystemService(LocationManager::class.java)
155
+ val isLocationEnabled = locationManager?.isLocationEnabled == true
156
+
157
+ if (locationTypeRequested && hasLocationPermission && isLocationEnabled) {
158
+ serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
159
+ Bridge.log("ForegroundService: Added location (has foreground location permission)")
160
+ } else {
161
+ Bridge.log(
162
+ "ForegroundService: No location type " +
163
+ "(requested=$locationTypeRequested, permission=$hasLocationPermission, enabled=$isLocationEnabled)"
164
+ )
165
+ }
166
+
127
167
  // Only use dataSync as absolute last resort fallback if no other types were added.
128
168
  // WARNING: dataSync has a 6-hour timeout on Android 14+ which will crash the app.
129
169
  // This should rarely happen since CHANGE_NETWORK_STATE is a normal permission.
@@ -141,11 +181,20 @@ class ForegroundService : Service() {
141
181
  val hasConnectedDevice =
142
182
  (serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE) != 0
143
183
  val hasMicrophone = (serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE) != 0
184
+ val hasLocation = (serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION) != 0
185
+ val hasMediaPlayback =
186
+ (serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) != 0
144
187
 
145
188
  return when {
189
+ hasConnectedDevice && hasMicrophone && hasLocation ->
190
+ "Glasses, microphone & location active"
191
+ hasConnectedDevice && hasLocation -> "Smart glasses & location active"
192
+ hasMicrophone && hasLocation -> "Microphone & location active"
193
+ hasLocation -> "Location active"
146
194
  hasConnectedDevice && hasMicrophone -> "Glasses & microphone active"
147
195
  hasConnectedDevice -> "Smart glasses connected"
148
196
  hasMicrophone -> "Microphone active"
197
+ hasMediaPlayback -> "Audio playback active"
149
198
  else -> "Syncing data"
150
199
  }
151
200
  }
@@ -160,6 +209,10 @@ class ForegroundService : Service() {
160
209
  types.add("connectedDevice")
161
210
  if (serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE != 0)
162
211
  types.add("microphone")
212
+ if (serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION != 0)
213
+ types.add("location")
214
+ if (serviceType and ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK != 0)
215
+ types.add("mediaPlayback")
163
216
 
164
217
  return types.joinToString("|")
165
218
  }