| /* |
| * Copyright (C) 2023 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 static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES; |
| import static com.android.server.telecom.AudioRoute.DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE; |
| import static com.android.server.telecom.AudioRoute.TYPE_INVALID; |
| import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER; |
| |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothLeAudio; |
| import android.bluetooth.BluetoothProfile; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.media.AudioAttributes; |
| import android.media.AudioDeviceAttributes; |
| import android.media.AudioDeviceCallback; |
| import android.media.AudioDeviceInfo; |
| import android.media.AudioManager; |
| import android.media.IAudioService; |
| import android.media.audiopolicy.AudioProductStrategy; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.telecom.CallAudioState; |
| import android.telecom.Log; |
| import android.telecom.Logging.Session; |
| import android.telecom.VideoProfile; |
| import android.util.ArrayMap; |
| import android.util.Pair; |
| |
| import androidx.annotation.NonNull; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.os.SomeArgs; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.server.telecom.bluetooth.BluetoothRouteManager; |
| import com.android.server.telecom.flags.FeatureFlags; |
| import com.android.server.telecom.metrics.ErrorStats; |
| import com.android.server.telecom.metrics.TelecomMetricsController; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.UUID; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| |
| public class CallAudioRouteController implements CallAudioRouteAdapter { |
| public static class Factory { |
| public CallAudioRouteController create( |
| Context context, CallsManager callsManager, |
| CallAudioManager.AudioServiceFactory audioServiceFactory, |
| AudioRoute.Factory audioRouteFactory, WiredHeadsetManager wiredHeadsetManager, |
| BluetoothRouteManager bluetoothRouteManager, StatusBarNotifier notifier, |
| FeatureFlags featureFlags, TelecomMetricsController metricsController, |
| AnomalyReporterAdapter anomalyReporterAdapter) { |
| return new CallAudioRouteController(context, |
| callsManager, |
| audioServiceFactory, |
| audioRouteFactory, |
| wiredHeadsetManager, |
| bluetoothRouteManager, |
| notifier, |
| featureFlags, |
| metricsController, |
| anomalyReporterAdapter); |
| } |
| } |
| private static final AudioRoute DUMMY_ROUTE = new AudioRoute(TYPE_INVALID, null, null); |
| private static final UUID AUDIO_ROUTING_EXTERNAL_CHANGE_UUID = |
| UUID.fromString("d9b38771-ff36-417b-8723-2363a870c702"); |
| private static final String AUDIO_ROUTING_EXTERNAL_CHANGE_MSG = |
| "Call audio routing was changed externally by another app via the AudioManager APIs." |
| + "This should only be handled by Telecom."; |
| private static final Map<Integer, Integer> ROUTE_MAP; |
| static { |
| ROUTE_MAP = new ArrayMap<>(); |
| ROUTE_MAP.put(TYPE_INVALID, 0); |
| ROUTE_MAP.put(AudioRoute.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE); |
| ROUTE_MAP.put(AudioRoute.TYPE_WIRED, CallAudioState.ROUTE_WIRED_HEADSET); |
| ROUTE_MAP.put(AudioRoute.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER); |
| ROUTE_MAP.put(AudioRoute.TYPE_DOCK, CallAudioState.ROUTE_SPEAKER); |
| ROUTE_MAP.put(AudioRoute.TYPE_BUS, CallAudioState.ROUTE_SPEAKER); |
| ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_SCO, CallAudioState.ROUTE_BLUETOOTH); |
| ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_HA, CallAudioState.ROUTE_BLUETOOTH); |
| ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_LE, CallAudioState.ROUTE_BLUETOOTH); |
| ROUTE_MAP.put(AudioRoute.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING); |
| } |
| |
| /** Valid values for the first argument for SWITCH_BASELINE_ROUTE */ |
| public static final int INCLUDE_BLUETOOTH_IN_BASELINE = 1; |
| |
| private final CallsManager mCallsManager; |
| private final Context mContext; |
| private AudioManager mAudioManager; |
| private CallAudioManager mCallAudioManager; |
| private final BluetoothRouteManager mBluetoothRouteManager; |
| private final CallAudioManager.AudioServiceFactory mAudioServiceFactory; |
| private final Handler mHandler; |
| private final WiredHeadsetManager mWiredHeadsetManager; |
| private Set<AudioRoute> mAvailableRoutes; |
| private Set<AudioRoute> mCallSupportedRoutes; |
| private AudioRoute mCurrentRoute; |
| private AudioRoute mEarpieceWiredRoute; |
| private AudioRoute mSpeakerDockRoute; |
| private AudioRoute mStreamingRoute; |
| private Set<AudioRoute> mStreamingRoutes; |
| private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes; |
| private Pair<Integer, String> mActiveBluetoothDevice; |
| private Map<Integer, String> mActiveDeviceCache; |
| private String mBluetoothAddressForRinging; |
| private Map<Integer, AudioRoute> mTypeRoutes; |
| private PendingAudioRoute mPendingAudioRoute; |
| private AudioRoute.Factory mAudioRouteFactory; |
| private StatusBarNotifier mStatusBarNotifier; |
| private AudioManager.OnCommunicationDeviceChangedListener mCommunicationDeviceListener; |
| private ExecutorService mAudioManagerListenerExecutor; |
| private AudioManager.OnPreferredDevicesForStrategyChangedListener mPreferredDeviceListener; |
| private AudioRoute mPreferredDeviceRoute; |
| private FeatureFlags mFeatureFlags; |
| private int mFocusType; |
| private int mCallSupportedRouteMask = -1; |
| private BluetoothDevice mScoAudioConnectedDevice; |
| private boolean mAvailableRoutesUpdated; |
| private boolean mUsePreferredDeviceStrategy; |
| private AudioDeviceInfo mCurrentCommunicationDevice; |
| private final Object mLock = new Object(); |
| private final TelecomSystem.SyncRoot mTelecomLock; |
| private CountDownLatch mAudioOperationsCompleteLatch; |
| private CountDownLatch mAudioActiveCompleteLatch; |
| private AnomalyReporterAdapter mAnomalyReporterAdapter; |
| |
| /** Receiver for added/removed device outputs that are reported by the audio fwk */ |
| public class AudioRoutesCallback extends AudioDeviceCallback { |
| @Override |
| public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { |
| Log.startSession("ARC.oADA"); |
| try { |
| updateAudioRoutes(addedDevices, true); |
| } finally { |
| Log.endSession(); |
| } |
| } |
| |
| @Override |
| public void onAudioDevicesRemoved(AudioDeviceInfo[] devices) { |
| Log.startSession("ARC.oADR"); |
| try { |
| updateAudioRoutes(devices, false); |
| } finally { |
| Log.endSession(); |
| } |
| } |
| |
| private void updateAudioRoutes(AudioDeviceInfo[] devices, boolean addDevices) { |
| Log.i(this, "updateAudioRoutes: add devices? %b", addDevices); |
| for (AudioDeviceInfo deviceInfo: devices) { |
| int audioRouteType = getAudioType(deviceInfo); |
| Log.i(this, "updateAudioRoutes: audioDeviceInfo: %s, audioRouteType: %d", |
| deviceInfo, audioRouteType); |
| // We should really only worry about handling earpiece and speaker. Bluetooth and |
| // wired headset routes are already dynamically updated. This logic can be updated |
| // once we support call audio route centralization. |
| if (audioRouteType == TYPE_INVALID || audioRouteType == AudioRoute.TYPE_WIRED |
| || BT_AUDIO_ROUTE_TYPES.contains(audioRouteType)) { |
| Log.i(this, "updateAudioRoutes: skipping route."); |
| continue; |
| } |
| if (addDevices) { |
| switch(audioRouteType) { |
| case AudioRoute.TYPE_SPEAKER: |
| createSpeakerRoute(); |
| break; |
| case AudioRoute.TYPE_EARPIECE: |
| createEarpieceRoute(); |
| break; |
| default: |
| break; |
| } |
| } else { |
| AudioRoute route = mTypeRoutes.remove(audioRouteType); |
| updateAvailableRoutes(route, false); |
| } |
| } |
| } |
| } |
| |
| private final BroadcastReceiver mMuteChangeReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| Log.startSession("CARC.mCR"); |
| try { |
| if (AudioManager.ACTION_MICROPHONE_MUTE_CHANGED.equals(intent.getAction())) { |
| if (mCallsManager.isInEmergencyCall()) { |
| Log.i(this, "Mute was externally changed when there's an emergency call. " |
| + "Forcing mute back off."); |
| sendMessageWithSessionInfo(MUTE_OFF); |
| } else { |
| sendMessageWithSessionInfo(MUTE_EXTERNALLY_CHANGED); |
| } |
| } else if (AudioManager.STREAM_MUTE_CHANGED_ACTION.equals(intent.getAction())) { |
| int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); |
| boolean isStreamMuted = intent.getBooleanExtra( |
| AudioManager.EXTRA_STREAM_VOLUME_MUTED, false); |
| |
| if (streamType == AudioManager.STREAM_RING && !isStreamMuted |
| && mCallAudioManager != null) { |
| Log.i(this, "Ring stream was un-muted."); |
| mCallAudioManager.onRingerModeChange(); |
| } |
| } else { |
| Log.w(this, "Received non-mute-change intent"); |
| } |
| } finally { |
| Log.endSession(); |
| } |
| } |
| }; |
| private CallAudioState mCallAudioState; |
| private boolean mIsMute; |
| private boolean mIsPending; |
| private boolean mIsActive; |
| private boolean mWasOnSpeaker; |
| private AudioRoutesCallback mAudioRoutesCallback; |
| private final TelecomMetricsController mMetricsController; |
| |
| public CallAudioRouteController( |
| Context context, CallsManager callsManager, |
| CallAudioManager.AudioServiceFactory audioServiceFactory, |
| AudioRoute.Factory audioRouteFactory, WiredHeadsetManager wiredHeadsetManager, |
| BluetoothRouteManager bluetoothRouteManager, StatusBarNotifier statusBarNotifier, |
| FeatureFlags featureFlags, TelecomMetricsController metricsController, |
| AnomalyReporterAdapter anomalyReporterAdapter) { |
| mContext = context; |
| mCallsManager = callsManager; |
| mAudioManager = context.getSystemService(AudioManager.class); |
| mAudioServiceFactory = audioServiceFactory; |
| mAudioRouteFactory = audioRouteFactory; |
| mWiredHeadsetManager = wiredHeadsetManager; |
| mIsMute = false; |
| mBluetoothRouteManager = bluetoothRouteManager; |
| mStatusBarNotifier = statusBarNotifier; |
| mFeatureFlags = featureFlags; |
| mMetricsController = metricsController; |
| mAnomalyReporterAdapter = anomalyReporterAdapter; |
| mFocusType = NO_FOCUS; |
| mScoAudioConnectedDevice = null; |
| mUsePreferredDeviceStrategy = true; |
| mWasOnSpeaker = false; |
| setCurrentCommunicationDevice(null); |
| mPreferredDeviceRoute = DUMMY_ROUTE; |
| |
| mTelecomLock = callsManager.getLock(); |
| HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName()); |
| handlerThread.start(); |
| |
| IntentFilter micMuteChangedFilter = new IntentFilter( |
| AudioManager.ACTION_MICROPHONE_MUTE_CHANGED); |
| micMuteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| context.registerReceiver(mMuteChangeReceiver, micMuteChangedFilter); |
| |
| IntentFilter muteChangedFilter = new IntentFilter(AudioManager.STREAM_MUTE_CHANGED_ACTION); |
| muteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| context.registerReceiver(mMuteChangeReceiver, muteChangedFilter); |
| |
| // Register AudioManager#onCommunicationDeviceChangedListener listener to receive updates |
| // to communication device (via AudioManager#setCommunicationDevice). This is a replacement |
| // to using broadcasts in the hopes of improving performance. |
| mAudioManagerListenerExecutor = Executors.newSingleThreadExecutor(); |
| mCommunicationDeviceListener = new AudioManager.OnCommunicationDeviceChangedListener() { |
| @Override |
| public void onCommunicationDeviceChanged(AudioDeviceInfo device) { |
| try { |
| Log.startSession("CARC.oCDC"); |
| @AudioRoute.AudioRouteType int audioType = getAudioType(device); |
| setCurrentCommunicationDevice(device); |
| Log.i(this, "onCommunicationDeviceChanged: device (%s), audioType (%d)", |
| device, audioType); |
| if (audioType == TYPE_SPEAKER) { |
| if (mCurrentRoute.getType() != TYPE_SPEAKER) { |
| sendMessageWithSessionInfo(SPEAKER_ON); |
| } |
| } else { |
| sendMessageWithSessionInfo(SPEAKER_OFF); |
| } |
| } finally { |
| Log.endSession(); |
| } |
| } |
| }; |
| |
| // Register the AudioManager. OnPreferredDevicesForStrategyChangedListener listener to |
| // receive updates for the communication device. This is a replacement to directly querying |
| // the preferred device via AudioManager#getPreferredDeviceForStrategy, which was known |
| // to hold up the invoking thread. |
| mPreferredDeviceListener = (strategy, devices) -> { |
| try { |
| Log.startSession("CARC.oPDFSCL"); |
| final AudioAttributes attr = new AudioAttributes.Builder() |
| .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) |
| .build(); |
| if (!devices.isEmpty() && strategy.supportsAudioAttributes(attr)) { |
| AudioRoute audioRoute = getPreferredDeviceAudioRoute(devices.getFirst()); |
| Log.i(this, "OnPreferredDevicesForStrategyChangedListener: preferred device " |
| + "was updated to %s", audioRoute); |
| // Get the first device listed |
| setPreferredDeviceRoute(audioRoute); |
| } |
| } finally { |
| Log.endSession(); |
| } |
| }; |
| |
| // Create handler |
| mHandler = new Handler(handlerThread.getLooper()) { |
| @Override |
| public void handleMessage(@NonNull Message msg) { |
| synchronized (this) { |
| preHandleMessage(msg); |
| String address; |
| BluetoothDevice bluetoothDevice; |
| int focus; |
| int handleEndTone; |
| @AudioRoute.AudioRouteType int type; |
| switch (msg.what) { |
| case CONNECT_WIRED_HEADSET: |
| handleWiredHeadsetConnected(); |
| break; |
| case DISCONNECT_WIRED_HEADSET: |
| handleWiredHeadsetDisconnected(); |
| break; |
| case CONNECT_DOCK: |
| handleDockConnected(); |
| break; |
| case DISCONNECT_DOCK: |
| handleDockDisconnected(); |
| break; |
| case BLUETOOTH_DEVICE_LIST_CHANGED: |
| break; |
| case BT_ACTIVE_DEVICE_PRESENT: |
| type = msg.arg1; |
| address = (String) ((SomeArgs) msg.obj).arg2; |
| handleBtActiveDevicePresent(type, address); |
| break; |
| case BT_ACTIVE_DEVICE_GONE: |
| type = msg.arg1; |
| handleBtActiveDeviceGone(type); |
| break; |
| case BT_DEVICE_ADDED: |
| type = msg.arg1; |
| bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2; |
| handleBtConnected(type, bluetoothDevice); |
| break; |
| case BT_DEVICE_REMOVED: |
| type = msg.arg1; |
| bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2; |
| handleBtDisconnected(type, bluetoothDevice); |
| break; |
| case SWITCH_EARPIECE: |
| case USER_SWITCH_EARPIECE: |
| handleSwitchEarpiece(msg.what == USER_SWITCH_EARPIECE); |
| break; |
| case SWITCH_BLUETOOTH: |
| case USER_SWITCH_BLUETOOTH: |
| address = (String) ((SomeArgs) msg.obj).arg2; |
| handleSwitchBluetooth(address, msg.what == USER_SWITCH_BLUETOOTH); |
| break; |
| case SWITCH_HEADSET: |
| case USER_SWITCH_HEADSET: |
| handleSwitchHeadset(msg.what == USER_SWITCH_HEADSET); |
| break; |
| case SWITCH_SPEAKER: |
| case USER_SWITCH_SPEAKER: |
| handleSwitchSpeaker(); |
| break; |
| case SWITCH_BASELINE_ROUTE: |
| address = (String) ((SomeArgs) msg.obj).arg2; |
| handleSwitchBaselineRoute(false, |
| msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE, address); |
| break; |
| case USER_SWITCH_BASELINE_ROUTE: |
| handleSwitchBaselineRoute(true, |
| msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE, null); |
| break; |
| case SPEAKER_ON: |
| handleSpeakerOn(); |
| break; |
| case SPEAKER_OFF: |
| handleSpeakerOff(); |
| break; |
| case STREAMING_FORCE_ENABLED: |
| handleStreamingEnabled(); |
| break; |
| case STREAMING_FORCE_DISABLED: |
| handleStreamingDisabled(); |
| break; |
| case BT_AUDIO_CONNECTED: |
| bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2; |
| handleBtAudioActive(bluetoothDevice); |
| break; |
| case BT_AUDIO_DISCONNECTED: |
| bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2; |
| handleBtAudioInactive(bluetoothDevice); |
| break; |
| case MUTE_ON: |
| handleMuteChanged(true); |
| break; |
| case MUTE_OFF: |
| handleMuteChanged(false); |
| break; |
| case MUTE_EXTERNALLY_CHANGED: |
| handleMuteChanged(mAudioManager.isMicrophoneMute()); |
| break; |
| case TOGGLE_MUTE: |
| handleMuteChanged(!mIsMute); |
| break; |
| case SWITCH_FOCUS: |
| focus = msg.arg1; |
| handleEndTone = (int) ((SomeArgs) msg.obj).arg2; |
| handleSwitchFocus(focus, handleEndTone); |
| break; |
| case EXIT_PENDING_ROUTE: |
| handleExitPendingRoute(); |
| break; |
| case UPDATE_SYSTEM_AUDIO_ROUTE: |
| // Based on the available routes for foreground call, adjust routing. |
| updateRouteForForeground(); |
| // Force update to notify all ICS/CS. |
| updateCallAudioState(new CallAudioState(mIsMute, |
| mCallAudioState.getRoute(), |
| mCallAudioState.getSupportedRouteMask(), |
| mCallAudioState.getActiveBluetoothDevice(), |
| mCallAudioState.getSupportedBluetoothDevices())); |
| default: |
| break; |
| } |
| postHandleMessage(msg); |
| } |
| } |
| }; |
| } |
| @Override |
| public void initialize() { |
| mAvailableRoutes = new HashSet<>(); |
| mCallSupportedRoutes = new HashSet<>(); |
| mBluetoothRoutes = Collections.synchronizedMap(new LinkedHashMap<>()); |
| mActiveDeviceCache = new HashMap<>(); |
| mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_SCO, null); |
| mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_HA, null); |
| mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_LE, null); |
| mActiveBluetoothDevice = null; |
| mTypeRoutes = new ArrayMap<>(); |
| mStreamingRoutes = new HashSet<>(); |
| mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager, mBluetoothRouteManager, |
| mFeatureFlags); |
| mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null); |
| mStreamingRoutes.add(mStreamingRoute); |
| |
| int supportMask = calculateSupportedRouteMaskInit(); |
| if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) { |
| int audioRouteType = AudioRoute.TYPE_SPEAKER; |
| createSpeakerRoute(); |
| } |
| |
| if ((supportMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) { |
| // Create wired headset routes |
| mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null, |
| mAudioManager); |
| if (mEarpieceWiredRoute == null) { |
| Log.w(this, "Can't find available audio device info for route TYPE_WIRED_HEADSET"); |
| } else { |
| mTypeRoutes.put(AudioRoute.TYPE_WIRED, mEarpieceWiredRoute); |
| updateAvailableRoutes(mEarpieceWiredRoute, true); |
| } |
| } else if ((supportMask & CallAudioState.ROUTE_EARPIECE) != 0) { |
| createEarpieceRoute(); |
| } |
| |
| // set current route |
| if (mEarpieceWiredRoute != null) { |
| mCurrentRoute = mEarpieceWiredRoute; |
| } else if (mSpeakerDockRoute != null) { |
| mCurrentRoute = mSpeakerDockRoute; |
| } else { |
| mCurrentRoute = DUMMY_ROUTE; |
| } |
| // Audio ops will only ever be completed if there's a call placed and it gains |
| // ACTIVE/RINGING focus, hence why the initial value is 0. |
| mAudioOperationsCompleteLatch = new CountDownLatch(0); |
| // This latch will be count down when ACTIVE/RINGING focus is gained. This is determined |
| // when the routing goes active. |
| mAudioActiveCompleteLatch = new CountDownLatch(1); |
| mIsActive = false; |
| mCallAudioState = new CallAudioState(mIsMute, ROUTE_MAP.get(mCurrentRoute.getType()), |
| supportMask, null, new HashSet<>()); |
| mAudioManager.addOnCommunicationDeviceChangedListener( |
| mAudioManagerListenerExecutor, mCommunicationDeviceListener); |
| mAudioManager.addOnPreferredDevicesForStrategyChangedListener(mAudioManagerListenerExecutor, |
| mPreferredDeviceListener); |
| mAudioRoutesCallback = new AudioRoutesCallback(); |
| mAudioManager.registerAudioDeviceCallback(mAudioRoutesCallback, mHandler); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message) { |
| sendMessageWithSessionInfo(message, 0, (String) null); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message, int arg) { |
| sendMessageWithSessionInfo(message, arg, (String) null); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message, int arg, String data) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = Log.createSubsession(); |
| args.arg2 = data; |
| sendMessage(message, arg, 0, args); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message, int arg, int data) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = Log.createSubsession(); |
| args.arg2 = data; |
| sendMessage(message, arg, 0, args); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfoAtFront(int message, int arg, int data) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = Log.createSubsession(); |
| args.arg2 = data; |
| mHandler.sendMessageAtFrontOfQueue(Message.obtain(mHandler, message, arg, 0, args)); |
| } |
| |
| @Override |
| public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = Log.createSubsession(); |
| args.arg2 = bluetoothDevice; |
| sendMessage(message, arg, 0, args); |
| } |
| |
| @Override |
| public void sendMessage(int message, Runnable r) { |
| r.run(); |
| } |
| |
| private void sendMessage(int what, int arg1, int arg2, Object obj) { |
| mHandler.sendMessage(Message.obtain(mHandler, what, arg1, arg2, obj)); |
| } |
| |
| @Override |
| public void setCallAudioManager(CallAudioManager callAudioManager) { |
| mCallAudioManager = callAudioManager; |
| } |
| |
| @Override |
| public CallAudioState getCurrentCallAudioState() { |
| return mCallAudioState; |
| } |
| |
| @Override |
| public boolean isHfpDeviceAvailable() { |
| return !mBluetoothRoutes.isEmpty(); |
| } |
| |
| @Override |
| public Handler getAdapterHandler() { |
| return mHandler; |
| } |
| |
| @Override |
| public PendingAudioRoute getPendingAudioRoute() { |
| return mPendingAudioRoute; |
| } |
| |
| @Override |
| public void dump(IndentingPrintWriter pw) { |
| } |
| |
| private void preHandleMessage(Message msg) { |
| if (msg.obj instanceof SomeArgs) { |
| Session session = (Session) ((SomeArgs) msg.obj).arg1; |
| String messageCodeName = MESSAGE_CODE_TO_NAME.get(msg.what, "unknown"); |
| Log.continueSession(session, "CARC.pM_" + messageCodeName); |
| Log.i(this, "Message received: %s=%d, arg1=%d", messageCodeName, msg.what, msg.arg1); |
| } |
| } |
| |
| private void postHandleMessage(Message msg) { |
| Log.endSession(); |
| if (msg.obj instanceof SomeArgs) { |
| ((SomeArgs) msg.obj).recycle(); |
| } |
| } |
| |
| public boolean isActive() { |
| return mIsActive; |
| } |
| |
| public boolean isPending() { |
| return mIsPending; |
| } |
| |
| private void routeTo(boolean isDestRouteActive, AudioRoute destRoute) { |
| if (destRoute == null || (!destRoute.equals(mStreamingRoute) |
| && !getCallSupportedRoutes().contains(destRoute))) { |
| Log.i(this, "Ignore routing to unavailable route: %s", destRoute); |
| if (mFeatureFlags.telecomMetricsSupport()) { |
| mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO, |
| ErrorStats.ERROR_AUDIO_ROUTE_UNAVAILABLE); |
| } |
| return; |
| } |
| // If another BT device connects during RINGING_FOCUS, in-band ringing will be disabled by |
| // default. In this case, we should adjust the active routing value so that we don't try |
| // to connect to the BT device as it will fail. |
| isDestRouteActive = maybeAdjustActiveRouting(destRoute, isDestRouteActive); |
| // It's possible that there are multiple HFP devices connected and if we receive SCO audio |
| // connected for the destination route's BT device, then we shouldn't disconnect SCO when |
| // clearing the communication device for the original route if it was also a HFP device. |
| // This does not apply to the route deactivation scenario. |
| boolean isScoDeviceAlreadyConnected = mScoAudioConnectedDevice != null && isDestRouteActive |
| && Objects.equals(mScoAudioConnectedDevice, mBluetoothRoutes.get(destRoute)); |
| if (mIsPending) { |
| if (destRoute.equals(mPendingAudioRoute.getDestRoute()) |
| && (mIsActive == isDestRouteActive)) { |
| return; |
| } |
| Log.i(this, "Override current pending route destination from %s(active=%b) to " |
| + "%s(active=%b)", |
| mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, isDestRouteActive); |
| // override pending route while keep waiting for still pending messages for the |
| // previous pending route |
| mPendingAudioRoute.setOrigRoute(mIsActive /* origin */, |
| mPendingAudioRoute.getDestRoute(), isDestRouteActive /* dest */, |
| isScoDeviceAlreadyConnected); |
| } else { |
| if (mCurrentRoute.equals(destRoute) && (mIsActive == isDestRouteActive)) { |
| return; |
| } |
| Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute, |
| mIsActive, destRoute, isDestRouteActive); |
| // route to pending route |
| if (getCallSupportedRoutes().contains(mCurrentRoute)) { |
| mPendingAudioRoute.setOrigRoute(mIsActive /* origin */, mCurrentRoute, |
| isDestRouteActive /* dest */, isScoDeviceAlreadyConnected); |
| } else { |
| // Avoid waiting for pending messages for an unavailable route |
| mPendingAudioRoute.setOrigRoute(mIsActive /* origin */, DUMMY_ROUTE, |
| isDestRouteActive /* dest */, isScoDeviceAlreadyConnected); |
| } |
| mIsPending = true; |
| } |
| mPendingAudioRoute.setDestRoute(isDestRouteActive, destRoute, |
| mBluetoothRoutes.get(destRoute), isScoDeviceAlreadyConnected); |
| mIsActive = isDestRouteActive; |
| mPendingAudioRoute.evaluatePendingState(); |
| if (mFeatureFlags.telecomMetricsSupport()) { |
| mMetricsController.getAudioRouteStats().onRouteEnter(mPendingAudioRoute); |
| } |
| } |
| |
| private void handleWiredHeadsetConnected() { |
| AudioRoute wiredHeadsetRoute = null; |
| try { |
| wiredHeadsetRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null, |
| mAudioManager); |
| } catch (IllegalArgumentException e) { |
| if (mFeatureFlags.telecomMetricsSupport()) { |
| mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO, |
| ErrorStats.ERROR_EXTERNAL_EXCEPTION); |
| } |
| Log.e(this, e, "Can't find available audio device info for route type:" |
| + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED)); |
| } |
| |
| if (wiredHeadsetRoute != null) { |
| updateAvailableRoutes(wiredHeadsetRoute, true); |
| updateAvailableRoutes(mEarpieceWiredRoute, false); |
| mTypeRoutes.put(AudioRoute.TYPE_WIRED, wiredHeadsetRoute); |
| mEarpieceWiredRoute = wiredHeadsetRoute; |
| routeTo(mIsActive, wiredHeadsetRoute); |
| onAvailableRoutesChanged(); |
| } |
| } |
| |
| public void handleWiredHeadsetDisconnected() { |
| // Update audio route states |
| AudioRoute wiredHeadsetRoute = mTypeRoutes.remove(AudioRoute.TYPE_WIRED); |
| if (wiredHeadsetRoute != null) { |
| updateAvailableRoutes(wiredHeadsetRoute, false); |
| mEarpieceWiredRoute = null; |
| } |
| AudioRoute earpieceRoute = null; |
| try { |
| earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE) == null |
| ? mAudioRouteFactory.create(AudioRoute.TYPE_EARPIECE, null, |
| mAudioManager) |
| : mTypeRoutes.get(AudioRoute.TYPE_EARPIECE); |
| } catch (IllegalArgumentException e) { |
| if (mFeatureFlags.telecomMetricsSupport()) { |
| mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO, |
| ErrorStats.ERROR_EXTERNAL_EXCEPTION); |
| } |
| Log.e(this, e, "Can't find available audio device info for route type:" |
| + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_EARPIECE)); |
| } |
| if (earpieceRoute != null) { |
| updateAvailableRoutes(earpieceRoute, true); |
| mEarpieceWiredRoute = earpieceRoute; |
| // In the case that the route was never created, ensure that we update the map. |
| mTypeRoutes.putIfAbsent(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute); |
| } |
| onAvailableRoutesChanged(); |
| |
| // Route to expected state |
| if (mCurrentRoute.equals(wiredHeadsetRoute)) { |
| // Preserve speaker routing if it was the last audio routing path when the wired headset |
| // disconnects. Ignore this special cased routing when the route isn't active |
| // (in other words, when we're not in a call). |
| AudioRoute route = mWasOnSpeaker && mIsActive && mSpeakerDockRoute != null |
| && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER |
| ? mSpeakerDockRoute : getBaseRoute(true, null); |
| routeTo(mIsActive, route); |
| } |
| } |
| |
| private void handleDockConnected() { |
| AudioRoute dockRoute = null; |
| try { |
| dockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_DOCK, null, mAudioManager); |
| } catch (IllegalArgumentException e) { |
| if (mFeatureFlags.telecomMetricsSupport()) { |
| mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO, |
| ErrorStats.ERROR_EXTERNAL_EXCEPTION); |
| } |
| Log.e(this, e, "Can't find available audio device info for route type:" |
| + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED)); |
| } |
| |
| if (dockRoute != null) { |
| updateAvailableRoutes(dockRoute, true); |
| updateAvailableRoutes(mSpeakerDockRoute, false); |
| mTypeRoutes.put(AudioRoute.TYPE_DOCK, dockRoute); |
| mSpeakerDockRoute = dockRoute; |
| routeTo(mIsActive, dockRoute); |
| onAvailableRoutesChanged(); |
| } |
| } |
| |
| public void handleDockDisconnected() { |
| // Update audio route states |
| AudioRoute dockRoute = mTypeRoutes.get(AudioRoute.TYPE_DOCK); |
| if (dockRoute != null) { |
| updateAvailableRoutes(dockRoute, false); |
| mSpeakerDockRoute = null; |
| } |
| AudioRoute speakerRoute = mTypeRoutes.get(AudioRoute.TYPE_SPEAKER); |
| if (speakerRoute != null) { |
| updateAvailableRoutes(speakerRoute, true); |
| mSpeakerDockRoute = speakerRoute; |
| } |
| onAvailableRoutesChanged(); |
| |
| // Route to expected state |
| if (mCurrentRoute.equals(dockRoute)) { |
| routeTo(mIsActive, getBaseRoute(true, null)); |
| } |
| } |
| |
| private void handleStreamingEnabled() { |
| if (!mCurrentRoute.equals(mStreamingRoute)) { |
| routeTo(mIsActive, mStreamingRoute); |
| } else { |
| Log.i(this, "ignore enable streaming, already in streaming"); |
| } |
| } |
| |
| private void handleStreamingDisabled() { |
| if (mCurrentRoute.equals(mStreamingRoute)) { |
| mCurrentRoute = DUMMY_ROUTE; |
| onAvailableRoutesChanged(); |
| routeTo(mIsActive, getBaseRoute(true, null)); |
| } else { |
| Log.i(this, "ignore disable streaming, not in streaming"); |
| } |
| } |
| |
| /** |
| * Handles the case when SCO audio is connected for the BT headset. This follows shortly after |
| * the BT device has been established as an active device (BT_ACTIVE_DEVICE_PRESENT) and doesn't |
| * apply to other BT device types. In this case, the pending audio route will process the |
| * BT_AUDIO_CONNECTED message that will trigger routing to the pending destination audio route; |
| * otherwise, routing will be ignored if there aren't pending routes to be processed. |
| * |
| * Message being handled: BT_AUDIO_CONNECTED |
| */ |
| private void handleBtAudioActive(BluetoothDevice bluetoothDevice) { |
| if (mIsPending && bluetoothDevice != null) { |
| Log.i(this, "handleBtAudioActive: is pending path"); |
| // Ensure we aren't keeping track of pending speaker off and SCO audio disconnected |
| // messages for this device if BT stack indicates that SCO audio is connected. |
| mPendingAudioRoute.clearPendingMessage( |
| new Pair<>(BT_AUDIO_DISCONNECTED, bluetoothDevice.getAddress())); |
| mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_OFF, null)); |
| // Maybe turn off speaker from notification bar. This will be a no-op if the enabled |
| // status is already off. |
| mStatusBarNotifier.notifySpeakerphone(false); |
| if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(), |
| bluetoothDevice.getAddress())) { |
| mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_CONNECTED, |
| bluetoothDevice.getAddress()), null); |
| } |
| } |
| } |
| |
| /** |
| * Handles the case when SCO audio is disconnected for the BT headset. In this case, the pending |
| * audio route will process the BT_AUDIO_DISCONNECTED message which will trigger routing to the |
| * pending destination audio route; otherwise, routing will be ignored if there aren't any |
| * pending routes to be processed. |
| * |
| * Message being handled: BT_AUDIO_DISCONNECTED |
| */ |
| private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) { |
| if (mIsPending && bluetoothDevice != null) { |
| Log.i(this, "handleBtAudioInactive: is pending path"); |
| // Ensure we aren't keeping track of pending s SCO audio connected messages for this |
| // device if the BT stack has indicated that SCO audio has disconnected. |
| mPendingAudioRoute.clearPendingMessage( |
| new Pair<>(BT_AUDIO_CONNECTED, bluetoothDevice.getAddress())); |
| if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(), |
| bluetoothDevice.getAddress())) { |
| mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED, |
| bluetoothDevice.getAddress()), null); |
| } |
| } |
| } |
| |
| /** |
| * This particular routing occurs when the BT device is trying to establish itself as a |
| * connected device (refer to BluetoothStateReceiver#handleConnectionStateChanged). The device |
| * is included as an available route and cached into the current BT routes. |
| * |
| * Message being handled: BT_DEVICE_ADDED |
| */ |
| private void handleBtConnected(@AudioRoute.AudioRouteType int type, |
| BluetoothDevice bluetoothDevice) { |
| if (containsHearingAidPair(type, bluetoothDevice)) { |
| return; |
| } |
| |
| AudioRoute bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(), |
| mAudioManager); |
| if (bluetoothRoute == null) { |
| Log.w(this, "Can't find available audio device info for route type:" |
| + AudioRoute.DEVICE_TYPE_STRINGS.get(type)); |
| } else { |
| Log.i(this, "bluetooth route added: " + bluetoothRoute); |
| updateAvailableRoutes(bluetoothRoute, true); |
| mBluetoothRoutes.put(bluetoothRoute, bluetoothDevice); |
| onAvailableRoutesChanged(); |
| } |
| } |
| |
| /** |
| * Handles the case when the BT device is in a disconnecting/disconnected state. In this case, |
| * the audio route for the specified device is removed from the available BT routes and the |
| * audio is routed to an available route if the current route is pointing to the device which |
| * got disconnected. |
| * |
| * Message being handled: BT_DEVICE_REMOVED |
| */ |
| private void handleBtDisconnected(@AudioRoute.AudioRouteType int type, |
| BluetoothDevice bluetoothDevice) { |
| // Clean up unavailable routes |
| AudioRoute bluetoothRoute = getBluetoothRoute(type, bluetoothDevice.getAddress()); |
| if (bluetoothRoute != null) { |
| Log.i(this, "bluetooth route removed: " + bluetoothRoute); |
| mBluetoothRoutes.remove(bluetoothRoute); |
| updateAvailableRoutes(bluetoothRoute, false); |
| onAvailableRoutesChanged(); |
| } |
| |
| // Fallback to an available route |
| if (Objects.equals(mCurrentRoute, bluetoothRoute)) { |
| routeTo(mIsActive, getBaseRoute(true, null)); |
| } |
| } |
| |
| /** |
| * This particular routing occurs when the specified bluetooth device is marked as the active |
| * device (refer to BluetoothStateReceiver#handleActiveDeviceChanged). This takes care of |
| * moving the call audio route to the bluetooth route. |
| * |
| * Message being handled: BT_ACTIVE_DEVICE_PRESENT |
| */ |
| private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type, |
| String deviceAddress) { |
| AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress); |
| boolean isBtDeviceCurrentActive = Objects.equals(bluetoothRoute, |
| getArbitraryBluetoothDevice()); |
| if (bluetoothRoute != null && isBtDeviceCurrentActive) { |
| Log.i(this, "request to route to bluetooth route: %s (active=%b)", bluetoothRoute, |
| mIsActive); |
| routeTo(mIsActive, bluetoothRoute); |
| } else { |
| Log.i(this, "request to route to unavailable bluetooth route or the route isn't the " |
| + "currently active device - type (%s), address (%s)", type, deviceAddress); |
| } |
| } |
| |
| /** |
| * Handles routing for when the active BT device is removed for a given audio route type. In |
| * this case, the audio is routed to another available route if the current route hasn't been |
| * adjusted yet or there is a pending destination route associated with the device type that |
| * went inactive. Note that BT_DEVICE_REMOVED will be processed first in this case, which will |
| * handle removing the BT route for the device that went inactive as well as falling back to |
| * an available route. |
| * |
| * Message being handled: BT_ACTIVE_DEVICE_GONE |
| */ |
| private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) { |
| // Determine what the active device for the BT audio type was so that we can exclude this |
| // device from being used when calculating the base route. |
| String previouslyActiveDeviceAddress = mActiveDeviceCache.get(type); |
| // It's possible that the dest route hasn't been set yet when the controller is first |
| // initialized. |
| boolean pendingRouteNeedsUpdate = mPendingAudioRoute.getDestRoute() != null |
| && mPendingAudioRoute.getDestRoute().getType() == type; |
| boolean currentRouteNeedsUpdate = mCurrentRoute.getType() == type; |
| if (pendingRouteNeedsUpdate) { |
| pendingRouteNeedsUpdate = mPendingAudioRoute.getDestRoute().getBluetoothAddress() |
| .equals(previouslyActiveDeviceAddress); |
| } |
| if (currentRouteNeedsUpdate) { |
| currentRouteNeedsUpdate = mCurrentRoute.getBluetoothAddress() |
| .equals(previouslyActiveDeviceAddress); |
| } |
| if ((mIsPending && pendingRouteNeedsUpdate) || (!mIsPending && currentRouteNeedsUpdate)) { |
| maybeDisableWasOnSpeaker(true); |
| // Fallback to an available route excluding the previously active device. |
| routeTo(mIsActive, getBaseRoute(true, previouslyActiveDeviceAddress)); |
| } |
| } |
| |
| private void handleMuteChanged(boolean mute) { |
| mIsMute = mute; |
| if (mIsMute != mAudioManager.isMicrophoneMute() && mIsActive) { |
| IAudioService audioService = mAudioServiceFactory.getAudioService(); |
| Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute, |
| audioService == null); |
| if (audioService != null) { |
| try { |
| audioService.setMicrophoneMute(mute, mContext.getOpPackageName(), |
| mCallsManager.getCurrentUserHandle().getIdentifier(), |
| mContext.getAttributionTag()); |
| } catch (RemoteException e) { |
| if (mFeatureFlags.telecomMetricsSupport()) { |
| mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO, |
| ErrorStats.ERROR_EXTERNAL_EXCEPTION); |
| } |
| Log.e(this, e, "Remote exception while toggling mute."); |
| return; |
| } |
| } |
| } |
| onMuteStateChanged(mIsMute); |
| } |
| |
| private void handleSwitchFocus(int focus, int handleEndTone) { |
| Log.i(this, "handleSwitchFocus: focus (%s)", focus); |
| mFocusType = focus; |
| switch (focus) { |
| case NO_FOCUS -> { |
| mWasOnSpeaker = false; |
| // Notify the CallAudioModeStateMachine that audio operations are complete so |
| // that we can relinquish audio focus. |
| mCallAudioManager.notifyAudioOperationsComplete(); |
| // Reset mute state after call ends. This should remain unaffected if audio routing |
| // never went active. |
| handleMuteChanged(false); |
| // Ensure we reset call audio state at the end of the call (i.e. if we're on |
| // speaker, route back to earpiece). If we're on BT, remain on BT if it's still |
| // connected. |
| AudioRoute route = calculateBaselineRoute(false, true, null); |
| routeTo(false, route); |
| // Clear pending messages |
| mPendingAudioRoute.clearPendingMessages(); |
| clearRingingBluetoothAddress(); |
| mUsePreferredDeviceStrategy = true; |
| } |
| case ACTIVE_FOCUS -> { |
| // Route to active baseline route (we may need to change audio route in the case |
| // when a video call is put on hold). Ignore route changes if we're handling playing |
| // the end tone. Otherwise, it's possible that we'll override the route a client has |
| // previously requested. |
| if (handleEndTone == 0) { |
| // Cache BT device switch in the case that inband ringing is disabled and audio |
| // was routed to a watch. When active focus is received, this selection will be |
| // honored provided that the current route is associated. |
| Log.i(this, "handleSwitchFocus (ACTIVE_FOCUS): mBluetoothAddressForRinging = " |
| + "%s, mCurrentRoute = %s", mBluetoothAddressForRinging, mCurrentRoute); |
| AudioRoute audioRoute = mBluetoothAddressForRinging != null |
| && mBluetoothAddressForRinging.equals( |
| mCurrentRoute.getBluetoothAddress()) |
| ? mCurrentRoute |
| : getBaseRoute(true, null); |
| // Once we have processed active focus once during the call, we can ignore using |
| // the preferred device strategy. |
| mUsePreferredDeviceStrategy = false; |
| routeTo(true, audioRoute); |
| clearRingingBluetoothAddress(); |
| } |
| } |
| case RINGING_FOCUS -> { |
| if (!mIsActive) { |
| AudioRoute route = getBaseRoute(true, null); |
| BluetoothDevice device = mBluetoothRoutes.get(route); |
| // Check if in-band ringtone is enabled for the device; if it isn't, move to |
| // inactive route. |
| if (device != null && !mBluetoothRouteManager |
| .isInbandRingEnabled(route.getType(), device)) { |
| routeTo(false, route); |
| } else { |
| routeTo(true, route); |
| } |
| } else { |
| // Route is already active. |
| BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute); |
| if (device != null && !mBluetoothRouteManager |
| .isInbandRingEnabled(mCurrentRoute.getType(), device)) { |
| routeTo(false, mCurrentRoute); |
| } |
| } |
| } |
| } |
| } |
| |
| public void handleSwitchEarpiece(boolean isUserRequest) { |
| AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE); |
| if (earpieceRoute != null && getCallSupportedRoutes().contains(earpieceRoute)) { |
| maybeDisableWasOnSpeaker(isUserRequest); |
| routeTo(mIsActive, earpieceRoute); |
| } else { |
| Log.i(this, "ignore switch earpiece request"); |
| } |
| } |
| |
| private void handleSwitchBluetooth(String address, boolean isUserRequest) { |
| Log.i(this, "handle switch to bluetooth with address %s", address); |
| AudioRoute bluetoothRoute = null; |
| BluetoothDevice bluetoothDevice = null; |
| if (address == null) { |
| bluetoothRoute = getArbitraryBluetoothDevice(); |
| bluetoothDevice = mBluetoothRoutes.get(bluetoothRoute); |
| } else { |
| for (AudioRoute route : getCallSupportedRoutes()) { |
| if (Objects.equals(address, route.getBluetoothAddress())) { |
| bluetoothRoute = route; |
| bluetoothDevice = mBluetoothRoutes.get(route); |
| break; |
| } |
| } |
| } |
| |
| if (bluetoothRoute != null && bluetoothDevice != null) { |
| maybeDisableWasOnSpeaker(isUserRequest); |
| if (mFocusType == RINGING_FOCUS) { |
| routeTo(mBluetoothRouteManager |
| .isInbandRingEnabled(bluetoothRoute.getType(), bluetoothDevice) |
| && mIsActive, bluetoothRoute); |
| mBluetoothAddressForRinging = bluetoothDevice.getAddress(); |
| } else { |
| routeTo(mIsActive, bluetoothRoute); |
| } |
| } else { |
| Log.i(this, "ignore switch bluetooth request to unavailable address"); |
| } |
| } |
| |
| /** |
| * Retrieve the active BT device, if available, otherwise return the most recently tracked |
| * active device, or null if none are available. |
| * @return {@link AudioRoute} of the BT device. |
| */ |
| private AudioRoute getArbitraryBluetoothDevice() { |
| synchronized (mLock) { |
| if (mActiveBluetoothDevice != null) { |
| return getBluetoothRoute( |
| mActiveBluetoothDevice.first, mActiveBluetoothDevice.second); |
| } else if (!mBluetoothRoutes.isEmpty()) { |
| return mBluetoothRoutes.keySet().stream().toList() |
| .get(mBluetoothRoutes.size() - 1); |
| } |
| return null; |
| } |
| } |
| |
| private void handleSwitchHeadset(boolean isUserRequest) { |
| AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED); |
| if (headsetRoute != null && getCallSupportedRoutes().contains(headsetRoute)) { |
| maybeDisableWasOnSpeaker(isUserRequest); |
| routeTo(mIsActive, headsetRoute); |
| } else { |
| Log.i(this, "ignore switch headset request"); |
| } |
| } |
| |
| private void handleSwitchSpeaker() { |
| if (mSpeakerDockRoute != null && getCallSupportedRoutes().contains(mSpeakerDockRoute) |
| && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER) { |
| routeTo(mIsActive, mSpeakerDockRoute); |
| } else { |
| Log.i(this, "ignore switch speaker request"); |
| } |
| } |
| |
| private void handleSwitchBaselineRoute(boolean isExplicitUserRequest, boolean includeBluetooth, |
| String btAddressToExclude) { |
| Log.i(this, "handleSwitchBaselineRoute: includeBluetooth: %b, " |
| + "btAddressToExclude: %s", includeBluetooth, btAddressToExclude); |
| AudioRoute pendingDestRoute = mPendingAudioRoute.getDestRoute(); |
| boolean areExcludedBtAndDestBtSame = btAddressToExclude != null |
| && pendingDestRoute != null |
| && Objects.equals(btAddressToExclude, pendingDestRoute.getBluetoothAddress()); |
| Pair<Integer, String> btDevicePendingMsg = |
| new Pair<>(BT_AUDIO_CONNECTED, btAddressToExclude); |
| |
| // If SCO is once again connected or there's a pending message for BT_AUDIO_CONNECTED, then |
| // we know that the device has reconnected or is in the middle of connecting. Ignore routing |
| // out of this BT device. |
| boolean isExcludedDeviceConnectingOrConnected = areExcludedBtAndDestBtSame |
| && (Objects.equals(mBluetoothRoutes.get(pendingDestRoute), mScoAudioConnectedDevice) |
| || mPendingAudioRoute.getPendingMessages().contains(btDevicePendingMsg)); |
| // Check if the pending audio route or current route is already different from the route |
| // including the BT device that should be excluded from route selection. |
| boolean isCurrentOrDestRouteDifferent = btAddressToExclude != null |
| && ((mIsPending && !btAddressToExclude.equals(mPendingAudioRoute.getDestRoute() |
| .getBluetoothAddress())) || (!mIsPending && !btAddressToExclude.equals( |
| mCurrentRoute.getBluetoothAddress()))); |
| if (isExcludedDeviceConnectingOrConnected) { |
| Log.i(this, "BT device with address (%s) is currently connecting/connected. " |
| + "Ignoring route switch.", btAddressToExclude); |
| return; |
| } else if (isCurrentOrDestRouteDifferent) { |
| Log.i(this, "Current or pending audio route isn't routed to device with address " |
| + "(%s). Ignoring route switch.", btAddressToExclude); |
| return; |
| } |
| maybeDisableWasOnSpeaker(isExplicitUserRequest); |
| routeTo(mIsActive, calculateBaselineRoute(isExplicitUserRequest, includeBluetooth, |
| btAddressToExclude)); |
| } |
| |
| private void handleSpeakerOn() { |
| if (isPending()) { |
| Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route"); |
| // Clear any pending speaker off message as the speaker has been explicitly turned on as |
| // indicated by the audio fwk. |
| mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_OFF, null)); |
| // Clear any pending BT_AUDIO_DISCONNECTED messages for connected BT devices if speaker |
| // has explicitly been turned on. |
| for (BluetoothDevice device: mBluetoothRoutes.values()) { |
| mPendingAudioRoute.clearPendingMessage(new Pair<>(BT_AUDIO_DISCONNECTED, |
| device.getAddress())); |
| } |
| mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null); |
| // Update status bar notification if we are in a call. |
| mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls()); |
| } else { |
| if (mSpeakerDockRoute != null && getCallSupportedRoutes().contains(mSpeakerDockRoute) |
| && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER |
| && mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) { |
| // This path should not be hit. This means that some other application manipulated |
| // the audio routing to turn speaker on. Telecom will always attempt to route to |
| // speaker via USER_SWITCH_SPEAKER and change the routing, which would create a |
| // pending route change. Generate an anomaly report if this happens. |
| mAnomalyReporterAdapter.reportAnomaly(AUDIO_ROUTING_EXTERNAL_CHANGE_UUID, |
| AUDIO_ROUTING_EXTERNAL_CHANGE_MSG); |
| routeTo(mIsActive, mSpeakerDockRoute); |
| // Since the route switching triggered by this message, we need to manually send it |
| // again so that we won't stuck in the pending route |
| if (mIsActive) { |
| sendMessageWithSessionInfo(SPEAKER_ON); |
| } |
| } |
| } |
| } |
| |
| private void handleSpeakerOff() { |
| if (isPending()) { |
| Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route"); |
| // Clear any pending speaker on message as the speaker has been explicitly turned off as |
| // indicated by the audio fwk. |
| mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null)); |
| mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null); |
| // Update status bar notification |
| mStatusBarNotifier.notifySpeakerphone(false); |
| } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) { |
| // This path should not be hit. This means that some other application manipulated the |
| // audio routing to turn speaker off. Telecom will always attempt to route out of |
| // speaker via user switches (I.e. USER_SWITCH_EARPIECE) and change the routing, which |
| // would create a pending route change. Generate an anomaly report if this happens. |
| mAnomalyReporterAdapter.reportAnomaly(AUDIO_ROUTING_EXTERNAL_CHANGE_UUID, |
| AUDIO_ROUTING_EXTERNAL_CHANGE_MSG); |
| AudioRoute newRoute = getBaseRoute(true, null); |
| routeTo(mIsActive, newRoute); |
| // Since the route switching triggered by this message, we need to manually send it |
| // again so that we won't stuck in the pending route. Do not send the additional |
| // SPEAKER_OFF msg if we find that audio wasn't routed out of speaker. This would have |
| // the potential to cause an infinite loop if routing doesn't change. |
| if (mIsActive && newRoute.getType() != TYPE_SPEAKER) { |
| sendMessageWithSessionInfo(SPEAKER_OFF); |
| } |
| onAvailableRoutesChanged(); |
| } |
| } |
| |
| /** |
| * This is invoked when there are no more pending audio routes to be processed, which signals |
| * a change for the current audio route and the call audio state to be updated accordingly. |
| */ |
| public void handleExitPendingRoute() { |
| if (mIsPending) { |
| mCurrentRoute = mPendingAudioRoute.getDestRoute(); |
| Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE, |
| "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")"); |
| mIsPending = false; |
| mPendingAudioRoute.clearPendingMessages(); |
| onCurrentRouteChanged(); |
| if (mIsActive) { |
| // Only set mWasOnSpeaker if the routing was active. We don't want to consider this |
| // selection outside of a call. |
| if (mCurrentRoute.getType() == TYPE_SPEAKER) { |
| mWasOnSpeaker = true; |
| } |
| // Reinitialize the audio ops complete latch since the routing went active. We |
| // should always expect operations to complete after this point. |
| if (mAudioOperationsCompleteLatch.getCount() == 0) { |
| mAudioOperationsCompleteLatch = new CountDownLatch(1); |
| } |
| mAudioActiveCompleteLatch.countDown(); |
| } else { |
| // Reinitialize the active routing latch when audio ops are complete so that it can |
| // once again be processed when a new call is placed/received. |
| if (mAudioActiveCompleteLatch.getCount() == 0) { |
| mAudioActiveCompleteLatch = new CountDownLatch(1); |
| } |
| mAudioOperationsCompleteLatch.countDown(); |
| } |
| if (mFeatureFlags.telecomMetricsSupport()) { |
| mMetricsController.getAudioRouteStats().onRouteExit(mPendingAudioRoute, true); |
| } |
| } |
| } |
| |
| private void onCurrentRouteChanged() { |
| synchronized (mLock) { |
| BluetoothDevice activeBluetoothDevice = null; |
| int route = ROUTE_MAP.get(mCurrentRoute.getType()); |
| if (route == CallAudioState.ROUTE_STREAMING) { |
| updateCallAudioState(new CallAudioState(mIsMute, route, route)); |
| return; |
| } |
| if (route == CallAudioState.ROUTE_BLUETOOTH) { |
| activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute); |
| } |
| updateCallAudioState(new CallAudioState(mIsMute, route, |
| mCallAudioState.getRawSupportedRouteMask(), activeBluetoothDevice, |
| mCallAudioState.getSupportedBluetoothDevices())); |
| } |
| } |
| |
| private void onAvailableRoutesChanged() { |
| synchronized (mLock) { |
| int routeMask = 0; |
| Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>(); |
| for (AudioRoute route : getCallSupportedRoutes()) { |
| routeMask |= ROUTE_MAP.get(route.getType()); |
| if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) { |
| BluetoothDevice deviceToAdd = mBluetoothRoutes.get(route); |
| // Only include the lead device for LE audio (otherwise, the routes will show |
| // two separate devices in the UI). |
| if (deviceToAdd != null && route.getType() == AudioRoute.TYPE_BLUETOOTH_LE |
| && getLeAudioService() != null) { |
| int groupId = getLeAudioService().getGroupId(deviceToAdd); |
| if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) { |
| deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId); |
| } |
| } |
| // This will only ever be null when the lead device (LE) is disconnected and |
| // try to obtain the lead device for the 2nd bud. |
| if (deviceToAdd != null) { |
| availableBluetoothDevices.add(deviceToAdd); |
| } |
| } |
| } |
| |
| updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask, |
| mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices)); |
| } |
| } |
| |
| private void onMuteStateChanged(boolean mute) { |
| updateCallAudioState(new CallAudioState(mute, mCallAudioState.getRoute(), |
| mCallAudioState.getSupportedRouteMask(), mCallAudioState.getActiveBluetoothDevice(), |
| mCallAudioState.getSupportedBluetoothDevices())); |
| } |
| |
| /** |
| * Retrieves the current call's supported audio route and adjusts the audio routing if the |
| * current route isn't supported. |
| */ |
| private void updateRouteForForeground() { |
| boolean updatedRouteForCall = updateCallSupportedAudioRoutes(); |
| // Ensure that current call audio state has updated routes for current call. |
| if (updatedRouteForCall) { |
| mCallAudioState = new CallAudioState(mIsMute, mCallAudioState.getRoute(), |
| mCallSupportedRouteMask, mCallAudioState.getActiveBluetoothDevice(), |
| mCallAudioState.getSupportedBluetoothDevices()); |
| // Update audio route if foreground call doesn't support the current route. |
| if ((mCallSupportedRouteMask & mCallAudioState.getRoute()) == 0) { |
| routeTo(mIsActive, getBaseRoute(true, null)); |
| } |
| } |
| } |
| |
| /** |
| * Update supported audio routes for the foreground call if present. |
| */ |
| private boolean updateCallSupportedAudioRoutes() { |
| int availableRouteMask = 0; |
| Call foregroundCall = mCallsManager.getForegroundCall(); |
| mCallSupportedRoutes.clear(); |
| if (foregroundCall != null) { |
| int foregroundCallSupportedRouteMask = foregroundCall.getSupportedAudioRoutes(); |
| for (AudioRoute route : getAvailableRoutes()) { |
| int routeType = ROUTE_MAP.get(route.getType()); |
| availableRouteMask |= routeType; |
| if ((routeType & foregroundCallSupportedRouteMask) == routeType) { |
| mCallSupportedRoutes.add(route); |
| } |
| } |
| mCallSupportedRouteMask = availableRouteMask & foregroundCallSupportedRouteMask; |
| return true; |
| } else { |
| mCallSupportedRouteMask = -1; |
| return false; |
| } |
| } |
| |
| private void updateCallAudioState(CallAudioState newCallAudioState) { |
| synchronized (mTelecomLock) { |
| Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState); |
| CallAudioState oldState = mCallAudioState; |
| mCallAudioState = newCallAudioState; |
| // Update status bar notification |
| mStatusBarNotifier.notifyMute(newCallAudioState.isMuted()); |
| mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState); |
| updateAudioStateForTrackedCalls(mCallAudioState); |
| } |
| } |
| |
| private void updateAudioStateForTrackedCalls(CallAudioState newCallAudioState) { |
| List<Call> calls = new ArrayList<>(mCallsManager.getTrackedCalls()); |
| for (Call call : calls) { |
| if (call != null && call.getConnectionService() != null) { |
| call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState); |
| } |
| } |
| } |
| |
| private AudioRoute getPreferredDeviceAudioRoute(AudioDeviceAttributes deviceAttr) { |
| Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr); |
| if (deviceAttr == null) { |
| return DUMMY_ROUTE; |
| } |
| |
| // Get corresponding audio route |
| @AudioRoute.AudioRouteType int type = DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get( |
| deviceAttr.getType()); |
| AudioDeviceInfo currentCommunicationDevice = getCurrentCommunicationDevice(); |
| // We will default to TYPE_INVALID if the currentCommunicationDevice is null or the type |
| // cannot be resolved from the given audio device info. |
| int communicationDeviceAudioType = getAudioType(currentCommunicationDevice); |
| // Sync the preferred device strategy with the current communication device if there's a |
| // valid audio device output set as the preferred device strategy. This will address timing |
| // issues between updates made to the preferred device strategy. From the audio fwk |
| // standpoint, updates to the communication device take precedent to changes in the |
| // preferred device strategy so the former should be used as the source of truth. |
| if (type != TYPE_INVALID && communicationDeviceAudioType != TYPE_INVALID |
| && communicationDeviceAudioType != type) { |
| type = communicationDeviceAudioType; |
| } |
| if (BT_AUDIO_ROUTE_TYPES.contains(type)) { |
| return getBluetoothRoute(type, deviceAttr.getAddress()); |
| } else { |
| return mTypeRoutes.get(type); |
| } |
| } |
| |
| private AudioRoute getPreferredAudioRouteFromDefault(boolean isExplicitUserRequest, |
| boolean includeBluetooth, String btAddressToExclude) { |
| boolean skipEarpiece = false; |
| Call foregroundCall = mCallAudioManager.getForegroundCall(); |
| if (!isExplicitUserRequest) { |
| synchronized (mTelecomLock) { |
| skipEarpiece = foregroundCall != null && foregroundCall.isActiveFocus() |
| && VideoProfile.isVideo(foregroundCall.getVideoState()); |
| Log.i(this, "skipEarpiece for video call?" + skipEarpiece); |
| } |
| } |
| // Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there |
| // are only wearables available. |
| AudioRoute activeWatchOrNonWatchDeviceRoute = |
| getActiveWatchOrNonWatchDeviceRoute(btAddressToExclude); |
| if ((!mCallSupportedRoutes.isEmpty() && (mCallSupportedRouteMask |
| & CallAudioState.ROUTE_BLUETOOTH) == 0) || mBluetoothRoutes.isEmpty() |
| || !includeBluetooth || activeWatchOrNonWatchDeviceRoute == null) { |
| Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to " |
| + "available non-BT route."); |
| boolean callSupportsEarpieceWiredRoute = mCallSupportedRoutes.isEmpty() |
| || mCallSupportedRoutes.contains(mEarpieceWiredRoute); |
| // If call supported route doesn't contain earpiece/wired/BT, it should have speaker |
| // enabled. Otherwise, no routes would be supported for the call which should never be |
| // the case. |
| AudioRoute defaultRoute = mEarpieceWiredRoute != null && callSupportsEarpieceWiredRoute |
| ? mEarpieceWiredRoute |
| : mSpeakerDockRoute; |
| // Ensure that we default to speaker route if we're in a video call, but disregard it if |
| // a wired headset is plugged in. Also consider the case when we're holding/unholding a |
| // call. If the route was on speaker mode, ensure that we preserve the route selection. |
| // Todo: Clean this up once logic to maintain current audio routing during |
| // active/ringing focus switch is addressed. |
| if ((skipEarpiece || mWasOnSpeaker) && defaultRoute != null |
| && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) { |
| Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to " |
| + "speaker route for (video) call."); |
| defaultRoute = mSpeakerDockRoute; |
| } |
| return defaultRoute; |
| } else { |
| // Most recent active route will always be the last in the array (ensure that we don't |
| // auto route to a wearable device unless it's already active). |
| String autoRoutingToWatchExcerpt = " (except inactive watch)"; |
| Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to " |
| + "most recently active BT route" + autoRoutingToWatchExcerpt + "."); |
| return activeWatchOrNonWatchDeviceRoute; |
| } |
| } |
| |
| private int calculateSupportedRouteMaskInit() { |
| Log.i(this, "calculateSupportedRouteMaskInit: is wired headset plugged in - %s", |
| mWiredHeadsetManager.isPluggedIn()); |
| int routeMask = CallAudioState.ROUTE_SPEAKER; |
| |
| if (mWiredHeadsetManager.isPluggedIn()) { |
| routeMask |= CallAudioState.ROUTE_WIRED_HEADSET; |
| } else { |
| AudioDeviceInfo[] deviceList = mAudioManager.getDevices( |
| AudioManager.GET_DEVICES_OUTPUTS); |
| // For debugging purposes in cases where the device list returned by the API fwk is |
| // empty and we don't end up adding the earpiece route upon init. |
| Log.i(this, "calculateSupportedRouteMaskInit: is device list size: %d", |
| deviceList.length); |
| for (AudioDeviceInfo device: deviceList) { |
| Log.i(this, "calculateSupportedRouteMaskInit: audio route type from audio " |
| + "device info: %d", device != null ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE |
| .getOrDefault(device.getType(), TYPE_INVALID) : TYPE_INVALID); |
| if (device != null && device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { |
| routeMask |= CallAudioState.ROUTE_EARPIECE; |
| break; |
| } |
| } |
| } |
| return routeMask; |
| } |
| |
| @VisibleForTesting |
| public Set<AudioRoute> getAvailableRoutes() { |
| if (mCurrentRoute.equals(mStreamingRoute)) { |
| return mStreamingRoutes; |
| } else { |
| return mAvailableRoutes; |
| } |
| } |
| |
| public Set<AudioRoute> getCallSupportedRoutes() { |
| if (mCurrentRoute.equals(mStreamingRoute)) { |
| return mStreamingRoutes; |
| } else { |
| if (mAvailableRoutesUpdated) { |
| updateCallSupportedAudioRoutes(); |
| mAvailableRoutesUpdated = false; |
| } |
| return mCallSupportedRoutes.isEmpty() ? mAvailableRoutes : mCallSupportedRoutes; |
| } |
| } |
| |
| public AudioRoute getCurrentRoute() { |
| return mCurrentRoute; |
| } |
| |
| public AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType, |
| String address) { |
| for (AudioRoute route : mBluetoothRoutes.keySet()) { |
| if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) { |
| return route; |
| } |
| } |
| return null; |
| } |
| |
| public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) { |
| // Catch-all case for all invocations to this method where we shouldn't be using |
| // getPreferredAudioRouteFromStrategy |
| if (!mUsePreferredDeviceStrategy) { |
| return calculateBaselineRoute(false, includeBluetooth, btAddressToExclude); |
| } |
| // Get the preferred device value cached from the listener |
| AudioRoute destRoute = getPreferredDeviceRoute(); |
| boolean isPreferredDeviceSet = destRoute != null && !destRoute.equals(DUMMY_ROUTE); |
| if (!isPreferredDeviceSet) { |
| Log.i(this, "getBaseRoute: preferred audio route is not reported by " |
| + "AudioManager; telecom to determine"); |
| } else { |
| Log.i(this, "getBaseRoute: preferred audio route is %s", destRoute); |
| } |
| if (!isPreferredDeviceSet || (destRoute.getBluetoothAddress() != null && (!includeBluetooth |
| || destRoute.getBluetoothAddress().equals(btAddressToExclude)))) { |
| destRoute = getPreferredAudioRouteFromDefault(false, includeBluetooth, btAddressToExclude); |
| } |
| if (destRoute != null && !getCallSupportedRoutes().contains(destRoute)) { |
| destRoute = null; |
| } |
| Log.i(this, "getBaseRoute - audio routing to %s", destRoute); |
| return destRoute; |
| } |
| |
| private AudioRoute calculateBaselineRoute(boolean isExplicitUserRequest, |
| boolean includeBluetooth, String btAddressToExclude) { |
| AudioRoute destRoute = getPreferredAudioRouteFromDefault(isExplicitUserRequest, |
| includeBluetooth, btAddressToExclude); |
| if (destRoute != null && !getCallSupportedRoutes().contains(destRoute)) { |
| destRoute = null; |
| } |
| Log.i(this, "getBaseRoute - audio routing to %s", destRoute); |
| return destRoute; |
| } |
| |
| /** |
| * Don't add additional AudioRoute when a hearing aid pair is detected. The devices have |
| * separate addresses, so we need to perform explicit handling to ensure we don't |
| * treat them as two separate devices. |
| */ |
| private boolean containsHearingAidPair(@AudioRoute.AudioRouteType int type, |
| BluetoothDevice bluetoothDevice) { |
| // Check if it is a hearing aid pair and skip connecting to the other device in this case. |
| // Traverse mBluetoothRoutes backwards as the most recently active device will be inserted |
| // last. |
| String existingHearingAidAddress = null; |
| List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList(); |
| for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) { |
| AudioRoute audioRoute = bluetoothRoutes.get(i); |
| if (audioRoute.getType() == AudioRoute.TYPE_BLUETOOTH_HA) { |
| existingHearingAidAddress = audioRoute.getBluetoothAddress(); |
| break; |
| } |
| } |
| |
| // Check that route is for hearing aid and that there exists another hearing aid route |
| // created for the first device (of the pair) that was connected. |
| if (type == AudioRoute.TYPE_BLUETOOTH_HA && existingHearingAidAddress != null) { |
| BluetoothAdapter bluetoothAdapter = mBluetoothRouteManager.getDeviceManager() |
| .getBluetoothAdapter(); |
| if (bluetoothAdapter != null) { |
| List<BluetoothDevice> activeHearingAids = |
| bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID); |
| for (BluetoothDevice hearingAid : activeHearingAids) { |
| if (hearingAid != null && hearingAid.getAddress() != null) { |
| String address = hearingAid.getAddress(); |
| if (address.equals(bluetoothDevice.getAddress()) |
| || address.equals(existingHearingAidAddress)) { |
| Log.i(this, "containsHearingAidPair: Detected a hearing aid " |
| + "pair, ignoring creating a new AudioRoute"); |
| return true; |
| } |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Prevent auto routing to a wearable device when calculating the default bluetooth audio route |
| * to move to. This function ensures that the most recently active non-wearable device is |
| * selected for routing unless a wearable device has already been identified as an active |
| * device. |
| */ |
| private AudioRoute getActiveWatchOrNonWatchDeviceRoute(String btAddressToExclude) { |
| List<AudioRoute> bluetoothRoutes = getAvailableBluetoothDevicesForRouting(); |
| // Traverse the routes from the most recently active recorded devices first. |
| AudioRoute nonWatchDeviceRoute = null; |
| for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) { |
| AudioRoute route = bluetoothRoutes.get(i); |
| BluetoothDevice device = mBluetoothRoutes.get(route); |
| // Skip excluded BT address and LE audio if it's not the lead device. |
| if (route.getBluetoothAddress().equals(btAddressToExclude) |
| || isLeAudioNonLeadDeviceOrServiceUnavailable(route.getType(), device)) { |
| continue; |
| } |
| // Check if the most recently active device is a watch device. |
| boolean isActiveDevice; |
| synchronized (mLock) { |
| isActiveDevice = mActiveBluetoothDevice != null |
| && device.getAddress().equals(mActiveBluetoothDevice.second); |
| } |
| if (i == (bluetoothRoutes.size() - 1) && mBluetoothRouteManager.isWatch(device) |
| && (device.equals(mCallAudioState.getActiveBluetoothDevice()) |
| || isActiveDevice)) { |
| Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: Routing to active watch - %s", |
| bluetoothRoutes.get(bluetoothRoutes.size() - 1)); |
| return bluetoothRoutes.get(bluetoothRoutes.size() - 1); |
| } |
| // Record the first occurrence of a non-watch device route if found. |
| if (!mBluetoothRouteManager.isWatch(device)) { |
| nonWatchDeviceRoute = route; |
| break; |
| } |
| } |
| |
| Log.i(this, "Routing to a non-watch device - %s", nonWatchDeviceRoute); |
| return nonWatchDeviceRoute; |
| } |
| |
| private List<AudioRoute> getAvailableBluetoothDevicesForRouting() { |
| List<AudioRoute> bluetoothRoutes = new ArrayList<>(mBluetoothRoutes.keySet()); |
| // Consider the active device (BT_ACTIVE_DEVICE_PRESENT) if it exists first. |
| AudioRoute activeDeviceRoute = getArbitraryBluetoothDevice(); |
| if (activeDeviceRoute != null && (bluetoothRoutes.isEmpty() |
| || !bluetoothRoutes.get(bluetoothRoutes.size() - 1).equals(activeDeviceRoute))) { |
| Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: active BT device (%s) present." |
| + "Considering this device for selection first.", activeDeviceRoute); |
| bluetoothRoutes.add(activeDeviceRoute); |
| } |
| return bluetoothRoutes; |
| } |
| |
| private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type, |
| BluetoothDevice device) { |
| BluetoothLeAudio leAudioService = getLeAudioService(); |
| if (type != AudioRoute.TYPE_BLUETOOTH_LE) { |
| return false; |
| } else if (leAudioService == null) { |
| return true; |
| } |
| |
| int groupId = leAudioService.getGroupId(device); |
| if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) { |
| BluetoothDevice leadDevice = leAudioService.getConnectedGroupLeadDevice(groupId); |
| Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice); |
| return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress()); |
| } |
| return false; |
| } |
| |
| private BluetoothLeAudio getLeAudioService() { |
| return mBluetoothRouteManager.getDeviceManager().getLeAudioService(); |
| } |
| |
| @VisibleForTesting |
| public void setAudioManager(AudioManager audioManager) { |
| mAudioManager = audioManager; |
| } |
| |
| @VisibleForTesting |
| public void setAudioRouteFactory(AudioRoute.Factory audioRouteFactory) { |
| mAudioRouteFactory = audioRouteFactory; |
| } |
| |
| public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() { |
| return mBluetoothRoutes; |
| } |
| |
| public void overrideIsPending(boolean isPending) { |
| mIsPending = isPending; |
| } |
| |
| @VisibleForTesting |
| public void setScoAudioConnectedDevice(BluetoothDevice device) { |
| mScoAudioConnectedDevice = device; |
| } |
| |
| private void clearRingingBluetoothAddress() { |
| mBluetoothAddressForRinging = null; |
| } |
| |
| /** |
| * Update the active bluetooth device being tracked (as well as for individual profiles). |
| * We need to keep track of active devices for individual profiles because of potential |
| * inconsistencies found in BluetoothStateReceiver#handleActiveDeviceChanged. When multiple |
| * profiles are paired, we could have a scenario where an active device A is replaced |
| * with an active device B (from a different profile), which is then removed as an active |
| * device shortly after, causing device A to be reactive. It's possible that the active device |
| * changed intent is never received again for device A so an active device cache is necessary |
| * to track these devices at a profile level. |
| * @param device {@link Pair} containing the BT audio route type (i.e. SCO/HA/LE) and the |
| * address of the device. |
| */ |
| public void updateActiveBluetoothDevice(Pair<Integer, String> device) { |
| synchronized (mLock) { |
| mActiveDeviceCache.put(device.first, device.second); |
| // Update most recently active device if address isn't null (meaning |
| // some device is active). |
| if (device.second != null) { |
| mActiveBluetoothDevice = device; |
| } else { |
| // If a device was removed, check to ensure that no other device is |
| //still considered active. |
| boolean hasActiveDevice = false; |
| List<Map.Entry<Integer, String>> activeBtDevices = |
| new ArrayList<>(mActiveDeviceCache.entrySet()); |
| for (Map.Entry<Integer, String> activeDevice : activeBtDevices) { |
| Integer btAudioType = activeDevice.getKey(); |
| String address = activeDevice.getValue(); |
| if (address != null) { |
| hasActiveDevice = true; |
| mActiveBluetoothDevice = new Pair<>(btAudioType, address); |
| break; |
| } |
| } |
| if (!hasActiveDevice) { |
| mActiveBluetoothDevice = null; |
| } |
| } |
| } |
| } |
| |
| private void updateAvailableRoutes(AudioRoute route, boolean includeRoute) { |
| if (route == null) { |
| return; |
| } |
| if (includeRoute) { |
| mAvailableRoutes.add(route); |
| } else { |
| mAvailableRoutes.remove(route); |
| } |
| mAvailableRoutesUpdated = true; |
| } |
| |
| @VisibleForTesting |
| public void setActive(boolean active) { |
| if (active) { |
| mFocusType = ACTIVE_FOCUS; |
| } else { |
| mFocusType = NO_FOCUS; |
| } |
| mIsActive = active; |
| } |
| |
| void fallBack(String btAddressToExclude) { |
| mMetricsController.getAudioRouteStats().onRouteExit(mPendingAudioRoute, false); |
| sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, |
| btAddressToExclude); |
| } |
| |
| public CountDownLatch getAudioOperationsCompleteLatch() { |
| return mAudioOperationsCompleteLatch; |
| } |
| |
| public CountDownLatch getAudioActiveCompleteLatch() { |
| return mAudioActiveCompleteLatch; |
| } |
| |
| private @AudioRoute.AudioRouteType int getAudioType(AudioDeviceInfo device) { |
| return device != null |
| ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault( |
| device.getType(), TYPE_INVALID) |
| : TYPE_INVALID; |
| } |
| |
| @VisibleForTesting |
| public boolean getUsePreferredDeviceStrategy() { |
| return mUsePreferredDeviceStrategy; |
| } |
| |
| @VisibleForTesting |
| public void setCurrentCommunicationDevice(AudioDeviceInfo device) { |
| synchronized (mLock) { |
| mCurrentCommunicationDevice = device; |
| } |
| } |
| |
| public AudioDeviceInfo getCurrentCommunicationDevice() { |
| synchronized (mLock) { |
| return mCurrentCommunicationDevice; |
| } |
| } |
| |
| public void setPreferredDeviceRoute(AudioRoute route) { |
| synchronized (mLock) { |
| mPreferredDeviceRoute = route; |
| } |
| } |
| |
| public AudioRoute getPreferredDeviceRoute() { |
| synchronized (mLock) { |
| return mPreferredDeviceRoute; |
| } |
| } |
| |
| private void maybeDisableWasOnSpeaker(boolean isUserRequest) { |
| if (isUserRequest) { |
| mWasOnSpeaker = false; |
| } |
| } |
| |
| /* |
| * Adjusts routing to go inactive if we're active in the case that we're processing |
| * RINGING_FOCUS and another BT headset is connected which causes in-band ringing to get |
| * disabled. If we stay in active routing, Telecom will send requests to connect to these BT |
| * devices while the call is ringing and each of these requests will fail at the BT stack side. |
| * By default, in-band ringtone is disabled when more than one BT device is paired. Instead, |
| * ringtone is played using the headset's default ringtone. |
| */ |
| private boolean maybeAdjustActiveRouting(AudioRoute destRoute, boolean isDestRouteActive) { |
| BluetoothDevice device = mBluetoothRoutes.get(destRoute); |
| // If routing is active and in-band ringing is disabled while the call is ringing, move to |
| // inactive routing. |
| if (isDestRouteActive && mFocusType == RINGING_FOCUS && device != null |
| && !mBluetoothRouteManager.isInbandRingEnabled(destRoute.getType(), device)) { |
| return false; |
| } |
| else if (!isDestRouteActive && mFocusType == RINGING_FOCUS && (device == null |
| || mBluetoothRouteManager.isInbandRingEnabled(destRoute.getType(), device))) { |
| // If the routing is inactive while the call is ringing and we re-evaluate this to find |
| // that we're routing to a non-BT device or a BT device that does support in-band |
| // ringing, then re-enable active routing (i.e. second HFP headset is disconnected |
| // while call is ringing). |
| return true; |
| } |
| return isDestRouteActive; |
| } |
| |
| private void createSpeakerRoute() { |
| int audioRouteType = TYPE_SPEAKER; |
| if (mSpeakerDockRoute == null) { |
| //create type speaker |
| mSpeakerDockRoute = mAudioRouteFactory.create(audioRouteType, null, |
| mAudioManager); |
| // If speaker route couldn't be instantiated, try for TYPE_BUS |
| if (mSpeakerDockRoute == null) { |
| Log.i(this, "createSpeakerRoute: Can't find available audio device info for " |
| + "route TYPE_SPEAKER, trying for TYPE_BUS"); |
| mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_BUS, null, |
| mAudioManager); |
| audioRouteType = AudioRoute.TYPE_BUS; |
| } |
| if (mSpeakerDockRoute == null) { |
| Log.w(this, "createSpeakerRoute: Can't find available audio device info " |
| + "for route TYPE_SPEAKER or TYPE_BUS."); |
| } else { |
| // Update available routes |
| mTypeRoutes.put(audioRouteType, mSpeakerDockRoute); |
| updateAvailableRoutes(mSpeakerDockRoute, true); |
| } |
| } else { |
| Log.i(this, "createSpeakerRoute: route already created. Skipping."); |
| } |
| } |
| |
| private void createEarpieceRoute() { |
| // Create earpiece route |
| if (mEarpieceWiredRoute != null) { |
| Log.i(this, "createEarpieceRoute: route already created. Skipping."); |
| return; |
| } |
| mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_EARPIECE, null, |
| mAudioManager); |
| if (mEarpieceWiredRoute == null) { |
| Log.w(this, "createEarpieceRoute: Can't find available audio device info for " |
| + "route TYPE_EARPIECE"); |
| } else { |
| mTypeRoutes.put(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute); |
| updateAvailableRoutes(mEarpieceWiredRoute, true); |
| } |
| } |
| |
| @VisibleForTesting |
| public AudioRoute getAudioRouteForTesting(int audioRouteType) { |
| return switch (audioRouteType) { |
| case AudioRoute.TYPE_EARPIECE, AudioRoute.TYPE_WIRED -> mEarpieceWiredRoute; |
| case AudioRoute.TYPE_SPEAKER -> mSpeakerDockRoute; |
| default -> DUMMY_ROUTE; |
| }; |
| } |
| |
| @VisibleForTesting |
| public AudioRoutesCallback getAudioRoutesCallback() { |
| return mAudioRoutesCallback; |
| } |
| } |