1 /*
2  * 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.VoipAppWithExtensions
18 
19 import android.app.Service
20 import android.content.Intent
21 import android.net.Uri
22 import android.os.Build
23 import android.os.IBinder
24 import android.telecom.DisconnectCause
25 import android.util.Log
26 import androidx.annotation.RequiresApi
27 import androidx.core.telecom.CallAttributesCompat
28 import androidx.core.telecom.CallAttributesCompat.Companion.DIRECTION_INCOMING
29 import androidx.core.telecom.CallAttributesCompat.Companion.DIRECTION_OUTGOING
30 import androidx.core.telecom.CallsManager
31 import androidx.core.telecom.extensions.Capability
32 import androidx.core.telecom.extensions.Participant
33 import androidx.core.telecom.extensions.ParticipantParcelable
34 import androidx.core.telecom.extensions.toParticipant
35 import androidx.core.telecom.test.ITestAppControl
36 import androidx.core.telecom.test.ITestAppControlCallback
37 import androidx.core.telecom.test.utils.TestUtils
38 import androidx.core.telecom.util.ExperimentalAppActions
39 import kotlin.coroutines.cancellation.CancellationException
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.Dispatchers
42 import kotlinx.coroutines.cancel
43 import kotlinx.coroutines.flow.MutableStateFlow
44 import kotlinx.coroutines.flow.drop
45 import kotlinx.coroutines.flow.launchIn
46 import kotlinx.coroutines.flow.onEach
47 import kotlinx.coroutines.launch
48 import kotlinx.coroutines.runBlocking
49 
50 @OptIn(ExperimentalAppActions::class)
51 @RequiresApi(Build.VERSION_CODES.O)
52 open class VoipAppWithExtensionsControl : Service() {
53     var mCallsManager: CallsManager? = null
54     private var mScope: CoroutineScope? = null
55     private var mCallback: ITestAppControlCallback? = null
56     private var participantsFlow: MutableStateFlow<List<Participant>> =
57         MutableStateFlow(emptyList())
58     private var activeParticipantFlow: MutableStateFlow<Participant?> = MutableStateFlow(null)
59     private var raisedHandsFlow: MutableStateFlow<List<Participant>> = MutableStateFlow(emptyList())
60     // TODO:: b/364316364 should be Pair(callId:String, value: Boolean)
61     private var isLocallySilencedFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
62     private var callIconFlow: MutableStateFlow<Uri> = MutableStateFlow(Uri.EMPTY)
63 
64     companion object {
65         val TAG = VoipAppWithExtensionsControl::class.java.simpleName
66         val CLASS_NAME = VoipAppWithExtensionsControl::class.java.canonicalName
67     }
68 
69     private val mBinder: ITestAppControl.Stub =
70         object : ITestAppControl.Stub() {
71 
setCallbacknull72             override fun setCallback(callback: ITestAppControlCallback) {
73                 mCallback = callback
74             }
75 
<lambda>null76             val mOnSetActiveLambda: suspend () -> Unit = { Log.i(TAG, "onSetActive: completing") }
77 
<lambda>null78             val mOnSetInActiveLambda: suspend () -> Unit = {
79                 Log.i(TAG, "onSetInactive: completing")
80             }
81 
<lambda>null82             val mOnAnswerLambda: suspend (type: Int) -> Unit = {
83                 Log.i(TAG, "onAnswer: callType=[$it]")
84             }
85 
<lambda>null86             val mOnDisconnectLambda: suspend (cause: DisconnectCause) -> Unit = {
87                 Log.i(TAG, "onDisconnect: disconnectCause=[$it]")
88             }
89 
addCallnull90             override fun addCall(
91                 requestId: Int,
92                 capabilities: List<Capability>,
93                 isOutgoing: Boolean
94             ) {
95                 Log.i(TAG, "VoipAppWithExtensionsControl: addCall: request")
96                 runBlocking {
97                     val call = VoipCall(mCallsManager!!, mCallback, capabilities)
98                     mScope?.launch {
99                         with(call) {
100                             addCall(
101                                 CallAttributesCompat(
102                                     "displayName" /* TODO:: make helper */,
103                                     Uri.parse("tel:123") /* TODO:: make helper */,
104                                     if (isOutgoing) DIRECTION_OUTGOING else DIRECTION_INCOMING
105                                 ),
106                                 mOnAnswerLambda,
107                                 mOnDisconnectLambda,
108                                 mOnSetActiveLambda,
109                                 mOnSetInActiveLambda
110                             ) {
111                                 launch { setActive() }
112                                 isMuted
113                                     .onEach { mCallback?.onGlobalMuteStateChanged(it) }
114                                     .launchIn(this)
115 
116                                 participantsFlow
117                                     .onEach {
118                                         TestUtils.printParticipants(it, "VoIP participants")
119                                         participantStateUpdater?.updateParticipants(it)
120                                     }
121                                     .launchIn(this)
122                                 raisedHandsFlow
123                                     .onEach {
124                                         TestUtils.printParticipants(it, "VoIP raised hands")
125                                         raiseHandStateUpdater?.updateRaisedHands(it)
126                                     }
127                                     .launchIn(this)
128                                 activeParticipantFlow
129                                     .onEach {
130                                         Log.i(TAG, "VOIP active participant: $it")
131                                         participantStateUpdater?.updateActiveParticipant(it)
132                                     }
133                                     .launchIn(this)
134                                 isLocallySilencedFlow
135                                     .drop(1) // ignore the first value from the voip app
136                                     // since only values from the test should be sent!
137                                     .onEach {
138                                         Log.i(TAG, "VoIP isLocallySilenced=[$it]")
139                                         // TODO:: b/364316364 gate on callId
140                                         localCallSilenceUpdater?.updateIsLocallySilenced(it)
141                                     }
142                                     .launchIn(this)
143                                 callIconFlow
144                                     .drop(1)
145                                     .onEach {
146                                         Log.i(TAG, "VoIP callIconFlow=[$it]")
147                                         callIconUpdater?.updateCallIconUri(it)
148                                     }
149                                     .launchIn(this)
150 
151                                 mCallback?.onCallAdded(requestId, this.getCallId().toString())
152                             }
153                         }
154                     }
155                 }
156             }
157 
updateParticipantsnull158             override fun updateParticipants(setOfParticipants: List<ParticipantParcelable>) {
159                 participantsFlow.value = setOfParticipants.map { it.toParticipant() }
160             }
161 
updateActiveParticipantnull162             override fun updateActiveParticipant(participant: ParticipantParcelable?) {
163                 activeParticipantFlow.value = participant?.toParticipant()
164             }
165 
updateRaisedHandsnull166             override fun updateRaisedHands(raisedHandsParticipants: List<ParticipantParcelable>) {
167                 raisedHandsFlow.value = raisedHandsParticipants.map { it.toParticipant() }
168             }
169 
170             // TODO:: b/364316364 add CallId arg.  Should be changing on a per call basis
updateIsLocallySilencednull171             override fun updateIsLocallySilenced(isLocallySilenced: Boolean) {
172                 isLocallySilencedFlow.value = isLocallySilenced
173             }
174 
updateCallIconnull175             override fun updateCallIcon(uri: Uri) {
176                 callIconFlow.value = uri
177             }
178         }
179 
onBindnull180     override fun onBind(intent: Intent?): IBinder? {
181         mScope = CoroutineScope(Dispatchers.Default)
182         if (intent?.component?.className.equals(getClassName())) {
183             mCallsManager = CallsManager(applicationContext)
184             return mBinder
185         }
186         return null
187     }
188 
onUnbindnull189     override fun onUnbind(intent: Intent?): Boolean {
190         mScope?.cancel(CancellationException("Control interface is unbinding"))
191         mScope = null
192         mCallback = null
193         participantsFlow.value = emptyList()
194         activeParticipantFlow.value = null
195         raisedHandsFlow.value = emptyList()
196         return false
197     }
198 
getClassNamenull199     open fun getClassName(): String? {
200         return CLASS_NAME
201     }
202 }
203