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