@mentra/bluetooth-sdk 0.1.12 → 0.1.13
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 +16 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +51 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +73 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +190 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +78 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +3 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +3 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3835 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +517 -560
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8712 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +11 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +3 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +7 -14
- package/build/BluetoothSdk.types.d.ts +39 -9
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +8 -6
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -2
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +3 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +43 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -5
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +38 -57
- package/ios/Source/BluetoothSdkDefaults.swift +23 -3
- package/ios/Source/DeviceManager.swift +37 -26
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +197 -26
- package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
- package/ios/Source/ObservableStore.swift +11 -0
- package/ios/Source/OtaManifest.swift +170 -0
- package/ios/Source/camera/CameraModels.swift +218 -8
- package/ios/Source/controllers/ControllerManager.swift +2 -5
- package/ios/Source/controllers/R1.swift +2 -5
- package/ios/Source/sgcs/G1.swift +6 -5
- package/ios/Source/sgcs/G2.swift +20 -18
- package/ios/Source/sgcs/Mach1.swift +6 -5
- package/ios/Source/sgcs/MentraLive.swift +129 -33
- package/ios/Source/sgcs/MentraNex.swift +6 -5
- package/ios/Source/sgcs/SGCManager.swift +3 -5
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/status/DeviceStatus.swift +1 -1
- package/package.json +6 -1
- package/src/BluetoothSdk.types.ts +40 -9
- package/src/_private/BluetoothSdkModule.ts +8 -9
- package/src/_private/photoRequestPayload.ts +45 -2
- package/src/debug.ts +9 -0
- package/src/index.ts +23 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7434
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import java.net.HttpURLConnection
|
|
4
|
+
import java.net.URI
|
|
5
|
+
import java.net.URL
|
|
6
|
+
import org.json.JSONArray
|
|
7
|
+
import org.json.JSONObject
|
|
8
|
+
|
|
9
|
+
internal object OtaManifestDefaults {
|
|
10
|
+
private const val SDK_OTA_RELEASE_BASE_URL =
|
|
11
|
+
"https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota"
|
|
12
|
+
// Keep prod as the legacy-device fallback: pre-override ASG builds ignore
|
|
13
|
+
// ota_start.ota_version_url and use their compiled MentraOS default.
|
|
14
|
+
const val PROD_OTA_VERSION_URL = "https://ota.mentraglass.com/prod_live_version.json"
|
|
15
|
+
|
|
16
|
+
fun defaultOtaVersionUrl(): String {
|
|
17
|
+
val sdkVersion = BuildConfig.SDK_VERSION.trim()
|
|
18
|
+
if (sdkVersion.isBlank() || sdkVersion == "unspecified") {
|
|
19
|
+
throw BluetoothException(
|
|
20
|
+
"missing_sdk_version",
|
|
21
|
+
"Cannot determine Bluetooth SDK version for the default OTA manifest URL.",
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
return "$SDK_OTA_RELEASE_BASE_URL/bluetooth-sdk-$sdkVersion-version.json"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
internal object OtaManifestChecker {
|
|
29
|
+
private const val ASG_CLIENT_PACKAGE = "com.mentra.asg_client"
|
|
30
|
+
|
|
31
|
+
fun normalizeHttpUrl(value: String): String {
|
|
32
|
+
val trimmed = value.trim()
|
|
33
|
+
if (trimmed.isEmpty()) {
|
|
34
|
+
throw BluetoothException("invalid_ota_url", "OTA version URL must be a non-empty http(s) URL.")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
val uri =
|
|
38
|
+
try {
|
|
39
|
+
URI(trimmed)
|
|
40
|
+
} catch (error: Exception) {
|
|
41
|
+
throw BluetoothException("invalid_ota_url", "OTA version URL must be a valid http(s) URL.", error)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
val scheme = uri.scheme?.lowercase()
|
|
45
|
+
if ((scheme != "http" && scheme != "https") || uri.host.isNullOrBlank()) {
|
|
46
|
+
throw BluetoothException("invalid_ota_url", "OTA version URL must be an http(s) URL.")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return uri.toString()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun fetch(otaVersionUrl: String): JSONObject {
|
|
53
|
+
val connection = URL(otaVersionUrl).openConnection() as HttpURLConnection
|
|
54
|
+
connection.connectTimeout = 15_000
|
|
55
|
+
connection.readTimeout = 15_000
|
|
56
|
+
connection.requestMethod = "GET"
|
|
57
|
+
return try {
|
|
58
|
+
val status = connection.responseCode
|
|
59
|
+
if (status !in 200..299) {
|
|
60
|
+
throw BluetoothException("ota_manifest_request_failed", "OTA manifest request failed with HTTP $status.")
|
|
61
|
+
}
|
|
62
|
+
JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
|
|
63
|
+
} finally {
|
|
64
|
+
connection.disconnect()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun hasUpdate(
|
|
69
|
+
currentBuildNumber: String,
|
|
70
|
+
currentMtkVersion: String,
|
|
71
|
+
currentBesVersion: String,
|
|
72
|
+
manifest: JSONObject,
|
|
73
|
+
): Boolean =
|
|
74
|
+
hasApkUpdate(currentBuildNumber, manifest) ||
|
|
75
|
+
hasMtkUpdate(manifest.optJSONArray("mtk_patches"), currentMtkVersion) ||
|
|
76
|
+
hasBesUpdate(manifest.optJSONObject("bes_firmware"), currentBesVersion)
|
|
77
|
+
|
|
78
|
+
fun hasMtkPatches(manifest: JSONObject): Boolean =
|
|
79
|
+
(manifest.optJSONArray("mtk_patches")?.length() ?: 0) > 0
|
|
80
|
+
|
|
81
|
+
fun hasBesFirmware(manifest: JSONObject): Boolean =
|
|
82
|
+
manifest.optJSONObject("bes_firmware") != null
|
|
83
|
+
|
|
84
|
+
private fun latestAppInfo(manifest: JSONObject): JSONObject {
|
|
85
|
+
val apps = manifest.optJSONObject("apps")
|
|
86
|
+
val app = apps?.optJSONObject(ASG_CLIENT_PACKAGE)
|
|
87
|
+
if (app != null && app.hasNumber("versionCode")) {
|
|
88
|
+
return app
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (manifest.hasNumber("versionCode")) {
|
|
92
|
+
return manifest
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw BluetoothException("invalid_ota_manifest", "OTA manifest is missing ASG app versionCode.")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private fun hasApkUpdate(currentBuildNumber: String, manifest: JSONObject): Boolean {
|
|
99
|
+
val currentVersion =
|
|
100
|
+
currentBuildNumber.toLongOrNull()
|
|
101
|
+
?: throw BluetoothException(
|
|
102
|
+
"invalid_glasses_version",
|
|
103
|
+
"Cannot check OTA update because glasses build number is invalid.",
|
|
104
|
+
)
|
|
105
|
+
val serverVersion = latestAppInfo(manifest).requiredLong("versionCode")
|
|
106
|
+
return serverVersion > currentVersion
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun hasMtkUpdate(patches: JSONArray?, currentVersion: String): Boolean {
|
|
110
|
+
if (patches == null || patches.length() == 0) return false
|
|
111
|
+
if (currentVersion.isBlank()) return false
|
|
112
|
+
|
|
113
|
+
for (index in 0 until patches.length()) {
|
|
114
|
+
val patch = patches.optJSONObject(index) ?: continue
|
|
115
|
+
val startFirmware = patch.optString("start_firmware", "")
|
|
116
|
+
if (startFirmware == currentVersion) return true
|
|
117
|
+
val serverDate = if (startFirmware.contains("_")) startFirmware.substringAfterLast("_") else startFirmware
|
|
118
|
+
if (serverDate == currentVersion) return true
|
|
119
|
+
}
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private fun hasBesUpdate(besFirmware: JSONObject?, currentVersion: String): Boolean {
|
|
124
|
+
if (besFirmware == null) return false
|
|
125
|
+
val serverVersion = besFirmware.optString("version", "")
|
|
126
|
+
if (serverVersion.isBlank()) {
|
|
127
|
+
throw BluetoothException("invalid_ota_manifest", "OTA manifest bes_firmware.version is missing.")
|
|
128
|
+
}
|
|
129
|
+
if (currentVersion.isBlank()) return true
|
|
130
|
+
return compareVersions(serverVersion, currentVersion) > 0
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun compareVersions(version1: String, version2: String): Int {
|
|
134
|
+
if (version1.contains(".") && version2.contains(".")) {
|
|
135
|
+
val parts1 = version1.split(".")
|
|
136
|
+
val parts2 = version2.split(".")
|
|
137
|
+
val maxLength = maxOf(parts1.size, parts2.size)
|
|
138
|
+
for (index in 0 until maxLength) {
|
|
139
|
+
val value1 = parts1.getOrNull(index)?.toIntOrNull() ?: 0
|
|
140
|
+
val value2 = parts2.getOrNull(index)?.toIntOrNull() ?: 0
|
|
141
|
+
if (value1 != value2) return value1 - value2
|
|
142
|
+
}
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
return version1.compareTo(version2)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private fun JSONObject.hasNumber(key: String): Boolean = !isNull(key) && opt(key) is Number
|
|
149
|
+
|
|
150
|
+
private fun JSONObject.requiredLong(key: String): Long =
|
|
151
|
+
(opt(key) as? Number)?.toLong()
|
|
152
|
+
?: throw BluetoothException("invalid_ota_manifest", "OTA manifest is missing ASG app versionCode.")
|
|
153
|
+
}
|
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk
|
|
2
2
|
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
|
|
3
5
|
enum class PhotoSize(val value: String) {
|
|
4
|
-
|
|
6
|
+
LOW("low"),
|
|
5
7
|
MEDIUM("medium"),
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
HIGH("high"),
|
|
9
|
+
MAX("max");
|
|
8
10
|
|
|
9
11
|
companion object {
|
|
10
12
|
@JvmStatic
|
|
11
|
-
fun
|
|
12
|
-
|
|
13
|
+
fun normalizeLegacy(value: String?): String =
|
|
14
|
+
when (value) {
|
|
15
|
+
"small" -> LOW.value
|
|
16
|
+
"large" -> HIGH.value
|
|
17
|
+
"full" -> MAX.value
|
|
18
|
+
else -> value ?: MEDIUM.value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@JvmStatic
|
|
22
|
+
fun fromValue(value: String?): PhotoSize {
|
|
23
|
+
val normalized = normalizeLegacy(value)
|
|
24
|
+
return values().firstOrNull { it.value == normalized } ?: MEDIUM
|
|
25
|
+
}
|
|
13
26
|
}
|
|
14
27
|
}
|
|
15
28
|
|
|
16
29
|
enum class ButtonPhotoSize(val value: String) {
|
|
17
|
-
|
|
30
|
+
LOW("low"),
|
|
18
31
|
MEDIUM("medium"),
|
|
19
|
-
|
|
32
|
+
HIGH("high"),
|
|
20
33
|
MAX("max");
|
|
21
34
|
|
|
22
35
|
companion object {
|
|
23
36
|
@JvmStatic
|
|
24
|
-
fun
|
|
25
|
-
|
|
37
|
+
fun normalizeLegacy(value: String?): String = PhotoSize.normalizeLegacy(value)
|
|
38
|
+
|
|
39
|
+
@JvmStatic
|
|
40
|
+
fun fromValue(value: String?): ButtonPhotoSize {
|
|
41
|
+
val normalized = normalizeLegacy(value)
|
|
42
|
+
return values().firstOrNull { it.value == normalized } ?: MEDIUM
|
|
43
|
+
}
|
|
26
44
|
}
|
|
27
45
|
}
|
|
28
46
|
|
|
@@ -39,7 +57,18 @@ enum class PhotoCompression(val value: String) {
|
|
|
39
57
|
}
|
|
40
58
|
|
|
41
59
|
data class ButtonPhotoSettings(
|
|
42
|
-
val size: ButtonPhotoSize,
|
|
60
|
+
val size: ButtonPhotoSize? = null,
|
|
61
|
+
val mfnr: Boolean? = null,
|
|
62
|
+
val zsl: Boolean? = null,
|
|
63
|
+
val noiseReduction: Boolean? = null,
|
|
64
|
+
val edgeEnhancement: Boolean? = null,
|
|
65
|
+
val ispDigitalGain: Int? = null,
|
|
66
|
+
val ispAnalogGain: String? = null,
|
|
67
|
+
val aeExposureDivisor: Int? = null,
|
|
68
|
+
val isoCap: Int? = null,
|
|
69
|
+
val compress: String? = null,
|
|
70
|
+
val sound: Boolean? = null,
|
|
71
|
+
val resetCaptureTuning: Boolean = false,
|
|
43
72
|
)
|
|
44
73
|
|
|
45
74
|
data class ButtonVideoRecordingSettings(
|
|
@@ -145,6 +174,15 @@ data class PhotoRequest @JvmOverloads constructor(
|
|
|
145
174
|
val exposureTimeNs: Double? = null,
|
|
146
175
|
/** Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure. */
|
|
147
176
|
val iso: Int? = null,
|
|
177
|
+
val aeExposureDivisor: Int? = null,
|
|
178
|
+
val isoCap: Int? = null,
|
|
179
|
+
val noiseReduction: Boolean? = null,
|
|
180
|
+
val edgeEnhancement: Boolean? = null,
|
|
181
|
+
val mfnr: Boolean? = null,
|
|
182
|
+
val zsl: Boolean? = null,
|
|
183
|
+
val ispDigitalGain: Int? = null,
|
|
184
|
+
val ispAnalogGain: String? = null,
|
|
185
|
+
val resetCaptureTuning: Boolean? = null,
|
|
148
186
|
) {
|
|
149
187
|
companion object {
|
|
150
188
|
/** Mirrors iOS `BluetoothSdkModule` defaults for keys omitted from the JS bridge. */
|
|
@@ -167,6 +205,12 @@ data class PhotoRequest @JvmOverloads constructor(
|
|
|
167
205
|
}
|
|
168
206
|
else -> null
|
|
169
207
|
}
|
|
208
|
+
val aeDivisor =
|
|
209
|
+
numberValue(values, "aeExposureDivisor")?.takeIf { it > 1 }
|
|
210
|
+
val isoCap = numberValue(values, "isoCap")?.takeIf { it > 0 }
|
|
211
|
+
val ispDigitalGain = numberValue(values, "ispDigitalGain")
|
|
212
|
+
val ispAnalogGain = stringValue(values, "ispAnalogGain")
|
|
213
|
+
|
|
170
214
|
return PhotoRequest(
|
|
171
215
|
requestId = stringValue(values, "requestId", "request_id").orEmpty(),
|
|
172
216
|
appId = stringValue(values, "appId", "app_id").orEmpty(),
|
|
@@ -179,8 +223,32 @@ data class PhotoRequest @JvmOverloads constructor(
|
|
|
179
223
|
sound = boolValue(values, "sound") ?: true,
|
|
180
224
|
exposureTimeNs = exposureTimeNs,
|
|
181
225
|
iso = iso,
|
|
226
|
+
aeExposureDivisor = aeDivisor,
|
|
227
|
+
isoCap = isoCap,
|
|
228
|
+
noiseReduction = boolValue(values, "noiseReduction"),
|
|
229
|
+
edgeEnhancement = boolValue(values, "edgeEnhancement"),
|
|
230
|
+
mfnr = boolValue(values, "mfnr"),
|
|
231
|
+
zsl = boolValue(values, "zsl"),
|
|
232
|
+
ispDigitalGain = ispDigitalGain,
|
|
233
|
+
ispAnalogGain = ispAnalogGain,
|
|
182
234
|
)
|
|
183
235
|
}
|
|
236
|
+
|
|
237
|
+
/** Adds optional scan-mode tuning fields to a `take_photo` JSON payload. */
|
|
238
|
+
@JvmStatic
|
|
239
|
+
fun appendScanFields(
|
|
240
|
+
json: JSONObject,
|
|
241
|
+
request: PhotoRequest,
|
|
242
|
+
) {
|
|
243
|
+
request.aeExposureDivisor?.let { json.put("aeExposureDivisor", it) }
|
|
244
|
+
request.isoCap?.let { json.put("isoCap", it) }
|
|
245
|
+
request.noiseReduction?.let { json.put("noiseReduction", it) }
|
|
246
|
+
request.edgeEnhancement?.let { json.put("edgeEnhancement", it) }
|
|
247
|
+
request.mfnr?.let { json.put("mfnr", it) }
|
|
248
|
+
request.zsl?.let { json.put("zsl", it) }
|
|
249
|
+
request.ispDigitalGain?.let { json.put("ispDigitalGain", it) }
|
|
250
|
+
request.ispAnalogGain?.let { json.put("ispAnalogGain", it) }
|
|
251
|
+
}
|
|
184
252
|
}
|
|
185
253
|
}
|
|
186
254
|
|
|
@@ -2,6 +2,7 @@ package com.mentra.bluetoothsdk.controllers
|
|
|
2
2
|
|
|
3
3
|
import com.mentra.bluetoothsdk.BluetoothSdkDefaults
|
|
4
4
|
import com.mentra.bluetoothsdk.DeviceStore
|
|
5
|
+
import com.mentra.bluetoothsdk.PhotoRequest
|
|
5
6
|
|
|
6
7
|
abstract class ControllerManager {
|
|
7
8
|
@JvmField var type: String = ""
|
|
@@ -15,19 +16,7 @@ abstract class ControllerManager {
|
|
|
15
16
|
abstract fun sendJson(jsonOriginal: Map<String, Any>, wakeUp: Boolean, requireAck: Boolean)
|
|
16
17
|
|
|
17
18
|
// Camera & Media
|
|
18
|
-
abstract fun requestPhoto(
|
|
19
|
-
requestId: String,
|
|
20
|
-
appId: String,
|
|
21
|
-
size: String?,
|
|
22
|
-
webhookUrl: String?,
|
|
23
|
-
authToken: String?,
|
|
24
|
-
compress: String?,
|
|
25
|
-
flash: Boolean,
|
|
26
|
-
save: Boolean,
|
|
27
|
-
sound: Boolean,
|
|
28
|
-
exposureTimeNs: Long?,
|
|
29
|
-
iso: Int?,
|
|
30
|
-
)
|
|
19
|
+
abstract fun requestPhoto(request: PhotoRequest)
|
|
31
20
|
abstract fun startStream(message: Map<String, Any>)
|
|
32
21
|
abstract fun stopStream()
|
|
33
22
|
abstract fun sendStreamKeepAlive(message: Map<String, Any>)
|
|
@@ -87,7 +76,7 @@ abstract class ControllerManager {
|
|
|
87
76
|
abstract fun sendWifiCredentials(ssid: String, password: String)
|
|
88
77
|
abstract fun forgetWifiNetwork(ssid: String)
|
|
89
78
|
abstract fun sendHotspotState(enabled: Boolean)
|
|
90
|
-
abstract fun sendOtaStart()
|
|
79
|
+
abstract fun sendOtaStart(otaVersionUrl: String? = null)
|
|
91
80
|
|
|
92
81
|
// User Context (for crash reporting)
|
|
93
82
|
abstract fun sendUserEmailToGlasses(email: String)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk.controllers
|
|
2
2
|
|
|
3
|
+
import com.mentra.bluetoothsdk.PhotoRequest
|
|
3
4
|
import android.Manifest
|
|
4
5
|
import android.bluetooth.BluetoothAdapter
|
|
5
6
|
import android.bluetooth.BluetoothDevice
|
|
@@ -861,11 +862,7 @@ class R1 : ControllerManager() {
|
|
|
861
862
|
override fun setMicEnabled(enabled: Boolean) {}
|
|
862
863
|
override fun sortMicRanking(list: MutableList<String>): MutableList<String> = list
|
|
863
864
|
override fun sendJson(jsonOriginal: Map<String, Any>, wakeUp: Boolean, requireAck: Boolean) {}
|
|
864
|
-
override fun requestPhoto(
|
|
865
|
-
requestId: String, appId: String, size: String?, webhookUrl: String?,
|
|
866
|
-
authToken: String?, compress: String?, flash: Boolean, save: Boolean, sound: Boolean,
|
|
867
|
-
exposureTimeNs: Long?, iso: Int?,
|
|
868
|
-
) {}
|
|
865
|
+
override fun requestPhoto(request: PhotoRequest) {}
|
|
869
866
|
override fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {}
|
|
870
867
|
override fun stopVideoRecording(requestId: String) {}
|
|
871
868
|
override fun startStream(message: Map<String, Any>) {}
|
|
@@ -901,7 +898,7 @@ class R1 : ControllerManager() {
|
|
|
901
898
|
override fun sendWifiCredentials(ssid: String, password: String) {}
|
|
902
899
|
override fun forgetWifiNetwork(ssid: String) {}
|
|
903
900
|
override fun sendHotspotState(enabled: Boolean) {}
|
|
904
|
-
override fun sendOtaStart() {}
|
|
901
|
+
override fun sendOtaStart(otaVersionUrl: String?) {}
|
|
905
902
|
override fun sendUserEmailToGlasses(email: String) {}
|
|
906
903
|
override fun queryGalleryStatus() {}
|
|
907
904
|
override fun sendGalleryMode() {}
|
|
@@ -49,6 +49,7 @@ class PhoneMic private constructor(private val context: Context) {
|
|
|
49
49
|
private const val FOCUS_REGAIN_DELAY_MS = 500L
|
|
50
50
|
private const val SAMSUNG_MIC_TEST_DELAY_MS = 500L
|
|
51
51
|
private const val MIC_SWITCH_DELAY_MS = 300L // Time for DeviceManager to switch mics
|
|
52
|
+
private const val AUDIO_SOURCE_HOTWORD = 1999
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
// Audio recording components
|
|
@@ -185,7 +186,42 @@ class PhoneMic private constructor(private val context: Context) {
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
fun isRecordingWithMode(mode: String): Boolean {
|
|
188
|
-
|
|
189
|
+
if (!isRecording.get()) {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
val record = audioRecord
|
|
194
|
+
val activelyRecording =
|
|
195
|
+
try {
|
|
196
|
+
record?.recordingState == AudioRecord.RECORDSTATE_RECORDING
|
|
197
|
+
} catch (e: Exception) {
|
|
198
|
+
Bridge.log("MIC: Failed to read AudioRecord state: ${e.message}")
|
|
199
|
+
false
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!activelyRecording) {
|
|
203
|
+
Bridge.log("MIC: Stale recording state detected, cleaning up")
|
|
204
|
+
cleanUpRecording()
|
|
205
|
+
abandonAudioFocus()
|
|
206
|
+
currentMicMode = ""
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return currentMicMode == mode
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fun hasBlockingMicInterruption(): Boolean {
|
|
214
|
+
val callActive =
|
|
215
|
+
isPhoneCallActive ||
|
|
216
|
+
try {
|
|
217
|
+
(telephonyManager?.callState ?: TelephonyManager.CALL_STATE_IDLE) !=
|
|
218
|
+
TelephonyManager.CALL_STATE_IDLE
|
|
219
|
+
} catch (e: Exception) {
|
|
220
|
+
Bridge.log("MIC: Failed to read telephony call state: ${e.message}")
|
|
221
|
+
true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return callActive || isExternalAudioActive
|
|
189
225
|
}
|
|
190
226
|
|
|
191
227
|
/**
|
|
@@ -544,7 +580,23 @@ class PhoneMic private constructor(private val context: Context) {
|
|
|
544
580
|
audioRecord?.let { ourAudioSessionIds.add(it.audioSessionId) }
|
|
545
581
|
|
|
546
582
|
// Start recording
|
|
547
|
-
audioRecord
|
|
583
|
+
val record = audioRecord ?: return false
|
|
584
|
+
try {
|
|
585
|
+
record.startRecording()
|
|
586
|
+
} catch (e: Exception) {
|
|
587
|
+
Bridge.log("MIC: AudioRecord.startRecording() failed: ${e.message}")
|
|
588
|
+
audioRecord?.release()
|
|
589
|
+
audioRecord = null
|
|
590
|
+
return false
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (record.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
|
|
594
|
+
Bridge.log("MIC: AudioRecord did not enter recording state")
|
|
595
|
+
record.release()
|
|
596
|
+
audioRecord = null
|
|
597
|
+
return false
|
|
598
|
+
}
|
|
599
|
+
|
|
548
600
|
isRecording.set(true)
|
|
549
601
|
|
|
550
602
|
// Start recording thread
|
|
@@ -577,15 +629,22 @@ class PhoneMic private constructor(private val context: Context) {
|
|
|
577
629
|
|
|
578
630
|
val audioBuffer = ShortArray(bufferSize / 2)
|
|
579
631
|
|
|
632
|
+
var unexpectedStopReason: String? = null
|
|
633
|
+
|
|
580
634
|
while (isRecording.get()) {
|
|
581
635
|
// Capture local reference to avoid use-after-free if cleanUpRecording()
|
|
582
636
|
// releases the AudioRecord while we're in read()
|
|
583
|
-
val record = audioRecord
|
|
637
|
+
val record = audioRecord
|
|
638
|
+
if (record == null) {
|
|
639
|
+
unexpectedStopReason = "AudioRecord missing"
|
|
640
|
+
break
|
|
641
|
+
}
|
|
584
642
|
|
|
585
643
|
val readResult = try {
|
|
586
644
|
record.read(audioBuffer, 0, audioBuffer.size)
|
|
587
645
|
} catch (e: Exception) {
|
|
588
646
|
Bridge.log("MIC: AudioRecord.read() exception: ${e.message}")
|
|
647
|
+
unexpectedStopReason = "AudioRecord.read() exception: ${e.message}"
|
|
589
648
|
break
|
|
590
649
|
}
|
|
591
650
|
|
|
@@ -603,9 +662,16 @@ class PhoneMic private constructor(private val context: Context) {
|
|
|
603
662
|
} else if (readResult < 0) {
|
|
604
663
|
// AudioRecord error (ERROR, ERROR_INVALID_OPERATION, ERROR_BAD_VALUE, ERROR_DEAD_OBJECT)
|
|
605
664
|
Bridge.log("MIC: AudioRecord.read() returned error: $readResult")
|
|
665
|
+
unexpectedStopReason = "AudioRecord.read() error: $readResult"
|
|
606
666
|
break
|
|
607
667
|
}
|
|
608
668
|
}
|
|
669
|
+
|
|
670
|
+
unexpectedStopReason?.let { reason ->
|
|
671
|
+
if (isRecording.get()) {
|
|
672
|
+
mainHandler.post { handleUnexpectedRecordingStop(reason) }
|
|
673
|
+
}
|
|
674
|
+
}
|
|
609
675
|
}
|
|
610
676
|
.apply {
|
|
611
677
|
name = "AudioRecordingThread"
|
|
@@ -613,6 +679,25 @@ class PhoneMic private constructor(private val context: Context) {
|
|
|
613
679
|
}
|
|
614
680
|
}
|
|
615
681
|
|
|
682
|
+
private fun handleUnexpectedRecordingStop(reason: String) {
|
|
683
|
+
if (!isRecording.compareAndSet(true, false)) {
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
Bridge.log("MIC: Recording stopped unexpectedly: $reason")
|
|
688
|
+
cleanUpRecording()
|
|
689
|
+
abandonAudioFocus()
|
|
690
|
+
|
|
691
|
+
if (audioManager.isBluetoothScoOn) {
|
|
692
|
+
audioManager.stopBluetoothSco()
|
|
693
|
+
audioManager.isBluetoothScoOn = false
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
audioManager.mode = AudioManager.MODE_NORMAL
|
|
697
|
+
currentMicMode = ""
|
|
698
|
+
notifyDeviceManager("recording_stopped", getAvailableInputDevices().values.toList())
|
|
699
|
+
}
|
|
700
|
+
|
|
616
701
|
private fun cleanUpRecording() {
|
|
617
702
|
isRecording.set(false)
|
|
618
703
|
|
|
@@ -778,7 +863,8 @@ class PhoneMic private constructor(private val context: Context) {
|
|
|
778
863
|
// Filter out our own recordings
|
|
779
864
|
val otherAppRecordings =
|
|
780
865
|
configs.filter { config ->
|
|
781
|
-
!ourAudioSessionIds.contains(config.clientAudioSessionId)
|
|
866
|
+
!ourAudioSessionIds.contains(config.clientAudioSessionId) &&
|
|
867
|
+
config.clientAudioSource != AUDIO_SOURCE_HOTWORD
|
|
782
868
|
}
|
|
783
869
|
|
|
784
870
|
val wasExternalActive = isExternalAudioActive
|