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