From a2ebad113741a6e5d20928efd85ac753568d9387 Mon Sep 17 00:00:00 2001 From: davidliu Date: Fri, 9 Aug 2024 16:02:29 +0900 Subject: [PATCH] Add RoomEvent.ParticipantAttributesChanged (#473) * Add RoomEvent.ParticipantAttributesChanged * fix test --- .../io/livekit/android/events/RoomEvent.kt | 16 +++++ .../main/java/io/livekit/android/room/Room.kt | 22 ++++++ .../room/RoomParticipantEventMockE2ETest.kt | 70 +++++++++++++++++++ .../LocalParticipantMockE2ETest.kt | 1 + .../livekit/android/sample/CallViewModel.kt | 4 ++ .../android/composesample/CallActivity.kt | 46 +++++++++--- .../composesample/ui/DebugMenuDialog.kt | 14 ++++ 7 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 livekit-android-test/src/test/java/io/livekit/android/room/RoomParticipantEventMockE2ETest.kt diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/events/RoomEvent.kt b/livekit-android-sdk/src/main/java/io/livekit/android/events/RoomEvent.kt index bc179fb5..90d93178 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/events/RoomEvent.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/events/RoomEvent.kt @@ -89,6 +89,22 @@ sealed class RoomEvent(val room: Room) : Event() { val prevMetadata: String?, ) : RoomEvent(room) + /** + * When a participant's attributes are changed, fired for all participants + */ + class ParticipantAttributesChanged( + room: Room, + participant: Participant, + /** + * The attributes that have changed and their new associated values. + */ + val changedAttributes: Map, + /** + * The old attributes prior to change. + */ + val oldAttributes: Map, + ) : RoomEvent(room) + class ParticipantNameChanged( room: Room, val participant: Participant, diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt index 811a6964..c85492ec 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt @@ -576,6 +576,17 @@ constructor( ) } + is ParticipantEvent.AttributesChanged -> { + emitWhenConnected( + RoomEvent.ParticipantAttributesChanged( + this@Room, + it.participant, + it.changedAttributes, + it.oldAttributes, + ), + ) + } + is ParticipantEvent.NameChanged -> { emitWhenConnected( RoomEvent.ParticipantNameChanged( @@ -684,6 +695,17 @@ constructor( ) } + is ParticipantEvent.AttributesChanged -> { + emitWhenConnected( + RoomEvent.ParticipantAttributesChanged( + this@Room, + it.participant, + it.changedAttributes, + it.oldAttributes, + ), + ) + } + is ParticipantEvent.NameChanged -> { emitWhenConnected( RoomEvent.ParticipantNameChanged( diff --git a/livekit-android-test/src/test/java/io/livekit/android/room/RoomParticipantEventMockE2ETest.kt b/livekit-android-test/src/test/java/io/livekit/android/room/RoomParticipantEventMockE2ETest.kt new file mode 100644 index 00000000..0b92bf03 --- /dev/null +++ b/livekit-android-test/src/test/java/io/livekit/android/room/RoomParticipantEventMockE2ETest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2023-2024 LiveKit, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.livekit.android.room + +import io.livekit.android.events.RoomEvent +import io.livekit.android.test.MockE2ETest +import io.livekit.android.test.assert.assertIsClass +import io.livekit.android.test.events.EventCollector +import io.livekit.android.test.mock.TestData +import kotlinx.coroutines.ExperimentalCoroutinesApi +import livekit.LivekitRtc.ParticipantUpdate +import livekit.LivekitRtc.SignalResponse +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) +class RoomParticipantEventMockE2ETest : MockE2ETest() { + + @Test + fun localParticipantAttributesChangedEvent() = runTest { + connect() + wsFactory.ws.clearRequests() + wsFactory.registerSignalRequestHandler { request -> + if (request.hasUpdateMetadata()) { + val newInfo = with(TestData.LOCAL_PARTICIPANT.toBuilder()) { + putAllAttributes(request.updateMetadata.attributesMap) + build() + } + + val response = with(SignalResponse.newBuilder()) { + update = with(ParticipantUpdate.newBuilder()) { + addParticipants(newInfo) + build() + } + build() + } + wsFactory.receiveMessage(response) + return@registerSignalRequestHandler true + } + return@registerSignalRequestHandler false + } + + val newAttributes = mapOf("attribute" to "changedValue") + + val collector = EventCollector(room.events, coroutineRule.scope) + room.localParticipant.updateAttributes(newAttributes) + + val events = collector.stopCollecting() + + assertEquals(1, events.size) + assertIsClass(RoomEvent.ParticipantAttributesChanged::class.java, events.first()) + } +} diff --git a/livekit-android-test/src/test/java/io/livekit/android/room/participant/LocalParticipantMockE2ETest.kt b/livekit-android-test/src/test/java/io/livekit/android/room/participant/LocalParticipantMockE2ETest.kt index 4a906327..caacb961 100644 --- a/livekit-android-test/src/test/java/io/livekit/android/room/participant/LocalParticipantMockE2ETest.kt +++ b/livekit-android-test/src/test/java/io/livekit/android/room/participant/LocalParticipantMockE2ETest.kt @@ -245,6 +245,7 @@ class LocalParticipantMockE2ETest : MockE2ETest() { listOf( RoomEvent.ParticipantMetadataChanged::class.java, RoomEvent.ParticipantNameChanged::class.java, + RoomEvent.ParticipantAttributesChanged::class.java, ), roomEvents, ) diff --git a/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt b/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt index 15f1ab47..eb8329bd 100644 --- a/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt +++ b/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt @@ -397,6 +397,10 @@ class CallViewModel( room.sendSimulateScenario(Room.SimulateScenario.SERVER_LEAVE_FULL_RECONNECT) } + fun updateAttribute(key: String, value: String) { + room.localParticipant.updateAttributes(mapOf(key to value)) + } + fun reconnect() { Timber.e { "Reconnecting." } mutablePrimarySpeaker.value = null diff --git a/sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt b/sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt index f50fbe34..7d09e6af 100644 --- a/sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt +++ b/sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt @@ -26,11 +26,38 @@ import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ExtendedFloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -111,6 +138,7 @@ class CallActivity : AppCompatActivity() { onSimulateNodeFailure = { viewModel.simulateNodeFailure() }, onSimulateLeaveFullReconnect = { viewModel.simulateServerLeaveFullReconnect() }, fullReconnect = { viewModel.reconnect() }, + onUpdateAttribute = { k, v -> viewModel.updateAttribute(k, v) }, ) } } @@ -165,7 +193,8 @@ class CallActivity : AppCompatActivity() { onSimulateMigration: () -> Unit = {}, onSimulateNodeFailure: () -> Unit = {}, fullReconnect: () -> Unit = {}, - onSimulateLeaveFullReconnect: () -> Unit, + onSimulateLeaveFullReconnect: () -> Unit = {}, + onUpdateAttribute: (key: String, value: String) -> Unit = { _, _ -> }, ) { AppTheme(darkTheme = true) { ConstraintLayout( @@ -432,10 +461,11 @@ class CallActivity : AppCompatActivity() { if (showDebugDialog) { DebugMenuDialog( onDismissRequest = { showDebugDialog = false }, - simulateMigration = { onSimulateMigration() }, - simulateNodeFailure = { onSimulateNodeFailure() }, - simulateLeaveFullReconnect = { onSimulateLeaveFullReconnect() }, - fullReconnect = { fullReconnect() }, + simulateMigration = onSimulateMigration, + simulateNodeFailure = onSimulateNodeFailure, + simulateLeaveFullReconnect = onSimulateLeaveFullReconnect, + fullReconnect = fullReconnect, + onUpdateAttribute = onUpdateAttribute, ) } } diff --git a/sample-app-compose/src/main/java/io/livekit/android/composesample/ui/DebugMenuDialog.kt b/sample-app-compose/src/main/java/io/livekit/android/composesample/ui/DebugMenuDialog.kt index c76dc498..e683b7c2 100644 --- a/sample-app-compose/src/main/java/io/livekit/android/composesample/ui/DebugMenuDialog.kt +++ b/sample-app-compose/src/main/java/io/livekit/android/composesample/ui/DebugMenuDialog.kt @@ -41,6 +41,7 @@ fun DebugMenuDialog( fullReconnect: () -> Unit = {}, simulateNodeFailure: () -> Unit = {}, simulateLeaveFullReconnect: () -> Unit = {}, + onUpdateAttribute: (key: String, value: String) -> Unit = { _, _ -> }, ) { Dialog(onDismissRequest = onDismissRequest) { Column( @@ -79,6 +80,19 @@ fun DebugMenuDialog( }) { Text("Reconnect to room") } + + Button( + onClick = { + attributeValue++ + onUpdateAttribute(attributeKey, attributeValue.toString()) + onDismissRequest() + }, + ) { + Text("Update Attribute") + } } } } + +val attributeKey = "key" +var attributeValue = 0