@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-dev.7
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 +38 -2
- package/android/build.gradle +7 -4
- package/android/lc3Lib/build.gradle +18 -4
- package/android/silero/build.gradle +9 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +39 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +198 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1174 -191
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +61 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +3 -12
- package/build/_private/sdkOtaManifest.d.ts.map +1 -1
- package/build/_private/sdkOtaManifest.js +5 -17
- package/build/_private/sdkOtaManifest.js.map +1 -1
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -2
- package/build/index.js.map +1 -1
- package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
- package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServer.types.js +2 -0
- package/build/ota-server/MentraOtaServer.types.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
- package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.js +3 -0
- package/build/ota-server/MentraOtaServerModule.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.js +12 -0
- package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/expo-module.config.json +3 -2
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +16 -2
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/MentraBluetoothSDK.podspec +5 -1
- package/ios/MentraOtaServerModule.swift +147 -0
- package/ios/MentraPhotoReceiverModule.swift +1 -81
- package/ios/Source/BluetoothSdkDefaults.swift +4 -1
- package/ios/Source/Bridge.swift +47 -3
- package/ios/Source/DeviceManager.swift +135 -10
- package/ios/Source/DeviceStore.swift +2 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +74 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/audio/AudioModels.swift +17 -0
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/events/BluetoothEvents.swift +27 -0
- package/ios/Source/sgcs/MentraLive.swift +439 -30
- package/ios/Source/sgcs/SGCManager.swift +11 -0
- package/ios/Source/status/DeviceStatus.swift +23 -0
- package/ios/Source/types/DeviceModels.swift +23 -2
- package/ios/Source/utils/BleJsonCompact.swift +73 -71
- package/package.json +5 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/verify-release-package.mjs +34 -0
- package/scripts/write-release-metadata.mjs +154 -0
- package/scripts/write-release-metadata.test.mjs +29 -0
- package/src/BluetoothSdk.types.ts +72 -3
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +5 -5
- package/src/_private/MentraLocalNetworkModule.ts +1 -1
- package/src/_private/sdkOtaManifest.ts +5 -17
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +19 -2
- package/src/ota-server/MentraOtaServer.types.ts +10 -0
- package/src/ota-server/MentraOtaServerModule.ts +23 -0
- package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.otaserver
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.mentra.bluetoothsdk.net.LocalIpv4
|
|
5
|
+
import expo.modules.kotlin.modules.Module
|
|
6
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
7
|
+
import java.io.File
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Expo surface for the hotspot-served OTA file server (OS-1676).
|
|
11
|
+
*
|
|
12
|
+
* JS hands over the rewritten manifest body and a map of sha256 -> local file path, and gets
|
|
13
|
+
* back the base URL the glasses should be pointed at via ota_start. The optional host
|
|
14
|
+
* argument lets the caller pass the authoritative scoped-network address (from
|
|
15
|
+
* MentraLocalNetwork's LinkProperties); otherwise interface scanning is used.
|
|
16
|
+
*/
|
|
17
|
+
class MentraOtaServerModule : Module() {
|
|
18
|
+
private var otaServer: LocalOtaServer? = null
|
|
19
|
+
|
|
20
|
+
override fun definition() = ModuleDefinition {
|
|
21
|
+
Name("MentraOtaServer")
|
|
22
|
+
|
|
23
|
+
AsyncFunction("startOtaServer") { manifestJson: String, artifactPaths: Map<String, String>, host: String? ->
|
|
24
|
+
startOtaServer(manifestJson, artifactPaths, host)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
AsyncFunction("stopOtaServer") {
|
|
28
|
+
stopOtaServerInternal()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// API parity with iOS. Android hotspot OTA obtains the authoritative address
|
|
32
|
+
// directly from the scoped Network and does not call this fallback.
|
|
33
|
+
AsyncFunction("waitForWifiAddress") { _: String, _: Int ->
|
|
34
|
+
LocalIpv4.bestLocalIpv4Address()
|
|
35
|
+
?: throw IllegalStateException("No Wi-Fi/LAN IPv4 address found for this phone.")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
OnDestroy {
|
|
39
|
+
closeOtaServerInternal()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Synchronized
|
|
44
|
+
private fun startOtaServer(
|
|
45
|
+
manifestJson: String,
|
|
46
|
+
artifactPaths: Map<String, String>,
|
|
47
|
+
hostOverride: String?,
|
|
48
|
+
): Map<String, Any> {
|
|
49
|
+
val artifacts = artifactPaths.mapValues { (_, path) ->
|
|
50
|
+
File(path.removePrefix("file://"))
|
|
51
|
+
}
|
|
52
|
+
artifacts.forEach { (key, file) ->
|
|
53
|
+
if (!file.isFile) {
|
|
54
|
+
throw IllegalArgumentException("Artifact $key not found at ${file.absolutePath}")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
val server = otaServer ?: LocalOtaServer(
|
|
59
|
+
onLog = { message -> emitStatus(message) },
|
|
60
|
+
).also {
|
|
61
|
+
otaServer = it
|
|
62
|
+
}
|
|
63
|
+
server.configure(manifestJson, artifacts)
|
|
64
|
+
|
|
65
|
+
val host = hostOverride?.takeIf { it.isNotBlank() }
|
|
66
|
+
?: LocalIpv4.bestLocalIpv4Address()
|
|
67
|
+
?: throw IllegalStateException("No Wi-Fi/LAN IPv4 address found for this phone.")
|
|
68
|
+
|
|
69
|
+
server.activePort?.let { activePort ->
|
|
70
|
+
emitStatus("OTA server ready at http://$host:$activePort/version.json")
|
|
71
|
+
return serverResult(host, activePort)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var lastError: Throwable? = null
|
|
75
|
+
for (port in OTA_PORTS) {
|
|
76
|
+
try {
|
|
77
|
+
val actualPort = server.start(host, port)
|
|
78
|
+
emitStatus("OTA server ready at http://$host:$actualPort/version.json")
|
|
79
|
+
return serverResult(host, actualPort)
|
|
80
|
+
} catch (error: Throwable) {
|
|
81
|
+
lastError = error
|
|
82
|
+
emitStatus("Port $port unavailable: ${error.message ?: error::class.java.simpleName}")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw IllegalStateException(
|
|
87
|
+
"Could not start OTA server: ${lastError?.message ?: "all ports unavailable"}",
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Synchronized
|
|
92
|
+
private fun stopOtaServerInternal() {
|
|
93
|
+
otaServer?.stop()
|
|
94
|
+
emitStatus("OTA server stopped")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Synchronized
|
|
98
|
+
private fun closeOtaServerInternal() {
|
|
99
|
+
otaServer?.close()
|
|
100
|
+
otaServer = null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun emitStatus(message: String) {
|
|
104
|
+
Log.d(TAG, message)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun serverResult(host: String, port: Int): Map<String, Any> {
|
|
108
|
+
return mapOf(
|
|
109
|
+
"baseUrl" to "http://$host:$port",
|
|
110
|
+
"manifestUrl" to "http://$host:$port/version.json",
|
|
111
|
+
"host" to host,
|
|
112
|
+
"port" to port,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private companion object {
|
|
117
|
+
const val TAG = "MentraOtaServer"
|
|
118
|
+
val OTA_PORTS = listOf(8791, 8792, 8793, 8794)
|
|
119
|
+
}
|
|
120
|
+
}
|
package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt
CHANGED
|
@@ -4,11 +4,10 @@ import android.content.Context
|
|
|
4
4
|
import android.net.Uri
|
|
5
5
|
import android.util.Log
|
|
6
6
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
7
|
+
import com.mentra.bluetoothsdk.net.LocalIpv4
|
|
7
8
|
import expo.modules.kotlin.exception.Exceptions
|
|
8
9
|
import expo.modules.kotlin.modules.Module
|
|
9
10
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
10
|
-
import java.net.Inet4Address
|
|
11
|
-
import java.net.NetworkInterface
|
|
12
11
|
|
|
13
12
|
class MentraPhotoReceiverModule : Module() {
|
|
14
13
|
private var photoUploadServer: LocalPhotoUploadServer? = null
|
|
@@ -45,7 +44,7 @@ class MentraPhotoReceiverModule : Module() {
|
|
|
45
44
|
photoUploadServer = it
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
val host = bestLocalIpv4Address()
|
|
47
|
+
val host = LocalIpv4.bestLocalIpv4Address()
|
|
49
48
|
?: throw IllegalStateException("No Wi-Fi/LAN IPv4 address found for this phone.")
|
|
50
49
|
|
|
51
50
|
server.activePort?.let { activePort ->
|
|
@@ -128,39 +127,6 @@ class MentraPhotoReceiverModule : Module() {
|
|
|
128
127
|
?: throw Exceptions.ReactContextLost()
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
private fun bestLocalIpv4Address(): String? {
|
|
132
|
-
val wifiPrivateCandidates = mutableListOf<Inet4Address>()
|
|
133
|
-
val wifiCandidates = mutableListOf<Inet4Address>()
|
|
134
|
-
val privateCandidates = mutableListOf<Inet4Address>()
|
|
135
|
-
val otherCandidates = mutableListOf<Inet4Address>()
|
|
136
|
-
val interfaces = NetworkInterface.getNetworkInterfaces()?.toList().orEmpty()
|
|
137
|
-
for (networkInterface in interfaces) {
|
|
138
|
-
if (!networkInterface.isUp || networkInterface.isLoopback) {
|
|
139
|
-
continue
|
|
140
|
-
}
|
|
141
|
-
val addresses = networkInterface.inetAddresses.toList()
|
|
142
|
-
.filterIsInstance<Inet4Address>()
|
|
143
|
-
.filter { address ->
|
|
144
|
-
!address.isLoopbackAddress &&
|
|
145
|
-
!address.isLinkLocalAddress
|
|
146
|
-
}
|
|
147
|
-
if (isWifiInterface(networkInterface.name)) {
|
|
148
|
-
wifiPrivateCandidates += addresses.filter { isPrivateIpv4(it.hostAddress.orEmpty()) }
|
|
149
|
-
wifiCandidates += addresses
|
|
150
|
-
} else {
|
|
151
|
-
privateCandidates += addresses.filter { isPrivateIpv4(it.hostAddress.orEmpty()) }
|
|
152
|
-
otherCandidates += addresses
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
wifiPrivateCandidates.firstOrNull() ?:
|
|
158
|
-
wifiCandidates.firstOrNull() ?:
|
|
159
|
-
privateCandidates.firstOrNull() ?:
|
|
160
|
-
otherCandidates.firstOrNull()
|
|
161
|
-
)?.hostAddress
|
|
162
|
-
}
|
|
163
|
-
|
|
164
130
|
private fun receiverEndpoint(host: String, port: Int): ReceiverEndpoint {
|
|
165
131
|
return ReceiverEndpoint(
|
|
166
132
|
uploadUrl = "http://$host:$port/upload",
|
|
@@ -177,19 +143,6 @@ class MentraPhotoReceiverModule : Module() {
|
|
|
177
143
|
)
|
|
178
144
|
}
|
|
179
145
|
|
|
180
|
-
private fun isWifiInterface(name: String): Boolean {
|
|
181
|
-
return name.startsWith("wlan") ||
|
|
182
|
-
name.startsWith("p2p") ||
|
|
183
|
-
name.startsWith("ap") ||
|
|
184
|
-
name.startsWith("swlan")
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private fun isPrivateIpv4(host: String): Boolean {
|
|
188
|
-
return host.startsWith("192.168.") ||
|
|
189
|
-
host.startsWith("10.") ||
|
|
190
|
-
host.matches(Regex("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"))
|
|
191
|
-
}
|
|
192
|
-
|
|
193
146
|
private companion object {
|
|
194
147
|
const val TAG = "MentraPhotoReceiver"
|
|
195
148
|
val PHOTO_PORTS = listOf(8787, 8788, 8789, 8790)
|
|
@@ -20,6 +20,35 @@ class ForegroundService : Service() {
|
|
|
20
20
|
const val NOTIFICATION_ID = 1001
|
|
21
21
|
const val ACTION_REFRESH_TYPES =
|
|
22
22
|
"com.mentra.bluetoothsdk.services.action.REFRESH_FOREGROUND_SERVICE_TYPES"
|
|
23
|
+
|
|
24
|
+
internal fun bootstrapServiceType(): Int =
|
|
25
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
26
|
+
|
|
27
|
+
internal fun preferredServiceType(
|
|
28
|
+
hasConnectedDeviceAccess: Boolean,
|
|
29
|
+
hasMicrophoneAccess: Boolean,
|
|
30
|
+
hasLocationAccess: Boolean,
|
|
31
|
+
includeMediaPlayback: Boolean = true,
|
|
32
|
+
): Int {
|
|
33
|
+
var serviceType = 0
|
|
34
|
+
|
|
35
|
+
if (includeMediaPlayback) {
|
|
36
|
+
serviceType =
|
|
37
|
+
serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
|
38
|
+
}
|
|
39
|
+
if (hasConnectedDeviceAccess) {
|
|
40
|
+
serviceType =
|
|
41
|
+
serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
|
42
|
+
}
|
|
43
|
+
if (hasMicrophoneAccess) {
|
|
44
|
+
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
|
45
|
+
}
|
|
46
|
+
if (hasLocationAccess) {
|
|
47
|
+
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return if (serviceType == 0) bootstrapServiceType() else serviceType
|
|
51
|
+
}
|
|
23
52
|
}
|
|
24
53
|
|
|
25
54
|
private var locationTypeRequested = false
|
|
@@ -28,7 +57,11 @@ class ForegroundService : Service() {
|
|
|
28
57
|
super.onCreate()
|
|
29
58
|
Bridge.log("ForegroundService: onCreate() called")
|
|
30
59
|
BleTraceLogger.logLifecycle(this, "ForegroundService", "service_create")
|
|
31
|
-
|
|
60
|
+
// Enter the foreground immediately with a type that has no runtime
|
|
61
|
+
// prerequisites. onStartCommand() replaces this bootstrap type with the
|
|
62
|
+
// eligible long-running types, deliberately omitting dataSync so Android 15's
|
|
63
|
+
// six-hour dataSync timer no longer applies.
|
|
64
|
+
startForegroundWithType(bootstrapServiceType())
|
|
32
65
|
}
|
|
33
66
|
|
|
34
67
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
@@ -50,8 +83,10 @@ class ForegroundService : Service() {
|
|
|
50
83
|
}
|
|
51
84
|
|
|
52
85
|
private fun startForegroundWithAutoDetectedType() {
|
|
53
|
-
|
|
86
|
+
startForegroundWithType(detectServiceType())
|
|
87
|
+
}
|
|
54
88
|
|
|
89
|
+
private fun startForegroundWithType(serviceType: Int) {
|
|
55
90
|
createNotificationChannel()
|
|
56
91
|
|
|
57
92
|
val notification =
|
|
@@ -65,7 +100,7 @@ class ForegroundService : Service() {
|
|
|
65
100
|
|
|
66
101
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
67
102
|
Bridge.log(
|
|
68
|
-
"ForegroundService: Starting with
|
|
103
|
+
"ForegroundService: Starting with type: ${getServiceTypeName(serviceType)}"
|
|
69
104
|
)
|
|
70
105
|
startForeground(NOTIFICATION_ID, notification, serviceType)
|
|
71
106
|
} else {
|
|
@@ -79,14 +114,11 @@ class ForegroundService : Service() {
|
|
|
79
114
|
return 0 // No service types before Android Q
|
|
80
115
|
}
|
|
81
116
|
|
|
82
|
-
var serviceType = 0
|
|
83
|
-
|
|
84
117
|
// Audio prompts can be initiated by glasses while the host Activity is
|
|
85
118
|
// backgrounded. mediaPlayback has no runtime prerequisite, so keep it
|
|
86
119
|
// active on the existing Mentra service for the entire connected
|
|
87
120
|
// session rather than trying to launch a second service after a wake
|
|
88
121
|
// phrase arrives.
|
|
89
|
-
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
|
90
122
|
Bridge.log("ForegroundService: Added mediaPlayback (supports background audio)")
|
|
91
123
|
|
|
92
124
|
// Check Bluetooth permissions
|
|
@@ -106,28 +138,32 @@ class ForegroundService : Service() {
|
|
|
106
138
|
}
|
|
107
139
|
}
|
|
108
140
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
val hasNetworkStatePermission =
|
|
141
|
+
// These normal permissions satisfy the connectedDevice FGS prerequisite without
|
|
142
|
+
// waiting for a runtime Bluetooth grant. CHANGE_WIFI_STATE is declared by the SDK;
|
|
143
|
+
// CHANGE_NETWORK_STATE is also accepted when a host app already declares it.
|
|
144
|
+
val hasConnectedDeviceManifestPermission =
|
|
114
145
|
ContextCompat.checkSelfPermission(
|
|
115
146
|
this,
|
|
116
147
|
android.Manifest.permission.CHANGE_NETWORK_STATE
|
|
117
|
-
) == PackageManager.PERMISSION_GRANTED
|
|
148
|
+
) == PackageManager.PERMISSION_GRANTED ||
|
|
149
|
+
ContextCompat.checkSelfPermission(
|
|
150
|
+
this,
|
|
151
|
+
android.Manifest.permission.CHANGE_WIFI_STATE
|
|
152
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
118
153
|
|
|
119
|
-
// Use connectedDevice if we have
|
|
120
|
-
// This avoids falling back to dataSync (which has a
|
|
154
|
+
// Use connectedDevice if we have either Bluetooth or a qualifying normal permission.
|
|
155
|
+
// This avoids falling back to dataSync (which has a six-hour timeout on Android 15+)
|
|
121
156
|
// when users deny Bluetooth permissions.
|
|
122
|
-
|
|
123
|
-
|
|
157
|
+
val hasConnectedDeviceAccess =
|
|
158
|
+
hasBluetoothPermission || hasConnectedDeviceManifestPermission
|
|
159
|
+
if (hasConnectedDeviceAccess) {
|
|
124
160
|
if (hasBluetoothPermission) {
|
|
125
161
|
Bridge.log("ForegroundService: Added connectedDevice (has Bluetooth permission)")
|
|
126
162
|
} else {
|
|
127
|
-
Bridge.log("ForegroundService: Added connectedDevice (has
|
|
163
|
+
Bridge.log("ForegroundService: Added connectedDevice (has network control permission)")
|
|
128
164
|
}
|
|
129
165
|
} else {
|
|
130
|
-
Bridge.log("ForegroundService: No
|
|
166
|
+
Bridge.log("ForegroundService: No qualifying permission for connectedDevice")
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
// Check microphone permission
|
|
@@ -136,7 +172,6 @@ class ForegroundService : Service() {
|
|
|
136
172
|
PackageManager.PERMISSION_GRANTED
|
|
137
173
|
|
|
138
174
|
if (hasMicPermission) {
|
|
139
|
-
serviceType = serviceType or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
|
140
175
|
Bridge.log("ForegroundService: Added microphone (has RECORD_AUDIO permission)")
|
|
141
176
|
} else {
|
|
142
177
|
Bridge.log("ForegroundService: No microphone permission")
|
|
@@ -154,8 +189,8 @@ class ForegroundService : Service() {
|
|
|
154
189
|
val locationManager = getSystemService(LocationManager::class.java)
|
|
155
190
|
val isLocationEnabled = locationManager?.isLocationEnabled == true
|
|
156
191
|
|
|
157
|
-
|
|
158
|
-
|
|
192
|
+
val hasLocationAccess = locationTypeRequested && hasLocationPermission && isLocationEnabled
|
|
193
|
+
if (hasLocationAccess) {
|
|
159
194
|
Bridge.log("ForegroundService: Added location (has foreground location permission)")
|
|
160
195
|
} else {
|
|
161
196
|
Bridge.log(
|
|
@@ -164,15 +199,11 @@ class ForegroundService : Service() {
|
|
|
164
199
|
)
|
|
165
200
|
}
|
|
166
201
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
Bridge.log("ForegroundService: WARNING - Using dataSync as fallback (6-hour timeout on Android 14+)")
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return serviceType
|
|
202
|
+
return preferredServiceType(
|
|
203
|
+
hasConnectedDeviceAccess = hasConnectedDeviceAccess,
|
|
204
|
+
hasMicrophoneAccess = hasMicPermission,
|
|
205
|
+
hasLocationAccess = hasLocationAccess,
|
|
206
|
+
)
|
|
176
207
|
}
|
|
177
208
|
|
|
178
209
|
private fun getNotificationText(serviceType: Int): String {
|