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