Add implementation of handle external messages from wired headset,
dock and speaker, mute toggle and focus switching.
Bug: b/306395598
Test: atest CallAudioRouteControllerTest
Change-Id: Ic89780cb681060b000e0ee52dc65b01a17e2828d
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index 5037cf5..cdf44a8 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -82,13 +82,9 @@
}
}
if (routeInfo == null) {
- CompletableFuture<Boolean> future = new CompletableFuture<>();
- mScheduledExecutorService.schedule(new Runnable() {
- @Override
- public void run() {
- createRetry(type, bluetoothAddress, audioManager, retryCount - 1);
- }
- }, RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+ mScheduledExecutorService.schedule(
+ () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
+ RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
} else {
mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
}
@@ -105,6 +101,7 @@
public static final int TYPE_BLUETOOTH_SCO = 5;
public static final int TYPE_BLUETOOTH_HA = 6;
public static final int TYPE_BLUETOOTH_LE = 7;
+ public static final int TYPE_STREAMING = 8;
@IntDef(prefix = "TYPE", value = {
TYPE_INVALID,
TYPE_EARPIECE,
@@ -113,7 +110,8 @@
TYPE_DOCK,
TYPE_BLUETOOTH_SCO,
TYPE_BLUETOOTH_HA,
- TYPE_BLUETOOTH_LE
+ TYPE_BLUETOOTH_LE,
+ TYPE_STREAMING
})
@Retention(RetentionPolicy.SOURCE)
public @interface AudioRouteType {}
@@ -145,6 +143,7 @@
DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_SCO, "TYPE_BLUETOOTH_SCO");
DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_HA, "TYPE_BLUETOOTH_HA");
DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_LE, "TYPE_BLUETOOTH_LE");
+ DEVICE_TYPE_STRINGS.put(TYPE_STREAMING, "TYPE_STREAMING");
}
public static final HashMap<Integer, Integer> DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE;
@@ -225,6 +224,7 @@
void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
AudioManager audioManager) {
if (pendingAudioRoute.isActive() && !active) {
+ Log.i(this, "clearCommunicationDevice");
audioManager.clearCommunicationDevice();
} else if (active) {
if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index f76d47d..5585d09 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -97,6 +97,9 @@
put(SPEAKER_ON, "SPEAKER_ON");
put(SPEAKER_OFF, "SPEAKER_OFF");
+ put(STREAMING_FORCE_ENABLED, "STREAMING_FORCE_ENABLED");
+ put(STREAMING_FORCE_DISABLED, "STREAMING_FORCE_DISABLED");
+
put(USER_SWITCH_EARPIECE, "USER_SWITCH_EARPIECE");
put(USER_SWITCH_BLUETOOTH, "USER_SWITCH_BLUETOOTH");
put(USER_SWITCH_HEADSET, "USER_SWITCH_HEADSET");
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index c1d7d0c..091c8fc 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -19,24 +19,36 @@
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;
@@ -57,20 +69,83 @@
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;
@@ -79,59 +154,142 @@
public CallAudioRouteController(
Context context,
CallsManager callsManager,
+ CallAudioManager.AudioServiceFactory audioServiceFactory,
AudioRoute.Factory audioRouteFactory,
- WiredHeadsetManager wiredHeadsetManager) {
+ 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(Message msg) {
- preHandleMessage(msg);
- String address;
- BluetoothDevice bluetoothDevice;
- @AudioRoute.AudioRouteType int type;
- switch (msg.what) {
- 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 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 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 EXIT_PENDING_ROUTE:
- handleExitPendingRoute();
- break;
- default:
- break;
+ 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);
}
- postHandleMessage(msg);
}
};
}
@@ -140,7 +298,10 @@
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) {
@@ -225,6 +386,7 @@
@Override
public void setCallAudioManager(CallAudioManager callAudioManager) {
+ mCallAudioManager = callAudioManager;
}
@Override
@@ -271,6 +433,9 @@
}
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;
@@ -280,22 +445,24 @@
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)) {
+ 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 (mAvailableRoutes.contains(mCurrentRoute)) {
+ 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();
@@ -309,6 +476,103 @@
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(),
@@ -360,13 +624,7 @@
// Fallback to an available route
if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
- // fallback policy
- AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
- if (destRoute != null && mAvailableRoutes.contains(destRoute)) {
- routeTo(mIsActive, destRoute);
- } else {
- routeTo(mIsActive, getPreferredAudioRouteFromDefault(true/* includeBluetooth */));
- }
+ routeTo(mIsActive, getBaseRoute(false));
}
}
@@ -384,11 +642,142 @@
if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
|| (!mIsPending && mCurrentRoute.getType() == type)) {
// Fallback to an available route
- AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
- if (destRoute != null && mAvailableRoutes.contains(destRoute)) {
- routeTo(mIsActive, destRoute);
+ 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, getPreferredAudioRouteFromDefault(false/* includeBluetooth */));
+ 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();
}
@@ -397,39 +786,53 @@
public void handleExitPendingRoute() {
if (mIsPending) {
Log.i(this, "Exit pending route and enter %s(active=%b)",
- mPendingAudioRoute.getDestRoute(), mPendingAudioRoute.isActive());
+ mPendingAudioRoute.getDestRoute(), mIsActive);
mCurrentRoute = mPendingAudioRoute.getDestRoute();
- mIsActive = mPendingAudioRoute.isActive();
mIsPending = false;
onCurrentRouteChanged();
}
}
private void onCurrentRouteChanged() {
- BluetoothDevice activeBluetoothDevice = null;
- int route = ROUTE_MAP.get(mCurrentRoute.getType());
- if (route == CallAudioState.ROUTE_BLUETOOTH) {
- activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute);
+ 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()));
}
- updateCallAudioState(new CallAudioState(mIsMute, route,
- mCallAudioState.getSupportedRouteMask(), activeBluetoothDevice,
- mCallAudioState.getSupportedBluetoothDevices()));
}
private void onAvailableRoutesChanged() {
- int routeMask = 0;
- Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
- for (AudioRoute route : mAvailableRoutes) {
- routeMask |= ROUTE_MAP.get(route.getType());
- if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
- availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+ 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));
}
- 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);
@@ -468,7 +871,7 @@
}
}
- public AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
+ private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
} else {
@@ -495,8 +898,13 @@
return routeMask;
}
+ @VisibleForTesting
public Set<AudioRoute> getAvailableRoutes() {
- return mAvailableRoutes;
+ if (mCurrentRoute.equals(mStreamingRoute)) {
+ return mStreamingRoutes;
+ } else {
+ return mAvailableRoutes;
+ }
}
public AudioRoute getCurrentRoute() {
@@ -513,6 +921,17 @@
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;
@@ -525,6 +944,11 @@
@VisibleForTesting
public void setActive(boolean active) {
+ if (active) {
+ mFocusType = ACTIVE_FOCUS;
+ } else {
+ mFocusType = NO_FOCUS;
+ }
mIsActive = active;
}
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index de601a5..97a53e2 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -118,6 +118,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IntentForwarderActivity;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
import com.android.server.telecom.callfiltering.BlockCheckerAdapter;
@@ -607,6 +608,7 @@
EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
CallAudioCommunicationDeviceTracker communicationDeviceTracker,
CallStreamingNotification callStreamingNotification,
+ BluetoothDeviceManager bluetoothDeviceManager,
FeatureFlags featureFlags,
IncomingCallFilterGraphProvider incomingCallFilterGraphProvider) {
@@ -632,6 +634,9 @@
mDtmfLocalTonePlayer =
new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
CallAudioRouteAdapter callAudioRouteAdapter;
+ // TODO: add another flag check when
+ // bluetoothDeviceManager.getBluetoothHeadset().isScoManagedByAudio()
+ // available and return true
if (!featureFlags.useRefactoredAudioRouteSwitching()) {
callAudioRouteAdapter = callAudioRouteStateMachineFactory.create(
context,
@@ -646,8 +651,8 @@
featureFlags
);
} else {
- callAudioRouteAdapter = new CallAudioRouteController(
- context, this, new AudioRoute.Factory(), wiredHeadsetManager);
+ callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
+ new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager);
}
callAudioRouteAdapter.initialize();
bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 5fa3048..8de62ed 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -76,9 +76,7 @@
public void onMessageReceived(int message) {
if (message == PENDING_ROUTE_FAILED) {
// Fallback to base route
- //TODO: Replace getPreferredAudioRouteFromDefault by getBaseRoute when available and
- // make the replaced one private
- mDestRoute = mCallAudioRouteController.getPreferredAudioRouteFromDefault(true);
+ mDestRoute = mCallAudioRouteController.getBaseRoute(true);
mCallAudioRouteController.sendMessageWithSessionInfo(
CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 9f6fcba..b4c3a4d 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -425,6 +425,7 @@
emergencyCallDiagnosticLogger,
communicationDeviceTracker,
callStreamingNotification,
+ bluetoothDeviceManager,
featureFlags,
IncomingCallFilterGraph::new);
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 27e5a7d..a0ffe63 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -25,9 +25,8 @@
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
-import android.media.AudioManager;
import android.media.AudioDeviceInfo;
-import android.media.audio.common.AudioDevice;
+import android.media.AudioManager;
import android.os.Bundle;
import android.telecom.Log;
import android.util.ArraySet;
@@ -41,13 +40,17 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.concurrent.Executor;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
public class BluetoothDeviceManager {
@@ -98,6 +101,9 @@
synchronized (mLock) {
String logString;
if (profile == BluetoothProfile.HEADSET) {
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture.complete((BluetoothHeadset) proxy);
+ }
mBluetoothHeadset = (BluetoothHeadset) proxy;
logString = "Got BluetoothHeadset: " + mBluetoothHeadset;
} else if (profile == BluetoothProfile.HEARING_AID) {
@@ -137,6 +143,9 @@
LinkedHashMap<String, BluetoothDevice> lostServiceDevices;
String logString;
if (profile == BluetoothProfile.HEADSET) {
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture.complete(null);
+ }
mBluetoothHeadset = null;
lostServiceDevices = mHfpDevicesByAddress;
mBluetoothRouteManager.onActiveDeviceChanged(null,
@@ -201,6 +210,7 @@
private BluetoothRouteManager mBluetoothRouteManager;
private BluetoothHeadset mBluetoothHeadset;
+ private CompletableFuture<BluetoothHeadset> mBluetoothHeadsetFuture;
private BluetoothHearingAid mBluetoothHearingAid;
private boolean mLeAudioCallbackRegistered = false;
private BluetoothLeAudio mBluetoothLeAudioService;
@@ -218,8 +228,12 @@
public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
CallAudioCommunicationDeviceTracker communicationDeviceTracker,
FeatureFlags featureFlags) {
+ mFeatureFlags = featureFlags;
if (bluetoothAdapter != null) {
mBluetoothAdapter = bluetoothAdapter;
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture = new CompletableFuture<>();
+ }
bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
BluetoothProfile.HEADSET);
bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
@@ -229,7 +243,6 @@
mAudioManager = context.getSystemService(AudioManager.class);
mExecutor = context.getMainExecutor();
mCommunicationDeviceTracker = communicationDeviceTracker;
- mFeatureFlags = featureFlags;
}
}
@@ -333,7 +346,19 @@
}
public BluetoothHeadset getBluetoothHeadset() {
- return mBluetoothHeadset;
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ try {
+ mBluetoothHeadset = mBluetoothHeadsetFuture.get(500L,
+ TimeUnit.MILLISECONDS);
+ return mBluetoothHeadset;
+ } catch (TimeoutException | InterruptedException | ExecutionException e) {
+ // ignore
+ Log.w(this, "Acquire BluetoothHeadset service failed due to: " + e);
+ return null;
+ }
+ } else {
+ return mBluetoothHeadset;
+ }
}
public BluetoothAdapter getBluetoothAdapter() {
@@ -402,7 +427,7 @@
mHearingAidDeviceSyncIds.put(device, hiSyncId);
targetDeviceMap = mHearingAidDevicesByAddress;
} else if (deviceType == DEVICE_TYPE_HEADSET) {
- if (mBluetoothHeadset == null) {
+ if (getBluetoothHeadset() == null) {
Log.w(this, "Headset service null when receiving device added broadcast");
return;
}
@@ -465,7 +490,7 @@
}
public void disconnectSco() {
- if (mBluetoothHeadset == null) {
+ if (getBluetoothHeadset() == null) {
Log.w(this, "Trying to disconnect audio but no headset service exists.");
} else {
mBluetoothHeadset.disconnectAudio();
@@ -650,7 +675,7 @@
callProfile = BluetoothProfile.HEARING_AID;
} else if (mHfpDevicesByAddress.containsKey(address)) {
Log.i(this, "Telecomm found HFP device for address: " + address);
- if (mBluetoothHeadset == null) {
+ if (getBluetoothHeadset() == null) {
Log.w(this, "Attempting to turn on audio when the headset service is null");
return false;
}
@@ -707,9 +732,15 @@
Log.w(this, "Couldn't set active device to %s", address);
return false;
}
- int scoConnectionRequest = mBluetoothHeadset.connectAudio();
- return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
- scoConnectionRequest == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+ if (getBluetoothHeadset() != null) {
+ int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+ return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+ scoConnectionRequest
+ == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+ } else {
+ Log.w(this, "Couldn't find bluetooth headset service");
+ return false;
+ }
} else {
Log.w(this, "Attempting to turn on audio for a disconnected device");
return false;
@@ -739,16 +770,20 @@
// Get the inband ringing enabled status of expected BT device to route call audio instead
// of using the address of currently connected device.
BluetoothDevice activeDevice = mBluetoothRouteManager.getMostRecentlyReportedActiveDevice();
- Log.i(this, "isInbandRingingEnabled: activeDevice: " + activeDevice);
- if (mBluetoothRouteManager.isCachedLeAudioDevice(activeDevice)) {
+ return isInbandRingEnabled(activeDevice);
+ }
+
+ public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+ Log.i(this, "isInbandRingEnabled: device: " + bluetoothDevice);
+ if (mBluetoothRouteManager.isCachedLeAudioDevice(bluetoothDevice)) {
if (mBluetoothLeAudioService == null) {
Log.i(this, "isInbandRingingEnabled: no leaudio service available.");
return false;
}
- int groupId = mBluetoothLeAudioService.getGroupId(activeDevice);
+ int groupId = mBluetoothLeAudioService.getGroupId(bluetoothDevice);
return mBluetoothLeAudioService.isInbandRingtoneEnabled(groupId);
} else {
- if (mBluetoothHeadset == null) {
+ if (getBluetoothHeadset() == null) {
Log.i(this, "isInbandRingingEnabled: no headset service available.");
return false;
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 235ba56..7da5339 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -957,6 +957,11 @@
return mDeviceManager.isInbandRingingEnabled();
}
+ @VisibleForTesting
+ public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+ return mDeviceManager.isInbandRingEnabled(bluetoothDevice);
+ }
+
private boolean addDevice(String address) {
if (mAudioConnectingStates.containsKey(address)) {
Log.i(this, "Attempting to add device %s twice.", address);
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index 648a831..c516c8e 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -403,7 +403,8 @@
when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
mBluetoothDeviceManager.connectAudio(device1.getAddress(), false);
- verify(mAdapter).setActiveDevice(device1, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+ verify(mAdapter).setActiveDevice(eq(device1),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_ALL));
mBluetoothDeviceManager.disconnectAudio();
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 08576fc..0a53eb0 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -16,18 +16,43 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.CallAudioRouteAdapter.ACTIVE_FOCUS;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_DOCK;
+import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_DOCK;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_ON;
+import static com.android.server.telecom.CallAudioRouteAdapter.NO_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.RINGING_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
+import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_DISABLED;
+import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_ENABLED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_SPEAKER;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -35,15 +60,19 @@
import android.bluetooth.BluetoothDevice;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
+import android.media.IAudioService;
import android.media.audiopolicy.AudioProductStrategy;
+import android.os.UserHandle;
import android.telecom.CallAudioState;
import androidx.test.filters.SmallTest;
import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import org.junit.After;
import org.junit.Before;
@@ -62,6 +91,9 @@
@Mock AudioManager mAudioManager;
@Mock AudioDeviceInfo mEarpieceDeviceInfo;
@Mock CallsManager mCallsManager;
+ @Mock CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+ @Mock IAudioService mAudioService;
+ @Mock BluetoothRouteManager mBluetoothRouteManager;
private AudioRoute mEarpieceRoute;
private AudioRoute mSpeakerRoute;
private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -90,10 +122,16 @@
new AudioDeviceInfo[] {
mEarpieceDeviceInfo
});
+ when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
+ .thenReturn(null);
+ when(mAudioServiceFactory.getAudioService()).thenReturn(mAudioService);
+ when(mContext.getAttributionTag()).thenReturn("");
doNothing().when(mCallsManager).onCallAudioStateChanged(any(CallAudioState.class),
any(CallAudioState.class));
- mController = new CallAudioRouteController(mContext, mCallsManager, mAudioRouteFactory,
- mWiredHeadsetManager);
+ when(mCallsManager.getCurrentUserHandle()).thenReturn(
+ new UserHandle(UserHandle.USER_SYSTEM));
+ mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
+ mAudioRouteFactory, mWiredHeadsetManager, mBluetoothRouteManager);
mController.setAudioRouteFactory(mAudioRouteFactory);
mController.setAudioManager(mAudioManager);
mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
@@ -148,6 +186,7 @@
AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
nullable(AudioDeviceInfo.class));
+
expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
| CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
@@ -166,9 +205,6 @@
@SmallTest
@Test
public void testActiveDeactivateBluetoothDevice() {
- when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
- .thenReturn(null);
-
mController.initialize();
mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
BLUETOOTH_DEVICE_1);
@@ -192,7 +228,256 @@
@SmallTest
@Test
- public void testSwitchFocusInBluetoothRoute() {
+ public void testSwitchFocusForBluetoothDeviceSupportInbandRinging() {
+ doAnswer(invocation -> {
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ return true;
+ }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
+ doAnswer(invocation -> {
+ mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0, BLUETOOTH_DEVICE_1);
+ return true;
+ }).when(mAudioManager).clearCommunicationDevice();
+ when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ BLUETOOTH_DEVICE_1);
+
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ assertFalse(mController.isActive());
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS);
+ verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
+ nullable(AudioDeviceInfo.class));
+ assertTrue(mController.isActive());
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ assertTrue(mController.isActive());
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+ verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+ assertFalse(mController.isActive());
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectWiredHeadset() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectDock() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(DISCONNECT_DOCK);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSpeakerToggle() {
+ mController.initialize();
+ mController.setActive(true);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSpeakerToggleWhenDockConnected() {
+ mController.initialize();
+ mController.setActive(true);
+ mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSwitchEarpiece() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_EARPIECE);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSwitchBluetooth() {
+ doAnswer(invocation -> {
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ return true;
+ }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
+
+ mController.initialize();
+ mController.setActive(true);
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ BLUETOOTH_DEVICE_1);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0,
+ BLUETOOTH_DEVICE_1.getAddress());
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void tesetSwitchSpeakerAndHeadset() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_HEADSET);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testEnableAndDisableStreaming() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(STREAMING_FORCE_ENABLED);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_STREAMING,
+ CallAudioState.ROUTE_STREAMING, null, new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(STREAMING_FORCE_DISABLED);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testToggleMute() throws Exception {
+ when(mAudioManager.isMasterMute()).thenReturn(false);
+
+ mController.initialize();
+ mController.setActive(true);
+
+ mController.sendMessageWithSessionInfo(MUTE_ON);
+ CallAudioState expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+ anyInt(), anyString());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ when(mAudioManager.isMasterMute()).thenReturn(true);
+ mController.sendMessageWithSessionInfo(MUTE_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(false), anyString(),
+ anyInt(), anyString());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
}
}
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index f814d3e..670875b 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -127,6 +127,7 @@
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
@@ -290,6 +291,7 @@
@Mock private PhoneCapability mPhoneCapability;
@Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
@Mock private CallStreamingNotification mCallStreamingNotification;
+ @Mock private BluetoothDeviceManager mBluetoothDeviceManager;
@Mock private FeatureFlags mFeatureFlags;
@Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
private CallsManager mCallsManager;
@@ -366,6 +368,7 @@
mEmergencyCallDiagnosticLogger,
mCommunicationDeviceTracker,
mCallStreamingNotification,
+ mBluetoothDeviceManager,
mFeatureFlags,
(call, listener, context, timeoutsAdapter, lock) -> mIncomingCallFilterGraph);