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.bluetooth.BluetoothDevice 20 import android.os.Build 21 import android.os.OutcomeReceiver 22 import android.telecom.CallAudioState 23 import android.telecom.CallEndpoint 24 import android.telecom.CallEndpointException 25 import androidx.annotation.RequiresApi 26 import androidx.core.telecom.test.Compatibility 27 import java.util.UUID 28 import java.util.concurrent.Executor 29 import kotlin.coroutines.resume 30 import kotlinx.coroutines.CoroutineScope 31 import kotlinx.coroutines.async 32 import kotlinx.coroutines.flow.MutableStateFlow 33 import kotlinx.coroutines.flow.StateFlow 34 import kotlinx.coroutines.flow.asStateFlow 35 import kotlinx.coroutines.flow.combine 36 import kotlinx.coroutines.flow.filterNotNull 37 import kotlinx.coroutines.flow.launchIn 38 import kotlinx.coroutines.suspendCancellableCoroutine 39 40 /** 41 * Tracks the current state of the available and current audio route on the device while in call, 42 * taking into account the device's android API version. 43 * 44 * @param coroutineScope The scope attached to the lifecycle of the Service 45 * @param callData The stream of calls that are active on this device 46 * @param onChangeAudioRoute The callback used when user has requested to change the audio route on 47 * the device for devices running an API version < UDC 48 * @param onRequestBluetoothAudio The callback used when the user has requested to change the audio 49 * route for devices running on API version < UDC 50 * @param onRequestEndpointChange The callback used when the user has requested to change the 51 * endpoint for devices running API version UDC+ 52 */ 53 class CallAudioRouteResolver( 54 private val coroutineScope: CoroutineScope, 55 callData: StateFlow<List<CallData>>, 56 private val onChangeAudioRoute: (Int) -> Unit, 57 private val onRequestBluetoothAudio: (BluetoothDevice) -> Unit, 58 private val onRequestEndpointChange: 59 (CallEndpoint, Executor, OutcomeReceiver<Void, CallEndpointException>) -> Unit 60 ) { 61 private val mIsCallAudioStateDeprecated = 62 Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 63 64 // Maps the CallAudioEndpoint to the associated BluetoothDevice (if applicable) for bkwds 65 // compatibility with devices running on API version < UDC 66 data class EndpointEntry(val endpoint: CallAudioEndpoint, val device: BluetoothDevice? = null) 67 68 private val mCurrentEndpoint: MutableStateFlow<CallAudioEndpoint?> = MutableStateFlow(null) 69 private val mAvailableEndpoints: MutableStateFlow<List<CallAudioEndpoint>> = 70 MutableStateFlow(emptyList()) 71 val currentEndpoint: StateFlow<CallAudioEndpoint?> = mCurrentEndpoint.asStateFlow() 72 val availableEndpoints = mAvailableEndpoints.asStateFlow() 73 74 private val mCallAudioState: MutableStateFlow<CallAudioState?> = MutableStateFlow(null) 75 private val mEndpoints: MutableStateFlow<List<EndpointEntry>> = MutableStateFlow(emptyList()) 76 private val mCurrentCallEndpoint: MutableStateFlow<CallEndpoint?> = MutableStateFlow(null) 77 private val mAvailableCallEndpoints: MutableStateFlow<List<CallEndpoint>> = 78 MutableStateFlow(emptyList()) 79 80 init { 81 if (!mIsCallAudioStateDeprecated) { 82 // bkwds compat functionality 83 mCallAudioState 84 .filterNotNull() 85 .combine(callData) { state, data -> 86 if (data.isNotEmpty()) { 87 mCurrentEndpoint.value = getCurrentEndpoint(state) 88 mEndpoints.value = createEndpointEntries(state) 89 mAvailableEndpoints.value = mEndpoints.value.map { it.endpoint } 90 } else { 91 mCurrentEndpoint.value = null 92 mEndpoints.value = emptyList() 93 mAvailableEndpoints.value = emptyList() 94 } 95 } 96 .launchIn(coroutineScope) 97 } else { 98 // UDC+ functionality 99 mAvailableCallEndpoints 100 .combine(callData) { endpoints, data -> 101 val availableEndpoints = 102 if (data.isNotEmpty()) { 103 endpoints.mapNotNull(::createCallAudioEndpoint) 104 } else { 105 emptyList() 106 } 107 mAvailableEndpoints.value = availableEndpoints 108 availableEndpoints 109 } 110 .combine(mCurrentCallEndpoint) { available, current -> 111 if (available.isEmpty()) { 112 mCurrentEndpoint.value = null 113 } 114 val audioEndpoint = current?.let { createCallAudioEndpoint(it) } 115 mCurrentEndpoint.value = available.firstOrNull { it.id == audioEndpoint?.id } 116 } 117 .launchIn(coroutineScope) 118 } 119 } 120 121 /** The audio state reported from the ICS has changed. */ 122 fun onCallAudioStateChanged(audioState: CallAudioState?) { 123 if (mIsCallAudioStateDeprecated) return 124 mCallAudioState.value = audioState 125 } 126 127 /** The call endpoint reported from the ICS has changed. */ 128 fun onCallEndpointChanged(callEndpoint: CallEndpoint) { 129 if (!mIsCallAudioStateDeprecated) return 130 mCurrentCallEndpoint.value = callEndpoint 131 } 132 133 /** The available endpoints reported from the ICS have changed. */ 134 fun onAvailableCallEndpointsChanged(availableEndpoints: MutableList<CallEndpoint>) { 135 if (!mIsCallAudioStateDeprecated) return 136 mAvailableCallEndpoints.value = availableEndpoints 137 } 138 139 /** 140 * Request to change the audio route using the provided [CallAudioEndpoint.id]. 141 * 142 * @return true if the operation succeeded, false if it did not because the endpoint doesn't 143 * exist. 144 */ 145 suspend fun onChangeAudioRoute(id: String): Boolean { 146 if (mIsCallAudioStateDeprecated) { 147 val endpoint = 148 mAvailableCallEndpoints.value.firstOrNull { it.identifier.toString() == id } 149 if (endpoint == null) return false 150 return coroutineScope.async { onRequestEndpointChange(endpoint) }.await() 151 } else { 152 val endpoint = mEndpoints.value.firstOrNull { it.endpoint.id == id } 153 if (endpoint == null) return false 154 if (endpoint.endpoint.audioRoute != AudioRoute.BLUETOOTH) { 155 onChangeAudioRoute(getAudioState(endpoint.endpoint.audioRoute)) 156 return true 157 } else { 158 if (endpoint.device == null) return false 159 onRequestBluetoothAudio(endpoint.device) 160 return true 161 } 162 } 163 } 164 165 /** Send a request to the InCallService to change the current endpoint. */ 166 private suspend fun onRequestEndpointChange(endpoint: CallEndpoint): Boolean = 167 suspendCancellableCoroutine { continuation -> 168 onRequestEndpointChange( 169 endpoint, 170 Runnable::run, 171 @RequiresApi(Build.VERSION_CODES.S) 172 object : OutcomeReceiver<Void, CallEndpointException> { 173 override fun onResult(result: Void?) { 174 continuation.resume(true) 175 } 176 177 override fun onError(error: CallEndpointException) { 178 continuation.resume(false) 179 } 180 } 181 ) 182 } 183 184 /** Maps from the Telecom [CallAudioState] to the app's [CallAudioEndpoint] */ 185 private fun getCurrentEndpoint(callAudioState: CallAudioState): CallAudioEndpoint { 186 if (CallAudioState.ROUTE_BLUETOOTH != callAudioState.route) { 187 return CallAudioEndpoint( 188 id = getAudioEndpointId(callAudioState.route), 189 audioRoute = getAudioEndpointRoute(callAudioState.route) 190 ) 191 } 192 val device: BluetoothDevice? = callAudioState.activeBluetoothDevice 193 if (device?.address != null) { 194 return CallAudioEndpoint( 195 id = device.address, 196 audioRoute = AudioRoute.BLUETOOTH, 197 frameworkName = getName(device) 198 ) 199 } 200 val exactMatch = mEndpoints.value.firstOrNull { it.device == device } 201 if (exactMatch != null) return exactMatch.endpoint 202 return CallAudioEndpoint( 203 id = "", 204 audioRoute = AudioRoute.BLUETOOTH, 205 frameworkName = device?.let { getName(it) } 206 ) 207 } 208 209 /** Create the [CallAudioEndpoint] from the telecom [CallEndpoint] for API UDC+ */ 210 private fun createCallAudioEndpoint(endpoint: CallEndpoint): CallAudioEndpoint? { 211 val id = Compatibility.getEndpointIdentifier(endpoint) ?: return null 212 val type = Compatibility.getEndpointType(endpoint) ?: return null 213 val name = Compatibility.getEndpointName(endpoint) ?: return null 214 return CallAudioEndpoint(id, getAudioRouteFromEndpointType(type), name) 215 } 216 217 /** Reconstruct the available audio routes from telecom state and construct [EndpointEntry]s */ 218 private fun createEndpointEntries(callAudioState: CallAudioState): List<EndpointEntry> { 219 return buildList { 220 if (CallAudioState.ROUTE_EARPIECE and callAudioState.supportedRouteMask > 0) { 221 add( 222 EndpointEntry( 223 CallAudioEndpoint( 224 id = getAudioEndpointId(CallAudioState.ROUTE_EARPIECE), 225 audioRoute = AudioRoute.EARPIECE 226 ) 227 ) 228 ) 229 } 230 if (CallAudioState.ROUTE_SPEAKER and callAudioState.supportedRouteMask > 0) { 231 add( 232 EndpointEntry( 233 CallAudioEndpoint( 234 id = getAudioEndpointId(CallAudioState.ROUTE_SPEAKER), 235 audioRoute = AudioRoute.SPEAKER 236 ) 237 ) 238 ) 239 } 240 if (CallAudioState.ROUTE_WIRED_HEADSET and callAudioState.supportedRouteMask > 0) { 241 add( 242 EndpointEntry( 243 CallAudioEndpoint( 244 id = getAudioEndpointId(CallAudioState.ROUTE_WIRED_HEADSET), 245 audioRoute = AudioRoute.HEADSET 246 ) 247 ) 248 ) 249 } 250 if (CallAudioState.ROUTE_STREAMING and callAudioState.supportedRouteMask > 0) { 251 add( 252 EndpointEntry( 253 CallAudioEndpoint( 254 id = getAudioEndpointId(CallAudioState.ROUTE_STREAMING), 255 audioRoute = AudioRoute.STREAMING 256 ) 257 ) 258 ) 259 } 260 // For Bluetooth, cache the BluetoothDevices associated with the route so we can choose 261 // them later 262 if (CallAudioState.ROUTE_BLUETOOTH and callAudioState.supportedRouteMask > 0) { 263 addAll( 264 callAudioState.supportedBluetoothDevices.map { device -> 265 EndpointEntry( 266 CallAudioEndpoint( 267 id = device.address?.toString() ?: UUID.randomUUID().toString(), 268 audioRoute = AudioRoute.BLUETOOTH, 269 frameworkName = getName(device) 270 ), 271 device 272 ) 273 } 274 ) 275 } 276 } 277 } 278 279 private fun getName(device: BluetoothDevice): String? { 280 var name = Compatibility.getBluetoothDeviceAlias(device) 281 if (name.isFailure) { 282 name = getBluetoothDeviceName(device) 283 } 284 return name.getOrDefault(null) 285 } 286 287 private fun getBluetoothDeviceName(device: BluetoothDevice): Result<String> { 288 return try { 289 Result.success(device.name ?: "") 290 } catch (e: SecurityException) { 291 Result.failure(e) 292 } 293 } 294 295 private fun getAudioEndpointId(audioState: Int): String { 296 return when (audioState) { 297 CallAudioState.ROUTE_EARPIECE -> "Earpiece" 298 CallAudioState.ROUTE_SPEAKER -> "Speaker" 299 CallAudioState.ROUTE_WIRED_HEADSET -> "Headset" 300 CallAudioState.ROUTE_BLUETOOTH -> "Bluetooth" 301 CallAudioState.ROUTE_STREAMING -> "Streaming" 302 else -> "Unknown" 303 } 304 } 305 306 private fun getAudioRouteFromEndpointType(endpointType: Int): AudioRoute { 307 return when (endpointType) { 308 CallEndpoint.TYPE_EARPIECE -> AudioRoute.EARPIECE 309 CallEndpoint.TYPE_SPEAKER -> AudioRoute.SPEAKER 310 CallEndpoint.TYPE_WIRED_HEADSET -> AudioRoute.HEADSET 311 CallEndpoint.TYPE_BLUETOOTH -> AudioRoute.BLUETOOTH 312 CallEndpoint.TYPE_STREAMING -> AudioRoute.STREAMING 313 else -> { 314 AudioRoute.UNKNOWN 315 } 316 } 317 } 318 319 private fun getAudioEndpointRoute(audioState: Int): AudioRoute { 320 return when (audioState) { 321 CallAudioState.ROUTE_EARPIECE -> AudioRoute.EARPIECE 322 CallAudioState.ROUTE_SPEAKER -> AudioRoute.SPEAKER 323 CallAudioState.ROUTE_WIRED_HEADSET -> AudioRoute.HEADSET 324 CallAudioState.ROUTE_BLUETOOTH -> AudioRoute.BLUETOOTH 325 CallAudioState.ROUTE_STREAMING -> AudioRoute.STREAMING 326 else -> AudioRoute.UNKNOWN 327 } 328 } 329 330 private fun getAudioState(audioRoute: AudioRoute): Int { 331 return when (audioRoute) { 332 AudioRoute.EARPIECE -> CallAudioState.ROUTE_EARPIECE 333 AudioRoute.SPEAKER -> CallAudioState.ROUTE_SPEAKER 334 AudioRoute.HEADSET -> CallAudioState.ROUTE_WIRED_HEADSET 335 AudioRoute.BLUETOOTH -> CallAudioState.ROUTE_BLUETOOTH 336 AudioRoute.STREAMING -> CallAudioState.ROUTE_STREAMING 337 else -> CallAudioState.ROUTE_EARPIECE 338 } 339 } 340 } 341