davidliu
Committed by GitHub

Add RoomEvent.ParticipantAttributesChanged (#473)

* Add RoomEvent.ParticipantAttributesChanged

* fix test
@@ -89,6 +89,22 @@ sealed class RoomEvent(val room: Room) : Event() { @@ -89,6 +89,22 @@ sealed class RoomEvent(val room: Room) : Event() {
89 val prevMetadata: String?, 89 val prevMetadata: String?,
90 ) : RoomEvent(room) 90 ) : RoomEvent(room)
91 91
  92 + /**
  93 + * When a participant's attributes are changed, fired for all participants
  94 + */
  95 + class ParticipantAttributesChanged(
  96 + room: Room,
  97 + participant: Participant,
  98 + /**
  99 + * The attributes that have changed and their new associated values.
  100 + */
  101 + val changedAttributes: Map<String, String>,
  102 + /**
  103 + * The old attributes prior to change.
  104 + */
  105 + val oldAttributes: Map<String, String>,
  106 + ) : RoomEvent(room)
  107 +
92 class ParticipantNameChanged( 108 class ParticipantNameChanged(
93 room: Room, 109 room: Room,
94 val participant: Participant, 110 val participant: Participant,
@@ -576,6 +576,17 @@ constructor( @@ -576,6 +576,17 @@ constructor(
576 ) 576 )
577 } 577 }
578 578
  579 + is ParticipantEvent.AttributesChanged -> {
  580 + emitWhenConnected(
  581 + RoomEvent.ParticipantAttributesChanged(
  582 + this@Room,
  583 + it.participant,
  584 + it.changedAttributes,
  585 + it.oldAttributes,
  586 + ),
  587 + )
  588 + }
  589 +
579 is ParticipantEvent.NameChanged -> { 590 is ParticipantEvent.NameChanged -> {
580 emitWhenConnected( 591 emitWhenConnected(
581 RoomEvent.ParticipantNameChanged( 592 RoomEvent.ParticipantNameChanged(
@@ -684,6 +695,17 @@ constructor( @@ -684,6 +695,17 @@ constructor(
684 ) 695 )
685 } 696 }
686 697
  698 + is ParticipantEvent.AttributesChanged -> {
  699 + emitWhenConnected(
  700 + RoomEvent.ParticipantAttributesChanged(
  701 + this@Room,
  702 + it.participant,
  703 + it.changedAttributes,
  704 + it.oldAttributes,
  705 + ),
  706 + )
  707 + }
  708 +
687 is ParticipantEvent.NameChanged -> { 709 is ParticipantEvent.NameChanged -> {
688 emitWhenConnected( 710 emitWhenConnected(
689 RoomEvent.ParticipantNameChanged( 711 RoomEvent.ParticipantNameChanged(
  1 +/*
  2 + * Copyright 2023-2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.room
  18 +
  19 +import io.livekit.android.events.RoomEvent
  20 +import io.livekit.android.test.MockE2ETest
  21 +import io.livekit.android.test.assert.assertIsClass
  22 +import io.livekit.android.test.events.EventCollector
  23 +import io.livekit.android.test.mock.TestData
  24 +import kotlinx.coroutines.ExperimentalCoroutinesApi
  25 +import livekit.LivekitRtc.ParticipantUpdate
  26 +import livekit.LivekitRtc.SignalResponse
  27 +import org.junit.Assert.assertEquals
  28 +import org.junit.Test
  29 +import org.junit.runner.RunWith
  30 +import org.robolectric.RobolectricTestRunner
  31 +
  32 +@ExperimentalCoroutinesApi
  33 +@RunWith(RobolectricTestRunner::class)
  34 +class RoomParticipantEventMockE2ETest : MockE2ETest() {
  35 +
  36 + @Test
  37 + fun localParticipantAttributesChangedEvent() = runTest {
  38 + connect()
  39 + wsFactory.ws.clearRequests()
  40 + wsFactory.registerSignalRequestHandler { request ->
  41 + if (request.hasUpdateMetadata()) {
  42 + val newInfo = with(TestData.LOCAL_PARTICIPANT.toBuilder()) {
  43 + putAllAttributes(request.updateMetadata.attributesMap)
  44 + build()
  45 + }
  46 +
  47 + val response = with(SignalResponse.newBuilder()) {
  48 + update = with(ParticipantUpdate.newBuilder()) {
  49 + addParticipants(newInfo)
  50 + build()
  51 + }
  52 + build()
  53 + }
  54 + wsFactory.receiveMessage(response)
  55 + return@registerSignalRequestHandler true
  56 + }
  57 + return@registerSignalRequestHandler false
  58 + }
  59 +
  60 + val newAttributes = mapOf("attribute" to "changedValue")
  61 +
  62 + val collector = EventCollector(room.events, coroutineRule.scope)
  63 + room.localParticipant.updateAttributes(newAttributes)
  64 +
  65 + val events = collector.stopCollecting()
  66 +
  67 + assertEquals(1, events.size)
  68 + assertIsClass(RoomEvent.ParticipantAttributesChanged::class.java, events.first())
  69 + }
  70 +}
@@ -245,6 +245,7 @@ class LocalParticipantMockE2ETest : MockE2ETest() { @@ -245,6 +245,7 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
245 listOf( 245 listOf(
246 RoomEvent.ParticipantMetadataChanged::class.java, 246 RoomEvent.ParticipantMetadataChanged::class.java,
247 RoomEvent.ParticipantNameChanged::class.java, 247 RoomEvent.ParticipantNameChanged::class.java,
  248 + RoomEvent.ParticipantAttributesChanged::class.java,
248 ), 249 ),
249 roomEvents, 250 roomEvents,
250 ) 251 )
@@ -397,6 +397,10 @@ class CallViewModel( @@ -397,6 +397,10 @@ class CallViewModel(
397 room.sendSimulateScenario(Room.SimulateScenario.SERVER_LEAVE_FULL_RECONNECT) 397 room.sendSimulateScenario(Room.SimulateScenario.SERVER_LEAVE_FULL_RECONNECT)
398 } 398 }
399 399
  400 + fun updateAttribute(key: String, value: String) {
  401 + room.localParticipant.updateAttributes(mapOf(key to value))
  402 + }
  403 +
400 fun reconnect() { 404 fun reconnect() {
401 Timber.e { "Reconnecting." } 405 Timber.e { "Reconnecting." }
402 mutablePrimarySpeaker.value = null 406 mutablePrimarySpeaker.value = null
@@ -26,11 +26,38 @@ import androidx.activity.compose.setContent @@ -26,11 +26,38 @@ import androidx.activity.compose.setContent
26 import androidx.activity.result.contract.ActivityResultContracts 26 import androidx.activity.result.contract.ActivityResultContracts
27 import androidx.appcompat.app.AppCompatActivity 27 import androidx.appcompat.app.AppCompatActivity
28 import androidx.compose.foundation.background 28 import androidx.compose.foundation.background
29 -import androidx.compose.foundation.layout.* 29 +import androidx.compose.foundation.layout.Arrangement
  30 +import androidx.compose.foundation.layout.Column
  31 +import androidx.compose.foundation.layout.Row
  32 +import androidx.compose.foundation.layout.Spacer
  33 +import androidx.compose.foundation.layout.aspectRatio
  34 +import androidx.compose.foundation.layout.fillMaxHeight
  35 +import androidx.compose.foundation.layout.fillMaxSize
  36 +import androidx.compose.foundation.layout.fillMaxWidth
  37 +import androidx.compose.foundation.layout.height
  38 +import androidx.compose.foundation.layout.padding
  39 +import androidx.compose.foundation.layout.size
  40 +import androidx.compose.foundation.layout.wrapContentSize
30 import androidx.compose.foundation.lazy.LazyRow 41 import androidx.compose.foundation.lazy.LazyRow
31 -import androidx.compose.material.*  
32 -import androidx.compose.runtime.* 42 +import androidx.compose.material.AlertDialog
  43 +import androidx.compose.material.Button
  44 +import androidx.compose.material.ExperimentalMaterialApi
  45 +import androidx.compose.material.ExtendedFloatingActionButton
  46 +import androidx.compose.material.Icon
  47 +import androidx.compose.material.MaterialTheme
  48 +import androidx.compose.material.OutlinedTextField
  49 +import androidx.compose.material.Scaffold
  50 +import androidx.compose.material.Surface
  51 +import androidx.compose.material.Text
  52 +import androidx.compose.material.rememberScaffoldState
  53 +import androidx.compose.runtime.Composable
  54 +import androidx.compose.runtime.collectAsState
  55 +import androidx.compose.runtime.getValue
33 import androidx.compose.runtime.livedata.observeAsState 56 import androidx.compose.runtime.livedata.observeAsState
  57 +import androidx.compose.runtime.mutableStateOf
  58 +import androidx.compose.runtime.remember
  59 +import androidx.compose.runtime.rememberCoroutineScope
  60 +import androidx.compose.runtime.setValue
34 import androidx.compose.ui.Alignment 61 import androidx.compose.ui.Alignment
35 import androidx.compose.ui.Modifier 62 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.graphics.Color 63 import androidx.compose.ui.graphics.Color
@@ -111,6 +138,7 @@ class CallActivity : AppCompatActivity() { @@ -111,6 +138,7 @@ class CallActivity : AppCompatActivity() {
111 onSimulateNodeFailure = { viewModel.simulateNodeFailure() }, 138 onSimulateNodeFailure = { viewModel.simulateNodeFailure() },
112 onSimulateLeaveFullReconnect = { viewModel.simulateServerLeaveFullReconnect() }, 139 onSimulateLeaveFullReconnect = { viewModel.simulateServerLeaveFullReconnect() },
113 fullReconnect = { viewModel.reconnect() }, 140 fullReconnect = { viewModel.reconnect() },
  141 + onUpdateAttribute = { k, v -> viewModel.updateAttribute(k, v) },
114 ) 142 )
115 } 143 }
116 } 144 }
@@ -165,7 +193,8 @@ class CallActivity : AppCompatActivity() { @@ -165,7 +193,8 @@ class CallActivity : AppCompatActivity() {
165 onSimulateMigration: () -> Unit = {}, 193 onSimulateMigration: () -> Unit = {},
166 onSimulateNodeFailure: () -> Unit = {}, 194 onSimulateNodeFailure: () -> Unit = {},
167 fullReconnect: () -> Unit = {}, 195 fullReconnect: () -> Unit = {},
168 - onSimulateLeaveFullReconnect: () -> Unit, 196 + onSimulateLeaveFullReconnect: () -> Unit = {},
  197 + onUpdateAttribute: (key: String, value: String) -> Unit = { _, _ -> },
169 ) { 198 ) {
170 AppTheme(darkTheme = true) { 199 AppTheme(darkTheme = true) {
171 ConstraintLayout( 200 ConstraintLayout(
@@ -432,10 +461,11 @@ class CallActivity : AppCompatActivity() { @@ -432,10 +461,11 @@ class CallActivity : AppCompatActivity() {
432 if (showDebugDialog) { 461 if (showDebugDialog) {
433 DebugMenuDialog( 462 DebugMenuDialog(
434 onDismissRequest = { showDebugDialog = false }, 463 onDismissRequest = { showDebugDialog = false },
435 - simulateMigration = { onSimulateMigration() },  
436 - simulateNodeFailure = { onSimulateNodeFailure() },  
437 - simulateLeaveFullReconnect = { onSimulateLeaveFullReconnect() },  
438 - fullReconnect = { fullReconnect() }, 464 + simulateMigration = onSimulateMigration,
  465 + simulateNodeFailure = onSimulateNodeFailure,
  466 + simulateLeaveFullReconnect = onSimulateLeaveFullReconnect,
  467 + fullReconnect = fullReconnect,
  468 + onUpdateAttribute = onUpdateAttribute,
439 ) 469 )
440 } 470 }
441 } 471 }
@@ -41,6 +41,7 @@ fun DebugMenuDialog( @@ -41,6 +41,7 @@ fun DebugMenuDialog(
41 fullReconnect: () -> Unit = {}, 41 fullReconnect: () -> Unit = {},
42 simulateNodeFailure: () -> Unit = {}, 42 simulateNodeFailure: () -> Unit = {},
43 simulateLeaveFullReconnect: () -> Unit = {}, 43 simulateLeaveFullReconnect: () -> Unit = {},
  44 + onUpdateAttribute: (key: String, value: String) -> Unit = { _, _ -> },
44 ) { 45 ) {
45 Dialog(onDismissRequest = onDismissRequest) { 46 Dialog(onDismissRequest = onDismissRequest) {
46 Column( 47 Column(
@@ -79,6 +80,19 @@ fun DebugMenuDialog( @@ -79,6 +80,19 @@ fun DebugMenuDialog(
79 }) { 80 }) {
80 Text("Reconnect to room") 81 Text("Reconnect to room")
81 } 82 }
  83 +
  84 + Button(
  85 + onClick = {
  86 + attributeValue++
  87 + onUpdateAttribute(attributeKey, attributeValue.toString())
  88 + onDismissRequest()
  89 + },
  90 + ) {
  91 + Text("Update Attribute")
  92 + }
82 } 93 }
83 } 94 }
84 } 95 }
  96 +
  97 +val attributeKey = "key"
  98 +var attributeValue = 0