diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/WebSocketRepository.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/WebSocketRepository.kt index 6afe0331819..a6675373270 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/WebSocketRepository.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/WebSocketRepository.kt @@ -8,6 +8,9 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Ar import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineEvent import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineListResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineResponse +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraCapabilitiesResponse +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraStreamTypes +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraWebRtcClientConfigResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.CompressedStateChangedEvent import io.homeassistant.companion.android.common.data.websocket.impl.entities.ConversationResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.CurrentUserResponse @@ -24,6 +27,8 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Te import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetTlvResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.TriggerEvent +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcCandidate +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcEvent import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.flow.Flow @@ -134,6 +139,55 @@ interface WebSocketRepository { * @return `true`/`false` indicating if it was enqueued, or `null` on unexpected failures */ suspend fun sendVoiceData(binaryHandlerId: Int, data: ByteArray): Boolean + + /** + * Get the stream types the frontend can use for a camera entity. Consumers should only start + * a WebRTC session when [CameraStreamTypes.WEB_RTC] is reported and fall back to HLS + * otherwise. + * + * Requires Home Assistant Core 2024.11 or later. + * + * @return [CameraCapabilitiesResponse] for the entity, or `null` if the server did not return + * a successful response. + */ + suspend fun getCameraCapabilities(entityId: String): CameraCapabilitiesResponse? + + /** + * Get the WebRTC client configuration (STUN/TURN servers and optional data channel label) to + * use when creating a peer connection for a camera entity. + * + * Requires Home Assistant Core 2024.11 or later. + * + * @return [CameraWebRtcClientConfigResponse] for the entity, or `null` if the server did not + * return a successful response, for example when the camera does not support WebRTC + * (`webrtc_get_client_config_failed`). + */ + suspend fun getCameraWebRtcClientConfig(entityId: String): CameraWebRtcClientConfigResponse? + + /** + * Start a WebRTC session for a camera entity by sending the SDP offer, and subscribe to the + * signaling events for this session. + * + * The subscription lifetime is the session lifetime: when the returned Flow is no longer + * collected the subscription is cancelled with `unsubscribe_events`, which closes the WebRTC + * session on the server (there is no dedicated close command). + * + * Requires Home Assistant Core 2024.11 or later (2024.12 or later for trickle ICE with + * `RTCIceCandidateInit` dictionaries). + * + * @return a Flow that will emit all [WebRtcEvent]s for the session, or `null` if the + * subscription could not be started. + */ + suspend fun startCameraWebRtcSession(entityId: String, offerSdp: String): Flow? + + /** + * Send a local ICE candidate for a WebRTC session previously started with + * [startCameraWebRtcSession]. + * + * @param sessionId the session identifier received in [WebRtcEvent.Session] + * @return `true` if the server accepted the candidate + */ + suspend fun sendCameraWebRtcCandidate(entityId: String, sessionId: String, candidate: WebRtcCandidate): Boolean } internal class WebSocketRepositoryFactory @Inject internal constructor( diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketConstants.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketConstants.kt index 5b457462ea4..e87ace6f2ff 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketConstants.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketConstants.kt @@ -8,6 +8,7 @@ internal object WebSocketConstants { const val EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" const val SUBSCRIBE_TYPE_ASSIST_PIPELINE_RUN = "assist_pipeline/run" + const val SUBSCRIBE_TYPE_CAMERA_WEBRTC_OFFER = "camera/webrtc/offer" const val SUBSCRIBE_TYPE_SUBSCRIBE_EVENTS = "subscribe_events" const val SUBSCRIBE_TYPE_SUBSCRIBE_ENTITIES = "subscribe_entities" const val SUBSCRIBE_TYPE_SUBSCRIBE_TRIGGER = "subscribe_trigger" diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketCoreImpl.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketCoreImpl.kt index 53f40214222..43291337e5d 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketCoreImpl.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketCoreImpl.kt @@ -17,6 +17,7 @@ import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketCo import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.EVENT_ENTITY_REGISTRY_UPDATED import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.EVENT_STATE_CHANGED import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_ASSIST_PIPELINE_RUN +import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_CAMERA_WEBRTC_OFFER import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_RENDER_TEMPLATE import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_SUBSCRIBE_ENTITIES import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_SUBSCRIBE_TRIGGER @@ -46,6 +47,8 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.St import io.homeassistant.companion.android.common.data.websocket.impl.entities.TemplateUpdatedEvent import io.homeassistant.companion.android.common.data.websocket.impl.entities.TriggerEvent import io.homeassistant.companion.android.common.data.websocket.impl.entities.UnknownTypeSocketResponse +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcEvent +import io.homeassistant.companion.android.common.data.websocket.impl.entities.webRtcJsonMapper import io.homeassistant.companion.android.common.util.FailFast import io.homeassistant.companion.android.common.util.MapAnySerializer import io.homeassistant.companion.android.common.util.kotlinJsonMapper @@ -921,6 +924,20 @@ internal class WebSocketCoreImpl( Timber.w("Received Assist pipeline event without type, skipping") return } + } else if (subscriptionType == SUBSCRIBE_TYPE_CAMERA_WEBRTC_OFFER) { + if (response.event != null) { + try { + webRtcJsonMapper.decodeFromJsonElement(response.event) + } catch (e: IllegalArgumentException) { + // Covers SerializationException too, a malformed event must not kill the + // whole message handling + Timber.w(e, "Received malformed WebRTC event, skipping") + return + } + } else { + Timber.w("Received no event for WebRTC subscription, skipping") + return + } } else if (eventResponseType != null && (eventResponseType as? JsonPrimitive)?.isString == true) { when (eventResponseType.content) { EVENT_STATE_CHANGED -> { diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketRepositoryImpl.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketRepositoryImpl.kt index b3b8f4745aa..97d9634090b 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketRepositoryImpl.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketRepositoryImpl.kt @@ -13,6 +13,7 @@ import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketCo import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.EVENT_ENTITY_REGISTRY_UPDATED import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.EVENT_STATE_CHANGED import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_ASSIST_PIPELINE_RUN +import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_CAMERA_WEBRTC_OFFER import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_PUSH_NOTIFICATION_CHANNEL import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_RENDER_TEMPLATE import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_SUBSCRIBE_ENTITIES @@ -23,6 +24,8 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Ar import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineEvent import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineListResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineResponse +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraCapabilitiesResponse +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraWebRtcClientConfigResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.CompressedStateChangedEvent import io.homeassistant.companion.android.common.data.websocket.impl.entities.ConversationResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.CurrentUserResponse @@ -41,6 +44,9 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Te import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetTlvResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.TriggerEvent +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcCandidate +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcEvent +import io.homeassistant.companion.android.common.data.websocket.impl.entities.webRtcJsonMapper import io.homeassistant.companion.android.common.util.kotlinJsonMapper import io.homeassistant.companion.android.common.util.toHexString import io.homeassistant.companion.android.database.server.ServerUserInfo @@ -50,6 +56,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.intOrNull import okhttp3.WebSocketListener @@ -420,6 +427,60 @@ class WebSocketRepositoryImpl internal constructor( return response?.success == true } + override suspend fun getCameraCapabilities(entityId: String): CameraCapabilitiesResponse? { + val socketResponse = webSocketCore.sendMessage( + mapOf( + "type" to "camera/capabilities", + "entity_id" to entityId, + ), + ) + + return mapResponse(socketResponse) + } + + override suspend fun getCameraWebRtcClientConfig(entityId: String): CameraWebRtcClientConfigResponse? { + val socketResponse = webSocketCore.sendMessage( + mapOf( + "type" to "camera/webrtc/get_client_config", + "entity_id" to entityId, + ), + ) + + // The response follows the camelCase W3C dictionaries so it cannot be decoded with the + // shared snake_case mapper used by mapResponse. The success check avoids decoding an + // error payload when the camera does not support WebRTC. + return socketResponse?.takeIf { it.success == true }?.result?.let { + webRtcJsonMapper.decodeFromJsonElement(it) + } + } + + override suspend fun startCameraWebRtcSession(entityId: String, offerSdp: String): Flow? = + webSocketCore.subscribeTo( + SUBSCRIBE_TYPE_CAMERA_WEBRTC_OFFER, + mapOf( + "entity_id" to entityId, + "offer" to offerSdp, + ), + ) + + override suspend fun sendCameraWebRtcCandidate( + entityId: String, + sessionId: String, + candidate: WebRtcCandidate, + ): Boolean { + val response = webSocketCore.sendMessage( + mapOf( + "type" to "camera/webrtc/candidate", + "entity_id" to entityId, + "session_id" to sessionId, + // Pre-encoded with the WebRTC mapper to keep the camelCase keys, the message + // serializer passes JsonElement values through untouched + "candidate" to webRtcJsonMapper.encodeToJsonElement(candidate), + ), + ) + return response?.success == true + } + /** * Update server entry in [serverManager] with information from a [CurrentUserResponse] like user * name and admin status. diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/CameraCapabilitiesResponse.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/CameraCapabilitiesResponse.kt new file mode 100644 index 00000000000..64d3a9559a2 --- /dev/null +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/CameraCapabilitiesResponse.kt @@ -0,0 +1,22 @@ +package io.homeassistant.companion.android.common.data.websocket.impl.entities + +import kotlinx.serialization.Serializable + +/** + * Response for the `camera/capabilities` WebSocket command. + * + * The server reports which stream types the frontend can use for a camera entity. Consumers + * should check for [CameraStreamTypes.WEB_RTC] before starting a WebRTC session and fall back to + * HLS otherwise. + */ +@Serializable +data class CameraCapabilitiesResponse(val frontendStreamTypes: List = emptyList()) + +/** + * Known values of [CameraCapabilitiesResponse.frontendStreamTypes], matching the + * `StreamType` enum of Home Assistant Core. + */ +object CameraStreamTypes { + const val HLS = "hls" + const val WEB_RTC = "web_rtc" +} diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/CameraWebRtcClientConfigResponse.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/CameraWebRtcClientConfigResponse.kt new file mode 100644 index 00000000000..144203bf53e --- /dev/null +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/CameraWebRtcClientConfigResponse.kt @@ -0,0 +1,52 @@ +package io.homeassistant.companion.android.common.data.websocket.impl.entities + +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonTransformingSerializer + +/** + * Response for the `camera/webrtc/get_client_config` WebSocket command. + * + * The keys are camelCase on the wire (they mirror the W3C `RTCConfiguration` dictionary), so this + * class must be deserialized with [webRtcJsonMapper] and not the shared snake_case mapper. + * + * @property configuration the `RTCConfiguration` to create the peer connection with + * @property dataChannel label of a data channel the client should open, used by some WebRTC + * providers (like go2rtc) to negotiate additional features. `null` when the provider does not use + * a data channel. + */ +@Serializable +data class CameraWebRtcClientConfigResponse( + val configuration: WebRtcConfiguration = WebRtcConfiguration(), + val dataChannel: String? = null, +) + +/** + * The subset of the W3C `RTCConfiguration` dictionary sent by Home Assistant Core. + */ +@Serializable +data class WebRtcConfiguration(val iceServers: List = emptyList()) + +/** + * A single `RTCIceServer` entry (STUN or TURN server) of an `RTCConfiguration`. + */ +@Serializable +data class WebRtcIceServer( + @Serializable(with = StringOrStringListSerializer::class) + val urls: List = emptyList(), + val username: String? = null, + val credential: String? = null, +) + +/** + * The `urls` member of `RTCIceServer` is allowed to be either a single string or a list of + * strings. This serializer normalizes both shapes to a list. + */ +private object StringOrStringListSerializer : + JsonTransformingSerializer>(ListSerializer(String.serializer())) { + override fun transformDeserialize(element: JsonElement): JsonElement = + element as? JsonArray ?: JsonArray(listOf(element)) +} diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/WebRtcEvent.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/WebRtcEvent.kt new file mode 100644 index 00000000000..8a35fc0c1aa --- /dev/null +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/WebRtcEvent.kt @@ -0,0 +1,87 @@ +package io.homeassistant.companion.android.common.data.websocket.impl.entities + +import io.homeassistant.companion.android.common.util.UnknownJsonContent +import io.homeassistant.companion.android.common.util.UnknownJsonContentBuilder +import io.homeassistant.companion.android.common.util.UnknownJsonContentDeserializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.modules.SerializersModule + +/** + * JSON mapper dedicated to the `camera/webrtc` WebSocket commands. + * + * These payloads cannot be handled by the shared [io.homeassistant.companion.android.common.util.kotlinJsonMapper]: + * its global snake_case naming strategy would also rewrite explicit `@SerialName` values, while + * the WebRTC API mixes snake_case keys (`session_id`) with the camelCase keys of the standard + * `RTCIceCandidateInit`/`RTCConfiguration` dictionaries (`sdpMid`, `iceServers`, ...). + */ +internal val webRtcJsonMapper = Json { + ignoreUnknownKeys = true + // Omit null values so optional candidate fields are sent the same way as the frontend, + // which leaves undefined values out of the JSON payload + explicitNulls = false + serializersModule = SerializersModule { + polymorphicDefaultDeserializer(WebRtcEvent::class) { className -> + object : UnknownJsonContentDeserializer() { + override val builder = UnknownJsonContentBuilder { content -> + WebRtcEvent.Unknown(className, content) + } + } + } + } +} + +/** + * Event received on a `camera/webrtc/offer` subscription. + * + * The server pushes these events while a WebRTC session is being negotiated: first a [Session] + * with the identifier needed to send candidates back, then an [Answer], then zero or more + * [Candidate]s (trickle ICE). An [Error] can arrive at any time and ends the negotiation. + */ +@Serializable +sealed interface WebRtcEvent { + + /** The server created a session and assigned it an identifier. */ + @Serializable + @SerialName("session") + data class Session(@SerialName("session_id") val sessionId: String) : WebRtcEvent + + /** The SDP answer from the camera or its WebRTC provider. */ + @Serializable + @SerialName("answer") + data class Answer(val answer: String) : WebRtcEvent + + /** A remote ICE candidate discovered by the camera or its WebRTC provider. */ + @Serializable + @SerialName("candidate") + data class Candidate(val candidate: WebRtcCandidate) : WebRtcEvent + + /** Negotiation failed, for example `webrtc_offer_failed`. */ + @Serializable + @SerialName("error") + data class Error(val code: String, val message: String? = null) : WebRtcEvent + + /** + * Fallback for event types this version of the app does not know, so that a server-side + * addition never breaks an ongoing subscription. + */ + data class Unknown(override val discriminator: String?, override val content: JsonElement) : + WebRtcEvent, + UnknownJsonContent +} + +/** + * A standard `RTCIceCandidateInit` dictionary, exchanged in both directions during trickle ICE. + * + * The keys are camelCase on the wire (like in the W3C WebRTC specification), which is why this + * class must be serialized with [webRtcJsonMapper] and not the shared snake_case mapper. + */ +@Serializable +data class WebRtcCandidate( + val candidate: String, + val sdpMid: String? = null, + val sdpMLineIndex: Int? = null, + val usernameFragment: String? = null, +) diff --git a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketRepositoryImplTest.kt b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketRepositoryImplTest.kt index c0fa12bf0e7..4869a574e3a 100644 --- a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketRepositoryImplTest.kt +++ b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/WebSocketRepositoryImplTest.kt @@ -3,8 +3,17 @@ package io.homeassistant.companion.android.common.data.websocket.impl import io.homeassistant.companion.android.common.data.servers.ServerManager import io.homeassistant.companion.android.common.data.websocket.WebSocketCore import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_ASSIST_PIPELINE_RUN +import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketConstants.SUBSCRIBE_TYPE_CAMERA_WEBRTC_OFFER import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineEvent +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraStreamTypes +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraWebRtcClientConfigResponse +import io.homeassistant.companion.android.common.data.websocket.impl.entities.MessageSocketResponse +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcCandidate +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcConfiguration +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcEvent +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcIceServer import io.homeassistant.companion.android.common.util.VOICE_SAMPLE_RATE +import io.homeassistant.companion.android.common.util.kotlinJsonMapper import io.homeassistant.companion.android.database.server.Server import io.homeassistant.companion.android.database.server.ServerConnectionInfo import io.homeassistant.companion.android.database.server.ServerSessionInfo @@ -17,9 +26,15 @@ import io.mockk.slot import io.mockk.unmockkAll import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.intOrNull import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -229,6 +244,142 @@ class WebSocketRepositoryImplTest { } } + @Nested + inner class CameraWebRtc { + + private fun captureSentMessage(response: MessageSocketResponse?): CapturingSlot> { + val messageSlot = slot>() + coEvery { webSocketCore.sendMessage(capture(messageSlot)) } returns response + return messageSlot + } + + private fun successResponse(resultJson: String): MessageSocketResponse = MessageSocketResponse( + id = 1, + success = true, + result = kotlinJsonMapper.parseToJsonElement(resultJson), + ) + + @Test + fun `Given a camera When getting capabilities Then message is sent and response decoded`() = runTest { + val messageSlot = captureSentMessage( + successResponse("""{"frontend_stream_types": ["hls", "web_rtc"]}"""), + ) + + val capabilities = repository.getCameraCapabilities("camera.front_door") + + assertEquals("camera/capabilities", messageSlot.captured["type"]) + assertEquals("camera.front_door", messageSlot.captured["entity_id"]) + assertEquals(listOf(CameraStreamTypes.HLS, CameraStreamTypes.WEB_RTC), capabilities?.frontendStreamTypes) + } + + @Test + fun `Given a camera When getting WebRTC client config Then camelCase response is decoded`() = runTest { + val messageSlot = captureSentMessage( + successResponse( + """ + { + "configuration": { + "iceServers": [ + {"urls": "stun:stun.home-assistant.io:80"}, + {"urls": ["turn:example.org:3478"], "username": "user", "credential": "secret"} + ] + }, + "dataChannel": "webrtc" + } + """.trimIndent(), + ), + ) + + val config = repository.getCameraWebRtcClientConfig("camera.front_door") + + assertEquals("camera/webrtc/get_client_config", messageSlot.captured["type"]) + assertEquals("camera.front_door", messageSlot.captured["entity_id"]) + assertEquals( + CameraWebRtcClientConfigResponse( + configuration = WebRtcConfiguration( + iceServers = listOf( + WebRtcIceServer(urls = listOf("stun:stun.home-assistant.io:80")), + WebRtcIceServer( + urls = listOf("turn:example.org:3478"), + username = "user", + credential = "secret", + ), + ), + ), + dataChannel = "webrtc", + ), + config, + ) + } + + @Test + fun `Given a failing command When getting WebRTC client config Then null is returned`() = runTest { + captureSentMessage(MessageSocketResponse(id = 1, success = false)) + + assertNull(repository.getCameraWebRtcClientConfig("camera.front_door")) + } + + @Test + fun `Given a camera When starting a WebRTC session Then offer subscription is started`() = runTest { + val dataSlot = slot>() + coEvery { + webSocketCore.subscribeTo( + type = SUBSCRIBE_TYPE_CAMERA_WEBRTC_OFFER, + data = capture(dataSlot), + timeout = any(), + ) + } returns emptyFlow() + + val events = repository.startCameraWebRtcSession("camera.front_door", "v=0 fake offer") + + assertNotNull(events) + assertEquals("camera.front_door", dataSlot.captured["entity_id"]) + assertEquals("v=0 fake offer", dataSlot.captured["offer"]) + } + + @Test + fun `Given a session When sending a candidate Then camelCase keys are kept and nulls omitted`() = runTest { + val messageSlot = captureSentMessage(MessageSocketResponse(id = 1, success = true)) + + val accepted = repository.sendCameraWebRtcCandidate( + entityId = "camera.front_door", + sessionId = "01JAYSESSION", + candidate = WebRtcCandidate( + candidate = "candidate:1 1 UDP 2130706431 192.168.1.2 54400 typ host", + sdpMid = "0", + sdpMLineIndex = 0, + usernameFragment = null, + ), + ) + + assertTrue(accepted) + assertEquals("camera/webrtc/candidate", messageSlot.captured["type"]) + assertEquals("camera.front_door", messageSlot.captured["entity_id"]) + assertEquals("01JAYSESSION", messageSlot.captured["session_id"]) + val candidateJson = messageSlot.captured["candidate"] as JsonObject + assertEquals( + "candidate:1 1 UDP 2130706431 192.168.1.2 54400 typ host", + (candidateJson["candidate"] as JsonPrimitive).content, + ) + assertEquals("0", (candidateJson["sdpMid"] as JsonPrimitive).content) + assertEquals(0, (candidateJson["sdpMLineIndex"] as JsonPrimitive).intOrNull) + assertFalse(candidateJson.containsKey("usernameFragment")) + } + + @Test + fun `Given a rejected candidate When sending a candidate Then false is returned`() = runTest { + captureSentMessage(MessageSocketResponse(id = 1, success = false)) + + val accepted = repository.sendCameraWebRtcCandidate( + entityId = "camera.front_door", + sessionId = "01JAYSESSION", + candidate = WebRtcCandidate(candidate = "candidate:1"), + ) + + assertFalse(accepted) + } + } + private fun createServer(deviceRegistryId: String? = null): Server { return Server( id = 1, diff --git a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/WebRtcEventTest.kt b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/WebRtcEventTest.kt new file mode 100644 index 00000000000..8d6fa8a9157 --- /dev/null +++ b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/websocket/impl/entities/WebRtcEventTest.kt @@ -0,0 +1,132 @@ +package io.homeassistant.companion.android.common.data.websocket.impl.entities + +import io.homeassistant.companion.android.common.util.kotlinJsonMapper +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonObject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Test + +class WebRtcEventTest { + + private inline fun decode(json: String): T = webRtcJsonMapper.decodeFromString(json) + + @Test + fun `Given a session event When decoding Then session id is extracted`() { + val event = decode("""{"type": "session", "session_id": "01JAYSESSION"}""") + + assertEquals(WebRtcEvent.Session(sessionId = "01JAYSESSION"), event) + } + + @Test + fun `Given an answer event When decoding Then sdp is extracted`() { + val event = decode("""{"type": "answer", "answer": "v=0 fake sdp"}""") + + assertEquals(WebRtcEvent.Answer(answer = "v=0 fake sdp"), event) + } + + @Test + fun `Given a candidate event When decoding Then camelCase candidate fields are extracted`() { + val event = decode( + """ + { + "type": "candidate", + "candidate": { + "candidate": "candidate:1 1 UDP 2130706431 192.168.1.2 54400 typ host", + "sdpMid": "0", + "sdpMLineIndex": 0, + "usernameFragment": "abcd" + } + } + """.trimIndent(), + ) + + assertEquals( + WebRtcEvent.Candidate( + candidate = WebRtcCandidate( + candidate = "candidate:1 1 UDP 2130706431 192.168.1.2 54400 typ host", + sdpMid = "0", + sdpMLineIndex = 0, + usernameFragment = "abcd", + ), + ), + event, + ) + } + + @Test + fun `Given a candidate event without optional fields When decoding Then defaults are used`() { + val event = decode( + """{"type": "candidate", "candidate": {"candidate": "candidate:1"}}""", + ) + + assertEquals( + WebRtcEvent.Candidate(candidate = WebRtcCandidate(candidate = "candidate:1")), + event, + ) + } + + @Test + fun `Given an error event When decoding Then code and message are extracted`() { + val event = decode( + """{"type": "error", "code": "webrtc_offer_failed", "message": "Camera does not support WebRTC"}""", + ) + + assertEquals( + WebRtcEvent.Error(code = "webrtc_offer_failed", message = "Camera does not support WebRTC"), + event, + ) + } + + @Test + fun `Given an unknown event type When decoding Then Unknown is returned instead of throwing`() { + val event = decode("""{"type": "brand_new_event", "value": 42}""") + + assertInstanceOf(WebRtcEvent.Unknown::class.java, event) + assertEquals("brand_new_event", (event as WebRtcEvent.Unknown).discriminator) + } + + @Test + fun `Given a candidate When encoding Then camelCase keys are kept and nulls omitted`() { + val json = webRtcJsonMapper.encodeToJsonElement( + WebRtcCandidate(candidate = "candidate:1", sdpMid = "0", sdpMLineIndex = 0, usernameFragment = null), + ).jsonObject + + assertEquals(setOf("candidate", "sdpMid", "sdpMLineIndex"), json.keys) + assertFalse(json.containsKey("usernameFragment")) + } + + @Test + fun `Given a client config with a single url string When decoding Then urls is normalized to a list`() { + val config = decode( + """{"configuration": {"iceServers": [{"urls": "stun:stun.home-assistant.io:80"}]}}""", + ) + + assertEquals( + CameraWebRtcClientConfigResponse( + configuration = WebRtcConfiguration( + iceServers = listOf(WebRtcIceServer(urls = listOf("stun:stun.home-assistant.io:80"))), + ), + ), + config, + ) + } + + @Test + fun `Given an empty client config When decoding Then defaults are used`() { + val config = decode("""{"configuration": {}}""") + + assertEquals(CameraWebRtcClientConfigResponse(), config) + } + + @Test + fun `Given a capabilities response When decoding with the shared mapper Then snake_case is handled`() { + val capabilities = + kotlinJsonMapper.decodeFromString( + """{"frontend_stream_types": ["hls", "web_rtc"]}""", + ) + + assertEquals(listOf(CameraStreamTypes.HLS, CameraStreamTypes.WEB_RTC), capabilities.frontendStreamTypes) + } +} diff --git a/docs/adr/0001-native-webrtc-session-library.md b/docs/adr/0001-native-webrtc-session-library.md new file mode 100644 index 00000000000..18429290b0e --- /dev/null +++ b/docs/adr/0001-native-webrtc-session-library.md @@ -0,0 +1,174 @@ +# ADR 0001: Native WebRTC session library for camera streaming and calls + +- **Status:** Proposed +- **Date:** 2026-07-07 +- **Scope:** Android companion app (design intentionally portable to iOS later) + +## Context + +Camera streaming in the companion app currently relies on two paths: ExoPlayer for HLS, and the +WebView (frontend dashboard cards) for WebRTC. The WebView path supports two-way audio via +`getUserMedia`, but this only works on **HTTPS origins**: Chromium enforces the secure-context +requirement for microphone capture inside the WebView, and there is no public API to relax it. A +large share of installations access Home Assistant over plain HTTP on the LAN +(`http://homeassistant.local:8123`), so two-way audio is simply unavailable to them today. + +In addition, a doorbell "incoming call" experience (CallStyle notification, full-screen intent on +the lock screen, answer-before-unlock, pre-negotiated media) cannot be built on a WebView at all: +there is no WebView to host while the app is backgrounded, and media capture must run inside a +foreground service with `camera`/`microphone` service types on Android 14+. + +## Decision + +Build a **UI-free WebRTC session engine** based on libwebrtc, structured as dedicated Gradle +modules with published-quality boundaries, with the companion app as its first consumer. Do +**not** implement WebRTC as a Media3/ExoPlayer `DataSource`, and do not keep relying on the +WebView for native camera surfaces. + +### Why not an ExoPlayer DataSource + +Media3's `DataSource` is a pull-based byte-stream abstraction: ExoPlayer reads bytes from a URI +and performs its own extraction, buffering, decoding and rendering. WebRTC is a stateful session: +SDP negotiation, ICE, DTLS-SRTP, adaptive jitter buffering and decoding all happen inside +libwebrtc, which emits *decoded frames* through `VideoSink`. There is no meaningful byte stream to +hand to ExoPlayer without destroying WebRTC's latency management. The two-way audio send path (mic +capture, encoding, echo cancellation, audio routing) has no representation in ExoPlayer's model at +all. RTSP exists as a Media3 module because RTSP is client-driven and pull-shaped; WebRTC is not. + +### Why not keep the WebView + +| Requirement | WebView | Native libwebrtc | +|---|---|---| +| Two-way audio over HTTP (LAN) | ❌ blocked by secure-context rule | ✅ app-level `RECORD_AUDIO` | +| Call notifications / lock-screen answer | ❌ needs foreground activity | ✅ foreground service + CallStyle | +| Pre-negotiation on push arrival | ❌ cold-start dashboard | ✅ start ICE immediately | +| Deterministic mic release | ⚠️ known lock bug (#6153) | ✅ owned lifecycle | +| Media encryption | ✅ DTLS-SRTP | ✅ DTLS-SRTP (mandatory regardless of signaling transport) | + +Note: on plain-HTTP setups only the SDP/candidate exchange is cleartext — the same exposure as the +rest of the Home Assistant session on that connection. Media is always encrypted. + +## Goals and non-goals + +**Goals:** live view with sub-second latency; push-to-talk and open-mic two-way audio; doorbell +call flow (push → ring → answer); reuse across native surfaces (camera tiles, widgets, +notifications, Wear later); testable signaling independent of libwebrtc; HTTP and HTTPS parity. + +**Non-goals:** replacing ExoPlayer for HLS/recorded playback; generic video-conferencing features +(rooms, SFU, simulcast); casting. + +## libwebrtc distribution + +We need raw libwebrtc bindings, not a vendor SDK — LiveKit/Stream/Daily client SDKs assume their +own servers, while our "server" is go2rtc behind the Home Assistant WebSocket API. + +**Selected: `io.github.webrtc-sdk:android-prefixed`** (libwebrtc fork maintained by LiveKit + +Flutter-WebRTC). It tracks upstream Chromium milestones closely, is the same build powering +flutter-webrtc's very large deployment base, carries SDK-oriented embedding fixes, and the +`-prefixed` variant is shaded to `livekit.org.webrtc`, avoiding class collisions if any dependency +ever ships `org.webrtc`. Licensing is BSD (libwebrtc), compatible with the app's Apache-2.0. + +**Alternatives considered:** + +- `io.getstream:stream-webrtc-android`: also a maintained pre-compiled libwebrtc with optional + Compose renderer helpers; historically lags upstream milestones slightly more than webrtc-sdk. +- Google's `org.webrtc:google-webrtc` Maven artifact: abandoned (last published 2018-era). +- Building libwebrtc ourselves: multi-day toolchain and a large CI burden; revisit only if we need + custom patches. + +Rule: all `livekit.org.webrtc` imports are isolated inside `:webrtc-core` so the artifact can be +swapped without touching consumers. + +## Module structure + +``` +:webrtc-core UI-free session engine (this has the only libwebrtc dependency) + ├── SignalingClient interface (transport-agnostic) + signaling DTOs + ├── CameraPlayer player abstraction shared with the HLS path later + ├── TwoWayAudio capability interface for talk-back + ├── WebRtcSession one PeerConnection = one session, pure-Kotlin state machine + └── PeerConnectionController thin facade over libwebrtc + real implementation + +:webrtc-signaling-ha SignalingClient implementation over the app's WebSocket layer (:common) + +:common gains the camera/webrtc/* WebSocket commands (internal transport detail) +``` + +Planned follow-ups (separate PRs): `:webrtc-compose` (renderer), dashboard/external-bus +integration with HLS fallback through `CameraPlayer`, two-way audio UI, and the call flow +(foreground service + CallStyle notification), which lives in `:app` because it needs FCM, +notification channels and Home Assistant registration, but is written against `:webrtc-core` +interfaces only. + +Dependency directions: `:webrtc-core` depends only on libwebrtc; `:webrtc-signaling-ha` depends on +`:webrtc-core` and `:common`; `:app` will depend on both. Neither new module may import from +`:app`. Publishing the modules (Maven Central) is deferred until the API is stable across two app +releases and a second consumer commits to using it. + +## Signaling protocol (Home Assistant WebSocket API) + +Verified against Home Assistant Core (`homeassistant/components/camera/webrtc.py`), available +since core 2024.11: + +``` +IDLE + ├─ optional: camera/capabilities {entity_id} → require "web_rtc" in + │ frontend_stream_types, else route to HLS path immediately + └─ start() → camera/webrtc/get_client_config {entity_id} + → result {configuration: RTCConfiguration, dataChannel?} +NEGOTIATING + ├─ createOffer(recvonly video+audio; sendrecv audio if two-way requested) + ├─ send camera/webrtc/offer {id: MSG_ID, entity_id, offer} + │ → immediate result (this is a SUBSCRIPTION on MSG_ID) + ├─ ← event {type:"session", session_id} (ULID) → store id + ├─ ← event {type:"answer", answer} → setRemoteDescription + ├─ ← event {type:"candidate", candidate} (any time) → addIceCandidate + ├─ ← event {type:"error", code, message} → Failed(code) + └─ local onIceCandidate → camera/webrtc/candidate + {entity_id, session_id, candidate: RTCIceCandidateInit dict + (candidate, sdpMid, sdpMLineIndex, usernameFragment)} +CONNECTED (PeerConnection state CONNECTED, first frame → Playing) + ├─ DISCONNECTED → wait → restartIce() with backoff (capped attempts) + └─ FAILED → tear down → surface Failed(cause) → UI may fall back to HLS +CLOSING + └─ stop()/release() → send unsubscribe_events {subscription: MSG_ID} + (there is NO dedicated close command — unsubscribing triggers + close_webrtc_session server-side; a dropped socket does the same) + → dispose tracks/sources/PeerConnection, restore audio +``` + +Error codes surfaced by core: `webrtc_offer_failed`, `webrtc_candidate_failed`, +`webrtc_get_client_config_failed` (the latter two also fire as command errors when the camera +lacks WebRTC support). + +The state machine is pure Kotlin with an injected `SignalingClient` and a thin +`PeerConnectionController` facade, so it is unit-testable with mocked SDP/ICE callbacks and no +native code. All native disposal is idempotent and ordered (tracks → sources → PeerConnection → +factory references). + +## Two-way audio design + +Offer the audio transceiver as `sendrecv` with the mic track initially disabled (fast path for +enabling talk without renegotiation; go2rtc's backchannel picks the direction up automatically). +`RECORD_AUDIO` is requested by the app, not a web origin — this is what makes HTTP parity work. +Audio focus, `MODE_IN_COMMUNICATION` enter/restore and Bluetooth routing are owned by the library +(follow-up PR) so consumers cannot leak the microphone — the #6153 class of bugs becomes +structural instead of per-consumer discipline. + +## Testing + +Unit-test the signaling layer and session state machine against mocked WebSocket responses and a +fake `PeerConnectionController` (happy path, error events, candidate-before-answer ordering, +socket drop mid-negotiation, teardown on every failure path). Instrumented tests against a real +go2rtc instance with a synthetic RTSP source are planned once the renderer lands (connect time, +first-frame latency, mic acquire/release, audio mode restoration). + +## Open questions + +1. Whether to pre-warm a `PeerConnectionFactory` at app start (faster first frame vs. memory). +2. H.265 handling: hardware decode support matrix vs. go2rtc transcode fallback. +3. iOS: the same core design maps to WebRTC.framework; decide whether interfaces live in a shared + KMP module now or later. +4. Session-close semantics rely on WebSocket subscription lifetime — decide how the call flow + keeps the socket (or a dedicated one) alive across the ring/answer window in the foreground + service. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9241fe3d4fd..1809243054b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -96,6 +96,7 @@ wear-tooling = "1.0.0" wearPhoneInteractions = "1.1.0" wearInput = "1.2.0" webkit = "1.15.0" +webrtcSdk = "144.7559.09" wear-remote-interactions = "1.2.0" work = "2.11.2" zxing = "4.3.0" @@ -275,6 +276,7 @@ wear-remote-interactions = { module = "androidx.wear:wear-remote-interactions", wear-tiles = { module = "androidx.wear.tiles:tiles", version.ref = "wear-tiles" } wear-tooling = { module = "androidx.wear:wear-tooling-preview", version.ref = "wear-tooling" } webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } +webrtc-sdk = { module = "io.github.webrtc-sdk:android-prefixed", version.ref = "webrtcSdk" } zxing = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" } [bundles] diff --git a/settings.gradle.kts b/settings.gradle.kts index 0c4245e748f..2f2f312af72 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,8 @@ include( ":lint", ":microwakeword", ":provides-sensor-processor", + ":webrtc-core", + ":webrtc-signaling-ha", ) rootProject.name = "home-assistant-android" diff --git a/webrtc-core/build.gradle.kts b/webrtc-core/build.gradle.kts new file mode 100644 index 00000000000..4e26ef60931 --- /dev/null +++ b/webrtc-core/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.homeassistant.android.common) +} + +android { + namespace = "io.homeassistant.companion.android.webrtc.core" +} + +dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) + + // This module is the only one allowed to depend on libwebrtc directly so the artifact can be + // swapped without touching consumers. `api` because VideoSink is part of the public player + // interfaces used by renderers. + api(libs.webrtc.sdk) + + testImplementation(libs.junit.jupiter.params) +} diff --git a/webrtc-core/gradle.lockfile b/webrtc-core/gradle.lockfile new file mode 100644 index 00000000000..8fd70fdd9d2 --- /dev/null +++ b/webrtc-core/gradle.lockfile @@ -0,0 +1,444 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :webrtc-core:dependencies --write-locks +androidx.activity:activity-compose:1.7.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.activity:activity-ktx:1.7.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.activity:activity:1.5.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity:1.7.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-jvm:1.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation:1.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-runtime:2.1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.arch.core:core-runtime:2.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.autofill:autofill:1.0.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.collection:collection-jvm:1.4.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection-jvm:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.collection:collection-ktx:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.collection:collection:1.1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.collection:collection:1.4.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.runtime:runtime-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.runtime:runtime-annotation-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.runtime:runtime-annotation:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.runtime:runtime-retain-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.runtime:runtime-retain:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.runtime:runtime-saveable-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.runtime:runtime-saveable:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.runtime:runtime:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-geometry-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-geometry:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-graphics-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-graphics:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test-manifest:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-text-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-text:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-unit-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-unit:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-util-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-util:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose:compose-bom:2026.06.01=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core-ktx:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.customview:customview-poolingcontainer:1.0.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.documentfile:documentfile:1.0.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.dynamicanimation:dynamicanimation:1.0.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.emoji2:emoji2:1.4.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.fragment:fragment:1.5.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.graphics:graphics-path:1.0.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.interpolator:interpolator:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.legacy:legacy-support-core-utils:1.0.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-common-jvm:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-livedata:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-process:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-android:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-compose-android:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-compose:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx-android:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-android:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.print:print:1.0.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.profileinstaller:profileinstaller:1.3.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.profileinstaller:profileinstaller:1.4.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.savedstate:savedstate-android:1.3.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.savedstate:savedstate-compose-android:1.3.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.savedstate:savedstate-compose:1.3.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.savedstate:savedstate-ktx:1.3.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate:1.3.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.startup:startup-runtime:1.1.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.test.espresso:espresso-core:3.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.7.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test.ext:junit:1.1.5=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test.services:storage:1.4.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test:annotation:1.0.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test:core:1.4.0=debugUnitTestCompileClasspath +androidx.test:core:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test:monitor:1.8.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test:runner:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.tracing:tracing:1.1.0=debugUnitTestCompileClasspath +androidx.tracing:tracing:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.transition:transition:1.6.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window-core-android:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.window:window-core:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.window:window:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +app.cash.turbine:turbine-jvm:1.2.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +app.cash.turbine:turbine:1.2.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +ch.qos.logback:logback-classic:1.3.14=ktlint +ch.qos.logback:logback-core:1.3.14=ktlint +com.almworks.sqlite4java:sqlite4java:1.0.392=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.android.tools.analytics-library:protos:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.analytics-library:shared:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.analytics-library:tracker:32.2.1=androidLintTool +com.android.tools.build:aapt2-proto:9.2.1-15009934=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.build:builder-model:9.2.1=androidLintTool +com.android.tools.build:manifest-merger:32.2.1=androidLintTool +com.android.tools.ddms:ddmlib:32.2.1=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.android.tools.emulator:proto:32.2.1=unified-test-platform-android-test-plugin-host-emulator-control +com.android.tools.external.com-intellij:intellij-core:32.2.1=androidLintTool +com.android.tools.external.com-intellij:kotlin-compiler:32.2.1=androidLintTool +com.android.tools.external.org-jetbrains:uast:32.2.1=androidLintTool +com.android.tools.layoutlib:layoutlib-api:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.lint:lint-api:32.2.1=androidLintTool +com.android.tools.lint:lint-checks:32.2.1=androidLintTool +com.android.tools.lint:lint-gradle:32.2.1=androidLintTool +com.android.tools.lint:lint-model:32.2.1=androidLintTool +com.android.tools.lint:lint-typedef-remover:32.2.1=androidLintTool +com.android.tools.lint:lint:32.2.1=androidLintTool +com.android.tools.utp:android-device-provider-ddmlib-proto:32.2.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-gradle-work-action +com.android.tools.utp:android-device-provider-ddmlib:32.2.1=unified-test-platform-android-device-provider-ddmlib +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:32.2.1=unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-additional-test-output:32.2.1=unified-test-platform-android-test-plugin-host-additional-test-output +com.android.tools.utp:android-test-plugin-host-apk-installer-proto:32.2.1=unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-apk-installer:32.2.1=unified-test-platform-android-test-plugin-host-apk-installer +com.android.tools.utp:android-test-plugin-host-coverage-proto:32.2.1=unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-coverage:32.2.1=unified-test-platform-android-test-plugin-host-coverage +com.android.tools.utp:android-test-plugin-host-device-info-proto:32.2.1=unified-test-platform-android-test-plugin-host-device-info +com.android.tools.utp:android-test-plugin-host-device-info:32.2.1=unified-test-platform-android-test-plugin-host-device-info +com.android.tools.utp:android-test-plugin-host-emulator-control-proto:32.2.1=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-emulator-control:32.2.1=unified-test-platform-android-test-plugin-host-emulator-control +com.android.tools.utp:android-test-plugin-host-logcat-proto:32.2.1=unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-logcat:32.2.1=unified-test-platform-android-test-plugin-host-logcat +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:32.2.1=unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-result-listener-gradle:32.2.1=unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.utp:gradle-work-action:32.2.1=unified-test-platform-gradle-work-action +com.android.tools.utp:utp-common:32.2.1=unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-logcat +com.android.tools:annotations:32.2.1=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.android.tools:common:32.2.1=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.android.tools:desugar_jdk_libs:2.1.5=coreLibraryDesugaring +com.android.tools:desugar_jdk_libs_configuration:2.1.5=coreLibraryDesugaring +com.android.tools:dvlib:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools:play-sdk-proto:32.2.1=androidLintTool +com.android.tools:repository:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools:sdk-common:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools:sdklib:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.github.ajalt.clikt:clikt-jvm:5.0.2=ktlint +com.github.ajalt.clikt:clikt:5.0.2=ktlint +com.google.android.gms:play-services-base:18.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.android.gms:play-services-basement:18.9.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.android.gms:play-services-tasks:18.2.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.android.gms:play-services-wearable:20.0.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.android:annotations:4.1.1.4=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-core +com.google.api.grpc:proto-google-common-protos:2.17.0=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core +com.google.api.grpc:proto-google-common-protos:2.48.0=unified-test-platform-android-test-plugin-host-emulator-control +com.google.auto.service:auto-service-annotations:1.1.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.auto.service:auto-service:1.1.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.auto.value:auto-value-annotations:1.11.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.auto:auto-common:1.2.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.code.findbugs:jsr305:3.0.2=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.code.gson:gson:2.10.1=unified-test-platform-core,unified-test-platform-gradle-work-action +com.google.code.gson:gson:2.11.0=androidLintTool,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.code.gson:gson:2.8.9=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher +com.google.crypto.tink:tink:1.18.0=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action +com.google.dagger:dagger-compiler:2.60=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.dagger:dagger-lint-aar:2.60=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:dagger-spi:2.60=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.dagger:dagger:2.48=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action +com.google.dagger:dagger:2.60=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:hilt-android-compiler:2.60=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.dagger:hilt-android-testing:2.60=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.dagger:hilt-android:2.60=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:hilt-compiler:2.60=hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest +com.google.dagger:hilt-core:2.60=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.devtools.ksp:symbol-processing-api:2.3.7=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.devtools.ksp:symbol-processing-api:2.3.9=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspPluginClasspath,kspPluginClasspathNonEmbeddable +com.google.errorprone:error_prone_annotation:2.41.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.23.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +com.google.errorprone:error_prone_annotations:2.28.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.errorprone:error_prone_annotations:2.30.0=unified-test-platform-android-test-plugin-host-emulator-control +com.google.errorprone:error_prone_annotations:2.36.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.47.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.googlejavaformat:google-java-format:1.33.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.guava:failureaccess:1.0.1=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +com.google.guava:failureaccess:1.0.2=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.guava:failureaccess:1.0.3=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.guava:guava:32.0.1-jre=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +com.google.guava:guava:33.3.1-jre=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.guava:guava:33.4.8-jre=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.guava:guava:33.6.0-jre=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.j2objc:j2objc-annotations:2.8=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.j2objc:j2objc-annotations:3.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.jimfs:jimfs:1.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.protobuf:protobuf-java-util:3.22.3=unified-test-platform-core +com.google.protobuf:protobuf-java-util:4.28.3=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.protobuf:protobuf-java:3.25.5=androidLintTool +com.google.protobuf:protobuf-java:4.28.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.protobuf:protobuf-kotlin:4.28.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.testing.platform:android-device-provider-local:0.0.9-alpha04=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.testing.platform:android-driver-instrumentation:0.0.9-alpha04=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control +com.google.testing.platform:android-test-plugin:0.0.9-alpha04=unified-test-platform-android-test-plugin +com.google.testing.platform:core-proto:0.0.9-alpha04=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.testing.platform:core:0.0.9-alpha04=unified-test-platform-core +com.google.testing.platform:launcher:0.0.9-alpha04=unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.testparameterinjector:test-parameter-injector:1.18=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.ibm.icu:icu4j:77.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.jakewharton.timber:timber:5.0.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.pinterest.ktlint:ktlint-cli-reporter-baseline:1.5.0=ktlint,ktlintBaselineReporter +com.pinterest.ktlint:ktlint-cli-reporter-checkstyle:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-core:1.5.0=ktlint,ktlintBaselineReporter +com.pinterest.ktlint:ktlint-cli-reporter-format:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-html:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-json:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-plain-summary:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-plain:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-sarif:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-ruleset-core:1.5.0=ktlint,ktlintRuleset +com.pinterest.ktlint:ktlint-cli:1.5.0=ktlint +com.pinterest.ktlint:ktlint-logger:1.5.0=ktlint,ktlintBaselineReporter,ktlintRuleset +com.pinterest.ktlint:ktlint-rule-engine-core:1.5.0=ktlint,ktlintBaselineReporter,ktlintRuleset +com.pinterest.ktlint:ktlint-rule-engine:1.5.0=ktlint +com.pinterest.ktlint:ktlint-ruleset-standard:1.5.0=ktlint,ktlintRuleset +com.squareup:javapoet:1.13.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.squareup:javawriter:2.1.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.squareup:kotlinpoet:1.11.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.sun.istack:istack-commons-runtime:3.0.8=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.sun.xml.fastinfoset:FastInfoset:1.2.16=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +commons-codec:commons-codec:1.17.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +commons-io:commons-io:2.16.1=androidLintTool,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +commons-logging:commons-logging:1.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +dev.drewhamilton.poko:poko-annotations-jvm:0.18.0=ktlint,ktlintBaselineReporter,ktlintRuleset +dev.drewhamilton.poko:poko-annotations:0.18.0=ktlint,ktlintBaselineReporter,ktlintRuleset +io.github.classgraph:classgraph:4.8.184=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.github.detekt.sarif4k:sarif4k-jvm:0.6.0=ktlint,ktlintReporter +io.github.detekt.sarif4k:sarif4k:0.6.0=ktlint,ktlintReporter +io.github.oshai:kotlin-logging-jvm:7.0.3=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset +io.github.oshai:kotlin-logging:5.1.0=ktlint,ktlintBaselineReporter,ktlintReporter +io.github.webrtc-sdk:android-prefixed:144.7559.09=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.grpc:grpc-api:1.57.2=unified-test-platform-core +io.grpc:grpc-api:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-context:1.57.2=unified-test-platform-core +io.grpc:grpc-context:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-core:1.57.2=unified-test-platform-core +io.grpc:grpc-core:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-netty:1.57.2=unified-test-platform-core +io.grpc:grpc-netty:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-protobuf-lite:1.57.2=unified-test-platform-core +io.grpc:grpc-protobuf-lite:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-protobuf:1.57.2=unified-test-platform-core +io.grpc:grpc-protobuf:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-services:1.57.2=unified-test-platform-core +io.grpc:grpc-stub:1.57.2=unified-test-platform-core +io.grpc:grpc-stub:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-util:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.mockk:mockk-agent-api-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-agent-api:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-agent-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-agent:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-core-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-core:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-dsl-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-dsl:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.netty:netty-buffer:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-buffer:4.1.93.Final=unified-test-platform-core +io.netty:netty-codec-http2:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec-http2:4.1.93.Final=unified-test-platform-core +io.netty:netty-codec-http:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec-http:4.1.93.Final=unified-test-platform-core +io.netty:netty-codec-socks:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec-socks:4.1.93.Final=unified-test-platform-core +io.netty:netty-codec:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec:4.1.93.Final=unified-test-platform-core +io.netty:netty-common:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-common:4.1.93.Final=unified-test-platform-core +io.netty:netty-handler-proxy:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-handler-proxy:4.1.93.Final=unified-test-platform-core +io.netty:netty-handler:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-handler:4.1.93.Final=unified-test-platform-core +io.netty:netty-resolver:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-resolver:4.1.93.Final=unified-test-platform-core +io.netty:netty-transport-native-unix-common:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-transport-native-unix-common:4.1.93.Final=unified-test-platform-core +io.netty:netty-transport:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-transport:4.1.93.Final=unified-test-platform-core +io.opencensus:opencensus-api:0.31.0=unified-test-platform-core +io.opencensus:opencensus-proto:0.2.0=unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +io.perfmark:perfmark-api:0.26.0=unified-test-platform-core +io.perfmark:perfmark-api:0.27.0=unified-test-platform-android-test-plugin-host-emulator-control +jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +jakarta.inject:jakarta.inject-api:2.0.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +javax.annotation:javax.annotation-api:1.3.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-test-plugin-host-emulator-control +javax.inject:javax.inject:1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action +junit:junit:4.13.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +net.java.dev.jna:jna-platform:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +net.java.dev.jna:jna:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +net.ltgt.gradle.incap:incap:0.2=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +net.sf.kxml:kxml2:2.3.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.apache.commons:commons-compress:1.27.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.commons:commons-lang3:3.16.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.httpcomponents:httpclient:4.5.6=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.httpcomponents:httpcore:4.4.16=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.httpcomponents:httpmime:4.5.6=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apiguardian:apiguardian-api:1.1.2=debugUnitTestCompileClasspath +org.bouncycastle:bcpkix-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.bouncycastle:bcprov-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.bouncycastle:bcprov-jdk18on:1.81=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.checkerframework:checker-compat-qual:2.5.3=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +org.checkerframework:checker-qual:3.33.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +org.checkerframework:checker-qual:3.43.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.codehaus.groovy:groovy:3.0.22=androidLintTool +org.codehaus.mojo:animal-sniffer-annotations:1.23=unified-test-platform-core +org.codehaus.mojo:animal-sniffer-annotations:1.24=unified-test-platform-android-test-plugin-host-emulator-control +org.conscrypt:conscrypt-openjdk-uber:2.5.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ec4j.core:ec4j-core:1.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset +org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.glassfish.jaxb:txw2:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.hamcrest:hamcrest-core:1.3=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.hamcrest:hamcrest-integration:1.3=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.hamcrest:hamcrest-library:1.3=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +org.jetbrains.kotlin:kotlin-reflect:1.6.10=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-reflect:1.8.21=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlin:kotlin-reflect:2.2.21=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-script-runtime:2.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-serialization-compiler-plugin-embeddable:2.4.0=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=unified-test-platform-android-test-plugin,unified-test-platform-core +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0=ktlintReporter +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.10=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlintReporter +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlintReporter +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib:1.8.21=unified-test-platform-android-test-plugin,unified-test-platform-core +org.jetbrains.kotlin:kotlin-stdlib:1.9.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib:2.1.0=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib:2.3.20=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspPluginClasspath,kspPluginClasspathNonEmbeddable +org.jetbrains.kotlin:kotlin-stdlib:2.4.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-tooling-core:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlinx:atomicfu-jvm:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:atomicfu:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.11.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.4.1=ktlintReporter +org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core:1.4.1=ktlintReporter +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.1=ktlintReporter +org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1=ktlintReporter +org.jetbrains:annotations:13.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspPluginClasspath,kspPluginClasspathNonEmbeddable,kspReleaseKotlinProcessorClasspath,ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset +org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jspecify:jspecify:1.0.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:6.1.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.jupiter:junit-jupiter:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.platform:junit-platform-commons:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.platform:junit-platform-engine:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.platform:junit-platform-launcher:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.vintage:junit-vintage-engine:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit:junit-bom:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jvnet.staxex:stax-ex:1.8.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.objenesis:objenesis:3.4=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ow2.asm:asm-analysis:9.9=androidLintTool +org.ow2.asm:asm-commons:9.8=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ow2.asm:asm-commons:9.9=androidLintTool +org.ow2.asm:asm-tree:9.8=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ow2.asm:asm-tree:9.9=androidLintTool +org.ow2.asm:asm:9.8=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ow2.asm:asm:9.9=androidLintTool +org.robolectric:annotations:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:junit:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:nativeruntime-dist-compat:1.0.18=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:nativeruntime:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:pluginapi:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:plugins-maven-dependency-resolver:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:resources:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:robolectric:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:sandbox:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:shadowapi:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:shadows-framework:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:utils-reflector:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:utils:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=ktlint +org.yaml:snakeyaml:2.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +empty=_agp_internal_debugAndroidTest_kspClasspath,_agp_internal_debugUnitTest_kspClasspath,androidApis,androidJdkImage,androidTestUtil,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,hiltCompileOnlyDebugAndroidTest,hiltCompileOnlyDebugUnitTest,kotlinCompilerPluginClasspath,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata diff --git a/webrtc-core/lint-baseline.xml b/webrtc-core/lint-baseline.xml new file mode 100644 index 00000000000..355b2f37dc1 --- /dev/null +++ b/webrtc-core/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/CameraPlayer.kt b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/CameraPlayer.kt new file mode 100644 index 00000000000..2df8b99c2bd --- /dev/null +++ b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/CameraPlayer.kt @@ -0,0 +1,83 @@ +package io.homeassistant.companion.android.webrtc.core + +import kotlinx.coroutines.flow.StateFlow +import livekit.org.webrtc.VideoSink + +/** + * State of a [CameraPlayer]. + */ +sealed interface PlayerState { + /** The player has not been started, or has been stopped. */ + data object Idle : PlayerState + + /** The player is negotiating a new session with the server. */ + data object Connecting : PlayerState + + /** A session is established but media is not flowing (yet or anymore). */ + data object Buffering : PlayerState + + /** Media is flowing. */ + data object Playing : PlayerState + + /** The session ended with an unrecoverable error. Consumers may fall back to HLS. */ + data class Failed(val failure: PlayerFailure) : PlayerState +} + +/** + * Reason a [CameraPlayer] ended in [PlayerState.Failed]. + */ +sealed interface PlayerFailure { + /** + * The server rejected the session, for example because the camera does not support WebRTC + * (`webrtc_offer_failed`, `webrtc_get_client_config_failed`). + */ + data class Signaling(val code: String?, val message: String?) : PlayerFailure + + /** The media connection could not be established, or was lost and could not be recovered. */ + data object ConnectionLost : PlayerFailure + + /** An unexpected local error, for example the peer connection could not be created. */ + data class Internal(val message: String?) : PlayerFailure +} + +/** + * A player rendering a live camera stream to one or more [VideoSink]s. + * + * This is the seam shared between the WebRTC implementation and (later) an ExoPlayer/HLS adapter, + * so consumers can fall back from WebRTC to HLS the same way the frontend degrades + * webrtc → mse → hls. + * + * All functions are safe to call from any thread. + */ +interface CameraPlayer { + /** Current state of the player. */ + val state: StateFlow + + /** + * Attach a sink that will receive the decoded video frames. Can be called at any time, + * including before [start]; the sink starts receiving frames as soon as the remote video + * track is available. + */ + fun attachVideoSink(sink: VideoSink) + + /** Detach a sink previously attached with [attachVideoSink]. */ + fun detachVideoSink(sink: VideoSink) + + /** + * Enable or disable the playback of the remote audio (the camera microphone). Enabled by + * default. This only mutes locally, the audio track keeps being received. + */ + fun setAudioEnabled(enabled: Boolean) + + /** Start the player. Does nothing if it is already started or [release] has been called. */ + fun start() + + /** + * Stop the player and end the session on the server. The player can be started again with + * [start], which negotiates a new session. + */ + fun stop() + + /** Stop the player and release all resources. The player cannot be used afterwards. */ + fun release() +} diff --git a/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/TwoWayAudio.kt b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/TwoWayAudio.kt new file mode 100644 index 00000000000..1d5d3f8a832 --- /dev/null +++ b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/TwoWayAudio.kt @@ -0,0 +1,38 @@ +package io.homeassistant.companion.android.webrtc.core + +import kotlinx.coroutines.flow.StateFlow + +/** + * State of the microphone of a [TwoWayAudio] session. + */ +sealed interface MicState { + /** The microphone is not capturing. */ + data object Off : MicState + + /** The microphone is live and audio is being sent to the camera. */ + data object Live : MicState + + /** + * The microphone cannot be enabled right now, for example because the session is not active. + * The intent is remembered and applied when the session (re)connects. + */ + data object Unavailable : MicState +} + +/** + * Capability interface for talk-back (two-way audio), implemented by the WebRTC player. + * + * The `RECORD_AUDIO` runtime permission must be granted by the consumer before enabling the + * microphone; this interface only controls the WebRTC audio track. + */ +interface TwoWayAudio { + /** Current state of the microphone. */ + val micState: StateFlow + + /** + * Enable or disable sending microphone audio to the camera. Designed to be push-to-talk + * friendly: the audio track is negotiated up front and toggling does not renegotiate the + * session. + */ + fun setMicEnabled(enabled: Boolean) +} diff --git a/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/PeerConnectionController.kt b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/PeerConnectionController.kt new file mode 100644 index 00000000000..6261b3b8452 --- /dev/null +++ b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/PeerConnectionController.kt @@ -0,0 +1,90 @@ +package io.homeassistant.companion.android.webrtc.core.session + +import io.homeassistant.companion.android.webrtc.core.signaling.IceCandidateInit +import io.homeassistant.companion.android.webrtc.core.signaling.RtcClientConfig +import kotlinx.coroutines.flow.Flow +import livekit.org.webrtc.VideoSink + +/** + * Thin facade over one libwebrtc `PeerConnection`, so [WebRtcSession] stays pure Kotlin and can + * be unit tested without native code. + * + * A controller wraps exactly one peer connection: it is created for one negotiation and disposed + * when the session ends or reconnects. Implementations must be safe to call from any thread. + */ +interface PeerConnectionController { + + /** + * Events from the peer connection. Events emitted before the Flow is collected are buffered, + * so no local candidate is lost between [createOffer] and the collection starting. + */ + val events: Flow + + /** + * Create the SDP offer (video receive-only, audio send-and-receive with the microphone + * disabled) and set it as local description, which starts ICE gathering. + * + * @return the SDP of the offer to send to the server + */ + suspend fun createOffer(): String + + /** + * Apply the SDP answer received from the server as remote description. + */ + suspend fun setAnswer(sdp: String) + + /** + * Add a remote ICE candidate. Must only be called after [setAnswer] succeeded. + * + * @return `true` if the candidate was accepted by the peer connection + */ + fun addRemoteCandidate(candidate: IceCandidateInit): Boolean + + /** Enable or disable the microphone track (created disabled). */ + fun setMicrophoneEnabled(enabled: Boolean) + + /** Enable or disable the playback of the remote audio track. */ + fun setRemoteAudioEnabled(enabled: Boolean) + + /** Attach a sink receiving the decoded remote video frames. */ + fun addVideoSink(sink: VideoSink) + + /** Detach a sink previously attached with [addVideoSink]. */ + fun removeVideoSink(sink: VideoSink) + + /** + * Release all native resources (tracks, sources, peer connection). Idempotent; the controller + * cannot be used afterwards. + */ + fun dispose() + + /** + * Creates one [PeerConnectionController] per negotiation. + */ + fun interface Factory { + fun create(config: RtcClientConfig): PeerConnectionController + } +} + +/** + * Event emitted by a [PeerConnectionController]. + */ +sealed interface PeerConnectionEvent { + /** A local ICE candidate was gathered and must be sent to the server. */ + data class LocalCandidate(val candidate: IceCandidateInit) : PeerConnectionEvent + + /** The overall connection state (ICE + DTLS) changed. */ + data class ConnectionStateChanged(val state: RtcConnectionState) : PeerConnectionEvent +} + +/** + * Connection state of the peer connection, mirroring the W3C `RTCPeerConnectionState`. + */ +enum class RtcConnectionState { + NEW, + CONNECTING, + CONNECTED, + DISCONNECTED, + FAILED, + CLOSED, +} diff --git a/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/WebRtcSession.kt b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/WebRtcSession.kt new file mode 100644 index 00000000000..b7d114dcf16 --- /dev/null +++ b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/WebRtcSession.kt @@ -0,0 +1,320 @@ +package io.homeassistant.companion.android.webrtc.core.session + +import io.homeassistant.companion.android.webrtc.core.CameraPlayer +import io.homeassistant.companion.android.webrtc.core.MicState +import io.homeassistant.companion.android.webrtc.core.PlayerFailure +import io.homeassistant.companion.android.webrtc.core.PlayerState +import io.homeassistant.companion.android.webrtc.core.TwoWayAudio +import io.homeassistant.companion.android.webrtc.core.signaling.IceCandidateInit +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingClient +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingEvent +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingException +import java.util.concurrent.CopyOnWriteArraySet +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import livekit.org.webrtc.VideoSink +import timber.log.Timber + +/** + * How long the peer connection may stay in DISCONNECTED before the session reconnects. Transient + * drops (like a Wi-Fi roam) often recover on their own within this window. + */ +private val DISCONNECTED_GRACE_PERIOD = 5.seconds + +/** + * How many times the session renegotiates after losing an established connection before giving + * up with [PlayerFailure.ConnectionLost]. + */ +private const val MAX_RECONNECT_ATTEMPTS = 3 + +private val RECONNECT_BASE_DELAY = 2.seconds + +/** + * One WebRTC session with one camera entity: negotiates through the injected [SignalingClient], + * drives one [PeerConnectionController] per negotiation and owns the full lifecycle + * (trickle ICE in both directions, candidate buffering, reconnection with capped backoff, and + * ordered teardown on every exit path). + * + * The Home Assistant WebRTC signaling API does not support renegotiation, so when an established + * connection is lost the session disposes the peer connection and negotiates a fresh one (up to + * [MAX_RECONNECT_ATTEMPTS] times) instead of using an ICE restart. + * + * @param entityId the camera entity to stream + * @param signalingClient the signaling backend, scoped to the right server + * @param controllerFactory creates one peer connection per negotiation + * @param dispatcher dispatcher running the session state machine, it must be serial (the default + * already is) + */ +class WebRtcSession( + private val entityId: String, + private val signalingClient: SignalingClient, + private val controllerFactory: PeerConnectionController.Factory, + dispatcher: CoroutineDispatcher = Dispatchers.Default.limitedParallelism(1), +) : CameraPlayer, + TwoWayAudio { + + private val scope = CoroutineScope(SupervisorJob() + dispatcher) + + private val _state = MutableStateFlow(PlayerState.Idle) + override val state: StateFlow = _state.asStateFlow() + + private val _micState = MutableStateFlow(MicState.Off) + override val micState: StateFlow = _micState.asStateFlow() + + private val videoSinks = CopyOnWriteArraySet() + + @Volatile + private var controller: PeerConnectionController? = null + + @Volatile + private var micEnabled = false + + @Volatile + private var audioEnabled = true + + private var sessionJob: Job? = null + private var released = false + + override fun start() { + synchronized(this) { + if (released || sessionJob?.isActive == true) return + // A cancelled session may still be running its cleanup: wait for it so two sessions + // never overlap and the previous controller is always disposed before a new one exists + val previousJob = sessionJob + sessionJob = scope.launch { + previousJob?.join() + runSession() + } + } + } + + override fun stop() { + synchronized(this) { + // The job reference is kept so a subsequent start() can await the cleanup + sessionJob?.cancel() + micEnabled = false + _micState.value = MicState.Off + _state.value = PlayerState.Idle + } + } + + override fun release() { + synchronized(this) { + if (released) return + released = true + stop() + scope.cancel() + } + } + + override fun attachVideoSink(sink: VideoSink) { + if (videoSinks.add(sink)) { + controller?.addVideoSink(sink) + } + } + + override fun detachVideoSink(sink: VideoSink) { + if (videoSinks.remove(sink)) { + controller?.removeVideoSink(sink) + } + } + + override fun setAudioEnabled(enabled: Boolean) { + audioEnabled = enabled + controller?.setRemoteAudioEnabled(enabled) + } + + override fun setMicEnabled(enabled: Boolean) { + micEnabled = enabled + val activeController = controller + if (!enabled) { + activeController?.setMicrophoneEnabled(false) + _micState.value = MicState.Off + } else if (activeController != null) { + activeController.setMicrophoneEnabled(true) + _micState.value = MicState.Live + } else { + // Remembered and applied when the session (re)connects + _micState.value = MicState.Unavailable + } + } + + private suspend fun runSession() { + var attempt = 0 + while (true) { + _state.value = if (attempt == 0) PlayerState.Connecting else PlayerState.Buffering + try { + negotiateAndStream(onConnected = { attempt = 0 }) + // The signaling subscription ended, the server closed the session + throw SessionRetryException() + } catch (e: CancellationException) { + throw e + } catch (e: SignalingException) { + Timber.w(e, "WebRTC signaling failed (${e.code})") + failWith(PlayerFailure.Signaling(e.code, e.message)) + return + } catch (e: SessionRetryException) { + // Fall through to the reconnection handling below + } catch (e: Exception) { + Timber.e(e, "WebRTC session failed unexpectedly") + failWith(PlayerFailure.Internal(e.message)) + return + } finally { + disposeController() + } + + attempt++ + if (attempt > MAX_RECONNECT_ATTEMPTS) { + Timber.w("WebRTC connection lost and could not be recovered") + failWith(PlayerFailure.ConnectionLost) + return + } + Timber.d("WebRTC connection lost, reconnecting (attempt $attempt)") + delay(RECONNECT_BASE_DELAY * (1 shl (attempt - 1))) + } + } + + private suspend fun negotiateAndStream(onConnected: () -> Unit) { + val config = signalingClient.getClientConfig(entityId) + val controller = controllerFactory.create(config) + this.controller = controller + videoSinks.forEach(controller::addVideoSink) + controller.setRemoteAudioEnabled(audioEnabled) + val offerSdp = controller.createOffer() + if (micEnabled) { + controller.setMicrophoneEnabled(true) + _micState.value = MicState.Live + } + + var sessionId: String? = null + var answerApplied = false + val pendingLocalCandidates = mutableListOf() + val pendingRemoteCandidates = mutableListOf() + + coroutineScope { + var disconnectedJob: Job? = null + + launch { + controller.events.collect { event -> + when (event) { + is PeerConnectionEvent.LocalCandidate -> { + val id = sessionId + if (id == null) { + // The server only accepts candidates once it assigned a session id + pendingLocalCandidates += event.candidate + } else { + sendLocalCandidate(id, event.candidate) + } + } + + is PeerConnectionEvent.ConnectionStateChanged -> when (event.state) { + RtcConnectionState.CONNECTED -> { + disconnectedJob?.cancel() + disconnectedJob = null + _state.value = PlayerState.Playing + onConnected() + } + + RtcConnectionState.DISCONNECTED -> { + _state.value = PlayerState.Buffering + // Restart the grace period so a stale timer from a previous drop + // cannot force a reconnection after the connection recovered + disconnectedJob?.cancel() + disconnectedJob = launch { + delay(DISCONNECTED_GRACE_PERIOD) + throw SessionRetryException() + } + } + + RtcConnectionState.FAILED -> throw SessionRetryException() + + else -> Unit + } + } + } + } + + signalingClient.openSession(entityId, offerSdp).collect { event -> + when (event) { + is SignalingEvent.Session -> { + sessionId = event.sessionId + pendingLocalCandidates.forEach { sendLocalCandidate(event.sessionId, it) } + pendingLocalCandidates.clear() + } + + is SignalingEvent.Answer -> { + controller.setAnswer(event.sdp) + answerApplied = true + _state.value = PlayerState.Buffering + pendingRemoteCandidates.forEach { addRemoteCandidate(controller, it) } + pendingRemoteCandidates.clear() + } + + is SignalingEvent.Candidate -> { + if (answerApplied) { + addRemoteCandidate(controller, event.candidate) + } else { + // Trickle ICE, the candidate can arrive before the answer but can + // only be applied once the remote description is set + pendingRemoteCandidates += event.candidate + } + } + + is SignalingEvent.Error -> throw SignalingException(event.code, event.message) + } + } + + // The subscription Flow completed, the server ended the session; stop the controller + // collector and let runSession decide whether to renegotiate + throw SessionRetryException() + } + } + + private suspend fun sendLocalCandidate(sessionId: String, candidate: IceCandidateInit) { + try { + if (!signalingClient.sendCandidate(entityId, sessionId, candidate)) { + Timber.w("Server rejected a local ICE candidate") + } + } catch (e: SignalingException) { + // Not fatal, the connection can still be established through the other candidates + Timber.w(e, "Failed to send a local ICE candidate") + } + } + + private fun addRemoteCandidate(controller: PeerConnectionController, candidate: IceCandidateInit) { + if (!controller.addRemoteCandidate(candidate)) { + Timber.w("Peer connection rejected a remote ICE candidate") + } + } + + private fun failWith(failure: PlayerFailure) { + _state.value = PlayerState.Failed(failure) + _micState.value = MicState.Off + } + + private fun disposeController() { + controller?.dispose() + controller = null + if (_micState.value == MicState.Live) { + _micState.value = MicState.Off + } + } +} + +/** + * Internal control-flow signal: the current negotiation is dead (connection lost or the server + * closed the session) and the session should renegotiate from scratch. + */ +private class SessionRetryException : Exception("WebRTC session lost") diff --git a/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/libwebrtc/LibWebRtcPeerConnectionController.kt b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/libwebrtc/LibWebRtcPeerConnectionController.kt new file mode 100644 index 00000000000..94773578768 --- /dev/null +++ b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/libwebrtc/LibWebRtcPeerConnectionController.kt @@ -0,0 +1,268 @@ +package io.homeassistant.companion.android.webrtc.core.session.libwebrtc + +import io.homeassistant.companion.android.webrtc.core.session.PeerConnectionController +import io.homeassistant.companion.android.webrtc.core.session.PeerConnectionEvent +import io.homeassistant.companion.android.webrtc.core.session.RtcConnectionState +import io.homeassistant.companion.android.webrtc.core.signaling.IceCandidateInit +import io.homeassistant.companion.android.webrtc.core.signaling.RtcClientConfig +import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import livekit.org.webrtc.AudioTrack +import livekit.org.webrtc.DataChannel +import livekit.org.webrtc.IceCandidate +import livekit.org.webrtc.MediaConstraints +import livekit.org.webrtc.MediaStream +import livekit.org.webrtc.MediaStreamTrack +import livekit.org.webrtc.PeerConnection +import livekit.org.webrtc.PeerConnectionFactory +import livekit.org.webrtc.RtpReceiver +import livekit.org.webrtc.RtpTransceiver +import livekit.org.webrtc.SdpObserver +import livekit.org.webrtc.SessionDescription +import livekit.org.webrtc.VideoSink +import livekit.org.webrtc.VideoTrack +import timber.log.Timber + +private const val MICROPHONE_TRACK_ID = "ha_microphone" + +/** + * [PeerConnectionController] implementation backed by libwebrtc. + * + * The peer connection is created with a receive-only video transceiver and a send-and-receive + * audio transceiver whose microphone track starts disabled, so talk-back can be toggled later + * without renegotiating. + */ +internal class LibWebRtcPeerConnectionController(factory: PeerConnectionFactory, config: RtcClientConfig) : + PeerConnectionController { + + private val eventsChannel = Channel(Channel.UNLIMITED) + override val events: Flow = eventsChannel.receiveAsFlow() + + private val videoSinks = CopyOnWriteArraySet() + private val disposed = AtomicBoolean(false) + + @Volatile + private var remoteVideoTrack: VideoTrack? = null + + @Volatile + private var remoteAudioTrack: AudioTrack? = null + + @Volatile + private var remoteAudioEnabled = true + + private val observer = object : PeerConnection.Observer { + override fun onIceCandidate(candidate: IceCandidate?) { + candidate ?: return + eventsChannel.trySend( + PeerConnectionEvent.LocalCandidate( + IceCandidateInit( + candidate = candidate.sdp, + sdpMid = candidate.sdpMid, + sdpMLineIndex = candidate.sdpMLineIndex, + ), + ), + ) + } + + override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) { + newState ?: return + eventsChannel.trySend(PeerConnectionEvent.ConnectionStateChanged(newState.toRtcConnectionState())) + } + + override fun onTrack(transceiver: RtpTransceiver?) { + when (val track = transceiver?.receiver?.track()) { + is VideoTrack -> { + remoteVideoTrack = track + videoSinks.forEach(track::addSink) + } + + is AudioTrack -> { + remoteAudioTrack = track + track.setEnabled(remoteAudioEnabled) + } + + else -> Unit + } + } + + override fun onSignalingChange(newState: PeerConnection.SignalingState?) {} + override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {} + override fun onIceConnectionReceivingChange(receiving: Boolean) {} + override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState?) {} + override fun onIceCandidatesRemoved(candidates: Array?) {} + override fun onAddStream(stream: MediaStream?) {} + override fun onRemoveStream(stream: MediaStream?) {} + override fun onDataChannel(dataChannel: DataChannel?) {} + override fun onRenegotiationNeeded() {} + override fun onAddTrack(receiver: RtpReceiver?, mediaStreams: Array?) {} + } + + private val peerConnection = checkNotNull(factory.createPeerConnection(config.toRtcConfiguration(), observer)) { + "PeerConnection could not be created" + } + private val audioSource = factory.createAudioSource(MediaConstraints()) + private val microphoneTrack = factory.createAudioTrack(MICROPHONE_TRACK_ID, audioSource).apply { + setEnabled(false) + } + private var dataChannel: DataChannel? = null + + init { + // The data channel (when the provider uses one, like go2rtc) and the transceivers must + // exist before the offer is created so they are part of the negotiated SDP + config.dataChannelLabel?.let { label -> + dataChannel = peerConnection.createDataChannel(label, DataChannel.Init()) + } + peerConnection.addTransceiver( + MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO, + RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY), + ) + peerConnection.addTransceiver( + microphoneTrack, + RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_RECV), + ) + } + + override suspend fun createOffer(): String { + val offer = suspendCancellableCoroutine { continuation -> + peerConnection.createOffer( + object : SdpObserverAdapter() { + override fun onCreateSuccess(description: SessionDescription?) { + if (description == null) { + continuation.resumeWithException(IllegalStateException("createOffer returned no SDP")) + } else { + continuation.resume(description) + } + } + + override fun onCreateFailure(error: String?) { + continuation.resumeWithException(IllegalStateException("createOffer failed: $error")) + } + }, + MediaConstraints(), + ) + } + suspendCancellableCoroutine { continuation -> + peerConnection.setLocalDescription( + object : SdpObserverAdapter() { + override fun onSetSuccess() { + continuation.resume(Unit) + } + + override fun onSetFailure(error: String?) { + continuation.resumeWithException(IllegalStateException("setLocalDescription failed: $error")) + } + }, + offer, + ) + } + return offer.description + } + + override suspend fun setAnswer(sdp: String) { + suspendCancellableCoroutine { continuation -> + peerConnection.setRemoteDescription( + object : SdpObserverAdapter() { + override fun onSetSuccess() { + continuation.resume(Unit) + } + + override fun onSetFailure(error: String?) { + continuation.resumeWithException(IllegalStateException("setRemoteDescription failed: $error")) + } + }, + SessionDescription(SessionDescription.Type.ANSWER, sdp), + ) + } + } + + override fun addRemoteCandidate(candidate: IceCandidateInit): Boolean = peerConnection.addIceCandidate( + IceCandidate( + candidate.sdpMid ?: "", + candidate.sdpMLineIndex ?: 0, + candidate.candidate, + ), + ) + + override fun setMicrophoneEnabled(enabled: Boolean) { + microphoneTrack.setEnabled(enabled) + } + + override fun setRemoteAudioEnabled(enabled: Boolean) { + remoteAudioEnabled = enabled + remoteAudioTrack?.setEnabled(enabled) + } + + override fun addVideoSink(sink: VideoSink) { + if (videoSinks.add(sink)) { + remoteVideoTrack?.addSink(sink) + } + } + + override fun removeVideoSink(sink: VideoSink) { + if (videoSinks.remove(sink)) { + remoteVideoTrack?.removeSink(sink) + } + } + + override fun dispose() { + if (disposed.getAndSet(true)) return + remoteVideoTrack?.let { track -> + videoSinks.forEach { sink -> + runCatching { track.removeSink(sink) } + .onFailure { Timber.w(it, "Failed to detach a video sink during disposal") } + } + } + // The remote tracks are owned by the peer connection and disposed with it + remoteVideoTrack = null + remoteAudioTrack = null + dataChannel?.dispose() + dataChannel = null + // Disposal order matters: peer connection (closes transports and its receivers/senders), + // then our local track wrapper, then its source + peerConnection.dispose() + microphoneTrack.dispose() + audioSource.dispose() + eventsChannel.close() + } +} + +private fun RtcClientConfig.toRtcConfiguration(): PeerConnection.RTCConfiguration { + val servers = iceServers.filter { it.urls.isNotEmpty() }.map { server -> + PeerConnection.IceServer.builder(server.urls) + .apply { + server.username?.let { setUsername(it) } + server.credential?.let { setPassword(it) } + } + .createIceServer() + } + return PeerConnection.RTCConfiguration(servers).apply { + sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN + continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY + } +} + +private fun PeerConnection.PeerConnectionState.toRtcConnectionState(): RtcConnectionState = when (this) { + PeerConnection.PeerConnectionState.NEW -> RtcConnectionState.NEW + PeerConnection.PeerConnectionState.CONNECTING -> RtcConnectionState.CONNECTING + PeerConnection.PeerConnectionState.CONNECTED -> RtcConnectionState.CONNECTED + PeerConnection.PeerConnectionState.DISCONNECTED -> RtcConnectionState.DISCONNECTED + PeerConnection.PeerConnectionState.FAILED -> RtcConnectionState.FAILED + PeerConnection.PeerConnectionState.CLOSED -> RtcConnectionState.CLOSED +} + +/** + * [SdpObserver] with no-op defaults so callers only override the callbacks of the operation they + * perform (create or set). + */ +private open class SdpObserverAdapter : SdpObserver { + override fun onCreateSuccess(description: SessionDescription?) {} + override fun onSetSuccess() {} + override fun onCreateFailure(error: String?) {} + override fun onSetFailure(error: String?) {} +} diff --git a/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/libwebrtc/LibWebRtcPeerConnectionControllerFactory.kt b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/libwebrtc/LibWebRtcPeerConnectionControllerFactory.kt new file mode 100644 index 00000000000..7791da4d6bc --- /dev/null +++ b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/session/libwebrtc/LibWebRtcPeerConnectionControllerFactory.kt @@ -0,0 +1,55 @@ +package io.homeassistant.companion.android.webrtc.core.session.libwebrtc + +import android.content.Context +import io.homeassistant.companion.android.webrtc.core.session.PeerConnectionController +import io.homeassistant.companion.android.webrtc.core.signaling.RtcClientConfig +import livekit.org.webrtc.DefaultVideoDecoderFactory +import livekit.org.webrtc.DefaultVideoEncoderFactory +import livekit.org.webrtc.EglBase +import livekit.org.webrtc.PeerConnectionFactory +import livekit.org.webrtc.audio.JavaAudioDeviceModule + +/** + * Creates libwebrtc backed [PeerConnectionController]s. + * + * The underlying `PeerConnectionFactory` (native threads, audio device module, codec factories) + * is expensive, so it is created lazily on first use and shared between all sessions. Keep a + * single instance of this factory per process. + */ +class LibWebRtcPeerConnectionControllerFactory(context: Context) : PeerConnectionController.Factory { + + private val applicationContext = context.applicationContext + + /** + * Shared EGL context. Renderers must use [EglBase.getEglBaseContext] of this instance so the + * hardware decoder and the view render to the same context. + */ + val eglBase: EglBase by lazy { EglBase.create() } + + private val peerConnectionFactory: PeerConnectionFactory by lazy { + PeerConnectionFactory.initialize( + PeerConnectionFactory.InitializationOptions.builder(applicationContext) + .createInitializationOptions(), + ) + val audioDeviceModule = JavaAudioDeviceModule.builder(applicationContext) + .setUseHardwareAcousticEchoCanceler(true) + .setUseHardwareNoiseSuppressor(true) + .createAudioDeviceModule() + PeerConnectionFactory.builder() + .setAudioDeviceModule(audioDeviceModule) + .setVideoDecoderFactory(DefaultVideoDecoderFactory(eglBase.eglBaseContext)) + .setVideoEncoderFactory( + DefaultVideoEncoderFactory( + eglBase.eglBaseContext, + // enableIntelVp8Encoder, irrelevant on Android devices + false, + // enableH264HighProfile + true, + ), + ) + .createPeerConnectionFactory() + } + + override fun create(config: RtcClientConfig): PeerConnectionController = + LibWebRtcPeerConnectionController(peerConnectionFactory, config) +} diff --git a/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/signaling/SignalingClient.kt b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/signaling/SignalingClient.kt new file mode 100644 index 00000000000..d18f54ffe6c --- /dev/null +++ b/webrtc-core/src/main/kotlin/io/homeassistant/companion/android/webrtc/core/signaling/SignalingClient.kt @@ -0,0 +1,109 @@ +package io.homeassistant.companion.android.webrtc.core.signaling + +import kotlinx.coroutines.flow.Flow + +/** + * Transport-agnostic client for the WebRTC signaling exchange. + * + * The Home Assistant implementation lives in the `:webrtc-signaling-ha` module; this interface + * keeps the session engine independent from the transport so it can be unit tested with fakes and + * reused with other backends. + */ +interface SignalingClient { + + /** + * Get the stream types available for a camera entity. + * + * @throws SignalingException if the server could not be queried + */ + suspend fun getStreamCapabilities(entityId: String): Set + + /** + * Get the configuration (STUN/TURN servers, optional data channel) to create the peer + * connection with. + * + * @throws SignalingException if the server rejected the request, for example when the camera + * does not support WebRTC + */ + suspend fun getClientConfig(entityId: String): RtcClientConfig + + /** + * Send the SDP offer for a new session and observe the signaling events for it. + * + * The returned Flow is cold: collecting it starts the session on the server and cancelling + * the collection closes the session. There is at most one [SignalingEvent.Session] and one + * [SignalingEvent.Answer] per collection, while [SignalingEvent.Candidate] can be emitted at + * any time (trickle ICE). + * + * @throws SignalingException from the collection if the session could not be started + */ + fun openSession(entityId: String, offerSdp: String): Flow + + /** + * Send a local ICE candidate for a session opened with [openSession]. + * + * @param sessionId the identifier received in [SignalingEvent.Session] + * @return `true` if the server accepted the candidate. A rejected candidate is not fatal, the + * connection can still be established through the other candidates. + */ + suspend fun sendCandidate(entityId: String, sessionId: String, candidate: IceCandidateInit): Boolean +} + +/** + * Stream types a camera entity supports, mirroring the `StreamType` values of Home Assistant + * Core's camera integration. + */ +enum class StreamType { + HLS, + WEB_RTC, +} + +/** + * Configuration for creating a peer connection, from the `RTCConfiguration` sent by the server. + * + * @property iceServers the STUN/TURN servers to gather candidates with + * @property dataChannelLabel label of a data channel to open before sending the offer, used by + * some WebRTC providers (like go2rtc). `null` when the provider does not use a data channel. + */ +data class RtcClientConfig(val iceServers: List = emptyList(), val dataChannelLabel: String? = null) + +/** + * A single STUN or TURN server entry, mirroring the W3C `RTCIceServer` dictionary. + */ +data class RtcIceServer(val urls: List, val username: String? = null, val credential: String? = null) + +/** + * An ICE candidate in either direction, mirroring the W3C `RTCIceCandidateInit` dictionary. + */ +data class IceCandidateInit( + val candidate: String, + val sdpMid: String? = null, + val sdpMLineIndex: Int? = null, + val usernameFragment: String? = null, +) + +/** + * Event received during the signaling exchange of one session. + */ +sealed interface SignalingEvent { + /** The server created the session; [sessionId] is needed to send local candidates. */ + data class Session(val sessionId: String) : SignalingEvent + + /** The SDP answer from the camera or its WebRTC provider. */ + data class Answer(val sdp: String) : SignalingEvent + + /** A remote ICE candidate (trickle ICE, can arrive before or after [Answer]). */ + data class Candidate(val candidate: IceCandidateInit) : SignalingEvent + + /** The negotiation failed and the session is unusable. */ + data class Error(val code: String?, val message: String?) : SignalingEvent +} + +/** + * Failure reported by the signaling backend. + * + * @property code machine readable error code from the server when available, for example + * `webrtc_offer_failed` or `webrtc_get_client_config_failed` + */ +class SignalingException(val code: String? = null, message: String? = null, cause: Throwable? = null) : + Exception(message, cause) diff --git a/webrtc-core/src/test/kotlin/io/homeassistant/companion/android/webrtc/core/session/Fakes.kt b/webrtc-core/src/test/kotlin/io/homeassistant/companion/android/webrtc/core/session/Fakes.kt new file mode 100644 index 00000000000..1f3846b098d --- /dev/null +++ b/webrtc-core/src/test/kotlin/io/homeassistant/companion/android/webrtc/core/session/Fakes.kt @@ -0,0 +1,128 @@ +package io.homeassistant.companion.android.webrtc.core.session + +import io.homeassistant.companion.android.webrtc.core.signaling.IceCandidateInit +import io.homeassistant.companion.android.webrtc.core.signaling.RtcClientConfig +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingClient +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingEvent +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingException +import io.homeassistant.companion.android.webrtc.core.signaling.StreamType +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.receiveAsFlow +import livekit.org.webrtc.VideoSink + +internal class FakeSignalingClient : SignalingClient { + + var clientConfig = RtcClientConfig() + var clientConfigException: SignalingException? = null + val sessionChannels = mutableListOf>() + val sentCandidates = mutableListOf>() + var sendCandidateResult = true + var openSessionCount = 0 + var activeSessionCount = 0 + var lastOfferSdp: String? = null + + /** The channel feeding the most recently opened session subscription. */ + val currentSession: Channel + get() = sessionChannels.last() + + override suspend fun getStreamCapabilities(entityId: String): Set = setOf(StreamType.HLS, StreamType.WEB_RTC) + + override suspend fun getClientConfig(entityId: String): RtcClientConfig { + clientConfigException?.let { throw it } + return clientConfig + } + + override fun openSession(entityId: String, offerSdp: String): Flow = flow { + openSessionCount++ + lastOfferSdp = offerSdp + val channel = Channel(Channel.UNLIMITED) + sessionChannels += channel + activeSessionCount++ + try { + emitAll(channel.receiveAsFlow()) + } finally { + activeSessionCount-- + } + } + + override suspend fun sendCandidate( + entityId: String, + sessionId: String, + candidate: IceCandidateInit, + ): Boolean { + sentCandidates += sessionId to candidate + return sendCandidateResult + } +} + +internal class FakePeerConnectionController(private val offerSdp: String) : PeerConnectionController { + + private val eventsChannel = Channel(Channel.UNLIMITED) + override val events: Flow = eventsChannel.receiveAsFlow() + + val appliedAnswers = mutableListOf() + val remoteCandidates = mutableListOf() + val sinks = mutableSetOf() + var microphoneEnabled: Boolean? = null + var remoteAudioEnabled: Boolean? = null + var disposed = false + var createOfferException: Exception? = null + + fun emit(event: PeerConnectionEvent) { + eventsChannel.trySend(event) + } + + override suspend fun createOffer(): String { + createOfferException?.let { throw it } + return offerSdp + } + + override suspend fun setAnswer(sdp: String) { + appliedAnswers += sdp + } + + override fun addRemoteCandidate(candidate: IceCandidateInit): Boolean { + remoteCandidates += candidate + return true + } + + override fun setMicrophoneEnabled(enabled: Boolean) { + microphoneEnabled = enabled + } + + override fun setRemoteAudioEnabled(enabled: Boolean) { + remoteAudioEnabled = enabled + } + + override fun addVideoSink(sink: VideoSink) { + sinks += sink + } + + override fun removeVideoSink(sink: VideoSink) { + sinks -= sink + } + + override fun dispose() { + disposed = true + eventsChannel.close() + } +} + +internal class FakePeerConnectionControllerFactory : PeerConnectionController.Factory { + + val controllers = mutableListOf() + var createException: Exception? = null + + val lastController: FakePeerConnectionController + get() = controllers.last() + + override fun create(config: RtcClientConfig): PeerConnectionController { + createException?.let { throw it } + return FakePeerConnectionController(offerSdp = "v=0 fake-offer-${controllers.size}").also { + controllers += it + } + } +} diff --git a/webrtc-core/src/test/kotlin/io/homeassistant/companion/android/webrtc/core/session/WebRtcSessionTest.kt b/webrtc-core/src/test/kotlin/io/homeassistant/companion/android/webrtc/core/session/WebRtcSessionTest.kt new file mode 100644 index 00000000000..a52085f3093 --- /dev/null +++ b/webrtc-core/src/test/kotlin/io/homeassistant/companion/android/webrtc/core/session/WebRtcSessionTest.kt @@ -0,0 +1,372 @@ +package io.homeassistant.companion.android.webrtc.core.session + +import io.homeassistant.companion.android.webrtc.core.MicState +import io.homeassistant.companion.android.webrtc.core.PlayerFailure +import io.homeassistant.companion.android.webrtc.core.PlayerState +import io.homeassistant.companion.android.webrtc.core.signaling.IceCandidateInit +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingEvent +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingException +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import livekit.org.webrtc.VideoSink +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +private const val ENTITY_ID = "camera.front_door" +private const val SESSION_ID = "01JAYSESSION" + +class WebRtcSessionTest { + + private val signaling = FakeSignalingClient() + private val factory = FakePeerConnectionControllerFactory() + + private fun TestScope.createSession() = WebRtcSession(ENTITY_ID, signaling, factory, StandardTestDispatcher(testScheduler)) + + private fun TestScope.startConnectedSession(session: WebRtcSession) { + session.start() + advanceUntilIdle() + signaling.currentSession.trySend(SignalingEvent.Session(SESSION_ID)) + signaling.currentSession.trySend(SignalingEvent.Answer("v=0 answer")) + advanceUntilIdle() + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.CONNECTED)) + advanceUntilIdle() + } + + @Test + fun `Given a camera When starting Then the offer is sent and the session reaches Playing`() = runTest { + val session = createSession() + + session.start() + assertEquals(PlayerState.Idle, session.state.value) + advanceUntilIdle() + + assertEquals(PlayerState.Connecting, session.state.value) + assertEquals(1, signaling.openSessionCount) + assertEquals("v=0 fake-offer-0", signaling.lastOfferSdp) + + signaling.currentSession.trySend(SignalingEvent.Session(SESSION_ID)) + signaling.currentSession.trySend(SignalingEvent.Answer("v=0 answer")) + advanceUntilIdle() + + assertEquals(listOf("v=0 answer"), factory.lastController.appliedAnswers) + assertEquals(PlayerState.Buffering, session.state.value) + + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.CONNECTED)) + advanceUntilIdle() + + assertEquals(PlayerState.Playing, session.state.value) + } + + @Test + fun `Given trickle ICE When candidates arrive before the answer Then they are applied after it`() = runTest { + val session = createSession() + session.start() + advanceUntilIdle() + + val earlyCandidate = IceCandidateInit(candidate = "candidate:early") + signaling.currentSession.trySend(SignalingEvent.Candidate(earlyCandidate)) + advanceUntilIdle() + assertTrue(factory.lastController.remoteCandidates.isEmpty()) + + signaling.currentSession.trySend(SignalingEvent.Session(SESSION_ID)) + signaling.currentSession.trySend(SignalingEvent.Answer("v=0 answer")) + advanceUntilIdle() + assertEquals(listOf(earlyCandidate), factory.lastController.remoteCandidates) + + val lateCandidate = IceCandidateInit(candidate = "candidate:late") + signaling.currentSession.trySend(SignalingEvent.Candidate(lateCandidate)) + advanceUntilIdle() + assertEquals(listOf(earlyCandidate, lateCandidate), factory.lastController.remoteCandidates) + } + + @Test + fun `Given trickle ICE When local candidates are found before the session id Then they are sent after it`() = runTest { + val session = createSession() + session.start() + advanceUntilIdle() + + val earlyCandidate = IceCandidateInit(candidate = "candidate:local-early") + factory.lastController.emit(PeerConnectionEvent.LocalCandidate(earlyCandidate)) + advanceUntilIdle() + assertTrue(signaling.sentCandidates.isEmpty()) + + signaling.currentSession.trySend(SignalingEvent.Session(SESSION_ID)) + advanceUntilIdle() + assertEquals(listOf(SESSION_ID to earlyCandidate), signaling.sentCandidates) + + val lateCandidate = IceCandidateInit(candidate = "candidate:local-late") + factory.lastController.emit(PeerConnectionEvent.LocalCandidate(lateCandidate)) + advanceUntilIdle() + assertEquals( + listOf(SESSION_ID to earlyCandidate, SESSION_ID to lateCandidate), + signaling.sentCandidates, + ) + } + + @Test + fun `Given an error event When negotiating Then the session fails and releases everything`() = runTest { + val session = createSession() + session.start() + advanceUntilIdle() + + signaling.currentSession.trySend( + SignalingEvent.Error(code = "webrtc_offer_failed", message = "Camera does not support WebRTC"), + ) + advanceUntilIdle() + + assertEquals( + PlayerState.Failed(PlayerFailure.Signaling("webrtc_offer_failed", "Camera does not support WebRTC")), + session.state.value, + ) + assertTrue(factory.lastController.disposed) + assertEquals(0, signaling.activeSessionCount) + } + + @Test + fun `Given a failing client config When starting Then the session fails without opening a session`() = runTest { + signaling.clientConfigException = SignalingException(code = "webrtc_get_client_config_failed") + val session = createSession() + + session.start() + advanceUntilIdle() + + assertEquals( + PlayerState.Failed(PlayerFailure.Signaling("webrtc_get_client_config_failed", null)), + session.state.value, + ) + assertEquals(0, signaling.openSessionCount) + } + + @Test + fun `Given an offer creation failure When starting Then the session fails with an internal error`() = runTest { + val session = createSession() + session.start() + advanceUntilIdle() + // The first negotiation is waiting for signaling events; force the next one to fail fast + // by failing the whole factory, then trigger a reconnection + factory.createException = IllegalStateException("no offer") + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.FAILED)) + advanceUntilIdle() + + assertEquals(PlayerState.Failed(PlayerFailure.Internal("no offer")), session.state.value) + assertEquals(0, signaling.activeSessionCount) + } + + @Test + fun `Given a connected session When stopping Then everything is released`() = runTest { + val session = createSession() + startConnectedSession(session) + assertEquals(PlayerState.Playing, session.state.value) + + session.stop() + advanceUntilIdle() + + assertEquals(PlayerState.Idle, session.state.value) + assertTrue(factory.lastController.disposed) + assertEquals(0, signaling.activeSessionCount) + } + + @Test + fun `Given a stopped session When starting again Then a new session starts after full cleanup`() = runTest { + val session = createSession() + startConnectedSession(session) + + session.stop() + // Restart immediately, without letting the cancelled session finish its cleanup first + session.start() + advanceUntilIdle() + + assertEquals(2, factory.controllers.size) + assertTrue(factory.controllers.first().disposed) + assertFalse(factory.lastController.disposed) + assertEquals(2, signaling.openSessionCount) + assertEquals(1, signaling.activeSessionCount) + } + + @Test + fun `Given repeated disconnections When recovering before the grace period Then no stale timer fires`() = runTest { + val session = createSession() + startConnectedSession(session) + + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.DISCONNECTED)) + advanceTimeBy(3.seconds) + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.DISCONNECTED)) + advanceTimeBy(3.seconds) + // The first timer would fire now if it were not cancelled by the second DISCONNECTED + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.CONNECTED)) + advanceUntilIdle() + + assertEquals(PlayerState.Playing, session.state.value) + assertEquals(1, signaling.openSessionCount) + assertFalse(factory.lastController.disposed) + } + + @Test + fun `Given a lost connection When the grace period expires Then the session renegotiates`() = runTest { + val session = createSession() + startConnectedSession(session) + + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.DISCONNECTED)) + advanceTimeBy(1.seconds) + assertEquals(PlayerState.Buffering, session.state.value) + assertEquals(1, signaling.openSessionCount) + + advanceUntilIdle() + + assertEquals(2, signaling.openSessionCount) + assertEquals(2, factory.controllers.size) + assertTrue(factory.controllers.first().disposed) + } + + @Test + fun `Given a transient disconnection When it recovers within the grace period Then nothing is renegotiated`() = runTest { + val session = createSession() + startConnectedSession(session) + + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.DISCONNECTED)) + advanceTimeBy(2.seconds) + assertEquals(PlayerState.Buffering, session.state.value) + + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.CONNECTED)) + advanceUntilIdle() + + assertEquals(PlayerState.Playing, session.state.value) + assertEquals(1, signaling.openSessionCount) + assertFalse(factory.lastController.disposed) + } + + @Test + fun `Given repeated connection failures When reconnecting Then the session gives up after the cap`() = runTest { + val session = createSession() + session.start() + advanceUntilIdle() + + // Initial negotiation + 3 reconnection attempts all fail + repeat(4) { + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.FAILED)) + advanceUntilIdle() + } + + assertEquals(PlayerState.Failed(PlayerFailure.ConnectionLost), session.state.value) + assertEquals(4, factory.controllers.size) + assertTrue(factory.controllers.all { it.disposed }) + assertEquals(0, signaling.activeSessionCount) + } + + @Test + fun `Given a server closed subscription When streaming Then the session renegotiates`() = runTest { + val session = createSession() + startConnectedSession(session) + + signaling.currentSession.close() + advanceUntilIdle() + + assertEquals(2, signaling.openSessionCount) + assertEquals(PlayerState.Buffering, session.state.value) + } + + @Test + fun `Given a mic request before start When the session connects Then the mic goes live`() = runTest { + val session = createSession() + session.setMicEnabled(true) + assertEquals(MicState.Unavailable, session.micState.value) + + startConnectedSession(session) + + assertEquals(MicState.Live, session.micState.value) + assertEquals(true, factory.lastController.microphoneEnabled) + } + + @Test + fun `Given a live mic When disabling it Then the track is disabled and the state is Off`() = runTest { + val session = createSession() + startConnectedSession(session) + session.setMicEnabled(true) + assertEquals(MicState.Live, session.micState.value) + + session.setMicEnabled(false) + + assertEquals(MicState.Off, session.micState.value) + assertEquals(false, factory.lastController.microphoneEnabled) + } + + @Test + fun `Given a live mic When the session reconnects Then the mic is enabled on the new connection`() = runTest { + val session = createSession() + startConnectedSession(session) + session.setMicEnabled(true) + + factory.lastController.emit(PeerConnectionEvent.ConnectionStateChanged(RtcConnectionState.FAILED)) + advanceUntilIdle() + + assertEquals(2, factory.controllers.size) + assertEquals(true, factory.lastController.microphoneEnabled) + } + + @Test + fun `Given a live mic When the session stops Then the mic state is Off`() = runTest { + val session = createSession() + startConnectedSession(session) + session.setMicEnabled(true) + + session.stop() + advanceUntilIdle() + + assertEquals(MicState.Off, session.micState.value) + } + + @Test + fun `Given attached sinks and muted audio When the session connects Then they are applied`() = runTest { + val session = createSession() + val sink = VideoSink { } + session.attachVideoSink(sink) + session.setAudioEnabled(false) + + startConnectedSession(session) + + assertTrue(sink in factory.lastController.sinks) + assertEquals(false, factory.lastController.remoteAudioEnabled) + + session.detachVideoSink(sink) + assertFalse(sink in factory.lastController.sinks) + } + + @Test + fun `Given a released session When starting again Then nothing happens`() = runTest { + val session = createSession() + startConnectedSession(session) + + session.release() + advanceUntilIdle() + assertEquals(PlayerState.Idle, session.state.value) + assertTrue(factory.lastController.disposed) + + session.start() + advanceUntilIdle() + + assertEquals(PlayerState.Idle, session.state.value) + assertEquals(1, signaling.openSessionCount) + } + + @Test + fun `Given a rejected local candidate When streaming Then the session keeps going`() = runTest { + signaling.sendCandidateResult = false + val session = createSession() + startConnectedSession(session) + + factory.lastController.emit( + PeerConnectionEvent.LocalCandidate(IceCandidateInit(candidate = "candidate:rejected")), + ) + advanceUntilIdle() + + assertEquals(PlayerState.Playing, session.state.value) + assertNull(session.state.value as? PlayerState.Failed) + } +} diff --git a/webrtc-signaling-ha/build.gradle.kts b/webrtc-signaling-ha/build.gradle.kts new file mode 100644 index 00000000000..84ce95dc28a --- /dev/null +++ b/webrtc-signaling-ha/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.homeassistant.android.common) +} + +android { + namespace = "io.homeassistant.companion.android.webrtc.signaling" +} + +dependencies { + api(project(":webrtc-core")) + implementation(project(":common")) + + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) +} diff --git a/webrtc-signaling-ha/gradle.lockfile b/webrtc-signaling-ha/gradle.lockfile new file mode 100644 index 00000000000..c7fbf33d73a --- /dev/null +++ b/webrtc-signaling-ha/gradle.lockfile @@ -0,0 +1,535 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :webrtc-signaling-ha:dependencies --write-locks +androidx.activity:activity-compose:1.8.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity-ktx:1.8.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity:1.5.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.activity:activity:1.8.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.annotation:annotation-experimental:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-jvm:1.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation:1.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.appcompat:appcompat-resources:1.7.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.appcompat:appcompat:1.7.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-runtime:2.1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.arch.core:core-runtime:2.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.autofill:autofill:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection-jvm:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection-ktx:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-core-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-core:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-layout-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-layout:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material3:material3-android:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material3:material3:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-core-android:1.7.8=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-core-desktop:1.7.8=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath +androidx.compose.material:material-icons-core:1.7.8=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-extended-android:1.7.8=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath +androidx.compose.material:material-icons-extended-desktop:1.7.8=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath +androidx.compose.material:material-icons-extended:1.7.8=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-ripple-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-ripple:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-annotation-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-annotation:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-retain-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-retain:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-saveable-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-saveable:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-geometry-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-geometry:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-graphics-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-graphics:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-test-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4-android:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test-manifest:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-text-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-text:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-data-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-data:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-preview-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-preview:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-unit-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-unit:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-util-android:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-util:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui:1.11.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose:compose-bom:2026.06.01=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.concurrent:concurrent-futures-ktx:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core-ktx:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.cursoradapter:cursoradapter:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.customview:customview-poolingcontainer:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.documentfile:documentfile:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.drawerlayout:drawerlayout:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.dynamicanimation:dynamicanimation:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.emoji2:emoji2-views-helper:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.emoji2:emoji2:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.exifinterface:exifinterface:1.3.6=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.fragment:fragment:1.5.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.fragment:fragment:1.5.4=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.graphics:graphics-path:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.interpolator:interpolator:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.legacy:legacy-support-core-utils:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common-java8:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common-jvm:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.6.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.lifecycle:lifecycle-livedata-core-ktx:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core:2.6.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.lifecycle:lifecycle-livedata:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata:2.6.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.lifecycle:lifecycle-process:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-android:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-compose-android:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-compose:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx-android:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.6.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.lifecycle:lifecycle-service:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-android:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.lifecycle:lifecycle-viewmodel:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel:2.6.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media3:media3-common:1.10.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media3:media3-container:1.10.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media3:media3-database:1.10.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media3:media3-datasource-cronet:1.10.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media3:media3-datasource-okhttp:1.10.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media3:media3-datasource:1.10.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media3:media3-decoder:1.10.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media3:media3-exoplayer:1.10.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media3:media3-extractor:1.10.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.media:media:1.8.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.paging:paging-common-android:3.3.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath +androidx.paging:paging-common-jvm:3.3.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath +androidx.paging:paging-common:3.3.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.print:print:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.resourceinspection:resourceinspection-annotation:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-common-jvm:2.8.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-common:2.8.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-ktx:2.8.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-paging-android:2.8.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-paging:2.8.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-runtime-android:2.8.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-runtime:2.8.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-android:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-compose-android:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-compose:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-ktx:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.savedstate:savedstate:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.sqlite:sqlite-android:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.sqlite:sqlite-framework-android:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.sqlite:sqlite-framework:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.sqlite:sqlite:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.startup:startup-runtime:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.test.espresso:espresso-core:3.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.7.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test.ext:junit:1.1.5=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test.services:storage:1.4.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test:annotation:1.0.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test:core:1.4.0=debugUnitTestCompileClasspath +androidx.test:core:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test:monitor:1.8.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.test:runner:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.tracing:tracing-ktx:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.tracing:tracing:1.1.0=debugUnitTestCompileClasspath +androidx.tracing:tracing:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.transition:transition:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.vectordrawable:vectordrawable-animated:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.vectordrawable:vectordrawable:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window-core-android:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window-core:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.work:work-runtime-ktx:2.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.work:work-runtime:2.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +app.cash.turbine:turbine-jvm:1.2.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +app.cash.turbine:turbine:1.2.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +ch.qos.logback:logback-classic:1.3.14=ktlint +ch.qos.logback:logback-core:1.3.14=ktlint +com.almworks.sqlite4java:sqlite4java:1.0.392=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.android.tools.analytics-library:protos:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.analytics-library:shared:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.analytics-library:tracker:32.2.1=androidLintTool +com.android.tools.build:aapt2-proto:9.2.1-15009934=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.build:builder-model:9.2.1=androidLintTool +com.android.tools.build:manifest-merger:32.2.1=androidLintTool +com.android.tools.ddms:ddmlib:32.2.1=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.android.tools.emulator:proto:32.2.1=unified-test-platform-android-test-plugin-host-emulator-control +com.android.tools.external.com-intellij:intellij-core:32.2.1=androidLintTool +com.android.tools.external.com-intellij:kotlin-compiler:32.2.1=androidLintTool +com.android.tools.external.org-jetbrains:uast:32.2.1=androidLintTool +com.android.tools.layoutlib:layoutlib-api:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.lint:lint-api:32.2.1=androidLintTool +com.android.tools.lint:lint-checks:32.2.1=androidLintTool +com.android.tools.lint:lint-gradle:32.2.1=androidLintTool +com.android.tools.lint:lint-model:32.2.1=androidLintTool +com.android.tools.lint:lint-typedef-remover:32.2.1=androidLintTool +com.android.tools.lint:lint:32.2.1=androidLintTool +com.android.tools.utp:android-device-provider-ddmlib-proto:32.2.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-gradle-work-action +com.android.tools.utp:android-device-provider-ddmlib:32.2.1=unified-test-platform-android-device-provider-ddmlib +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:32.2.1=unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-additional-test-output:32.2.1=unified-test-platform-android-test-plugin-host-additional-test-output +com.android.tools.utp:android-test-plugin-host-apk-installer-proto:32.2.1=unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-apk-installer:32.2.1=unified-test-platform-android-test-plugin-host-apk-installer +com.android.tools.utp:android-test-plugin-host-coverage-proto:32.2.1=unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-coverage:32.2.1=unified-test-platform-android-test-plugin-host-coverage +com.android.tools.utp:android-test-plugin-host-device-info-proto:32.2.1=unified-test-platform-android-test-plugin-host-device-info +com.android.tools.utp:android-test-plugin-host-device-info:32.2.1=unified-test-platform-android-test-plugin-host-device-info +com.android.tools.utp:android-test-plugin-host-emulator-control-proto:32.2.1=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-emulator-control:32.2.1=unified-test-platform-android-test-plugin-host-emulator-control +com.android.tools.utp:android-test-plugin-host-logcat-proto:32.2.1=unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-logcat:32.2.1=unified-test-platform-android-test-plugin-host-logcat +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:32.2.1=unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-result-listener-gradle:32.2.1=unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.utp:gradle-work-action:32.2.1=unified-test-platform-gradle-work-action +com.android.tools.utp:utp-common:32.2.1=unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-logcat +com.android.tools:annotations:32.2.1=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.android.tools:common:32.2.1=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.android.tools:desugar_jdk_libs:2.1.5=coreLibraryDesugaring +com.android.tools:desugar_jdk_libs_configuration:2.1.5=coreLibraryDesugaring +com.android.tools:dvlib:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools:play-sdk-proto:32.2.1=androidLintTool +com.android.tools:repository:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools:sdk-common:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools:sdklib:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.github.ajalt.clikt:clikt-jvm:5.0.2=ktlint +com.github.ajalt.clikt:clikt:5.0.2=ktlint +com.google.android.gms:play-services-base:18.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.android.gms:play-services-basement:18.9.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.android.gms:play-services-tasks:18.2.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.android.gms:play-services-wearable:20.0.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.android:annotations:4.1.1.4=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-core +com.google.api.grpc:proto-google-common-protos:2.17.0=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core +com.google.api.grpc:proto-google-common-protos:2.48.0=unified-test-platform-android-test-plugin-host-emulator-control +com.google.auto.service:auto-service-annotations:1.1.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.auto.service:auto-service:1.1.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.auto.value:auto-value-annotations:1.11.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.auto:auto-common:1.2.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.code.findbugs:jsr305:3.0.2=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.code.gson:gson:2.10.1=unified-test-platform-core,unified-test-platform-gradle-work-action +com.google.code.gson:gson:2.11.0=androidLintTool,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.code.gson:gson:2.8.9=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher +com.google.crypto.tink:tink:1.18.0=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action +com.google.dagger:dagger-compiler:2.60=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.dagger:dagger-lint-aar:2.60=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:dagger-spi:2.60=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.dagger:dagger:2.48=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action +com.google.dagger:dagger:2.60=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:hilt-android-compiler:2.60=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.dagger:hilt-android-testing:2.60=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.dagger:hilt-android:2.60=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.dagger:hilt-compiler:2.60=hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest +com.google.dagger:hilt-core:2.60=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.devtools.ksp:symbol-processing-api:2.3.7=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.devtools.ksp:symbol-processing-api:2.3.9=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspPluginClasspath,kspPluginClasspathNonEmbeddable +com.google.errorprone:error_prone_annotation:2.41.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.23.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +com.google.errorprone:error_prone_annotations:2.28.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.errorprone:error_prone_annotations:2.30.0=unified-test-platform-android-test-plugin-host-emulator-control +com.google.errorprone:error_prone_annotations:2.36.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.47.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.googlejavaformat:google-java-format:1.33.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.guava:failureaccess:1.0.1=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +com.google.guava:failureaccess:1.0.2=androidLintTool,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.guava:failureaccess:1.0.3=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.guava:guava:32.0.1-jre=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +com.google.guava:guava:33.3.1-android=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.guava:guava:33.3.1-jre=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.guava:guava:33.4.8-jre=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.google.guava:guava:33.6.0-jre=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,releaseCompileClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.j2objc:j2objc-annotations:2.8=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.j2objc:j2objc-annotations:3.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.google.jimfs:jimfs:1.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.protobuf:protobuf-java-util:3.22.3=unified-test-platform-core +com.google.protobuf:protobuf-java-util:4.28.3=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.protobuf:protobuf-java:3.25.5=androidLintTool +com.google.protobuf:protobuf-java:4.28.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.protobuf:protobuf-javalite:4.35.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.protobuf:protobuf-kotlin:4.28.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.testing.platform:android-device-provider-local:0.0.9-alpha04=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.testing.platform:android-driver-instrumentation:0.0.9-alpha04=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control +com.google.testing.platform:android-test-plugin:0.0.9-alpha04=unified-test-platform-android-test-plugin +com.google.testing.platform:core-proto:0.0.9-alpha04=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.testing.platform:core:0.0.9-alpha04=unified-test-platform-core +com.google.testing.platform:launcher:0.0.9-alpha04=unified-test-platform-gradle-work-action,unified-test-platform-launcher +com.google.testparameterinjector:test-parameter-injector:1.18=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.ibm.icu:icu4j:77.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.jakewharton.timber:timber:5.0.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.mikepenz:community-material-typeface:7.0.96.2-kotlin=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.mikepenz:iconics-core:5.5.0-compose01=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.mikepenz:iconics-typeface-api:5.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.pinterest.ktlint:ktlint-cli-reporter-baseline:1.5.0=ktlint,ktlintBaselineReporter +com.pinterest.ktlint:ktlint-cli-reporter-checkstyle:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-core:1.5.0=ktlint,ktlintBaselineReporter +com.pinterest.ktlint:ktlint-cli-reporter-format:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-html:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-json:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-plain-summary:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-plain:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-reporter-sarif:1.5.0=ktlint +com.pinterest.ktlint:ktlint-cli-ruleset-core:1.5.0=ktlint,ktlintRuleset +com.pinterest.ktlint:ktlint-cli:1.5.0=ktlint +com.pinterest.ktlint:ktlint-logger:1.5.0=ktlint,ktlintBaselineReporter,ktlintRuleset +com.pinterest.ktlint:ktlint-rule-engine-core:1.5.0=ktlint,ktlintBaselineReporter,ktlintRuleset +com.pinterest.ktlint:ktlint-rule-engine:1.5.0=ktlint +com.pinterest.ktlint:ktlint-ruleset-standard:1.5.0=ktlint,ktlintRuleset +com.squareup.okhttp3:logging-interceptor:5.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okhttp3:okhttp-android:5.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okhttp3:okhttp-bom:5.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +com.squareup.okhttp3:okhttp:5.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okio:okio-jvm:3.17.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +com.squareup.okio:okio:3.17.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okio:okio:3.6.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +com.squareup.retrofit2:converter-kotlinx-serialization:3.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.retrofit2:retrofit-bom:3.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.retrofit2:retrofit:3.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup:javapoet:1.13.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.squareup:javawriter:2.1.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +com.squareup:kotlinpoet:1.11.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +com.sun.istack:istack-commons-runtime:3.0.8=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.sun.xml.fastinfoset:FastInfoset:1.2.16=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.vdurmont:emoji-java:5.1.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +commons-codec:commons-codec:1.17.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +commons-io:commons-io:2.16.1=androidLintTool,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +commons-logging:commons-logging:1.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +dev.drewhamilton.poko:poko-annotations-jvm:0.18.0=ktlint,ktlintBaselineReporter,ktlintRuleset +dev.drewhamilton.poko:poko-annotations:0.18.0=ktlint,ktlintBaselineReporter,ktlintRuleset +io.github.classgraph:classgraph:4.8.184=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.github.detekt.sarif4k:sarif4k-jvm:0.6.0=ktlint,ktlintReporter +io.github.detekt.sarif4k:sarif4k:0.6.0=ktlint,ktlintReporter +io.github.oshai:kotlin-logging-jvm:7.0.3=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset +io.github.oshai:kotlin-logging:5.1.0=ktlint,ktlintBaselineReporter,ktlintReporter +io.github.webrtc-sdk:android-prefixed:144.7559.09=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.grpc:grpc-api:1.57.2=unified-test-platform-core +io.grpc:grpc-api:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-context:1.57.2=unified-test-platform-core +io.grpc:grpc-context:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-core:1.57.2=unified-test-platform-core +io.grpc:grpc-core:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-netty:1.57.2=unified-test-platform-core +io.grpc:grpc-netty:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-protobuf-lite:1.57.2=unified-test-platform-core +io.grpc:grpc-protobuf-lite:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-protobuf:1.57.2=unified-test-platform-core +io.grpc:grpc-protobuf:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-services:1.57.2=unified-test-platform-core +io.grpc:grpc-stub:1.57.2=unified-test-platform-core +io.grpc:grpc-stub:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-util:1.69.1=unified-test-platform-android-test-plugin-host-emulator-control +io.mockk:mockk-agent-api-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-agent-api:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-agent-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-agent:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-core-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-core:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-dsl-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-dsl:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk-jvm:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.mockk:mockk:1.14.11=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +io.netty:netty-buffer:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-buffer:4.1.93.Final=unified-test-platform-core +io.netty:netty-codec-http2:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec-http2:4.1.93.Final=unified-test-platform-core +io.netty:netty-codec-http:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec-http:4.1.93.Final=unified-test-platform-core +io.netty:netty-codec-socks:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec-socks:4.1.93.Final=unified-test-platform-core +io.netty:netty-codec:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec:4.1.93.Final=unified-test-platform-core +io.netty:netty-common:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-common:4.1.93.Final=unified-test-platform-core +io.netty:netty-handler-proxy:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-handler-proxy:4.1.93.Final=unified-test-platform-core +io.netty:netty-handler:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-handler:4.1.93.Final=unified-test-platform-core +io.netty:netty-resolver:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-resolver:4.1.93.Final=unified-test-platform-core +io.netty:netty-transport-native-unix-common:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-transport-native-unix-common:4.1.93.Final=unified-test-platform-core +io.netty:netty-transport:4.1.110.Final=unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-transport:4.1.93.Final=unified-test-platform-core +io.opencensus:opencensus-api:0.31.0=unified-test-platform-core +io.opencensus:opencensus-proto:0.2.0=unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +io.perfmark:perfmark-api:0.26.0=unified-test-platform-core +io.perfmark:perfmark-api:0.27.0=unified-test-platform-android-test-plugin-host-emulator-control +jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +jakarta.inject:jakarta.inject-api:2.0.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +javax.annotation:javax.annotation-api:1.3.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,unified-test-platform-android-test-plugin-host-emulator-control +javax.inject:javax.inject:1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action +junit:junit:4.13.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +net.java.dev.jna:jna-platform:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +net.java.dev.jna:jna:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +net.ltgt.gradle.incap:incap:0.2=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +net.sf.kxml:kxml2:2.3.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.altbeacon:android-beacon-library:2.21.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.apache.commons:commons-compress:1.27.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.commons:commons-lang3:3.16.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.httpcomponents:httpclient:4.5.6=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.httpcomponents:httpcore:4.4.16=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.httpcomponents:httpmime:4.5.6=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apiguardian:apiguardian-api:1.1.2=debugUnitTestCompileClasspath +org.bouncycastle:bcpkix-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.bouncycastle:bcprov-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.bouncycastle:bcprov-jdk18on:1.81=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.79=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.checkerframework:checker-compat-qual:2.5.3=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +org.checkerframework:checker-qual:3.33.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +org.checkerframework:checker-qual:3.43.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.chromium.net:cronet-api:143.7445.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.chromium.net:cronet-shared:143.7445.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.codehaus.groovy:groovy:3.0.22=androidLintTool +org.codehaus.mojo:animal-sniffer-annotations:1.23=unified-test-platform-core +org.codehaus.mojo:animal-sniffer-annotations:1.24=unified-test-platform-android-test-plugin-host-emulator-control +org.conscrypt:conscrypt-openjdk-uber:2.5.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ec4j.core:ec4j-core:1.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset +org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.glassfish.jaxb:txw2:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.hamcrest:hamcrest-core:1.3=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.hamcrest:hamcrest-integration:1.3=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.hamcrest:hamcrest-library:1.3=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.compose.runtime:runtime:1.9.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-bom:1.8.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath +org.jetbrains.kotlin:kotlin-parcelize-runtime:2.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-reflect:1.6.10=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-reflect:1.8.21=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlin:kotlin-reflect:2.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-script-runtime:2.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-serialization-compiler-plugin-embeddable:2.4.0=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=unified-test-platform-android-test-plugin,unified-test-platform-core +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0=ktlintReporter +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.10=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib-common:2.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlintReporter +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.21=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlintReporter +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.21=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.8.21=unified-test-platform-android-test-plugin,unified-test-platform-core +org.jetbrains.kotlin:kotlin-stdlib:1.9.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib:2.1.0=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset +org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib:2.3.20=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspPluginClasspath,kspPluginClasspathNonEmbeddable +org.jetbrains.kotlin:kotlin-stdlib:2.4.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-tooling-core:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlinx:atomicfu-jvm:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:atomicfu:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4=ktlint,ktlintBaselineReporter,ktlintRuleset +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.11.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.4.1=ktlintReporter +org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core:1.4.1=ktlintReporter +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.1=ktlintReporter +org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1=ktlintReporter +org.jetbrains:annotations:13.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspPluginClasspath,kspPluginClasspathNonEmbeddable,kspReleaseKotlinProcessorClasspath,ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset +org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jspecify:jspecify:1.0.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugAndroidTestKotlinProcessorClasspath,kspDebugKotlinProcessorClasspath,kspDebugUnitTestKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:6.1.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.jupiter:junit-jupiter:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.platform:junit-platform-commons:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.platform:junit-platform-engine:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.platform:junit-platform-launcher:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit.vintage:junit-vintage-engine:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.junit:junit-bom:6.1.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jvnet.staxex:stax-ex:1.8.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.objenesis:objenesis:3.4=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ow2.asm:asm-analysis:9.9=androidLintTool +org.ow2.asm:asm-commons:9.8=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ow2.asm:asm-commons:9.9=androidLintTool +org.ow2.asm:asm-tree:9.8=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ow2.asm:asm-tree:9.9=androidLintTool +org.ow2.asm:asm:9.8=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.ow2.asm:asm:9.9=androidLintTool +org.robolectric:annotations:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:junit:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:nativeruntime-dist-compat:1.0.18=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:nativeruntime:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:pluginapi:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:plugins-maven-dependency-resolver:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:resources:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:robolectric:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:sandbox:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:shadowapi:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:shadows-framework:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:utils-reflector:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.robolectric:utils:4.16.1=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=ktlint +org.yaml:snakeyaml:2.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +empty=_agp_internal_debugAndroidTest_kspClasspath,_agp_internal_debugUnitTest_kspClasspath,androidApis,androidJdkImage,androidTestUtil,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,hiltCompileOnlyDebugAndroidTest,hiltCompileOnlyDebugUnitTest,kotlinCompilerPluginClasspath,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata diff --git a/webrtc-signaling-ha/lint-baseline.xml b/webrtc-signaling-ha/lint-baseline.xml new file mode 100644 index 00000000000..355b2f37dc1 --- /dev/null +++ b/webrtc-signaling-ha/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/webrtc-signaling-ha/src/main/kotlin/io/homeassistant/companion/android/webrtc/signaling/HaSignalingClient.kt b/webrtc-signaling-ha/src/main/kotlin/io/homeassistant/companion/android/webrtc/signaling/HaSignalingClient.kt new file mode 100644 index 00000000000..bbd8581a80e --- /dev/null +++ b/webrtc-signaling-ha/src/main/kotlin/io/homeassistant/companion/android/webrtc/signaling/HaSignalingClient.kt @@ -0,0 +1,92 @@ +package io.homeassistant.companion.android.webrtc.signaling + +import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraStreamTypes +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcCandidate +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcEvent +import io.homeassistant.companion.android.webrtc.core.signaling.IceCandidateInit +import io.homeassistant.companion.android.webrtc.core.signaling.RtcClientConfig +import io.homeassistant.companion.android.webrtc.core.signaling.RtcIceServer +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingClient +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingEvent +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingException +import io.homeassistant.companion.android.webrtc.core.signaling.StreamType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapNotNull +import timber.log.Timber + +/** + * [SignalingClient] implementation over the Home Assistant WebSocket API + * (`camera/capabilities` and the `camera/webrtc` commands). + * + * The WebRTC signaling API requires Home Assistant Core 2024.12 or later; consumers should + * check the server version and the camera capabilities before starting a session. + * + * @param webSocketRepository the WebSocket repository of the server the camera belongs to + */ +class HaSignalingClient(private val webSocketRepository: WebSocketRepository) : SignalingClient { + + override suspend fun getStreamCapabilities(entityId: String): Set { + val capabilities = webSocketRepository.getCameraCapabilities(entityId) + ?: throw SignalingException(message = "Unable to get the camera capabilities") + return capabilities.frontendStreamTypes.mapNotNull { streamType -> + when (streamType) { + CameraStreamTypes.HLS -> StreamType.HLS + CameraStreamTypes.WEB_RTC -> StreamType.WEB_RTC + else -> { + Timber.d("Ignoring unknown camera stream type $streamType") + null + } + } + }.toSet() + } + + override suspend fun getClientConfig(entityId: String): RtcClientConfig { + val config = webSocketRepository.getCameraWebRtcClientConfig(entityId) + ?: throw SignalingException(message = "Unable to get the WebRTC client configuration") + return RtcClientConfig( + iceServers = config.configuration.iceServers.map { server -> + RtcIceServer(urls = server.urls, username = server.username, credential = server.credential) + }, + dataChannelLabel = config.dataChannel, + ) + } + + override fun openSession(entityId: String, offerSdp: String): Flow = flow { + val events = webSocketRepository.startCameraWebRtcSession(entityId, offerSdp) + ?: throw SignalingException(message = "The WebRTC session could not be started") + emitAll(events.mapNotNull { it.toSignalingEvent() }) + } + + override suspend fun sendCandidate(entityId: String, sessionId: String, candidate: IceCandidateInit): Boolean = + webSocketRepository.sendCameraWebRtcCandidate( + entityId = entityId, + sessionId = sessionId, + candidate = WebRtcCandidate( + candidate = candidate.candidate, + sdpMid = candidate.sdpMid, + sdpMLineIndex = candidate.sdpMLineIndex, + usernameFragment = candidate.usernameFragment, + ), + ) +} + +private fun WebRtcEvent.toSignalingEvent(): SignalingEvent? = when (this) { + is WebRtcEvent.Session -> SignalingEvent.Session(sessionId = sessionId) + is WebRtcEvent.Answer -> SignalingEvent.Answer(sdp = answer) + is WebRtcEvent.Candidate -> SignalingEvent.Candidate( + candidate = IceCandidateInit( + candidate = candidate.candidate, + sdpMid = candidate.sdpMid, + sdpMLineIndex = candidate.sdpMLineIndex, + usernameFragment = candidate.usernameFragment, + ), + ) + is WebRtcEvent.Error -> SignalingEvent.Error(code = code, message = message) + is WebRtcEvent.Unknown -> { + Timber.d("Ignoring unknown WebRTC signaling event $discriminator") + null + } +} diff --git a/webrtc-signaling-ha/src/test/kotlin/io/homeassistant/companion/android/webrtc/signaling/HaSignalingClientTest.kt b/webrtc-signaling-ha/src/test/kotlin/io/homeassistant/companion/android/webrtc/signaling/HaSignalingClientTest.kt new file mode 100644 index 00000000000..3c76ad007e9 --- /dev/null +++ b/webrtc-signaling-ha/src/test/kotlin/io/homeassistant/companion/android/webrtc/signaling/HaSignalingClientTest.kt @@ -0,0 +1,165 @@ +package io.homeassistant.companion.android.webrtc.signaling + +import app.cash.turbine.test +import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraCapabilitiesResponse +import io.homeassistant.companion.android.common.data.websocket.impl.entities.CameraWebRtcClientConfigResponse +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcCandidate +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcConfiguration +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcEvent +import io.homeassistant.companion.android.common.data.websocket.impl.entities.WebRtcIceServer +import io.homeassistant.companion.android.webrtc.core.signaling.IceCandidateInit +import io.homeassistant.companion.android.webrtc.core.signaling.RtcClientConfig +import io.homeassistant.companion.android.webrtc.core.signaling.RtcIceServer +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingEvent +import io.homeassistant.companion.android.webrtc.core.signaling.SignalingException +import io.homeassistant.companion.android.webrtc.core.signaling.StreamType +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonNull +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +private const val ENTITY_ID = "camera.front_door" +private const val SESSION_ID = "01JAYSESSION" + +class HaSignalingClientTest { + + private val webSocketRepository: WebSocketRepository = mockk() + private val client = HaSignalingClient(webSocketRepository) + + @AfterEach + fun tearDown() { + unmockkAll() + } + + @Test + fun `Given a camera with streams When getting capabilities Then known types are mapped`() = runTest { + coEvery { webSocketRepository.getCameraCapabilities(ENTITY_ID) } returns + CameraCapabilitiesResponse(frontendStreamTypes = listOf("hls", "web_rtc", "brand_new_type")) + + val capabilities = client.getStreamCapabilities(ENTITY_ID) + + assertEquals(setOf(StreamType.HLS, StreamType.WEB_RTC), capabilities) + } + + @Test + fun `Given no response When getting capabilities Then a SignalingException is thrown`() = runTest { + coEvery { webSocketRepository.getCameraCapabilities(ENTITY_ID) } returns null + + val result = runCatching { client.getStreamCapabilities(ENTITY_ID) } + + assertTrue(result.exceptionOrNull() is SignalingException) + } + + @Test + fun `Given a client config When getting it Then it is mapped to the core model`() = runTest { + coEvery { webSocketRepository.getCameraWebRtcClientConfig(ENTITY_ID) } returns + CameraWebRtcClientConfigResponse( + configuration = WebRtcConfiguration( + iceServers = listOf( + WebRtcIceServer( + urls = listOf("turn:example.org:3478"), + username = "user", + credential = "secret", + ), + ), + ), + dataChannel = "webrtc", + ) + + val config = client.getClientConfig(ENTITY_ID) + + assertEquals( + RtcClientConfig( + iceServers = listOf( + RtcIceServer(urls = listOf("turn:example.org:3478"), username = "user", credential = "secret"), + ), + dataChannelLabel = "webrtc", + ), + config, + ) + } + + @Test + fun `Given no response When getting the client config Then a SignalingException is thrown`() = runTest { + coEvery { webSocketRepository.getCameraWebRtcClientConfig(ENTITY_ID) } returns null + + val result = runCatching { client.getClientConfig(ENTITY_ID) } + + assertTrue(result.exceptionOrNull() is SignalingException) + } + + @Test + fun `Given a session When opening it Then the events are mapped and unknown ones dropped`() = runTest { + coEvery { webSocketRepository.startCameraWebRtcSession(ENTITY_ID, "v=0 offer") } returns flowOf( + WebRtcEvent.Session(sessionId = SESSION_ID), + WebRtcEvent.Answer(answer = "v=0 answer"), + WebRtcEvent.Unknown(discriminator = "brand_new_event", content = JsonNull), + WebRtcEvent.Candidate(candidate = WebRtcCandidate(candidate = "candidate:1", sdpMid = "0")), + WebRtcEvent.Error(code = "webrtc_offer_failed", message = "boom"), + ) + + client.openSession(ENTITY_ID, "v=0 offer").test { + assertEquals(SignalingEvent.Session(sessionId = SESSION_ID), awaitItem()) + assertEquals(SignalingEvent.Answer(sdp = "v=0 answer"), awaitItem()) + assertEquals( + SignalingEvent.Candidate(candidate = IceCandidateInit(candidate = "candidate:1", sdpMid = "0")), + awaitItem(), + ) + assertEquals(SignalingEvent.Error(code = "webrtc_offer_failed", message = "boom"), awaitItem()) + awaitComplete() + } + } + + @Test + fun `Given no subscription When opening a session Then the flow fails with a SignalingException`() = runTest { + coEvery { webSocketRepository.startCameraWebRtcSession(ENTITY_ID, "v=0 offer") } returns null + + client.openSession(ENTITY_ID, "v=0 offer").test { + assertTrue(awaitError() is SignalingException) + } + } + + @Test + fun `Given a candidate When sending it Then it is mapped and the result returned`() = runTest { + coEvery { + webSocketRepository.sendCameraWebRtcCandidate(ENTITY_ID, SESSION_ID, any()) + } returns true + + val accepted = client.sendCandidate( + entityId = ENTITY_ID, + sessionId = SESSION_ID, + candidate = IceCandidateInit(candidate = "candidate:1", sdpMid = "0", sdpMLineIndex = 0), + ) + + assertTrue(accepted) + coVerify { + webSocketRepository.sendCameraWebRtcCandidate( + ENTITY_ID, + SESSION_ID, + WebRtcCandidate(candidate = "candidate:1", sdpMid = "0", sdpMLineIndex = 0), + ) + } + } + + @Test + fun `Given a rejected candidate When sending it Then false is returned`() = runTest { + coEvery { webSocketRepository.sendCameraWebRtcCandidate(ENTITY_ID, SESSION_ID, any()) } returns false + + val accepted = client.sendCandidate( + entityId = ENTITY_ID, + sessionId = SESSION_ID, + candidate = IceCandidateInit(candidate = "candidate:1"), + ) + + assertFalse(accepted) + } +}