diff --git a/apps/android/app/src/main/java/ai/openclaw/android/node/CameraHandler.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/CameraHandler.kt index 658c117ff31..f142a11f82e 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/node/CameraHandler.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/CameraHandler.kt @@ -9,9 +9,25 @@ import ai.openclaw.android.gateway.GatewaySession import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody +internal fun parseCameraClipUploadUrl(responseBody: String): String? { + if (responseBody.isBlank()) return null + val root = + try { + Json.parseToJsonElement(responseBody).asObjectOrNull() + } catch (_: Throwable) { + return null + } ?: return null + val urlPrimitive = root["url"] as? JsonPrimitive ?: return null + if (!urlPrimitive.isString) return null + return urlPrimitive.contentOrNull?.trim()?.ifEmpty { null } +} + class CameraHandler( private val appContext: Context, private val camera: CameraCaptureManager, @@ -69,7 +85,7 @@ class CameraHandler( clipLogFile?.appendText("[CLIP $ts] $msg\n") android.util.Log.w("openclaw", "camera.clip: $msg") } - val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false + val includeAudio = parseIncludeAudio(paramsJson) ?: true if (includeAudio) externalAudioCaptureActive.value = true try { clipLogFile?.writeText("") // clear @@ -123,9 +139,7 @@ class CameraHandler( clipLog("upload response: ${resp.code} $respBody") filePayload.file.delete() if (!resp.isSuccessful) throw Exception("upload failed: HTTP ${resp.code}") - // Parse URL from response - val urlMatch = Regex("\"url\":\"([^\"]+)\"").find(respBody) - urlMatch?.groupValues?.get(1) ?: throw Exception("no url in response: $respBody") + parseCameraClipUploadUrl(respBody) ?: throw Exception("no url in response: $respBody") } } catch (err: Throwable) { clipLog("upload failed: ${err.message}, falling back to base64") @@ -154,4 +168,24 @@ class CameraHandler( if (includeAudio) externalAudioCaptureActive.value = false } } + + private fun parseIncludeAudio(paramsJson: String?): Boolean? { + if (paramsJson.isNullOrBlank()) return null + val root = + try { + Json.parseToJsonElement(paramsJson).asObjectOrNull() + } catch (_: Throwable) { + null + } ?: return null + val value = + (root["includeAudio"] as? JsonPrimitive) + ?.contentOrNull + ?.trim() + ?.lowercase() + return when (value) { + "true" -> true + "false" -> false + else -> null + } + } } diff --git a/apps/android/app/src/test/java/ai/openclaw/android/node/CameraHandlerTest.kt b/apps/android/app/src/test/java/ai/openclaw/android/node/CameraHandlerTest.kt new file mode 100644 index 00000000000..d0e76bcdb7d --- /dev/null +++ b/apps/android/app/src/test/java/ai/openclaw/android/node/CameraHandlerTest.kt @@ -0,0 +1,31 @@ +package ai.openclaw.android.node + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class CameraHandlerTest { + @Test + fun parseCameraClipUploadUrl_returnsUrlForValidPayload() { + val actual = parseCameraClipUploadUrl("""{"url":"https://example.com/upload/clip.mp4"}""") + + assertEquals("https://example.com/upload/clip.mp4", actual) + } + + @Test + fun parseCameraClipUploadUrl_trimsUrlWhitespace() { + val actual = parseCameraClipUploadUrl("""{"url":" https://example.com/u.mp4 "}""") + + assertEquals("https://example.com/u.mp4", actual) + } + + @Test + fun parseCameraClipUploadUrl_returnsNullForMalformedPayloads() { + assertNull(parseCameraClipUploadUrl("")) + assertNull(parseCameraClipUploadUrl("not-json")) + assertNull(parseCameraClipUploadUrl("""{"ok":true}""")) + assertNull(parseCameraClipUploadUrl("""{"url":123}""")) + assertNull(parseCameraClipUploadUrl("""{"url":" "}""")) + } +} +