blob: 4738cd49f5dff43f7e74dbd9cfbbb2a6d6834557 [file] [log] [blame]
/*
* 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.i(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)) {
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();
if (requestedRoute == currentAudioState.getRoute()) {
if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH) {
// The audio route (earpiece, speaker, etc.) is already active
// and Telecom can ignore the spam request!
Log.i(this, "iCERE: user requested a non-BT route that is already active");
return true;
} else if (hasSameBluetoothAddress(currentAudioState, requestedAddress)) {
// if the requested (BT route, device) is active, ignore the request...
Log.i(this, "iCERE: user requested a BT endpoint that is already active");
return true;
}
}
return false;
}
public boolean hasSameBluetoothAddress(CallAudioState audioState, String requestedAddress) {
boolean hasActiveBtDevice = audioState.getActiveBluetoothDevice() != null;
return hasActiveBtDevice && requestedAddress.equals(
audioState.getActiveBluetoothDevice().getAddress());
}
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());
}
}
}