Loading res/values/strings.xml +12 −0 Original line number Diff line number Diff line Loading @@ -383,4 +383,16 @@ <string name="cancel">Cancel</string> <!-- Button label for generic back action [CHAR LIMIT=20] --> <string name="back">Back</string> <!-- The user-visible name of the earpiece type CallEndpoint --> <string name="callendpoint_name_earpiece">Earpiece</string> <!-- The user-visible name of the bluetooth type CallEndpoint --> <string name="callendpoint_name_bluetooth">Bluetooth</string> <!-- The user-visible name of the wired headset type CallEndpoint --> <string name="callendpoint_name_wiredheadset">Wired headset</string> <!-- The user-visible name of the speaker type CallEndpoint --> <string name="callendpoint_name_speaker">Speaker</string> <!-- The user-visible name of the streaming type CallEndpoint --> <string name="callendpoint_name_streaming">External</string> <!-- The user-visible name of the unknown new type CallEndpoint --> <string name="callendpoint_name_unknown">Unknown</string> </resources> src/com/android/server/telecom/CallAudioManager.java +2 −1 Original line number Diff line number Diff line Loading @@ -413,7 +413,8 @@ public class CallAudioManager extends CallsManagerListenerBase { * @param bluetoothAddress the address of the desired bluetooth device, if route is * {@link CallAudioState#ROUTE_BLUETOOTH}. */ void setAudioRoute(int route, String bluetoothAddress) { @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) public void setAudioRoute(int route, String bluetoothAddress) { Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route)); switch (route) { case CallAudioState.ROUTE_BLUETOOTH: Loading src/com/android/server/telecom/CallEndpointController.java 0 → 100644 +345 −0 Original line number Diff line number Diff line /* * 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); 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); } @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 (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); } 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); } } } 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); } } } 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); } } } 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_BLUETOOTH) { for (BluetoothDevice device : state.getSupportedBluetoothDevices()) { CallEndpoint endpoint = findMatchingBluetoothEndpoint(device); if (endpoint == null) { endpoint = new CallEndpoint( device.getName() != null ? device.getName() : "", 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) { 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 src/com/android/server/telecom/CallEndpointControllerFactory.java 0 → 100644 +27 −0 Original line number Diff line number Diff line /* * 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; /** * Abstracts out creation of CallEndpointController for unit test purposes. */ public interface CallEndpointControllerFactory { CallEndpointController create(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager); } No newline at end of file src/com/android/server/telecom/CallsManager.java +46 −1 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.PersistableBundle; import android.os.Process; import android.os.ResultReceiver; import android.os.SystemClock; import android.os.SystemVibrator; import android.os.Trace; Loading @@ -77,6 +78,7 @@ import android.provider.CallLog.Calls; import android.provider.Settings; import android.sysprop.TelephonyProperties; import android.telecom.CallAudioState; import android.telecom.CallEndpoint; import android.telecom.CallScreeningService; import android.telecom.CallerInfo; import android.telecom.Conference; Loading Loading @@ -174,6 +176,9 @@ public class CallsManager extends Call.ListenerBase void onIncomingCallAnswered(Call call); void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage); void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState); void onCallEndpointChanged(CallEndpoint callEndpoint); void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints); void onMuteStateChanged(boolean isMuted); void onRingbackRequested(Call call, boolean ringback); void onIsConferencedChanged(Call call); void onIsVoipAudioModeChanged(Call call); Loading Loading @@ -376,6 +381,7 @@ public class CallsManager extends Call.ListenerBase private final Handler mHandler = new Handler(Looper.getMainLooper()); private final EmergencyCallHelper mEmergencyCallHelper; private final RoleManagerAdapter mRoleManagerAdapter; private final CallEndpointController mCallEndpointController; private final ConnectionServiceFocusManager.CallsManagerRequester mRequester = new ConnectionServiceFocusManager.CallsManagerRequester() { Loading Loading @@ -496,7 +502,8 @@ public class CallsManager extends Call.ListenerBase InCallControllerFactory inCallControllerFactory, CallDiagnosticServiceController callDiagnosticServiceController, RoleManagerAdapter roleManagerAdapter, ToastFactory toastFactory) { ToastFactory toastFactory, CallEndpointControllerFactory callEndpointControllerFactory) { mContext = context; mLock = lock; mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter; Loading Loading @@ -550,6 +557,7 @@ public class CallsManager extends Call.ListenerBase mInCallController = inCallControllerFactory.create(context, mLock, this, systemStateHelper, defaultDialerCache, mTimeoutsAdapter, emergencyCallHelper); mCallEndpointController = callEndpointControllerFactory.create(context, mLock, this); mCallDiagnosticServiceController = callDiagnosticServiceController; mCallDiagnosticServiceController.setInCallTonePlayerFactory(playerFactory); mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer, Loading Loading @@ -581,6 +589,7 @@ public class CallsManager extends Call.ListenerBase mListeners.add(statusBarNotifier); mListeners.add(mCallLogManager); mListeners.add(mInCallController); mListeners.add(mCallEndpointController); mListeners.add(mCallDiagnosticServiceController); mListeners.add(mCallAudioManager); mListeners.add(mCallRecordingTonePlayer); Loading Loading @@ -1193,6 +1202,10 @@ public class CallsManager extends Call.ListenerBase return mInCallController; } public CallEndpointController getCallEndpointController() { return mCallEndpointController; } EmergencyCallHelper getEmergencyCallHelper() { return mEmergencyCallHelper; } Loading Loading @@ -3010,6 +3023,14 @@ public class CallsManager extends Call.ListenerBase mCallAudioManager.setAudioRoute(route, bluetoothAddress); } /** * Called by the in-call UI to change the CallEndpoint */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) { mCallEndpointController.requestCallEndpointChange(endpoint, callback); } /** Called by the in-call UI to turn the proximity sensor on. */ void turnOnProximitySensor() { mProximitySensorManager.turnOn(); Loading Loading @@ -3068,6 +3089,30 @@ public class CallsManager extends Call.ListenerBase } } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void updateCallEndpoint(CallEndpoint callEndpoint) { Log.v(this, "updateCallEndpoint"); for (CallsManagerListener listener : mListeners) { listener.onCallEndpointChanged(callEndpoint); } } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void updateAvailableCallEndpoints(Set<CallEndpoint> availableCallEndpoints) { Log.v(this, "updateAvailableCallEndpoints"); for (CallsManagerListener listener : mListeners) { listener.onAvailableCallEndpointsChanged(availableCallEndpoints); } } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void updateMuteState(boolean isMuted) { Log.v(this, "updateMuteState"); for (CallsManagerListener listener : mListeners) { listener.onMuteStateChanged(isMuted); } } /** * Called when disconnect tone is started or stopped, including any InCallTone * after disconnected call. Loading Loading
res/values/strings.xml +12 −0 Original line number Diff line number Diff line Loading @@ -383,4 +383,16 @@ <string name="cancel">Cancel</string> <!-- Button label for generic back action [CHAR LIMIT=20] --> <string name="back">Back</string> <!-- The user-visible name of the earpiece type CallEndpoint --> <string name="callendpoint_name_earpiece">Earpiece</string> <!-- The user-visible name of the bluetooth type CallEndpoint --> <string name="callendpoint_name_bluetooth">Bluetooth</string> <!-- The user-visible name of the wired headset type CallEndpoint --> <string name="callendpoint_name_wiredheadset">Wired headset</string> <!-- The user-visible name of the speaker type CallEndpoint --> <string name="callendpoint_name_speaker">Speaker</string> <!-- The user-visible name of the streaming type CallEndpoint --> <string name="callendpoint_name_streaming">External</string> <!-- The user-visible name of the unknown new type CallEndpoint --> <string name="callendpoint_name_unknown">Unknown</string> </resources>
src/com/android/server/telecom/CallAudioManager.java +2 −1 Original line number Diff line number Diff line Loading @@ -413,7 +413,8 @@ public class CallAudioManager extends CallsManagerListenerBase { * @param bluetoothAddress the address of the desired bluetooth device, if route is * {@link CallAudioState#ROUTE_BLUETOOTH}. */ void setAudioRoute(int route, String bluetoothAddress) { @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) public void setAudioRoute(int route, String bluetoothAddress) { Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route)); switch (route) { case CallAudioState.ROUTE_BLUETOOTH: Loading
src/com/android/server/telecom/CallEndpointController.java 0 → 100644 +345 −0 Original line number Diff line number Diff line /* * 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); 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); } @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 (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); } 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); } } } 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); } } } 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); } } } 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_BLUETOOTH) { for (BluetoothDevice device : state.getSupportedBluetoothDevices()) { CallEndpoint endpoint = findMatchingBluetoothEndpoint(device); if (endpoint == null) { endpoint = new CallEndpoint( device.getName() != null ? device.getName() : "", 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) { 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
src/com/android/server/telecom/CallEndpointControllerFactory.java 0 → 100644 +27 −0 Original line number Diff line number Diff line /* * 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; /** * Abstracts out creation of CallEndpointController for unit test purposes. */ public interface CallEndpointControllerFactory { CallEndpointController create(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager); } No newline at end of file
src/com/android/server/telecom/CallsManager.java +46 −1 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.PersistableBundle; import android.os.Process; import android.os.ResultReceiver; import android.os.SystemClock; import android.os.SystemVibrator; import android.os.Trace; Loading @@ -77,6 +78,7 @@ import android.provider.CallLog.Calls; import android.provider.Settings; import android.sysprop.TelephonyProperties; import android.telecom.CallAudioState; import android.telecom.CallEndpoint; import android.telecom.CallScreeningService; import android.telecom.CallerInfo; import android.telecom.Conference; Loading Loading @@ -174,6 +176,9 @@ public class CallsManager extends Call.ListenerBase void onIncomingCallAnswered(Call call); void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage); void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState); void onCallEndpointChanged(CallEndpoint callEndpoint); void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints); void onMuteStateChanged(boolean isMuted); void onRingbackRequested(Call call, boolean ringback); void onIsConferencedChanged(Call call); void onIsVoipAudioModeChanged(Call call); Loading Loading @@ -376,6 +381,7 @@ public class CallsManager extends Call.ListenerBase private final Handler mHandler = new Handler(Looper.getMainLooper()); private final EmergencyCallHelper mEmergencyCallHelper; private final RoleManagerAdapter mRoleManagerAdapter; private final CallEndpointController mCallEndpointController; private final ConnectionServiceFocusManager.CallsManagerRequester mRequester = new ConnectionServiceFocusManager.CallsManagerRequester() { Loading Loading @@ -496,7 +502,8 @@ public class CallsManager extends Call.ListenerBase InCallControllerFactory inCallControllerFactory, CallDiagnosticServiceController callDiagnosticServiceController, RoleManagerAdapter roleManagerAdapter, ToastFactory toastFactory) { ToastFactory toastFactory, CallEndpointControllerFactory callEndpointControllerFactory) { mContext = context; mLock = lock; mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter; Loading Loading @@ -550,6 +557,7 @@ public class CallsManager extends Call.ListenerBase mInCallController = inCallControllerFactory.create(context, mLock, this, systemStateHelper, defaultDialerCache, mTimeoutsAdapter, emergencyCallHelper); mCallEndpointController = callEndpointControllerFactory.create(context, mLock, this); mCallDiagnosticServiceController = callDiagnosticServiceController; mCallDiagnosticServiceController.setInCallTonePlayerFactory(playerFactory); mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer, Loading Loading @@ -581,6 +589,7 @@ public class CallsManager extends Call.ListenerBase mListeners.add(statusBarNotifier); mListeners.add(mCallLogManager); mListeners.add(mInCallController); mListeners.add(mCallEndpointController); mListeners.add(mCallDiagnosticServiceController); mListeners.add(mCallAudioManager); mListeners.add(mCallRecordingTonePlayer); Loading Loading @@ -1193,6 +1202,10 @@ public class CallsManager extends Call.ListenerBase return mInCallController; } public CallEndpointController getCallEndpointController() { return mCallEndpointController; } EmergencyCallHelper getEmergencyCallHelper() { return mEmergencyCallHelper; } Loading Loading @@ -3010,6 +3023,14 @@ public class CallsManager extends Call.ListenerBase mCallAudioManager.setAudioRoute(route, bluetoothAddress); } /** * Called by the in-call UI to change the CallEndpoint */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) { mCallEndpointController.requestCallEndpointChange(endpoint, callback); } /** Called by the in-call UI to turn the proximity sensor on. */ void turnOnProximitySensor() { mProximitySensorManager.turnOn(); Loading Loading @@ -3068,6 +3089,30 @@ public class CallsManager extends Call.ListenerBase } } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void updateCallEndpoint(CallEndpoint callEndpoint) { Log.v(this, "updateCallEndpoint"); for (CallsManagerListener listener : mListeners) { listener.onCallEndpointChanged(callEndpoint); } } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void updateAvailableCallEndpoints(Set<CallEndpoint> availableCallEndpoints) { Log.v(this, "updateAvailableCallEndpoints"); for (CallsManagerListener listener : mListeners) { listener.onAvailableCallEndpointsChanged(availableCallEndpoints); } } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void updateMuteState(boolean isMuted) { Log.v(this, "updateMuteState"); for (CallsManagerListener listener : mListeners) { listener.onMuteStateChanged(isMuted); } } /** * Called when disconnect tone is started or stopped, including any InCallTone * after disconnected call. Loading