summaryrefslogtreecommitdiff
path: root/src/com/android/server/telecom/CallEndpointController.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/server/telecom/CallEndpointController.java')
-rw-r--r--src/com/android/server/telecom/CallEndpointController.java399
1 files changed, 399 insertions, 0 deletions
diff --git a/src/com/android/server/telecom/CallEndpointController.java b/src/com/android/server/telecom/CallEndpointController.java
new file mode 100644
index 000000000..7e11b47f4
--- /dev/null
+++ b/src/com/android/server/telecom/CallEndpointController.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.Context;
+import android.bluetooth.BluetoothDevice;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import android.os.ResultReceiver;
+import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
+import android.telecom.CallEndpointException;
+import android.telecom.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides to {@link CallsManager} the service that can request change of CallEndpoint to the
+ * {@link CallAudioManager}. And notify change of CallEndpoint status to {@link CallsManager}
+ */
+public class CallEndpointController extends CallsManagerListenerBase {
+ public static final int CHANGE_TIMEOUT_SEC = 2;
+ public static final int RESULT_REQUEST_SUCCESS = 0;
+ public static final int RESULT_ENDPOINT_DOES_NOT_EXIST = 1;
+ public static final int RESULT_REQUEST_TIME_OUT = 2;
+ public static final int RESULT_ANOTHER_REQUEST = 3;
+ public static final int RESULT_UNSPECIFIED_ERROR = 4;
+
+ private final Context mContext;
+ private final CallsManager mCallsManager;
+ private final HashMap<Integer, Integer> mRouteToTypeMap;
+ private final HashMap<Integer, Integer> mTypeToRouteMap;
+ private final Map<ParcelUuid, String> mBluetoothAddressMap = new HashMap<>();
+ private final Set<CallEndpoint> mAvailableCallEndpoints = new HashSet<>();
+ private CallEndpoint mActiveCallEndpoint;
+ private ParcelUuid mRequestedEndpointId;
+ private CompletableFuture<Integer> mPendingChangeRequest;
+
+ public CallEndpointController(Context context, CallsManager callsManager) {
+ mContext = context;
+ mCallsManager = callsManager;
+
+ mRouteToTypeMap = new HashMap<>(5);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_EARPIECE, CallEndpoint.TYPE_EARPIECE);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_BLUETOOTH, CallEndpoint.TYPE_BLUETOOTH);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_WIRED_HEADSET, CallEndpoint.TYPE_WIRED_HEADSET);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_SPEAKER, CallEndpoint.TYPE_SPEAKER);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_STREAMING, CallEndpoint.TYPE_STREAMING);
+
+ mTypeToRouteMap = new HashMap<>(5);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_BLUETOOTH, CallAudioState.ROUTE_BLUETOOTH);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING);
+ }
+
+ @VisibleForTesting
+ public CallEndpoint getCurrentCallEndpoint() {
+ return mActiveCallEndpoint;
+ }
+
+ @VisibleForTesting
+ public Set<CallEndpoint> getAvailableEndpoints() {
+ return mAvailableCallEndpoints;
+ }
+
+ public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+ Log.d(this, "requestCallEndpointChange %s", endpoint);
+ int route = mTypeToRouteMap.get(endpoint.getEndpointType());
+ String bluetoothAddress = getBluetoothAddress(endpoint);
+
+ if (findMatchingTypeEndpoint(endpoint.getEndpointType()) == null ||
+ (route == CallAudioState.ROUTE_BLUETOOTH && bluetoothAddress == null)) {
+ callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED,
+ getErrorResult(RESULT_ENDPOINT_DOES_NOT_EXIST));
+ return;
+ }
+
+ if (isCurrentEndpointRequestedEndpoint(route, bluetoothAddress)) {
+ Log.d(this, "requestCallEndpointChange: requested endpoint is already active");
+ callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
+ return;
+ }
+
+ if (mPendingChangeRequest != null && !mPendingChangeRequest.isDone()) {
+ mPendingChangeRequest.complete(RESULT_ANOTHER_REQUEST);
+ mPendingChangeRequest = null;
+ mRequestedEndpointId = null;
+ }
+
+ mPendingChangeRequest = new CompletableFuture<Integer>()
+ .completeOnTimeout(RESULT_REQUEST_TIME_OUT, CHANGE_TIMEOUT_SEC, TimeUnit.SECONDS);
+
+ mPendingChangeRequest.thenAcceptAsync((result) -> {
+ if (result == RESULT_REQUEST_SUCCESS) {
+ callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
+ } else {
+ callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED, getErrorResult(result));
+ }
+ });
+ mRequestedEndpointId = endpoint.getIdentifier();
+ mCallsManager.getCallAudioManager().setAudioRoute(route, bluetoothAddress);
+ }
+
+ public boolean isCurrentEndpointRequestedEndpoint(int requestedRoute, String requestedAddress) {
+ if (mCallsManager.getCallAudioManager() == null
+ || mCallsManager.getCallAudioManager().getCallAudioState() == null) {
+ return false;
+ }
+ CallAudioState currentAudioState = mCallsManager.getCallAudioManager().getCallAudioState();
+ // requested non-bt endpoint is already active
+ if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH &&
+ requestedRoute == currentAudioState.getRoute()) {
+ return true;
+ }
+ // requested bt endpoint is already active
+ if (requestedRoute == CallAudioState.ROUTE_BLUETOOTH &&
+ currentAudioState.getActiveBluetoothDevice() != null &&
+ requestedAddress.equals(
+ currentAudioState.getActiveBluetoothDevice().getAddress())) {
+ return true;
+ }
+ return false;
+ }
+
+ private Bundle getErrorResult(int result) {
+ String message;
+ int resultCode;
+ switch (result) {
+ case RESULT_ENDPOINT_DOES_NOT_EXIST:
+ message = "Requested CallEndpoint does not exist";
+ resultCode = CallEndpointException.ERROR_ENDPOINT_DOES_NOT_EXIST;
+ break;
+ case RESULT_REQUEST_TIME_OUT:
+ message = "The operation was not completed on time";
+ resultCode = CallEndpointException.ERROR_REQUEST_TIME_OUT;
+ break;
+ case RESULT_ANOTHER_REQUEST:
+ message = "The operation was canceled by another request";
+ resultCode = CallEndpointException.ERROR_ANOTHER_REQUEST;
+ break;
+ default:
+ message = "The operation has failed due to an unknown or unspecified error";
+ resultCode = CallEndpointException.ERROR_UNSPECIFIED;
+ }
+ CallEndpointException exception = new CallEndpointException(message, resultCode);
+ Bundle extras = new Bundle();
+ extras.putParcelable(CallEndpointException.CHANGE_ERROR, exception);
+ return extras;
+ }
+
+ @VisibleForTesting
+ public String getBluetoothAddress(CallEndpoint endpoint) {
+ return mBluetoothAddressMap.get(endpoint.getIdentifier());
+ }
+
+ private void notifyCallEndpointChange() {
+ if (mActiveCallEndpoint == null) {
+ Log.i(this, "notifyCallEndpointChange, invalid CallEndpoint");
+ return;
+ }
+
+ if (mRequestedEndpointId != null && mPendingChangeRequest != null &&
+ mRequestedEndpointId.equals(mActiveCallEndpoint.getIdentifier())) {
+ mPendingChangeRequest.complete(RESULT_REQUEST_SUCCESS);
+ mPendingChangeRequest = null;
+ mRequestedEndpointId = null;
+ }
+ mCallsManager.updateCallEndpoint(mActiveCallEndpoint);
+
+ Set<Call> calls = mCallsManager.getTrackedCalls();
+ for (Call call : calls) {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
+ } else if (call != null && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper()
+ .onCallEndpointChanged(call, mActiveCallEndpoint);
+ }
+ }
+ }
+
+ private void notifyAvailableCallEndpointsChange() {
+ mCallsManager.updateAvailableCallEndpoints(mAvailableCallEndpoints);
+
+ Set<Call> calls = mCallsManager.getTrackedCalls();
+ for (Call call : calls) {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onAvailableCallEndpointsChanged(call,
+ mAvailableCallEndpoints);
+ } else if (call != null && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper()
+ .onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints);
+ }
+ }
+ }
+
+ private void notifyMuteStateChange(boolean isMuted) {
+ mCallsManager.updateMuteState(isMuted);
+
+ Set<Call> calls = mCallsManager.getTrackedCalls();
+ for (Call call : calls) {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onMuteStateChanged(call, isMuted);
+ } else if (call != null && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted);
+ }
+ }
+ }
+
+ private void createAvailableCallEndpoints(CallAudioState state) {
+ Set<CallEndpoint> newAvailableEndpoints = new HashSet<>();
+ Map<ParcelUuid, String> newBluetoothDevices = new HashMap<>();
+
+ mRouteToTypeMap.forEach((route, type) -> {
+ if ((state.getSupportedRouteMask() & route) != 0) {
+ if (type == CallEndpoint.TYPE_STREAMING) {
+ if (state.getRoute() == CallAudioState.ROUTE_STREAMING) {
+ if (mActiveCallEndpoint == null
+ || mActiveCallEndpoint.getEndpointType() != type) {
+ mActiveCallEndpoint = new CallEndpoint(getEndpointName(type) != null
+ ? getEndpointName(type) : "", type);
+ }
+ }
+ } else if (type == CallEndpoint.TYPE_BLUETOOTH) {
+ for (BluetoothDevice device : state.getSupportedBluetoothDevices()) {
+ CallEndpoint endpoint = findMatchingBluetoothEndpoint(device);
+ if (endpoint == null) {
+ String deviceName = device.getName();
+ endpoint = new CallEndpoint(
+ deviceName != null ? deviceName : "",
+ CallEndpoint.TYPE_BLUETOOTH);
+ }
+ newAvailableEndpoints.add(endpoint);
+ newBluetoothDevices.put(endpoint.getIdentifier(), device.getAddress());
+
+ BluetoothDevice activeDevice = state.getActiveBluetoothDevice();
+ if (state.getRoute() == route && device.equals(activeDevice)) {
+ mActiveCallEndpoint = endpoint;
+ }
+ }
+ } else {
+ CallEndpoint endpoint = findMatchingTypeEndpoint(type);
+ if (endpoint == null) {
+ endpoint = new CallEndpoint(
+ getEndpointName(type) != null ? getEndpointName(type) : "", type);
+ }
+ newAvailableEndpoints.add(endpoint);
+ if (state.getRoute() == route) {
+ mActiveCallEndpoint = endpoint;
+ }
+ }
+ }
+ });
+ mAvailableCallEndpoints.clear();
+ mAvailableCallEndpoints.addAll(newAvailableEndpoints);
+ mBluetoothAddressMap.clear();
+ mBluetoothAddressMap.putAll(newBluetoothDevices);
+ }
+
+ private CallEndpoint findMatchingTypeEndpoint(int targetType) {
+ for (CallEndpoint endpoint : mAvailableCallEndpoints) {
+ if (endpoint.getEndpointType() == targetType) {
+ return endpoint;
+ }
+ }
+ return null;
+ }
+
+ private CallEndpoint findMatchingBluetoothEndpoint(BluetoothDevice device) {
+ final String targetAddress = device.getAddress();
+ if (targetAddress != null) {
+ for (CallEndpoint endpoint : mAvailableCallEndpoints) {
+ final String address = mBluetoothAddressMap.get(endpoint.getIdentifier());
+ if (targetAddress.equals(address)) {
+ return endpoint;
+ }
+ }
+ }
+ return null;
+ }
+
+ private boolean isAvailableEndpointChanged(CallAudioState oldState, CallAudioState newState) {
+ if (oldState == null) {
+ return true;
+ }
+ if ((oldState.getSupportedRouteMask() ^ newState.getSupportedRouteMask()) != 0) {
+ return true;
+ }
+ if (oldState.getSupportedBluetoothDevices().size() !=
+ newState.getSupportedBluetoothDevices().size()) {
+ return true;
+ }
+ for (BluetoothDevice device : newState.getSupportedBluetoothDevices()) {
+ if (!oldState.getSupportedBluetoothDevices().contains(device)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isEndpointChanged(CallAudioState oldState, CallAudioState newState) {
+ if (oldState == null) {
+ return true;
+ }
+ if (oldState.getRoute() != newState.getRoute()) {
+ return true;
+ }
+ if (newState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
+ if (oldState.getActiveBluetoothDevice() == null) {
+ if (newState.getActiveBluetoothDevice() == null) {
+ return false;
+ }
+ return true;
+ }
+ return !oldState.getActiveBluetoothDevice().equals(newState.getActiveBluetoothDevice());
+ }
+ return false;
+ }
+
+ private boolean isMuteStateChanged(CallAudioState oldState, CallAudioState newState) {
+ if (oldState == null) {
+ return true;
+ }
+ return oldState.isMuted() != newState.isMuted();
+ }
+
+ private CharSequence getEndpointName(int endpointType) {
+ switch (endpointType) {
+ case CallEndpoint.TYPE_EARPIECE:
+ return mContext.getText(R.string.callendpoint_name_earpiece);
+ case CallEndpoint.TYPE_BLUETOOTH:
+ return mContext.getText(R.string.callendpoint_name_bluetooth);
+ case CallEndpoint.TYPE_WIRED_HEADSET:
+ return mContext.getText(R.string.callendpoint_name_wiredheadset);
+ case CallEndpoint.TYPE_SPEAKER:
+ return mContext.getText(R.string.callendpoint_name_speaker);
+ case CallEndpoint.TYPE_STREAMING:
+ return mContext.getText(R.string.callendpoint_name_streaming);
+ default:
+ return mContext.getText(R.string.callendpoint_name_unknown);
+ }
+ }
+
+ @Override
+ public void onCallAudioStateChanged(CallAudioState oldState, CallAudioState newState) {
+ Log.i(this, "onCallAudioStateChanged, audioState: %s -> %s", oldState, newState);
+
+ if (newState == null) {
+ Log.i(this, "onCallAudioStateChanged, invalid audioState");
+ return;
+ }
+
+ createAvailableCallEndpoints(newState);
+
+ boolean isforce = true;
+ if (isAvailableEndpointChanged(oldState, newState)) {
+ notifyAvailableCallEndpointsChange();
+ isforce = false;
+ }
+
+ if (isEndpointChanged(oldState, newState)) {
+ notifyCallEndpointChange();
+ isforce = false;
+ }
+
+ if (isMuteStateChanged(oldState, newState)) {
+ notifyMuteStateChange(newState.isMuted());
+ isforce = false;
+ }
+
+ if (isforce) {
+ notifyAvailableCallEndpointsChange();
+ notifyCallEndpointChange();
+ notifyMuteStateChange(newState.isMuted());
+ }
+ }
+} \ No newline at end of file