1 /* 2 * Copyright 2022, 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 com.android.server.telecom; 18 19 import android.content.Context; 20 import android.bluetooth.BluetoothDevice; 21 import android.os.Bundle; 22 import android.os.ParcelUuid; 23 import android.os.ResultReceiver; 24 import android.telecom.CallAudioState; 25 import android.telecom.CallEndpoint; 26 import android.telecom.CallEndpointException; 27 import android.telecom.Log; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.server.telecom.flags.FeatureFlags; 31 32 import java.util.HashMap; 33 import java.util.Map; 34 import java.util.HashSet; 35 import java.util.Set; 36 import java.util.concurrent.CompletableFuture; 37 import java.util.concurrent.TimeUnit; 38 39 /** 40 * Provides to {@link CallsManager} the service that can request change of CallEndpoint to the 41 * {@link CallAudioManager}. And notify change of CallEndpoint status to {@link CallsManager} 42 */ 43 public class CallEndpointController extends CallsManagerListenerBase { 44 public static final int CHANGE_TIMEOUT_SEC = 2; 45 public static final int RESULT_REQUEST_SUCCESS = 0; 46 public static final int RESULT_ENDPOINT_DOES_NOT_EXIST = 1; 47 public static final int RESULT_REQUEST_TIME_OUT = 2; 48 public static final int RESULT_ANOTHER_REQUEST = 3; 49 public static final int RESULT_UNSPECIFIED_ERROR = 4; 50 51 private final Context mContext; 52 private final CallsManager mCallsManager; 53 private final FeatureFlags mFeatureFlags; 54 private final HashMap<Integer, Integer> mRouteToTypeMap; 55 private final HashMap<Integer, Integer> mTypeToRouteMap; 56 private final Map<ParcelUuid, String> mBluetoothAddressMap = new HashMap<>(); 57 private final Set<CallEndpoint> mAvailableCallEndpoints = new HashSet<>(); 58 private CallEndpoint mActiveCallEndpoint; 59 private ParcelUuid mRequestedEndpointId; 60 private CompletableFuture<Integer> mPendingChangeRequest; 61 CallEndpointController(Context context, CallsManager callsManager, FeatureFlags flags)62 public CallEndpointController(Context context, CallsManager callsManager, FeatureFlags flags) { 63 mContext = context; 64 mCallsManager = callsManager; 65 mFeatureFlags = flags; 66 mRouteToTypeMap = new HashMap<>(5); 67 mRouteToTypeMap.put(CallAudioState.ROUTE_EARPIECE, CallEndpoint.TYPE_EARPIECE); 68 mRouteToTypeMap.put(CallAudioState.ROUTE_BLUETOOTH, CallEndpoint.TYPE_BLUETOOTH); 69 mRouteToTypeMap.put(CallAudioState.ROUTE_WIRED_HEADSET, CallEndpoint.TYPE_WIRED_HEADSET); 70 mRouteToTypeMap.put(CallAudioState.ROUTE_SPEAKER, CallEndpoint.TYPE_SPEAKER); 71 mRouteToTypeMap.put(CallAudioState.ROUTE_STREAMING, CallEndpoint.TYPE_STREAMING); 72 73 mTypeToRouteMap = new HashMap<>(5); 74 mTypeToRouteMap.put(CallEndpoint.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE); 75 mTypeToRouteMap.put(CallEndpoint.TYPE_BLUETOOTH, CallAudioState.ROUTE_BLUETOOTH); 76 mTypeToRouteMap.put(CallEndpoint.TYPE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET); 77 mTypeToRouteMap.put(CallEndpoint.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER); 78 mTypeToRouteMap.put(CallEndpoint.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING); 79 } 80 81 @VisibleForTesting getCurrentCallEndpoint()82 public CallEndpoint getCurrentCallEndpoint() { 83 return mActiveCallEndpoint; 84 } 85 86 @VisibleForTesting getAvailableEndpoints()87 public Set<CallEndpoint> getAvailableEndpoints() { 88 return mAvailableCallEndpoints; 89 } 90 requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback)91 public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) { 92 Log.i(this, "requestCallEndpointChange %s", endpoint); 93 int route = mTypeToRouteMap.get(endpoint.getEndpointType()); 94 String bluetoothAddress = getBluetoothAddress(endpoint); 95 96 if (findMatchingTypeEndpoint(endpoint.getEndpointType()) == null || 97 (route == CallAudioState.ROUTE_BLUETOOTH && bluetoothAddress == null)) { 98 callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED, 99 getErrorResult(RESULT_ENDPOINT_DOES_NOT_EXIST)); 100 return; 101 } 102 103 if (isCurrentEndpointRequestedEndpoint(route, bluetoothAddress)) { 104 callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle()); 105 return; 106 } 107 108 if (mPendingChangeRequest != null && !mPendingChangeRequest.isDone()) { 109 mPendingChangeRequest.complete(RESULT_ANOTHER_REQUEST); 110 mPendingChangeRequest = null; 111 mRequestedEndpointId = null; 112 } 113 114 mPendingChangeRequest = new CompletableFuture<Integer>() 115 .completeOnTimeout(RESULT_REQUEST_TIME_OUT, CHANGE_TIMEOUT_SEC, TimeUnit.SECONDS); 116 117 mPendingChangeRequest.thenAcceptAsync((result) -> { 118 if (result == RESULT_REQUEST_SUCCESS) { 119 callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle()); 120 } else { 121 callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED, getErrorResult(result)); 122 } 123 }); 124 mRequestedEndpointId = endpoint.getIdentifier(); 125 mCallsManager.getCallAudioManager().setAudioRoute(route, bluetoothAddress); 126 } 127 isCurrentEndpointRequestedEndpoint(int requestedRoute, String requestedAddress)128 public boolean isCurrentEndpointRequestedEndpoint(int requestedRoute, String requestedAddress) { 129 if (mCallsManager.getCallAudioManager() == null 130 || mCallsManager.getCallAudioManager().getCallAudioState() == null) { 131 return false; 132 } 133 CallAudioState currentAudioState = mCallsManager.getCallAudioManager().getCallAudioState(); 134 if (requestedRoute == currentAudioState.getRoute()) { 135 if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH) { 136 // The audio route (earpiece, speaker, etc.) is already active 137 // and Telecom can ignore the spam request! 138 Log.i(this, "iCERE: user requested a non-BT route that is already active"); 139 return true; 140 } else if (hasSameBluetoothAddress(currentAudioState, requestedAddress)) { 141 // if the requested (BT route, device) is active, ignore the request... 142 Log.i(this, "iCERE: user requested a BT endpoint that is already active"); 143 return true; 144 } 145 } 146 return false; 147 } 148 hasSameBluetoothAddress(CallAudioState audioState, String requestedAddress)149 public boolean hasSameBluetoothAddress(CallAudioState audioState, String requestedAddress) { 150 boolean hasActiveBtDevice = audioState.getActiveBluetoothDevice() != null; 151 return hasActiveBtDevice && requestedAddress.equals( 152 audioState.getActiveBluetoothDevice().getAddress()); 153 } 154 getErrorResult(int result)155 private Bundle getErrorResult(int result) { 156 String message; 157 int resultCode; 158 switch (result) { 159 case RESULT_ENDPOINT_DOES_NOT_EXIST: 160 message = "Requested CallEndpoint does not exist"; 161 resultCode = CallEndpointException.ERROR_ENDPOINT_DOES_NOT_EXIST; 162 break; 163 case RESULT_REQUEST_TIME_OUT: 164 message = "The operation was not completed on time"; 165 resultCode = CallEndpointException.ERROR_REQUEST_TIME_OUT; 166 break; 167 case RESULT_ANOTHER_REQUEST: 168 message = "The operation was canceled by another request"; 169 resultCode = CallEndpointException.ERROR_ANOTHER_REQUEST; 170 break; 171 default: 172 message = "The operation has failed due to an unknown or unspecified error"; 173 resultCode = CallEndpointException.ERROR_UNSPECIFIED; 174 } 175 CallEndpointException exception = new CallEndpointException(message, resultCode); 176 Bundle extras = new Bundle(); 177 extras.putParcelable(CallEndpointException.CHANGE_ERROR, exception); 178 return extras; 179 } 180 181 @VisibleForTesting getBluetoothAddress(CallEndpoint endpoint)182 public String getBluetoothAddress(CallEndpoint endpoint) { 183 return mBluetoothAddressMap.get(endpoint.getIdentifier()); 184 } 185 notifyCallEndpointChange()186 private void notifyCallEndpointChange() { 187 if (mActiveCallEndpoint == null) { 188 Log.i(this, "notifyCallEndpointChange, invalid CallEndpoint"); 189 return; 190 } 191 192 if (mRequestedEndpointId != null && mPendingChangeRequest != null && 193 mRequestedEndpointId.equals(mActiveCallEndpoint.getIdentifier())) { 194 mPendingChangeRequest.complete(RESULT_REQUEST_SUCCESS); 195 mPendingChangeRequest = null; 196 mRequestedEndpointId = null; 197 } 198 mCallsManager.updateCallEndpoint(mActiveCallEndpoint); 199 200 Set<Call> calls = mCallsManager.getTrackedCalls(); 201 for (Call call : calls) { 202 if (mFeatureFlags.cacheCallAudioCallbacks()) { 203 onCallEndpointChangedOrCache(call); 204 } else { 205 if (call != null && call.getConnectionService() != null) { 206 call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint); 207 } else if (call != null && call.getTransactionServiceWrapper() != null) { 208 call.getTransactionServiceWrapper() 209 .onCallEndpointChanged(call, mActiveCallEndpoint); 210 } 211 } 212 } 213 } 214 onCallEndpointChangedOrCache(Call call)215 private void onCallEndpointChangedOrCache(Call call) { 216 if (call == null) { 217 return; 218 } 219 CallSourceService service = call.getService(); 220 if (service != null) { 221 service.onCallEndpointChanged(call, mActiveCallEndpoint); 222 } else { 223 call.cacheServiceCallback(new CachedCurrentEndpointChange(mActiveCallEndpoint)); 224 } 225 } 226 notifyAvailableCallEndpointsChange()227 private void notifyAvailableCallEndpointsChange() { 228 mCallsManager.updateAvailableCallEndpoints(mAvailableCallEndpoints); 229 230 Set<Call> calls = mCallsManager.getTrackedCalls(); 231 for (Call call : calls) { 232 if (mFeatureFlags.cacheCallAudioCallbacks()) { 233 onAvailableEndpointsChangedOrCache(call); 234 } else { 235 if (call != null && call.getConnectionService() != null) { 236 call.getConnectionService().onAvailableCallEndpointsChanged(call, 237 mAvailableCallEndpoints); 238 } else if (call != null && call.getTransactionServiceWrapper() != null) { 239 call.getTransactionServiceWrapper().onAvailableCallEndpointsChanged(call, 240 mAvailableCallEndpoints); 241 } 242 } 243 } 244 } 245 onAvailableEndpointsChangedOrCache(Call call)246 private void onAvailableEndpointsChangedOrCache(Call call) { 247 if (call == null) { 248 return; 249 } 250 CallSourceService service = call.getService(); 251 if (service != null) { 252 service.onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints); 253 } else { 254 call.cacheServiceCallback(new CachedAvailableEndpointsChange(mAvailableCallEndpoints)); 255 } 256 } 257 notifyMuteStateChange(boolean isMuted)258 private void notifyMuteStateChange(boolean isMuted) { 259 mCallsManager.updateMuteState(isMuted); 260 261 Set<Call> calls = mCallsManager.getTrackedCalls(); 262 for (Call call : calls) { 263 if (mFeatureFlags.cacheCallAudioCallbacks()) { 264 onMuteStateChangedOrCache(call, isMuted); 265 } else { 266 if (call != null && call.getConnectionService() != null) { 267 call.getConnectionService().onMuteStateChanged(call, isMuted); 268 } else if (call != null && call.getTransactionServiceWrapper() != null) { 269 call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted); 270 } 271 } 272 } 273 } 274 onMuteStateChangedOrCache(Call call, boolean isMuted)275 private void onMuteStateChangedOrCache(Call call, boolean isMuted){ 276 if (call == null) { 277 return; 278 } 279 CallSourceService service = call.getService(); 280 if (service != null) { 281 service.onMuteStateChanged(call, isMuted); 282 } else { 283 call.cacheServiceCallback(new CachedMuteStateChange(isMuted)); 284 } 285 } 286 createAvailableCallEndpoints(CallAudioState state)287 private void createAvailableCallEndpoints(CallAudioState state) { 288 Set<CallEndpoint> newAvailableEndpoints = new HashSet<>(); 289 Map<ParcelUuid, String> newBluetoothDevices = new HashMap<>(); 290 291 mRouteToTypeMap.forEach((route, type) -> { 292 if ((state.getSupportedRouteMask() & route) != 0) { 293 if (type == CallEndpoint.TYPE_STREAMING) { 294 if (state.getRoute() == CallAudioState.ROUTE_STREAMING) { 295 if (mActiveCallEndpoint == null 296 || mActiveCallEndpoint.getEndpointType() != type) { 297 mActiveCallEndpoint = new CallEndpoint(getEndpointName(type) != null 298 ? getEndpointName(type) : "", type); 299 } 300 } 301 } else if (type == CallEndpoint.TYPE_BLUETOOTH) { 302 for (BluetoothDevice device : state.getSupportedBluetoothDevices()) { 303 CallEndpoint endpoint = findMatchingBluetoothEndpoint(device); 304 if (endpoint == null) { 305 String deviceName = device.getName(); 306 endpoint = new CallEndpoint( 307 deviceName != null ? deviceName : "", 308 CallEndpoint.TYPE_BLUETOOTH); 309 } 310 newAvailableEndpoints.add(endpoint); 311 newBluetoothDevices.put(endpoint.getIdentifier(), device.getAddress()); 312 313 BluetoothDevice activeDevice = state.getActiveBluetoothDevice(); 314 if (state.getRoute() == route && device.equals(activeDevice)) { 315 mActiveCallEndpoint = endpoint; 316 } 317 } 318 } else { 319 CallEndpoint endpoint = findMatchingTypeEndpoint(type); 320 if (endpoint == null) { 321 endpoint = new CallEndpoint( 322 getEndpointName(type) != null ? getEndpointName(type) : "", type); 323 } 324 newAvailableEndpoints.add(endpoint); 325 if (state.getRoute() == route) { 326 mActiveCallEndpoint = endpoint; 327 } 328 } 329 } 330 }); 331 mAvailableCallEndpoints.clear(); 332 mAvailableCallEndpoints.addAll(newAvailableEndpoints); 333 mBluetoothAddressMap.clear(); 334 mBluetoothAddressMap.putAll(newBluetoothDevices); 335 } 336 findMatchingTypeEndpoint(int targetType)337 private CallEndpoint findMatchingTypeEndpoint(int targetType) { 338 for (CallEndpoint endpoint : mAvailableCallEndpoints) { 339 if (endpoint.getEndpointType() == targetType) { 340 return endpoint; 341 } 342 } 343 return null; 344 } 345 findMatchingBluetoothEndpoint(BluetoothDevice device)346 private CallEndpoint findMatchingBluetoothEndpoint(BluetoothDevice device) { 347 final String targetAddress = device.getAddress(); 348 if (targetAddress != null) { 349 for (CallEndpoint endpoint : mAvailableCallEndpoints) { 350 final String address = mBluetoothAddressMap.get(endpoint.getIdentifier()); 351 if (targetAddress.equals(address)) { 352 return endpoint; 353 } 354 } 355 } 356 return null; 357 } 358 isAvailableEndpointChanged(CallAudioState oldState, CallAudioState newState)359 private boolean isAvailableEndpointChanged(CallAudioState oldState, CallAudioState newState) { 360 if (oldState == null) { 361 return true; 362 } 363 if ((oldState.getSupportedRouteMask() ^ newState.getSupportedRouteMask()) != 0) { 364 return true; 365 } 366 if (oldState.getSupportedBluetoothDevices().size() != 367 newState.getSupportedBluetoothDevices().size()) { 368 return true; 369 } 370 for (BluetoothDevice device : newState.getSupportedBluetoothDevices()) { 371 if (!oldState.getSupportedBluetoothDevices().contains(device)) { 372 return true; 373 } 374 } 375 return false; 376 } 377 isEndpointChanged(CallAudioState oldState, CallAudioState newState)378 private boolean isEndpointChanged(CallAudioState oldState, CallAudioState newState) { 379 if (oldState == null) { 380 return true; 381 } 382 if (oldState.getRoute() != newState.getRoute()) { 383 return true; 384 } 385 if (newState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) { 386 if (oldState.getActiveBluetoothDevice() == null) { 387 if (newState.getActiveBluetoothDevice() == null) { 388 return false; 389 } 390 return true; 391 } 392 return !oldState.getActiveBluetoothDevice().equals(newState.getActiveBluetoothDevice()); 393 } 394 return false; 395 } 396 isMuteStateChanged(CallAudioState oldState, CallAudioState newState)397 private boolean isMuteStateChanged(CallAudioState oldState, CallAudioState newState) { 398 if (oldState == null) { 399 return true; 400 } 401 return oldState.isMuted() != newState.isMuted(); 402 } 403 getEndpointName(int endpointType)404 private CharSequence getEndpointName(int endpointType) { 405 switch (endpointType) { 406 case CallEndpoint.TYPE_EARPIECE: 407 return mContext.getText(R.string.callendpoint_name_earpiece); 408 case CallEndpoint.TYPE_BLUETOOTH: 409 return mContext.getText(R.string.callendpoint_name_bluetooth); 410 case CallEndpoint.TYPE_WIRED_HEADSET: 411 return mContext.getText(R.string.callendpoint_name_wiredheadset); 412 case CallEndpoint.TYPE_SPEAKER: 413 return mContext.getText(R.string.callendpoint_name_speaker); 414 case CallEndpoint.TYPE_STREAMING: 415 return mContext.getText(R.string.callendpoint_name_streaming); 416 default: 417 return mContext.getText(R.string.callendpoint_name_unknown); 418 } 419 } 420 421 @Override onCallAudioStateChanged(CallAudioState oldState, CallAudioState newState)422 public void onCallAudioStateChanged(CallAudioState oldState, CallAudioState newState) { 423 Log.i(this, "onCallAudioStateChanged, audioState: %s -> %s", oldState, newState); 424 425 if (newState == null) { 426 Log.i(this, "onCallAudioStateChanged, invalid audioState"); 427 return; 428 } 429 430 createAvailableCallEndpoints(newState); 431 432 boolean isforce = true; 433 if (isAvailableEndpointChanged(oldState, newState)) { 434 notifyAvailableCallEndpointsChange(); 435 isforce = false; 436 } 437 438 if (isEndpointChanged(oldState, newState)) { 439 notifyCallEndpointChange(); 440 isforce = false; 441 } 442 443 if (isMuteStateChanged(oldState, newState)) { 444 notifyMuteStateChange(newState.isMuted()); 445 isforce = false; 446 } 447 448 if (isforce) { 449 notifyAvailableCallEndpointsChange(); 450 notifyCallEndpointChange(); 451 notifyMuteStateChange(newState.isMuted()); 452 } 453 } 454 }