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.content.ComponentName 20 import android.content.Context 21 import android.content.Context.BIND_AUTO_CREATE 22 import android.content.Intent 23 import android.content.ServiceConnection 24 import android.os.IBinder 25 import android.util.Log 26 import kotlinx.coroutines.ExperimentalCoroutinesApi 27 import kotlinx.coroutines.flow.Flow 28 import kotlinx.coroutines.flow.MutableStateFlow 29 import kotlinx.coroutines.flow.emptyFlow 30 import kotlinx.coroutines.flow.flatMapConcat 31 import kotlinx.coroutines.flow.getAndUpdate 32 33 data class LocalServiceConnection( 34 val isConnected: Boolean, 35 val context: Context? = null, 36 val serviceConnection: ServiceConnection? = null, 37 val connection: LocalIcsBinder? = null 38 ) 39 40 /** 41 * Manages the connection for the Provider of "remote" calls, which are calls from the app's 42 * [InCallServiceImpl]. 43 */ 44 @OptIn(ExperimentalCoroutinesApi::class) 45 class RemoteCallProvider { 46 private companion object { 47 const val LOG_TAG = "RemoteCallProvider" 48 } 49 50 private val connectedService: MutableStateFlow<LocalServiceConnection> = 51 MutableStateFlow(LocalServiceConnection(false)) 52 53 /** Bind to the app's [LocalIcsBinder.Connector] Service implementation */ 54 fun connectService(context: Context) { 55 if (connectedService.value.isConnected) return 56 val intent = Intent(context, InCallServiceImpl::class.java) 57 val serviceConnection = 58 object : ServiceConnection { 59 override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 60 if (service == null) return 61 val localService = service as LocalIcsBinder.Connector 62 connectedService.value = 63 LocalServiceConnection(true, context, this, localService.getService()) 64 } 65 66 override fun onServiceDisconnected(name: ComponentName?) { 67 // Unlikely since the Service is in the same process. Re-evaluate if the service 68 // is moved to another process. 69 Log.w(LOG_TAG, "onServiceDisconnected: Unexpected call") 70 } 71 } 72 Log.i(LOG_TAG, "connectToIcs: Binding to ICS locally") 73 context.bindService(intent, serviceConnection, BIND_AUTO_CREATE) 74 } 75 76 /** Disconnect from the app;s [LocalIcsBinder.Connector] Service implementation */ 77 fun disconnectService() { 78 val localConnection = connectedService.getAndUpdate { LocalServiceConnection(false) } 79 localConnection.serviceConnection?.let { conn -> 80 Log.i(LOG_TAG, "connectToIcs: Unbinding to ICS locally") 81 localConnection.context?.unbindService(conn) 82 } 83 } 84 85 /** 86 * Stream the [CallData] representing each active Call on the device. The Flow will be empty 87 * until the remote Service connects. 88 */ 89 fun streamCallData(): Flow<List<CallData>> { 90 return connectedService.flatMapConcat { conn -> 91 if (!conn.isConnected) { 92 emptyFlow() 93 } else { 94 conn.connection?.callData ?: emptyFlow() 95 } 96 } 97 } 98 99 /** 100 * Stream the global mute state of the device. The Flow will be empty until the remote Service 101 * connects. 102 */ 103 fun streamMuteData(): Flow<Boolean> { 104 return connectedService.flatMapConcat { conn -> 105 if (!conn.isConnected) { 106 emptyFlow() 107 } else { 108 conn.connection?.isMuted ?: emptyFlow() 109 } 110 } 111 } 112 113 /** 114 * Stream the [CallAudioEndpoint] representing the current endpoint of the active call. The Flow 115 * will be empty until the remote Service connects. 116 */ 117 fun streamCurrentEndpointData(): Flow<CallAudioEndpoint?> { 118 return connectedService.flatMapConcat { conn -> 119 if (!conn.isConnected) { 120 emptyFlow() 121 } else { 122 conn.connection?.currentAudioEndpoint ?: emptyFlow() 123 } 124 } 125 } 126 127 /** 128 * Stream the List of [CallAudioEndpoint]s representing the available endpoints of the active 129 * call. The Flow will be empty until the remote Service connects. 130 */ 131 fun streamAvailableEndpointData(): Flow<List<CallAudioEndpoint>> { 132 return connectedService.flatMapConcat { conn -> 133 if (!conn.isConnected) { 134 emptyFlow() 135 } else { 136 conn.connection?.availableAudioEndpoints ?: emptyFlow() 137 } 138 } 139 } 140 141 /** Request to change the global mute state of the device. */ 142 fun onChangeMuteState(isMuted: Boolean) { 143 val service = connectedService.value 144 if (!service.isConnected) return 145 service.connection?.onChangeMuteState(isMuted) 146 } 147 148 /** Request to change the current audio route of the active call. */ 149 suspend fun onChangeAudioRoute(id: String) { 150 val service = connectedService.value 151 if (!service.isConnected) return 152 service.connection?.onChangeAudioRoute(id) 153 } 154 } 155