blob: d38577d1e331cfae3c9421ea37cba2117c2b16cd [file] [log] [blame]
/*
* 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.TYPE_INVALID;
import android.app.ActivityManager;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.IAudioService;
import android.media.audiopolicy.AudioProductStrategy;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.util.ArrayMap;
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 java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class CallAudioRouteController implements CallAudioRouteAdapter {
private static final long TIMEOUT_LIMIT = 2000L;
private static final AudioRoute DUMMY_ROUTE = new AudioRoute(TYPE_INVALID, null, null);
private static final Map<Integer, Integer> ROUTE_MAP;
static {
ROUTE_MAP = new ArrayMap<>();
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_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);
}
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 AudioRoute mCurrentRoute;
private AudioRoute mEarpieceWiredRoute;
private AudioRoute mSpeakerDockRoute;
private AudioRoute mStreamingRoute;
private Set<AudioRoute> mStreamingRoutes;
private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes;
private Map<Integer, AudioRoute> mTypeRoutes;
private PendingAudioRoute mPendingAudioRoute;
private AudioRoute.Factory mAudioRouteFactory;
private int mFocusType;
private final Object mLock = new Object();
private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.startSession("CARC.mSPCR");
try {
if (AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED.equals(intent.getAction())) {
if (mAudioManager != null) {
AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
if ((info != null) &&
(info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
sendMessageWithSessionInfo(SPEAKER_ON);
} else {
sendMessageWithSessionInfo(SPEAKER_OFF);
}
}
} else {
Log.w(this, "Received non-speakerphone-change intent");
}
} finally {
Log.endSession();
}
}
};
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;
public CallAudioRouteController(
Context context,
CallsManager callsManager,
CallAudioManager.AudioServiceFactory audioServiceFactory,
AudioRoute.Factory audioRouteFactory,
WiredHeadsetManager wiredHeadsetManager,
BluetoothRouteManager bluetoothRouteManager) {
mContext = context;
mCallsManager = callsManager;
mAudioManager = context.getSystemService(AudioManager.class);
mAudioServiceFactory = audioServiceFactory;
mAudioRouteFactory = audioRouteFactory;
mWiredHeadsetManager = wiredHeadsetManager;
mIsMute = false;
mBluetoothRouteManager = bluetoothRouteManager;
mFocusType = NO_FOCUS;
HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
handlerThread.start();
// Register broadcast receivers
IntentFilter speakerChangedFilter = new IntentFilter(
AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED);
speakerChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
context.registerReceiver(mSpeakerPhoneChangeReceiver, speakerChangedFilter);
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);
// Create handler
mHandler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
synchronized (this) {
preHandleMessage(msg);
String address;
BluetoothDevice bluetoothDevice;
int focus;
@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();
break;
case SWITCH_BLUETOOTH:
case USER_SWITCH_BLUETOOTH:
address = (String) ((SomeArgs) msg.obj).arg2;
handleSwitchBluetooth(address);
break;
case SWITCH_HEADSET:
case USER_SWITCH_HEADSET:
handleSwitchHeadset();
break;
case SWITCH_SPEAKER:
case USER_SWITCH_SPEAKER:
handleSwitchSpeaker();
break;
case USER_SWITCH_BASELINE_ROUTE:
handleSwitchBaselineRoute();
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.isMasterMute());
break;
case SWITCH_FOCUS:
focus = msg.arg1;
handleSwitchFocus(focus);
break;
case EXIT_PENDING_ROUTE:
handleExitPendingRoute();
break;
default:
break;
}
postHandleMessage(msg);
}
}
};
}
@Override
public void initialize() {
mAvailableRoutes = new HashSet<>();
mBluetoothRoutes = new ArrayMap<>();
mTypeRoutes = new ArrayMap<>();
mStreamingRoutes = new HashSet<>();
mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
mStreamingRoutes.add(mStreamingRoute);
int supportMask = calculateSupportedRouteMask();
if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
// Create spekaer routes
mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
mAudioManager);
if (mSpeakerDockRoute == null) {
Log.w(this, "Can't find available audio device info for route TYPE_SPEAKER");
} else {
mTypeRoutes.put(AudioRoute.TYPE_SPEAKER, mSpeakerDockRoute);
mAvailableRoutes.add(mSpeakerDockRoute);
}
}
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);
mAvailableRoutes.add(mEarpieceWiredRoute);
}
} else if ((supportMask & CallAudioState.ROUTE_EARPIECE) != 0) {
// Create earpiece routes
mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_EARPIECE, null,
mAudioManager);
if (mEarpieceWiredRoute == null) {
Log.w(this, "Can't find available audio device info for route TYPE_EARPIECE");
} else {
mTypeRoutes.put(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute);
mAvailableRoutes.add(mEarpieceWiredRoute);
}
}
// set current route
if (mEarpieceWiredRoute != null) {
mCurrentRoute = mEarpieceWiredRoute;
} else {
mCurrentRoute = mSpeakerDockRoute;
}
mIsActive = false;
mCallAudioState = new CallAudioState(mIsMute, ROUTE_MAP.get(mCurrentRoute.getType()),
supportMask, null, new HashSet<>());
}
@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, 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 null;
}
@Override
public boolean isHfpDeviceAvailable() {
return !mBluetoothRoutes.isEmpty();
}
@Override
public Handler getAdapterHandler() {
return mHandler;
}
@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 active, AudioRoute destRoute) {
if (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
return;
}
if (mIsPending) {
if (destRoute.equals(mPendingAudioRoute.getDestRoute()) && (mIsActive == active)) {
return;
}
Log.i(this, "Override current pending route destination from %s(active=%b) to "
+ "%s(active=%b)",
mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
// override pending route while keep waiting for still pending messages for the
// previous pending route
mIsActive = active;
mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
mPendingAudioRoute.setDestRoute(active, destRoute);
} else {
if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
return;
}
Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute,
mIsActive, destRoute, active);
// route to pending route
if (getAvailableRoutes().contains(mCurrentRoute)) {
mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
} else {
// Avoid waiting for pending messages for an unavailable route
mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
}
mPendingAudioRoute.setDestRoute(active, destRoute);
mIsActive = active;
mIsPending = true;
}
mPendingAudioRoute.evaluatePendingState();
postTimeoutMessage();
}
private void postTimeoutMessage() {
// reset timeout handler
mHandler.removeMessages(PENDING_ROUTE_TIMEOUT);
mHandler.postDelayed(() -> mHandler.sendMessage(
Message.obtain(mHandler, PENDING_ROUTE_TIMEOUT)), TIMEOUT_LIMIT);
}
private void handleWiredHeadsetConnected() {
AudioRoute wiredHeadsetRoute = null;
try {
wiredHeadsetRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null,
mAudioManager);
} catch (IllegalArgumentException e) {
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) {
mAvailableRoutes.add(wiredHeadsetRoute);
mAvailableRoutes.remove(mEarpieceWiredRoute);
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) {
mAvailableRoutes.remove(wiredHeadsetRoute);
mEarpieceWiredRoute = null;
}
AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
if (earpieceRoute != null) {
mAvailableRoutes.add(earpieceRoute);
mEarpieceWiredRoute = earpieceRoute;
}
onAvailableRoutesChanged();
// Route to expected state
if (mCurrentRoute.equals(wiredHeadsetRoute)) {
routeTo(mIsActive, getBaseRoute(true));
}
}
private void handleDockConnected() {
AudioRoute dockRoute = null;
try {
dockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_DOCK, null, mAudioManager);
} catch (IllegalArgumentException e) {
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) {
mAvailableRoutes.add(dockRoute);
mAvailableRoutes.remove(mSpeakerDockRoute);
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) {
mAvailableRoutes.remove(dockRoute);
mSpeakerDockRoute = null;
}
AudioRoute speakerRoute = mTypeRoutes.get(AudioRoute.TYPE_SPEAKER);
if (speakerRoute != null) {
mAvailableRoutes.add(speakerRoute);
mSpeakerDockRoute = speakerRoute;
}
onAvailableRoutesChanged();
// Route to expected state
if (mCurrentRoute.equals(dockRoute)) {
routeTo(mIsActive, getBaseRoute(true));
}
}
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));
} else {
Log.i(this, "ignore disable streaming, not in streaming");
}
}
private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
if (mIsPending) {
if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED);
}
} else {
// ignore, not triggered by telecom
}
}
private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) {
if (mIsPending) {
if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED);
}
} else {
// ignore, not triggered by telecom
}
}
private void handleBtConnected(@AudioRoute.AudioRouteType int type,
BluetoothDevice bluetoothDevice) {
AudioRoute bluetoothRoute = null;
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);
mAvailableRoutes.add(bluetoothRoute);
mBluetoothRoutes.put(bluetoothRoute, bluetoothDevice);
onAvailableRoutesChanged();
}
}
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);
mAvailableRoutes.remove(bluetoothRoute);
onAvailableRoutesChanged();
}
// Fallback to an available route
if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
routeTo(mIsActive, getBaseRoute(false));
}
}
private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type,
String deviceAddress) {
AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress);
if (bluetoothRoute != null) {
Log.i(this, "request to route to bluetooth route: %s(active=%b)", bluetoothRoute,
mIsActive);
routeTo(mIsActive, bluetoothRoute);
}
}
private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) {
if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
|| (!mIsPending && mCurrentRoute.getType() == type)) {
// Fallback to an available route
routeTo(mIsActive, getBaseRoute(true));
}
}
private void handleMuteChanged(boolean mute) {
mIsMute = mute;
if (mIsMute != mAudioManager.isMasterMute() && 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) {
Log.e(this, e, "Remote exception while toggling mute.");
return;
}
}
}
onMuteStateChanged(mIsMute);
}
private void handleSwitchFocus(int focus) {
mFocusType = focus;
switch (focus) {
case NO_FOCUS -> {
if (mIsActive) {
handleMuteChanged(false);
routeTo(false, mCurrentRoute);
}
}
case ACTIVE_FOCUS -> {
if (!mIsActive) {
routeTo(true, getBaseRoute(true));
}
}
case RINGING_FOCUS -> {
if (!mIsActive) {
AudioRoute route = getBaseRoute(true);
BluetoothDevice device = mBluetoothRoutes.get(route);
if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
routeTo(false, route);
} else {
routeTo(true, route);
}
} else {
// active
BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
routeTo(false, mCurrentRoute);
}
}
}
}
}
public void handleSwitchEarpiece() {
AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
if (earpieceRoute != null && getAvailableRoutes().contains(earpieceRoute)) {
routeTo(mIsActive, earpieceRoute);
} else {
Log.i(this, "ignore switch earpiece request");
}
}
private void handleSwitchBluetooth(String address) {
Log.i(this, "handle switch to bluetooth with address %s", address);
AudioRoute bluetoothRoute = null;
BluetoothDevice bluetoothDevice = null;
for (AudioRoute route : getAvailableRoutes()) {
if (Objects.equals(address, route.getBluetoothAddress())) {
bluetoothRoute = route;
bluetoothDevice = mBluetoothRoutes.get(route);
break;
}
}
if (bluetoothRoute != null && bluetoothDevice != null) {
if (mFocusType == RINGING_FOCUS) {
routeTo(mBluetoothRouteManager.isInbandRingEnabled(bluetoothDevice) && mIsActive,
bluetoothRoute);
} else {
routeTo(mIsActive, bluetoothRoute);
}
} else {
Log.i(this, "ignore switch bluetooth request");
}
}
private void handleSwitchHeadset() {
AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED);
if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
routeTo(mIsActive, headsetRoute);
} else {
Log.i(this, "ignore switch speaker request");
}
}
private void handleSwitchSpeaker() {
if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
routeTo(mIsActive, mSpeakerDockRoute);
} else {
Log.i(this, "ignore switch speaker request");
}
}
private void handleSwitchBaselineRoute() {
routeTo(mIsActive, getBaseRoute(true));
}
private void handleSpeakerOn() {
if (isPending()) {
mPendingAudioRoute.onMessageReceived(SPEAKER_ON);
} else {
if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
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()) {
mPendingAudioRoute.onMessageReceived(SPEAKER_OFF);
} else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
routeTo(mIsActive, getBaseRoute(true));
// 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_OFF);
}
onAvailableRoutesChanged();
}
}
public void handleExitPendingRoute() {
if (mIsPending) {
Log.i(this, "Exit pending route and enter %s(active=%b)",
mPendingAudioRoute.getDestRoute(), mIsActive);
mCurrentRoute = mPendingAudioRoute.getDestRoute();
mIsPending = false;
onCurrentRouteChanged();
}
}
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 : getAvailableRoutes()) {
routeMask |= ROUTE_MAP.get(route.getType());
if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
availableBluetoothDevices.add(mBluetoothRoutes.get(route));
}
}
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()));
}
private void updateCallAudioState(CallAudioState callAudioState) {
Log.i(this, "updateCallAudioState: " + callAudioState);
CallAudioState oldState = mCallAudioState;
mCallAudioState = callAudioState;
mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
updateAudioStateForTrackedCalls(mCallAudioState);
}
private void updateAudioStateForTrackedCalls(CallAudioState newCallAudioState) {
Set<Call> calls = mCallsManager.getTrackedCalls();
for (Call call : calls) {
if (call != null && call.getConnectionService() != null) {
call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
}
}
}
private AudioRoute getPreferredAudioRouteFromStrategy() {
// Get audio produce strategy
AudioProductStrategy strategy = null;
final AudioAttributes attr = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build();
List<AudioProductStrategy> strategies = AudioManager.getAudioProductStrategies();
for (AudioProductStrategy s : strategies) {
if (s.supportsAudioAttributes(attr)) {
strategy = s;
}
}
if (strategy == null) {
return null;
}
// Get preferred device
AudioDeviceAttributes deviceAttr = mAudioManager.getPreferredDeviceForStrategy(strategy);
if (deviceAttr == null) {
return null;
}
// Get corresponding audio route
@AudioRoute.AudioRouteType int type = AudioRoute.DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.get(
deviceAttr.getType());
if (BT_AUDIO_ROUTE_TYPES.contains(type)) {
return getBluetoothRoute(type, deviceAttr.getAddress());
} else {
return mTypeRoutes.get(deviceAttr.getType());
}
}
private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
} else {
// Most recent active route will always be the last in the array
return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
}
}
private int calculateSupportedRouteMask() {
int routeMask = CallAudioState.ROUTE_SPEAKER;
if (mWiredHeadsetManager.isPluggedIn()) {
routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
} else {
AudioDeviceInfo[] deviceList = mAudioManager.getDevices(
AudioManager.GET_DEVICES_OUTPUTS);
for (AudioDeviceInfo device: deviceList) {
if (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 AudioRoute getCurrentRoute() {
return mCurrentRoute;
}
private 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) {
AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
if (destRoute == null) {
destRoute = getPreferredAudioRouteFromDefault(includeBluetooth);
}
if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
destRoute = null;
}
return destRoute;
}
@VisibleForTesting
public void setAudioManager(AudioManager audioManager) {
mAudioManager = audioManager;
}
@VisibleForTesting
public void setAudioRouteFactory(AudioRoute.Factory audioRouteFactory) {
mAudioRouteFactory = audioRouteFactory;
}
@VisibleForTesting
public void setActive(boolean active) {
if (active) {
mFocusType = ACTIVE_FOCUS;
} else {
mFocusType = NO_FOCUS;
}
mIsActive = active;
}
}