fix(android): refresh hello on sms permission grant
This commit is contained in:
@@ -118,6 +118,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
|
|||||||
runtime.setTalkEnabled(enabled)
|
runtime.setTalkEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshBridgeHello() {
|
||||||
|
runtime.refreshBridgeHello()
|
||||||
|
}
|
||||||
|
|
||||||
fun connect(endpoint: BridgeEndpoint) {
|
fun connect(endpoint: BridgeEndpoint) {
|
||||||
runtime.connect(endpoint)
|
runtime.connect(endpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,73 +367,112 @@ class NodeRuntime(context: Context) {
|
|||||||
prefs.setTalkEnabled(value)
|
prefs.setTalkEnabled(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildInvokeCommands(): List<String> =
|
||||||
|
buildList {
|
||||||
|
add(ClawdisCanvasCommand.Present.rawValue)
|
||||||
|
add(ClawdisCanvasCommand.Hide.rawValue)
|
||||||
|
add(ClawdisCanvasCommand.Navigate.rawValue)
|
||||||
|
add(ClawdisCanvasCommand.Eval.rawValue)
|
||||||
|
add(ClawdisCanvasCommand.Snapshot.rawValue)
|
||||||
|
add(ClawdisCanvasA2UICommand.Push.rawValue)
|
||||||
|
add(ClawdisCanvasA2UICommand.PushJSONL.rawValue)
|
||||||
|
add(ClawdisCanvasA2UICommand.Reset.rawValue)
|
||||||
|
add(ClawdisScreenCommand.Record.rawValue)
|
||||||
|
if (cameraEnabled.value) {
|
||||||
|
add(ClawdisCameraCommand.Snap.rawValue)
|
||||||
|
add(ClawdisCameraCommand.Clip.rawValue)
|
||||||
|
}
|
||||||
|
if (locationMode.value != LocationMode.Off) {
|
||||||
|
add(ClawdisLocationCommand.Get.rawValue)
|
||||||
|
}
|
||||||
|
if (sms.canSendSms()) {
|
||||||
|
add(ClawdisSmsCommand.Send.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildCapabilities(): List<String> =
|
||||||
|
buildList {
|
||||||
|
add(ClawdisCapability.Canvas.rawValue)
|
||||||
|
add(ClawdisCapability.Screen.rawValue)
|
||||||
|
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue)
|
||||||
|
if (sms.canSendSms()) add(ClawdisCapability.Sms.rawValue)
|
||||||
|
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
|
||||||
|
add(ClawdisCapability.VoiceWake.rawValue)
|
||||||
|
}
|
||||||
|
if (locationMode.value != LocationMode.Off) {
|
||||||
|
add(ClawdisCapability.Location.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildPairingHello(token: String?): BridgePairingClient.Hello {
|
||||||
|
val modelIdentifier = listOfNotNull(Build.MANUFACTURER, Build.MODEL)
|
||||||
|
.joinToString(" ")
|
||||||
|
.trim()
|
||||||
|
.ifEmpty { null }
|
||||||
|
val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" }
|
||||||
|
val advertisedVersion =
|
||||||
|
if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) {
|
||||||
|
"$versionName-dev"
|
||||||
|
} else {
|
||||||
|
versionName
|
||||||
|
}
|
||||||
|
return BridgePairingClient.Hello(
|
||||||
|
nodeId = instanceId.value,
|
||||||
|
displayName = displayName.value,
|
||||||
|
token = token,
|
||||||
|
platform = "Android",
|
||||||
|
version = advertisedVersion,
|
||||||
|
deviceFamily = "Android",
|
||||||
|
modelIdentifier = modelIdentifier,
|
||||||
|
caps = buildCapabilities(),
|
||||||
|
commands = buildInvokeCommands(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSessionHello(token: String?): BridgeSession.Hello {
|
||||||
|
val modelIdentifier = listOfNotNull(Build.MANUFACTURER, Build.MODEL)
|
||||||
|
.joinToString(" ")
|
||||||
|
.trim()
|
||||||
|
.ifEmpty { null }
|
||||||
|
val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" }
|
||||||
|
val advertisedVersion =
|
||||||
|
if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) {
|
||||||
|
"$versionName-dev"
|
||||||
|
} else {
|
||||||
|
versionName
|
||||||
|
}
|
||||||
|
return BridgeSession.Hello(
|
||||||
|
nodeId = instanceId.value,
|
||||||
|
displayName = displayName.value,
|
||||||
|
token = token,
|
||||||
|
platform = "Android",
|
||||||
|
version = advertisedVersion,
|
||||||
|
deviceFamily = "Android",
|
||||||
|
modelIdentifier = modelIdentifier,
|
||||||
|
caps = buildCapabilities(),
|
||||||
|
commands = buildInvokeCommands(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshBridgeHello() {
|
||||||
|
scope.launch {
|
||||||
|
if (!_isConnected.value) return@launch
|
||||||
|
val token = prefs.loadBridgeToken()
|
||||||
|
if (token.isNullOrBlank()) return@launch
|
||||||
|
session.updateHello(buildSessionHello(token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun connect(endpoint: BridgeEndpoint) {
|
fun connect(endpoint: BridgeEndpoint) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_statusText.value = "Connecting…"
|
_statusText.value = "Connecting…"
|
||||||
val storedToken = prefs.loadBridgeToken()
|
val storedToken = prefs.loadBridgeToken()
|
||||||
val modelIdentifier = listOfNotNull(Build.MANUFACTURER, Build.MODEL)
|
|
||||||
.joinToString(" ")
|
|
||||||
.trim()
|
|
||||||
.ifEmpty { null }
|
|
||||||
|
|
||||||
val invokeCommands =
|
|
||||||
buildList {
|
|
||||||
add(ClawdisCanvasCommand.Present.rawValue)
|
|
||||||
add(ClawdisCanvasCommand.Hide.rawValue)
|
|
||||||
add(ClawdisCanvasCommand.Navigate.rawValue)
|
|
||||||
add(ClawdisCanvasCommand.Eval.rawValue)
|
|
||||||
add(ClawdisCanvasCommand.Snapshot.rawValue)
|
|
||||||
add(ClawdisCanvasA2UICommand.Push.rawValue)
|
|
||||||
add(ClawdisCanvasA2UICommand.PushJSONL.rawValue)
|
|
||||||
add(ClawdisCanvasA2UICommand.Reset.rawValue)
|
|
||||||
add(ClawdisScreenCommand.Record.rawValue)
|
|
||||||
if (cameraEnabled.value) {
|
|
||||||
add(ClawdisCameraCommand.Snap.rawValue)
|
|
||||||
add(ClawdisCameraCommand.Clip.rawValue)
|
|
||||||
}
|
|
||||||
if (locationMode.value != LocationMode.Off) {
|
|
||||||
add(ClawdisLocationCommand.Get.rawValue)
|
|
||||||
}
|
|
||||||
if (sms.canSendSms()) {
|
|
||||||
add(ClawdisSmsCommand.Send.rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val resolved =
|
val resolved =
|
||||||
if (storedToken.isNullOrBlank()) {
|
if (storedToken.isNullOrBlank()) {
|
||||||
_statusText.value = "Pairing…"
|
_statusText.value = "Pairing…"
|
||||||
val caps = buildList {
|
|
||||||
add(ClawdisCapability.Canvas.rawValue)
|
|
||||||
add(ClawdisCapability.Screen.rawValue)
|
|
||||||
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue)
|
|
||||||
if (sms.canSendSms()) add(ClawdisCapability.Sms.rawValue)
|
|
||||||
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
|
|
||||||
add(ClawdisCapability.VoiceWake.rawValue)
|
|
||||||
}
|
|
||||||
if (locationMode.value != LocationMode.Off) {
|
|
||||||
add(ClawdisCapability.Location.rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" }
|
|
||||||
val advertisedVersion =
|
|
||||||
if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) {
|
|
||||||
"$versionName-dev"
|
|
||||||
} else {
|
|
||||||
versionName
|
|
||||||
}
|
|
||||||
BridgePairingClient().pairAndHello(
|
BridgePairingClient().pairAndHello(
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
hello =
|
hello = buildPairingHello(token = null),
|
||||||
BridgePairingClient.Hello(
|
|
||||||
nodeId = instanceId.value,
|
|
||||||
displayName = displayName.value,
|
|
||||||
token = null,
|
|
||||||
platform = "Android",
|
|
||||||
version = advertisedVersion,
|
|
||||||
deviceFamily = "Android",
|
|
||||||
modelIdentifier = modelIdentifier,
|
|
||||||
caps = caps,
|
|
||||||
commands = invokeCommands,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
BridgePairingClient.PairResult(ok = true, token = storedToken.trim())
|
BridgePairingClient.PairResult(ok = true, token = storedToken.trim())
|
||||||
@@ -447,39 +486,9 @@ class NodeRuntime(context: Context) {
|
|||||||
|
|
||||||
val authToken = requireNotNull(resolved.token).trim()
|
val authToken = requireNotNull(resolved.token).trim()
|
||||||
prefs.saveBridgeToken(authToken)
|
prefs.saveBridgeToken(authToken)
|
||||||
val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" }
|
|
||||||
val advertisedVersion =
|
|
||||||
if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) {
|
|
||||||
"$versionName-dev"
|
|
||||||
} else {
|
|
||||||
versionName
|
|
||||||
}
|
|
||||||
session.connect(
|
session.connect(
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
hello =
|
hello = buildSessionHello(token = authToken),
|
||||||
BridgeSession.Hello(
|
|
||||||
nodeId = instanceId.value,
|
|
||||||
displayName = displayName.value,
|
|
||||||
token = authToken,
|
|
||||||
platform = "Android",
|
|
||||||
version = advertisedVersion,
|
|
||||||
deviceFamily = "Android",
|
|
||||||
modelIdentifier = modelIdentifier,
|
|
||||||
caps =
|
|
||||||
buildList {
|
|
||||||
add(ClawdisCapability.Canvas.rawValue)
|
|
||||||
add(ClawdisCapability.Screen.rawValue)
|
|
||||||
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue)
|
|
||||||
if (sms.canSendSms()) add(ClawdisCapability.Sms.rawValue)
|
|
||||||
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
|
|
||||||
add(ClawdisCapability.VoiceWake.rawValue)
|
|
||||||
}
|
|
||||||
if (locationMode.value != LocationMode.Off) {
|
|
||||||
add(ClawdisCapability.Location.rawValue)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
commands = invokeCommands,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,13 @@ class BridgeSession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateHello(hello: Hello) {
|
||||||
|
val target = desired ?: return
|
||||||
|
desired = target.first to hello
|
||||||
|
val conn = currentConnection ?: return
|
||||||
|
conn.sendJson(buildHelloJson(hello))
|
||||||
|
}
|
||||||
|
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
desired = null
|
desired = null
|
||||||
// Unblock connectOnce() read loop. Coroutine cancellation alone won't interrupt BufferedReader.readLine().
|
// Unblock connectOnce() read loop. Coroutine cancellation alone won't interrupt BufferedReader.readLine().
|
||||||
@@ -196,20 +203,7 @@ class BridgeSession(
|
|||||||
currentConnection = conn
|
currentConnection = conn
|
||||||
|
|
||||||
try {
|
try {
|
||||||
conn.sendJson(
|
conn.sendJson(buildHelloJson(hello))
|
||||||
buildJsonObject {
|
|
||||||
put("type", JsonPrimitive("hello"))
|
|
||||||
put("nodeId", JsonPrimitive(hello.nodeId))
|
|
||||||
hello.displayName?.let { put("displayName", JsonPrimitive(it)) }
|
|
||||||
hello.token?.let { put("token", JsonPrimitive(it)) }
|
|
||||||
hello.platform?.let { put("platform", JsonPrimitive(it)) }
|
|
||||||
hello.version?.let { put("version", JsonPrimitive(it)) }
|
|
||||||
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
|
|
||||||
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
|
||||||
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
|
||||||
hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
val firstLine = reader.readLine() ?: throw IllegalStateException("bridge closed connection")
|
val firstLine = reader.readLine() ?: throw IllegalStateException("bridge closed connection")
|
||||||
val first = json.parseToJsonElement(firstLine).asObjectOrNull()
|
val first = json.parseToJsonElement(firstLine).asObjectOrNull()
|
||||||
@@ -307,6 +301,20 @@ class BridgeSession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildHelloJson(hello: Hello): JsonObject =
|
||||||
|
buildJsonObject {
|
||||||
|
put("type", JsonPrimitive("hello"))
|
||||||
|
put("nodeId", JsonPrimitive(hello.nodeId))
|
||||||
|
hello.displayName?.let { put("displayName", JsonPrimitive(it)) }
|
||||||
|
hello.token?.let { put("token", JsonPrimitive(it)) }
|
||||||
|
hello.platform?.let { put("platform", JsonPrimitive(it)) }
|
||||||
|
hello.version?.let { put("version", JsonPrimitive(it)) }
|
||||||
|
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
|
||||||
|
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
|
||||||
|
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
||||||
|
hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun normalizeCanvasHostUrl(raw: String?, endpoint: BridgeEndpoint): String? {
|
private fun normalizeCanvasHostUrl(raw: String?, endpoint: BridgeEndpoint): String? {
|
||||||
val trimmed = raw?.trim().orEmpty()
|
val trimmed = raw?.trim().orEmpty()
|
||||||
val parsed = trimmed.takeIf { it.isNotBlank() }?.let { runCatching { URI(it) }.getOrNull() }
|
val parsed = trimmed.takeIf { it.isNotBlank() }?.let { runCatching { URI(it) }.getOrNull() }
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
val smsPermissionLauncher =
|
val smsPermissionLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
smsPermissionGranted = granted
|
smsPermissionGranted = granted
|
||||||
|
viewModel.refreshBridgeHello()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCameraEnabledChecked(checked: Boolean) {
|
fun setCameraEnabledChecked(checked: Boolean) {
|
||||||
|
|||||||
Reference in New Issue
Block a user