1 /* <lambda>null2 * Copyright 2024 The Android Open Source Project 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 androidx.core.telecom.test.services 18 19 import android.graphics.Bitmap 20 import android.net.Uri 21 import android.telecom.Call 22 import android.telecom.TelecomManager 23 import android.telecom.VideoProfile 24 import android.util.Log 25 import androidx.core.telecom.CallControlResult 26 import androidx.core.telecom.CallException.Companion.ERROR_CALL_IS_NOT_BEING_TRACKED 27 import androidx.core.telecom.extensions.KickParticipantAction 28 import androidx.core.telecom.extensions.LocalCallSilenceExtensionRemote 29 import androidx.core.telecom.extensions.Participant 30 import androidx.core.telecom.extensions.RaiseHandAction 31 import androidx.core.telecom.test.Compatibility 32 import androidx.core.telecom.test.ui.calling.CallStateTransition 33 import androidx.core.telecom.util.ExperimentalAppActions 34 import kotlinx.coroutines.channels.awaitClose 35 import kotlinx.coroutines.channels.trySendBlocking 36 import kotlinx.coroutines.flow.Flow 37 import kotlinx.coroutines.flow.MutableStateFlow 38 import kotlinx.coroutines.flow.callbackFlow 39 import kotlinx.coroutines.flow.combine 40 import kotlinx.coroutines.flow.flowOf 41 import kotlinx.coroutines.flow.map 42 43 /** Track the kick participant support for this application */ 44 @OptIn(ExperimentalAppActions::class) 45 class KickParticipantDataEmitter { 46 companion object { 47 private val unsupportedAction = 48 object : KickParticipantAction { 49 override var isSupported: Boolean = false 50 51 override suspend fun requestKickParticipant( 52 participant: Participant 53 ): CallControlResult { 54 return CallControlResult.Error(ERROR_CALL_IS_NOT_BEING_TRACKED) 55 } 56 } 57 /** Implementation used when kicking participants is unsupported */ 58 val UNSUPPORTED = KickParticipantDataEmitter().collect(unsupportedAction) 59 } 60 61 /** Collect updates to [KickParticipantData] related to the call */ 62 fun collect(action: KickParticipantAction): Flow<KickParticipantData> { 63 return flowOf(createKickParticipantData(action)) 64 } 65 66 private fun createKickParticipantData(action: KickParticipantAction): KickParticipantData { 67 return KickParticipantData(action) 68 } 69 } 70 71 /** Track the raised hands state of participants in the call */ 72 @OptIn(ExperimentalAppActions::class) 73 class RaiseHandDataEmitter { 74 companion object { 75 private val unsupportedAction = 76 object : RaiseHandAction { 77 override var isSupported: Boolean = false 78 requestRaisedHandStateChangenull79 override suspend fun requestRaisedHandStateChange( 80 isRaised: Boolean 81 ): CallControlResult { 82 return CallControlResult.Error(ERROR_CALL_IS_NOT_BEING_TRACKED) 83 } 84 } 85 /** The implementation used when not supported */ 86 val UNSUPPORTED = RaiseHandDataEmitter().collect(unsupportedAction) 87 } 88 89 private val raisedHands: MutableStateFlow<List<Participant>> = MutableStateFlow(emptyList()) 90 91 /** The raised hands state of the participants has changed */ onRaisedHandsChangednull92 fun onRaisedHandsChanged(newRaisedHands: List<Participant>) { 93 raisedHands.value = newRaisedHands 94 } 95 96 /** Collect updates to the [RaiseHandData] related to this call */ collectnull97 fun collect(action: RaiseHandAction): Flow<RaiseHandData> { 98 return raisedHands.map { raisedHands -> createRaiseHandData(action, raisedHands) } 99 } 100 createRaiseHandDatanull101 private fun createRaiseHandData( 102 action: RaiseHandAction, 103 raisedHands: List<Participant> 104 ): RaiseHandData { 105 return RaiseHandData(raisedHands, action) 106 } 107 } 108 109 @OptIn(ExperimentalAppActions::class) 110 class LocalCallSilenceExtensionDataEmitter { 111 private val mLcsDataFlow: MutableStateFlow<Boolean> = MutableStateFlow(false) 112 onVoipAppUpdatenull113 fun onVoipAppUpdate(isSilenced: Boolean) { 114 mLcsDataFlow.value = isSilenced 115 } 116 onInCallServiceUpdatenull117 fun onInCallServiceUpdate(isSilenced: Boolean) { 118 mLcsDataFlow.value = isSilenced 119 } 120 collectnull121 fun collect(e: LocalCallSilenceExtensionRemote): Flow<LocalCallSilenceData> { 122 return mLcsDataFlow.map { LocalCallSilenceData(it, ::onInCallServiceUpdate, e) } 123 } 124 } 125 126 class CallIconExtensionDataEmitter { 127 private val mCallIconFlow: MutableStateFlow<CallIconData> = 128 MutableStateFlow(CallIconData(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))) 129 onVoipAppUpdatenull130 fun onVoipAppUpdate(newBitmap: Bitmap) { 131 mCallIconFlow.value = CallIconData(newBitmap) 132 } 133 collectnull134 fun collect(): Flow<CallIconData> { 135 return mCallIconFlow 136 } 137 } 138 139 /** 140 * A class responsible for emitting meeting summary data. 141 * 142 * This class tracks changes to participant count and the current speaker, combining them into a 143 * [MeetingSummaryData] object and emitting it as a [Flow]. 144 */ 145 class MeetingSummaryExtensionDataEmitter { 146 companion object { 147 /** The default value for the current speaker when no speaker is active. */ 148 const val CURRENT_SPEAKER_DEFAULT = "" 149 150 /** The default value for the participant count when no participants are present. */ 151 const val PARTICIPANT_COUNT_DEFAULT = 0 152 } 153 154 private val currentSpeaker: MutableStateFlow<String> = MutableStateFlow(CURRENT_SPEAKER_DEFAULT) 155 private val participantCount: MutableStateFlow<Int> = 156 MutableStateFlow(PARTICIPANT_COUNT_DEFAULT) 157 158 /** 159 * Updates the participant count. 160 * 161 * @param newCount The new number of participants in the meeting. 162 */ onParticipantCountChangednull163 fun onParticipantCountChanged(newCount: Int) { 164 participantCount.value = newCount 165 } 166 167 /** 168 * Updates the current speaker. 169 * 170 * @param speaker The name or identifier of the current speaker. 171 */ onCurrentSpeakerChangednull172 fun onCurrentSpeakerChanged(speaker: CharSequence?) { 173 if (speaker == null) { 174 currentSpeaker.value = "no active speaker" 175 } else { 176 currentSpeaker.value = speaker.toString() 177 } 178 } 179 180 /** 181 * Collects the current speaker and participant count, combining them into a 182 * [MeetingSummaryData] flow. 183 * 184 * This function uses the `combine` operator to merge the latest values from the 185 * `participantCount` and `currentSpeaker` StateFlows. Whenever either value changes, a new 186 * [MeetingSummaryData] object is emitted. 187 * 188 * @return A [Flow] of [MeetingSummaryData] objects representing the current state of the 189 * meeting summary. 190 */ collectnull191 fun collect(): Flow<MeetingSummaryData> { 192 return participantCount.combine(currentSpeaker) { count, speaker -> 193 MeetingSummaryData(speaker, count) 194 } 195 } 196 } 197 198 /** 199 * Track and update listeners when the [ParticipantExtensionData] related to a call changes, 200 * including the optional raise hand and kick participant extensions. 201 */ 202 @OptIn(ExperimentalAppActions::class) 203 class ParticipantExtensionDataEmitter { 204 private val activeParticipant: MutableStateFlow<Participant?> = MutableStateFlow(null) 205 private val participants: MutableStateFlow<Set<Participant>> = MutableStateFlow(emptySet()) 206 207 /** The participants in the call have changed */ onParticipantsChangednull208 fun onParticipantsChanged(newParticipants: Set<Participant>) { 209 participants.value = newParticipants 210 } 211 212 /** The active participant in the call has changed */ onActiveParticipantChangednull213 fun onActiveParticipantChanged(participant: Participant?) { 214 activeParticipant.value = participant 215 } 216 217 /** 218 * Collect updates to the [ParticipantExtensionData] related to this call based on the support 219 * state of this extension + actions 220 */ collectnull221 fun collect( 222 isSupported: Boolean, 223 raiseHandDataEmitter: Flow<RaiseHandData> = RaiseHandDataEmitter.UNSUPPORTED, 224 kickParticipantDataEmitter: Flow<KickParticipantData> = 225 KickParticipantDataEmitter.UNSUPPORTED 226 ): Flow<ParticipantExtensionData> { 227 return participants 228 .combine(activeParticipant) { newParticipants, newActiveParticipant -> 229 createExtensionData(isSupported, newActiveParticipant, newParticipants) 230 } 231 .combine(raiseHandDataEmitter) { data, rhData -> 232 ParticipantExtensionData( 233 isSupported = data.isSupported, 234 activeParticipant = data.activeParticipant, 235 selfParticipant = data.selfParticipant, 236 participants = data.participants, 237 raiseHandData = rhData, 238 kickParticipantData = data.kickParticipantData 239 ) 240 } 241 .combine(kickParticipantDataEmitter) { data, kpData -> 242 ParticipantExtensionData( 243 isSupported = data.isSupported, 244 activeParticipant = data.activeParticipant, 245 selfParticipant = data.selfParticipant, 246 participants = data.participants, 247 raiseHandData = data.raiseHandData, 248 kickParticipantData = kpData 249 ) 250 } 251 } 252 createExtensionDatanull253 private fun createExtensionData( 254 isSupported: Boolean, 255 activeParticipant: Participant? = null, 256 participants: Set<Participant> = emptySet() 257 ): ParticipantExtensionData { 258 // For now, the first element is considered ourself 259 val self = participants.firstOrNull() 260 return ParticipantExtensionData(isSupported, activeParticipant, self, participants) 261 } 262 } 263 264 /** 265 * Track a [Call] and begin to stream [BaseCallData] using [collect] whenever the call data changes. 266 */ 267 class CallDataEmitter(val trackedCall: IcsCall) { 268 private companion object { 269 const val LOG_TAG = "CallDataProducer" 270 } 271 272 /** Collect on changes to the [BaseCallData] related to the [trackedCall] */ collectnull273 fun collect(): Flow<BaseCallData> { 274 return createCallDataFlow() 275 } 276 <lambda>null277 private fun createCallDataFlow(): Flow<BaseCallData> = callbackFlow { 278 val callback = 279 object : Call.Callback() { 280 override fun onStateChanged(call: Call?, state: Int) { 281 if (call != trackedCall.call) return 282 val callData = createCallData(trackedCall) 283 Log.v(LOG_TAG, "onStateChanged: call ${trackedCall.id}: $callData") 284 trySendBlocking(callData) 285 } 286 287 override fun onDetailsChanged(call: Call?, details: Call.Details?) { 288 if (call != trackedCall.call) return 289 val callData = createCallData(trackedCall) 290 Log.v(LOG_TAG, "onDetailsChanged: call ${trackedCall.id}: $callData") 291 trySendBlocking(callData) 292 } 293 294 override fun onCallDestroyed(call: Call?) { 295 if (call != trackedCall.call) return 296 Log.v(LOG_TAG, "call ${trackedCall.id}: destroyed") 297 channel.close() 298 } 299 } 300 if (trackedCall.call.details != null) { 301 val callData = createCallData(trackedCall) 302 Log.v(LOG_TAG, "call ${trackedCall.id}: $callData") 303 trySendBlocking(callData) 304 } 305 trackedCall.call.registerCallback(callback) 306 awaitClose { trackedCall.call.unregisterCallback(callback) } 307 } 308 createCallDatanull309 private fun createCallData(icsCall: IcsCall): BaseCallData { 310 return BaseCallData( 311 id = icsCall.id, 312 phoneAccountHandle = icsCall.call.details.accountHandle, 313 name = 314 when (icsCall.call.details.callerDisplayNamePresentation) { 315 TelecomManager.PRESENTATION_ALLOWED -> 316 icsCall.call.details.callerDisplayName ?: "" 317 TelecomManager.PRESENTATION_RESTRICTED -> "Restricted" 318 TelecomManager.PRESENTATION_UNKNOWN -> "Unknown" 319 else -> icsCall.call.details.callerDisplayName ?: "" 320 }, 321 contactName = Compatibility.getContactDisplayName(icsCall.call.details), 322 contactUri = Compatibility.getContactPhotoUri(icsCall.call.details), 323 number = icsCall.call.details.handle ?: Uri.parse("unknown:UNKNOWN_ID_${icsCall.id}"), 324 state = getState(Compatibility.getCallState(icsCall.call)), 325 direction = 326 when (icsCall.call.details.callDirection) { 327 Call.Details.DIRECTION_INCOMING -> Direction.INCOMING 328 else -> Direction.OUTGOING 329 }, 330 callType = 331 when (VideoProfile.isVideo(icsCall.call.details.videoState)) { 332 true -> CallType.VIDEO 333 false -> CallType.AUDIO 334 }, 335 capabilities = getCapabilities(icsCall.call.details.callCapabilities), 336 onStateChanged = ::onChangeCallState 337 ) 338 } 339 onChangeCallStatenull340 private fun onChangeCallState(transition: CallStateTransition) { 341 when (transition) { 342 CallStateTransition.HOLD -> trackedCall.call.hold() 343 CallStateTransition.UNHOLD -> trackedCall.call.unhold() 344 CallStateTransition.ANSWER -> trackedCall.call.answer(VideoProfile.STATE_AUDIO_ONLY) 345 CallStateTransition.DISCONNECT -> trackedCall.call.disconnect() 346 CallStateTransition.NONE -> {} 347 } 348 } 349 getStatenull350 private fun getState(telecomState: Int): CallState { 351 return when (telecomState) { 352 Call.STATE_RINGING -> CallState.INCOMING 353 Call.STATE_DIALING -> CallState.DIALING 354 Call.STATE_ACTIVE -> CallState.ACTIVE 355 Call.STATE_HOLDING -> CallState.HELD 356 Call.STATE_DISCONNECTING -> CallState.DISCONNECTING 357 Call.STATE_DISCONNECTED -> CallState.DISCONNECTED 358 else -> CallState.UNKNOWN 359 } 360 } 361 getCapabilitiesnull362 private fun getCapabilities(capabilities: Int): List<Capability> { 363 val capabilitiesList = ArrayList<Capability>() 364 if (canHold(capabilities)) { 365 capabilitiesList.add(Capability.SUPPORTS_HOLD) 366 } 367 return capabilitiesList 368 } 369 canHoldnull370 private fun canHold(capabilities: Int): Boolean { 371 return (Call.Details.CAPABILITY_HOLD and capabilities) > 0 372 } 373 } 374