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