Merge "Refactor CallAudioManager's audio routing"
diff --git a/src/com/android/server/telecom/BluetoothManager.java b/src/com/android/server/telecom/BluetoothManager.java
index 4d7bdb6..389d0f6 100644
--- a/src/com/android/server/telecom/BluetoothManager.java
+++ b/src/com/android/server/telecom/BluetoothManager.java
@@ -28,6 +28,7 @@
import android.os.Looper;
import android.os.SystemClock;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.List;
@@ -37,6 +38,9 @@
* overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
*/
public class BluetoothManager {
+ public interface BluetoothStateListener {
+ void onBluetoothStateChange(BluetoothManager bluetoothManager);
+ }
private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
new BluetoothProfile.ServiceListener() {
@@ -83,7 +87,7 @@
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final BluetoothAdapter mBluetoothAdapter;
- private final CallAudioManager mCallAudioManager;
+ private BluetoothStateListener mBluetoothStateListener;
private BluetoothHeadset mBluetoothHeadset;
private boolean mBluetoothConnectionPending = false;
@@ -102,9 +106,8 @@
private final Context mContext;
- public BluetoothManager(Context context, CallAudioManager callAudioManager) {
+ public BluetoothManager(Context context) {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
- mCallAudioManager = callAudioManager;
mContext = context;
if (mBluetoothAdapter != null) {
@@ -119,6 +122,10 @@
context.registerReceiver(mReceiver, intentFilter);
}
+ public void setBluetoothStateListener(BluetoothStateListener bluetoothStateListener) {
+ mBluetoothStateListener = bluetoothStateListener;
+ }
+
//
// Bluetooth helper methods.
//
@@ -138,7 +145,8 @@
* available to the user (i.e. if the device is BT-capable
* and a headset is connected.)
*/
- boolean isBluetoothAvailable() {
+ @VisibleForTesting
+ public boolean isBluetoothAvailable() {
Log.v(this, "isBluetoothAvailable()...");
// There's no need to ask the Bluetooth system service if BT is enabled:
@@ -208,7 +216,8 @@
* that the BT audio connection is currently being set
* up, and will be connected soon.)
*/
- /* package */ boolean isBluetoothAudioConnectedOrPending() {
+ @VisibleForTesting
+ public boolean isBluetoothAudioConnectedOrPending() {
if (isBluetoothAudioConnected()) {
Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
return true;
@@ -233,10 +242,11 @@
* Notified audio manager of a change to the bluetooth state.
*/
void updateBluetoothState() {
- mCallAudioManager.onBluetoothStateChange(this);
+ mBluetoothStateListener.onBluetoothStateChange(this);
}
- void connectBluetoothAudio() {
+ @VisibleForTesting
+ public void connectBluetoothAudio() {
Log.v(this, "connectBluetoothAudio()...");
if (mBluetoothHeadset != null) {
mBluetoothHeadset.connectAudio();
@@ -254,7 +264,8 @@
Timeouts.getBluetoothPendingTimeoutMillis(mContext.getContentResolver()));
}
- void disconnectBluetoothAudio() {
+ @VisibleForTesting
+ public void disconnectBluetoothAudio() {
Log.v(this, "disconnectBluetoothAudio()...");
if (mBluetoothHeadset != null) {
mBluetoothHeadset.disconnectAudio();
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 233ca1a..c36761a 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -376,7 +376,7 @@
mCallerInfoAsyncQueryFactory = callerInfoAsyncQueryFactory;
setHandle(handle);
mPostDialDigits = handle != null
- ? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : null;
+ ? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : "";
mGatewayInfo = gatewayInfo;
setConnectionManagerPhoneAccount(connectionManagerPhoneAccountHandle);
setTargetPhoneAccount(targetPhoneAccountHandle);
@@ -858,7 +858,8 @@
return mConferenceLevelActiveCall;
}
- ConnectionServiceWrapper getConnectionService() {
+ @VisibleForTesting
+ public ConnectionServiceWrapper getConnectionService() {
return mConnectionService;
}
@@ -1155,7 +1156,8 @@
}
/** Checks if this is a live call or not. */
- boolean isAlive() {
+ @VisibleForTesting
+ public boolean isAlive() {
switch (mState) {
case CallState.NEW:
case CallState.RINGING:
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 3a9d0fe..32a5120 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -16,34 +16,23 @@
package com.android.server.telecom;
-import android.app.ActivityManagerNative;
import android.content.Context;
-import android.content.pm.UserInfo;
import android.media.AudioManager;
import android.media.IAudioService;
-import android.os.Binder;
import android.os.Handler;
-import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.UserHandle;
-import android.provider.MediaStore;
import android.telecom.CallAudioState;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
-import java.util.Objects;
-
/**
* This class manages audio modes, streams and other properties.
*/
@VisibleForTesting
-public class CallAudioManager extends CallsManagerListenerBase
- implements WiredHeadsetManager.Listener, DockManager.Listener {
+public class CallAudioManager extends CallsManagerListenerBase {
private static final int STREAM_NONE = -1;
private static final String STREAM_DESCRIPTION_NONE = "STEAM_NONE";
@@ -64,9 +53,7 @@
private static final String MODE_DESCRIPTION_IN_COMMUNICATION = "MODE_IN_COMMUNICATION";
private static final int MSG_AUDIO_MANAGER_INITIALIZE = 0;
- private static final int MSG_AUDIO_MANAGER_TURN_ON_SPEAKER = 1;
private static final int MSG_AUDIO_MANAGER_ABANDON_AUDIO_FOCUS_FOR_CALL = 2;
- private static final int MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE = 3;
private static final int MSG_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS_FOR_CALL = 4;
private static final int MSG_AUDIO_MANAGER_SET_MODE = 5;
@@ -81,45 +68,10 @@
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
break;
}
- case MSG_AUDIO_MANAGER_TURN_ON_SPEAKER: {
- boolean on = (msg.arg1 != 0);
- // Wired headset and earpiece work the same way
- if (mAudioManager.isSpeakerphoneOn() != on) {
- Log.i(this, "turning speaker phone %s", on);
- mAudioManager.setSpeakerphoneOn(on);
- }
- break;
- }
case MSG_AUDIO_MANAGER_ABANDON_AUDIO_FOCUS_FOR_CALL: {
mAudioManager.abandonAudioFocusForCall();
break;
}
- case MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE: {
- boolean mute = (msg.arg1 != 0);
- if (mute != mAudioManager.isMicrophoneMute()) {
- IAudioService audio = mAudioServiceFactory.getAudioService();
- Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]",
- mute, audio == null);
- if (audio != null) {
- try {
- // We use the audio service directly here so that we can specify
- // the current user. Telecom runs in the system_server process which
- // may run as a separate user from the foreground user. If we
- // used AudioManager directly, we would change mute for the system's
- // user and not the current foreground, which we want to avoid.
- audio.setMicrophoneMute(
- mute, mContext.getOpPackageName(), getCurrentUserId());
-
- } catch (RemoteException e) {
- Log.e(this, e, "Remote exception while toggling mute.");
- }
- // TODO: Check microphone state after attempting to set to ensure that
- // our state corroborates AudioManager's state.
- }
- }
-
- break;
- }
case MSG_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS_FOR_CALL: {
int stream = msg.arg1;
mAudioManager.requestAudioFocusForCall(
@@ -159,48 +111,32 @@
private final Context mContext;
private final TelecomSystem.SyncRoot mLock;
- private final StatusBarNotifier mStatusBarNotifier;
- private final BluetoothManager mBluetoothManager;
- private final WiredHeadsetManager mWiredHeadsetManager;
- private final DockManager mDockManager;
private final CallsManager mCallsManager;
- private final AudioServiceFactory mAudioServiceFactory;
+ private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
- private CallAudioState mCallAudioState;
private int mAudioFocusStreamType;
private boolean mIsRinging;
private boolean mIsTonePlaying;
- private boolean mWasSpeakerOn;
private int mMostRecentlyUsedMode = AudioManager.MODE_IN_CALL;
private Call mCallToSpeedUpMTAudio = null;
- CallAudioManager(
+ public CallAudioManager(
Context context,
TelecomSystem.SyncRoot lock,
- StatusBarNotifier statusBarNotifier,
- WiredHeadsetManager wiredHeadsetManager,
- DockManager dockManager,
CallsManager callsManager,
- AudioServiceFactory audioServiceFactory) {
+ CallAudioRouteStateMachine callAudioRouteStateMachine) {
mContext = context;
mLock = lock;
mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_INITIALIZE, 0, 0).sendToTarget();
- mStatusBarNotifier = statusBarNotifier;
- mBluetoothManager = new BluetoothManager(context, this);
- mWiredHeadsetManager = wiredHeadsetManager;
mCallsManager = callsManager;
-
- mWiredHeadsetManager.addListener(this);
- mDockManager = dockManager;
- mDockManager.addListener(this);
-
- saveAudioState(getInitialAudioState(null));
mAudioFocusStreamType = STREAM_NONE;
- mAudioServiceFactory = audioServiceFactory;
+
+ mCallAudioRouteStateMachine = callAudioRouteStateMachine;
}
- CallAudioState getCallAudioState() {
- return mCallAudioState;
+ @VisibleForTesting
+ public CallAudioState getCallAudioState() {
+ return mCallAudioRouteStateMachine.getCurrentCallAudioState();
}
@Override
@@ -211,8 +147,7 @@
if (hasFocus() && getForegroundCall() == call) {
if (!call.isIncoming()) {
// Unmute new outgoing call.
- setSystemAudioState(false, mCallAudioState.getRoute(),
- mCallAudioState.getSupportedRouteMask());
+ mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.MUTE_OFF);
}
}
}
@@ -220,13 +155,13 @@
@Override
public void onCallRemoved(Call call) {
Log.v(this, "onCallRemoved");
+ if (mCallsManager.getCalls().isEmpty()) {
+ Log.v(this, "all calls removed, resetting system audio to default state");
+ mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.REINITIALIZE);
+ }
+
// If we didn't already have focus, there's nothing to do.
if (hasFocus()) {
- if (mCallsManager.getCalls().isEmpty()) {
- Log.v(this, "all calls removed, resetting system audio to default state");
- setInitialAudioState(null, false /* force */);
- mWasSpeakerOn = false;
- }
updateAudioStreamAndMode(call);
}
}
@@ -240,19 +175,12 @@
@Override
public void onIncomingCallAnswered(Call call) {
Log.v(this, "onIncomingCallAnswered");
- int route = mCallAudioState.getRoute();
- // We do two things:
- // (1) If this is the first call, then we can to turn on bluetooth if available.
- // (2) Unmute the audio for the new incoming call.
- boolean isOnlyCall = mCallsManager.getCalls().size() == 1;
- if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) {
- mBluetoothManager.connectBluetoothAudio();
- route = CallAudioState.ROUTE_BLUETOOTH;
+ if (mCallsManager.getCalls().size() == 1) {
+ mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.HAS_FOCUS);
}
- setSystemAudioState(false /* isMute */, route, mCallAudioState.getSupportedRouteMask());
-
if (call.can(android.telecom.Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO)) {
Log.v(this, "Speed up audio setup for IMS MT call.");
mCallToSpeedUpMTAudio = call;
@@ -272,73 +200,8 @@
updateAudioStreamAndMode(call);
}
- /**
- * Updates the audio route when the headset plugged in state changes. For example, if audio is
- * being routed over speakerphone and a headset is plugged in then switch to wired headset.
- */
- @Override
- public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
- // This can happen even when there are no calls and we don't have focus.
- if (!hasFocus()) {
- return;
- }
-
- boolean isCurrentlyWiredHeadset = mCallAudioState.getRoute()
- == CallAudioState.ROUTE_WIRED_HEADSET;
-
- int newRoute = mCallAudioState.getRoute(); // start out with existing route
- if (newIsPluggedIn) {
- newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
- } else if (isCurrentlyWiredHeadset) {
- Call call = getForegroundCall();
- boolean hasLiveCall = call != null && call.isAlive();
-
- if (hasLiveCall) {
- // In order of preference when a wireless headset is unplugged.
- if (mWasSpeakerOn) {
- newRoute = CallAudioState.ROUTE_SPEAKER;
- } else {
- newRoute = CallAudioState.ROUTE_EARPIECE;
- }
-
- // We don't automatically connect to bluetooth when user unplugs their wired headset
- // and they were previously using the wired. Wired and earpiece are effectively the
- // same choice in that they replace each other as an option when wired headsets
- // are plugged in and out. This means that keeping it earpiece is a bit more
- // consistent with the status quo. Bluetooth also has more danger associated with
- // choosing it in the wrong curcumstance because bluetooth devices can be
- // semi-public (like in a very-occupied car) where earpiece doesn't carry that risk.
- }
- }
-
- // We need to call this every time even if we do not change the route because the supported
- // routes changed either to include or not include WIRED_HEADSET.
- setSystemAudioState(mCallAudioState.isMuted(), newRoute, calculateSupportedRoutes());
- }
-
- @Override
- public void onDockChanged(boolean isDocked) {
- // This can happen even when there are no calls and we don't have focus.
- if (!hasFocus()) {
- return;
- }
-
- if (isDocked) {
- // Device just docked, turn to speakerphone. Only do so if the route is currently
- // earpiece so that we dont switch out of a BT headset or a wired headset.
- if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE) {
- setAudioRoute(CallAudioState.ROUTE_SPEAKER);
- }
- } else {
- // Device just undocked, remove from speakerphone if possible.
- if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
- setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
- }
- }
- }
-
void toggleMute() {
- mute(!mCallAudioState.isMuted());
+ mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.TOGGLE_MUTE);
}
void mute(boolean shouldMute) {
@@ -354,15 +217,8 @@
Log.v(this, "ignoring mute for emergency call");
}
- if (mCallAudioState.isMuted() != shouldMute) {
- // We user CallsManager's foreground call so that we dont ignore ringing calls
- // for logging purposes
- Log.event(mCallsManager.getForegroundCall(), Log.Events.MUTE,
- shouldMute ? "on" : "off");
-
- setSystemAudioState(shouldMute, mCallAudioState.getRoute(),
- mCallAudioState.getSupportedRouteMask());
- }
+ mCallAudioRouteStateMachine.sendMessage(shouldMute
+ ? CallAudioRouteStateMachine.MUTE_ON : CallAudioRouteStateMachine.MUTE_OFF);
}
/**
@@ -371,28 +227,30 @@
* @param route The new audio route to use. See {@link CallAudioState}.
*/
void setAudioRoute(int route) {
- // This can happen even when there are no calls and we don't have focus.
- if (!hasFocus()) {
- return;
- }
-
Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
-
- // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
- int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask());
-
- // If route is unsupported, do nothing.
- if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) {
- Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
- return;
- }
-
- if (mCallAudioState.getRoute() != newRoute) {
- // Remember the new speaker state so it can be restored when the user plugs and unplugs
- // a headset.
- mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
- setSystemAudioState(mCallAudioState.isMuted(), newRoute,
- mCallAudioState.getSupportedRouteMask());
+ switch (route) {
+ case CallAudioState.ROUTE_BLUETOOTH:
+ mCallAudioRouteStateMachine.sendMessage(
+ CallAudioRouteStateMachine.SWITCH_BLUETOOTH);
+ return;
+ case CallAudioState.ROUTE_SPEAKER:
+ mCallAudioRouteStateMachine.sendMessage(
+ CallAudioRouteStateMachine.SWITCH_SPEAKER);
+ return;
+ case CallAudioState.ROUTE_WIRED_HEADSET:
+ mCallAudioRouteStateMachine.sendMessage(
+ CallAudioRouteStateMachine.SWITCH_HEADSET);
+ return;
+ case CallAudioState.ROUTE_EARPIECE:
+ mCallAudioRouteStateMachine.sendMessage(
+ CallAudioRouteStateMachine.SWITCH_EARPIECE);
+ return;
+ case CallAudioState.ROUTE_WIRED_OR_EARPIECE:
+ mCallAudioRouteStateMachine.sendMessage(
+ CallAudioRouteStateMachine.SWITCH_WIRED_OR_EARPIECE);
+ return;
+ default:
+ Log.wtf(this, "Invalid route specified: %d", route);
}
}
@@ -426,44 +284,6 @@
}
}
- /**
- * Updates the audio routing according to the bluetooth state.
- */
- void onBluetoothStateChange(BluetoothManager bluetoothManager) {
- // This can happen even when there are no calls and we don't have focus.
- if (!hasFocus()) {
- return;
- }
-
- int supportedRoutes = calculateSupportedRoutes();
- int newRoute = mCallAudioState.getRoute();
- if (bluetoothManager.isBluetoothAudioConnectedOrPending()) {
- newRoute = CallAudioState.ROUTE_BLUETOOTH;
- } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
- newRoute = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE,
- supportedRoutes);
- // Do not switch to speaker when bluetooth disconnects.
- mWasSpeakerOn = false;
- }
-
- setSystemAudioState(mCallAudioState.isMuted(), newRoute, supportedRoutes);
- }
-
- boolean isBluetoothAudioOn() {
- return mBluetoothManager.isBluetoothAudioConnected();
- }
-
- boolean isBluetoothDeviceAvailable() {
- return mBluetoothManager.isBluetoothAvailable();
- }
-
- private void saveAudioState(CallAudioState callAudioState) {
- mCallAudioState = callAudioState;
- mStatusBarNotifier.notifyMute(mCallAudioState.isMuted());
- mStatusBarNotifier.notifySpeakerphone(mCallAudioState.getRoute()
- == CallAudioState.ROUTE_SPEAKER);
- }
-
private void onCallUpdated(Call call) {
updateAudioStreamAndMode(call);
if (call != null && call.getState() == CallState.ACTIVE &&
@@ -472,70 +292,6 @@
}
}
- private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
- setSystemAudioState(false /* force */, isMuted, route, supportedRouteMask);
- }
-
- private void setSystemAudioState(
- boolean force, boolean isMuted, int route, int supportedRouteMask) {
- if (!hasFocus()) {
- return;
- }
-
- CallAudioState oldAudioState = mCallAudioState;
- saveAudioState(new CallAudioState(isMuted, route, supportedRouteMask));
- if (!force && Objects.equals(oldAudioState, mCallAudioState)) {
- return;
- }
-
- Log.i(this, "setSystemAudioState: changing from %s to %s", oldAudioState, mCallAudioState);
- Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
- CallAudioState.audioRouteToString(mCallAudioState.getRoute()));
-
- mAudioManagerHandler.obtainMessage(
- MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE,
- mCallAudioState.isMuted() ? 1 : 0,
- 0)
- .sendToTarget();
-
- // Audio route.
- if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
- turnOnSpeaker(false);
- turnOnBluetooth(true);
- } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
- turnOnBluetooth(false);
- turnOnSpeaker(true);
- } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE ||
- mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {
- turnOnBluetooth(false);
- turnOnSpeaker(false);
- }
-
- if (!oldAudioState.equals(mCallAudioState)) {
- mCallsManager.onCallAudioStateChanged(oldAudioState, mCallAudioState);
- updateAudioForForegroundCall();
- }
- }
-
- private void turnOnSpeaker(boolean on) {
- mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_TURN_ON_SPEAKER, on ? 1 : 0, 0)
- .sendToTarget();
- }
-
- private void turnOnBluetooth(boolean on) {
- if (mBluetoothManager.isBluetoothAvailable()) {
- boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();
- if (on != isAlreadyOn) {
- Log.i(this, "connecting bluetooth %s", on);
- if (on) {
- mBluetoothManager.connectBluetoothAudio();
- } else {
- mBluetoothManager.disconnectBluetoothAudio();
- }
- }
- }
- }
-
private void updateAudioStreamAndMode() {
updateAudioStreamAndMode(null /* call */);
}
@@ -544,7 +300,6 @@
Log.i(this, "updateAudioStreamAndMode : mIsRinging: %b, mIsTonePlaying: %b, call: %s",
mIsRinging, mIsTonePlaying, callToUpdate);
- boolean wasVoiceCall = mAudioFocusStreamType == AudioManager.STREAM_VOICE_CALL;
if (mIsRinging) {
Log.i(this, "updateAudioStreamAndMode : ringing");
requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
@@ -587,14 +342,6 @@
// focus will be correctly abandoned by the if clause above.
}
}
-
- boolean isVoiceCall = mAudioFocusStreamType == AudioManager.STREAM_VOICE_CALL;
-
- // If we transition from not a voice call to a voice call, we need to set an initial audio
- // state for the call.
- if (!wasVoiceCall && isVoiceCall) {
- setInitialAudioState(callToUpdate, true /* force */);
- }
}
private void requestAudioFocusAndSetMode(int stream, int mode) {
@@ -615,6 +362,8 @@
.sendToTarget();
}
mAudioFocusStreamType = stream;
+ mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.HAS_FOCUS);
setMode(mode);
}
@@ -628,6 +377,8 @@
mAudioFocusStreamType = STREAM_NONE;
mCallToSpeedUpMTAudio = null;
}
+ mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.NO_FOCUS);
}
/**
@@ -640,77 +391,11 @@
mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_SET_MODE, newMode, 0).sendToTarget();
}
- private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
- // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
- // ROUTE_WIRED_OR_EARPIECE so that callers dont have to make a call to check which is
- // supported before calling setAudioRoute.
- if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
- route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
- if (route == 0) {
- Log.wtf(this, "One of wired headset or earpiece should always be valid.");
- // assume earpiece in this case.
- route = CallAudioState.ROUTE_EARPIECE;
- }
- }
- return route;
- }
-
- private int calculateSupportedRoutes() {
- int routeMask = CallAudioState.ROUTE_SPEAKER;
-
- if (mWiredHeadsetManager.isPluggedIn()) {
- routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
- } else {
- routeMask |= CallAudioState.ROUTE_EARPIECE;
- }
-
- if (mBluetoothManager.isBluetoothAvailable()) {
- routeMask |= CallAudioState.ROUTE_BLUETOOTH;
- }
-
- return routeMask;
- }
-
- private CallAudioState getInitialAudioState(Call call) {
- int supportedRouteMask = calculateSupportedRoutes();
- int route = selectWiredOrEarpiece(
- CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
-
- // We want the UI to indicate that "bluetooth is in use" in two slightly different cases:
- // (a) The obvious case: if a bluetooth headset is currently in use for an ongoing call.
- // (b) The not-so-obvious case: if an incoming call is ringing, and we expect that audio
- // *will* be routed to a bluetooth headset once the call is answered. In this case, just
- // check if the headset is available. Note this only applies when we are dealing with
- // the first call.
- if (call != null && mBluetoothManager.isBluetoothAvailable()) {
- switch(call.getState()) {
- case CallState.ACTIVE:
- case CallState.ON_HOLD:
- case CallState.DIALING:
- case CallState.CONNECTING:
- case CallState.RINGING:
- route = CallAudioState.ROUTE_BLUETOOTH;
- break;
- default:
- break;
- }
- }
-
- return new CallAudioState(false, route, supportedRouteMask);
- }
-
- private void setInitialAudioState(Call call, boolean force) {
- CallAudioState audioState = getInitialAudioState(call);
- Log.i(this, "setInitialAudioState : audioState = %s, call = %s", audioState, call);
- setSystemAudioState(
- force, audioState.isMuted(), audioState.getRoute(),
- audioState.getSupportedRouteMask());
- }
-
private void updateAudioForForegroundCall() {
Call call = mCallsManager.getForegroundCall();
if (call != null && call.getConnectionService() != null) {
- call.getConnectionService().onCallAudioStateChanged(call, mCallAudioState);
+ call.getConnectionService().onCallAudioStateChanged(call,
+ mCallAudioRouteStateMachine.getCurrentCallAudioState());
}
}
@@ -738,19 +423,6 @@
return mAudioFocusStreamType != STREAM_NONE;
}
- private int getCurrentUserId() {
- final long ident = Binder.clearCallingIdentity();
- try {
- UserInfo currentUser = ActivityManagerNative.getDefault().getCurrentUser();
- return currentUser.id;
- } catch (RemoteException e) {
- // Activity manager not running, nothing we can do assume user 0.
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- return UserHandle.USER_OWNER;
- }
-
/**
* Translates an {@link AudioManager} stream type to a human-readable string description.
*
@@ -813,23 +485,10 @@
* @param pw The {@code IndentingPrintWriter} to write the state to.
*/
public void dump(IndentingPrintWriter pw) {
- pw.println("mAudioState: " + mCallAudioState);
- pw.println("mBluetoothManager:");
- pw.increaseIndent();
- mBluetoothManager.dump(pw);
- pw.decreaseIndent();
- if (mWiredHeadsetManager != null) {
- pw.println("mWiredHeadsetManager:");
- pw.increaseIndent();
- mWiredHeadsetManager.dump(pw);
- pw.decreaseIndent();
- } else {
- pw.println("mWiredHeadsetManager: null");
- }
+ pw.println("mAudioState: " + mCallAudioRouteStateMachine.getCurrentCallAudioState());
pw.println("mAudioFocusStreamType: " + streamTypeToString(mAudioFocusStreamType));
pw.println("mIsRinging: " + mIsRinging);
pw.println("mIsTonePlaying: " + mIsTonePlaying);
- pw.println("mWasSpeakerOn: " + mWasSpeakerOn);
pw.println("mMostRecentlyUsedMode: " + modeToString(mMostRecentlyUsedMode));
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
new file mode 100644
index 0000000..26a4cb8
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 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;
+
+/**
+ * A class that acts as a listener to things that could change call audio routing, namely
+ * bluetooth status, wired headset status, and dock status.
+ */
+public class CallAudioRoutePeripheralAdapter implements BluetoothManager.BluetoothStateListener,
+ WiredHeadsetManager.Listener, DockManager.Listener {
+ private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+ private final BluetoothManager mBluetoothManager;
+
+ public CallAudioRoutePeripheralAdapter(
+ CallAudioRouteStateMachine callAudioRouteStateMachine,
+ BluetoothManager bluetoothManager,
+ WiredHeadsetManager wiredHeadsetManager,
+ DockManager dockManager) {
+ mCallAudioRouteStateMachine = callAudioRouteStateMachine;
+ mBluetoothManager = bluetoothManager;
+
+ mBluetoothManager.setBluetoothStateListener(this);
+ wiredHeadsetManager.addListener(this);
+ dockManager.addListener(this);
+ }
+
+ @Override
+ public void onBluetoothStateChange(BluetoothManager bluetoothManager) {
+ if (bluetoothManager.isBluetoothAvailable()) {
+ mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.CONNECT_BLUETOOTH);
+ } else {
+ mCallAudioRouteStateMachine.sendMessage(
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH);
+ }
+ }
+
+ public boolean isBluetoothAudioOn() {
+ return mBluetoothManager.isBluetoothAudioConnected();
+ }
+
+ /**
+ * Updates the audio route when the headset plugged in state changes. For example, if audio is
+ * being routed over speakerphone and a headset is plugged in then switch to wired headset.
+ */
+ @Override
+ public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+ if (!oldIsPluggedIn && newIsPluggedIn) {
+ mCallAudioRouteStateMachine.sendMessage(
+ CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
+ } else if (oldIsPluggedIn && !newIsPluggedIn){
+ mCallAudioRouteStateMachine.sendMessage(
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+ }
+ }
+
+ @Override
+ public void onDockChanged(boolean isDocked) {
+ mCallAudioRouteStateMachine.sendMessage(
+ isDocked ? CallAudioRouteStateMachine.CONNECT_DOCK
+ : CallAudioRouteStateMachine.DISCONNECT_DOCK
+ );
+ }
+}
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
new file mode 100644
index 0000000..e6ef8f4
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -0,0 +1,1132 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+
+import android.app.ActivityManagerNative;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.os.Binder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.telecom.CallAudioState;
+
+import com.android.internal.util.IState;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.util.HashMap;
+
+/**
+ * This class describes the available routes of a call as a state machine.
+ * Transitions are caused solely by the commands sent as messages. Possible values for msg.what
+ * are defined as event constants in this file.
+ *
+ * The eight states are all instances of the abstract base class, {@link AudioState}. Each state
+ * is a combination of one of the four audio routes (earpiece, wired headset, bluetooth, and
+ * speakerphone) and audio focus status (active or quiescent).
+ *
+ * Messages are processed first by the processMessage method in the base class, AudioState.
+ * Any messages not completely handled by AudioState are further processed by the same method in
+ * the route-specific abstract classes: {@link EarpieceRoute}, {@link HeadsetRoute},
+ * {@link BluetoothRoute}, and {@link SpeakerRoute}. Finally, messages that are not handled at
+ * this level are then processed by the classes corresponding to the state instances themselves.
+ *
+ * There are several variables carrying additional state. These include:
+ * mAvailableRoutes: A bitmask describing which audio routes are available
+ * mWasOnSpeaker: A boolean indicating whether we should switch to speakerphone after disconnecting
+ * from a wired headset
+ * mIsMuted: a boolean indicating whether the audio is muted
+ */
+public class CallAudioRouteStateMachine extends StateMachine {
+ /** Direct the audio stream through the device's earpiece. */
+ public static final int ROUTE_EARPIECE = CallAudioState.ROUTE_EARPIECE;
+
+ /** Direct the audio stream through Bluetooth. */
+ public static final int ROUTE_BLUETOOTH = CallAudioState.ROUTE_BLUETOOTH;
+
+ /** Direct the audio stream through a wired headset. */
+ public static final int ROUTE_WIRED_HEADSET = CallAudioState.ROUTE_WIRED_HEADSET;
+
+ /** Direct the audio stream through the device's speakerphone. */
+ public static final int ROUTE_SPEAKER = CallAudioState.ROUTE_SPEAKER;
+
+ /** Valid values for msg.what */
+ public static final int CONNECT_WIRED_HEADSET = 1;
+ public static final int DISCONNECT_WIRED_HEADSET = 2;
+ public static final int CONNECT_BLUETOOTH = 3;
+ public static final int DISCONNECT_BLUETOOTH = 4;
+ public static final int CONNECT_DOCK = 5;
+ public static final int DISCONNECT_DOCK = 6;
+
+ public static final int SWITCH_EARPIECE = 1001;
+ public static final int SWITCH_BLUETOOTH = 1002;
+ public static final int SWITCH_HEADSET = 1003;
+ public static final int SWITCH_SPEAKER = 1004;
+ public static final int SWITCH_WIRED_OR_EARPIECE = 1005;
+
+ public static final int REINITIALIZE = 2001;
+
+ public static final int MUTE_ON = 3001;
+ public static final int MUTE_OFF = 3002;
+ public static final int TOGGLE_MUTE = 3003;
+
+ public static final int SWITCH_FOCUS = 4001;
+
+ /** Valid values for mAudioFocusType */
+ public static final int NO_FOCUS = 1;
+ public static final int HAS_FOCUS = 2;
+
+ private static final String ACTIVE_EARPIECE_ROUTE_NAME = "ActiveEarpieceRoute";
+ private static final String ACTIVE_BLUETOOTH_ROUTE_NAME = "ActiveBluetoothRoute";
+ private static final String ACTIVE_SPEAKER_ROUTE_NAME = "ActiveSpeakerRoute";
+ private static final String ACTIVE_HEADSET_ROUTE_NAME = "ActiveHeadsetRoute";
+ private static final String QUIESCENT_EARPIECE_ROUTE_NAME = "QuiescentEarpieceRoute";
+ private static final String QUIESCENT_BLUETOOTH_ROUTE_NAME = "QuiescentBluetoothRoute";
+ private static final String QUIESCENT_SPEAKER_ROUTE_NAME = "QuiescentSpeakerRoute";
+ private static final String QUIESCENT_HEADSET_ROUTE_NAME = "QuiescentHeadsetRoute";
+
+ public static final String NAME = CallAudioRouteStateMachine.class.getName();
+
+ abstract class AudioState extends State {
+ @Override
+ public void enter() {
+ super.enter();
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Entering state " + getName());
+ }
+
+ @Override
+ public void exit() {
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Leaving state " + getName());
+ super.exit();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ Log.d(this, "Message received: %s", msg);
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Wired headset connected");
+ mAvailableRoutes &= ~ROUTE_EARPIECE;
+ mAvailableRoutes |= ROUTE_WIRED_HEADSET;
+ return NOT_HANDLED;
+ case CONNECT_BLUETOOTH:
+ // This case is here because the bluetooth manager sends out a lot of spurious
+ // state changes, and no layers above this one can tell which are actual changes
+ // in connection/disconnection status. This filters it out.
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ return HANDLED; // Do nothing if we already have bluetooth as enabled.
+ } else {
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Bluetooth connected");
+ mAvailableRoutes |= ROUTE_BLUETOOTH;
+ return NOT_HANDLED;
+ }
+ case DISCONNECT_WIRED_HEADSET:
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Wired headset disconnected");
+ mAvailableRoutes &= ~ROUTE_WIRED_HEADSET;
+ mAvailableRoutes |= ROUTE_EARPIECE;
+ return NOT_HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) == 0) {
+ return HANDLED;
+ } else {
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Bluetooth disconnected");
+ mAvailableRoutes &= ~ROUTE_BLUETOOTH;
+ return NOT_HANDLED;
+ }
+ case SWITCH_WIRED_OR_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ sendInternalMessage(SWITCH_EARPIECE);
+ } else if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ sendInternalMessage(SWITCH_HEADSET);
+ } else {
+ Log.e(this, new IllegalStateException(),
+ "Neither headset nor earpiece are available. Defaulting to " +
+ "earpiece.");
+ sendInternalMessage(SWITCH_EARPIECE);
+ }
+ return HANDLED;
+ case REINITIALIZE:
+ CallAudioState initState = getInitialAudioState();
+ mAvailableRoutes = initState.getSupportedRouteMask();
+ mIsMuted = initState.isMuted();
+ mWasOnSpeaker = initState.getRoute() == ROUTE_SPEAKER;
+ transitionTo(mRouteCodeToQuiescentState.get(initState.getRoute()));
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+
+ // Behavior will depend on whether the state is an active one or a quiescent one.
+ abstract public void updateSystemAudioState();
+ abstract public boolean isActive();
+ }
+
+ class ActiveEarpieceRoute extends EarpieceRoute {
+ @Override
+ public String getName() {
+ return ACTIVE_EARPIECE_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ setSpeakerphoneOn(false);
+ setBluetoothOn(false);
+ CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
+ mAvailableRoutes);
+ setSystemAudioState(mCurrentCallAudioState, newState);
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ CallAudioState oldAudioState = mCurrentCallAudioState;
+ updateInternalCallAudioState();
+ setSystemAudioState(oldAudioState, mCurrentCallAudioState);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ // Nothing to do here
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mActiveBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mActiveHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mActiveSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == NO_FOCUS) {
+ transitionTo(mQuiescentEarpieceRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class QuiescentEarpieceRoute extends EarpieceRoute {
+ @Override
+ public String getName() {
+ return QUIESCENT_EARPIECE_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ // Nothing to do here
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mQuiescentBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mQuiescentHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mQuiescentSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == HAS_FOCUS) {
+ transitionTo(mActiveEarpieceRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ abstract class EarpieceRoute extends AudioState {
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ sendInternalMessage(SWITCH_HEADSET);
+ return HANDLED;
+ case CONNECT_BLUETOOTH:
+ sendInternalMessage(SWITCH_BLUETOOTH);
+ return HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case DISCONNECT_WIRED_HEADSET:
+ Log.e(this, new IllegalStateException(),
+ "Wired headset should not go from connected to not when on " +
+ "earpiece");
+ updateSystemAudioState();
+ return HANDLED;
+ case CONNECT_DOCK:
+ sendInternalMessage(SWITCH_SPEAKER);
+ return HANDLED;
+ case DISCONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class ActiveHeadsetRoute extends HeadsetRoute {
+ @Override
+ public String getName() {
+ return ACTIVE_HEADSET_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ setSpeakerphoneOn(false);
+ setBluetoothOn(false);
+ CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
+ mAvailableRoutes);
+ setSystemAudioState(mCurrentCallAudioState, newState);
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ CallAudioState oldAudioState = mCurrentCallAudioState;
+ updateInternalCallAudioState();
+ setSystemAudioState(oldAudioState, mCurrentCallAudioState);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mActiveEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mActiveBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mActiveSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == NO_FOCUS) {
+ transitionTo(mQuiescentHeadsetRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class QuiescentHeadsetRoute extends HeadsetRoute {
+ @Override
+ public String getName() {
+ return QUIESCENT_HEADSET_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mQuiescentEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mQuiescentBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mQuiescentSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == HAS_FOCUS) {
+ transitionTo(mActiveHeadsetRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ abstract class HeadsetRoute extends AudioState {
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ Log.e(this, new IllegalStateException(),
+ "Wired headset should already be connected.");
+ mAvailableRoutes |= ROUTE_WIRED_HEADSET;
+ updateSystemAudioState();
+ return HANDLED;
+ case CONNECT_BLUETOOTH:
+ sendInternalMessage(SWITCH_BLUETOOTH);
+ return HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case DISCONNECT_WIRED_HEADSET:
+ if (mWasOnSpeaker) {
+ sendInternalMessage(SWITCH_SPEAKER);
+ } else {
+ sendInternalMessage(SWITCH_EARPIECE);
+ }
+ return HANDLED;
+ case CONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ case DISCONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class ActiveBluetoothRoute extends BluetoothRoute {
+ @Override
+ public String getName() {
+ return ACTIVE_BLUETOOTH_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ setSpeakerphoneOn(false);
+ setBluetoothOn(true);
+ CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
+ mAvailableRoutes);
+ setSystemAudioState(mCurrentCallAudioState, newState);
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ CallAudioState oldAudioState = mCurrentCallAudioState;
+ updateInternalCallAudioState();
+ setSystemAudioState(oldAudioState, mCurrentCallAudioState);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mActiveEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mActiveHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mActiveSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == NO_FOCUS) {
+ transitionTo(mQuiescentBluetoothRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class QuiescentBluetoothRoute extends BluetoothRoute {
+ @Override
+ public String getName() {
+ return QUIESCENT_BLUETOOTH_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mQuiescentEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mQuiescentHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mQuiescentSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == HAS_FOCUS) {
+ transitionTo(mActiveBluetoothRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ abstract class BluetoothRoute extends AudioState {
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ sendInternalMessage(SWITCH_HEADSET);
+ return HANDLED;
+ case CONNECT_BLUETOOTH:
+ // We can't tell when a change in bluetooth state corresponds to an
+ // actual connection or disconnection, so we'll just ignore it if we're already
+ // in the bluetooth route.
+ return HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ sendInternalMessage(SWITCH_WIRED_OR_EARPIECE);
+ mWasOnSpeaker = false;
+ return HANDLED;
+ case DISCONNECT_WIRED_HEADSET:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case CONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ case DISCONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class ActiveSpeakerRoute extends SpeakerRoute {
+ @Override
+ public String getName() {
+ return ACTIVE_SPEAKER_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ mWasOnSpeaker = true;
+ setSpeakerphoneOn(true);
+ setBluetoothOn(false);
+ CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER,
+ mAvailableRoutes);
+ setSystemAudioState(mCurrentCallAudioState, newState);
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ CallAudioState oldAudioState = mCurrentCallAudioState;
+ updateInternalCallAudioState();
+ setSystemAudioState(oldAudioState, mCurrentCallAudioState);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch(msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mActiveEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mActiveBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mActiveHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == NO_FOCUS) {
+ transitionTo(mQuiescentSpeakerRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class QuiescentSpeakerRoute extends SpeakerRoute {
+ @Override
+ public String getName() {
+ return QUIESCENT_SPEAKER_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ // Omit setting mWasOnSpeaker to true here, since this does not reflect a call
+ // actually being on speakerphone.
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch(msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mQuiescentEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mQuiescentBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mQuiescentHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == HAS_FOCUS) {
+ transitionTo(mActiveSpeakerRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ abstract class SpeakerRoute extends AudioState {
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ sendInternalMessage(SWITCH_HEADSET);
+ return HANDLED;
+ case CONNECT_BLUETOOTH:
+ sendInternalMessage(SWITCH_BLUETOOTH);
+ return HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case DISCONNECT_WIRED_HEADSET:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case CONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ case DISCONNECT_DOCK:
+ sendInternalMessage(SWITCH_WIRED_OR_EARPIECE);
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ private final ActiveEarpieceRoute mActiveEarpieceRoute = new ActiveEarpieceRoute();
+ private final ActiveHeadsetRoute mActiveHeadsetRoute = new ActiveHeadsetRoute();
+ private final ActiveBluetoothRoute mActiveBluetoothRoute = new ActiveBluetoothRoute();
+ private final ActiveSpeakerRoute mActiveSpeakerRoute = new ActiveSpeakerRoute();
+ private final QuiescentEarpieceRoute mQuiescentEarpieceRoute = new QuiescentEarpieceRoute();
+ private final QuiescentHeadsetRoute mQuiescentHeadsetRoute = new QuiescentHeadsetRoute();
+ private final QuiescentBluetoothRoute mQuiescentBluetoothRoute = new QuiescentBluetoothRoute();
+ private final QuiescentSpeakerRoute mQuiescentSpeakerRoute = new QuiescentSpeakerRoute();
+
+ /**
+ * A few pieces of hidden state. Used to avoid exponential explosion of number of explicit
+ * states
+ */
+ private int mAvailableRoutes;
+ private boolean mWasOnSpeaker;
+ private boolean mIsMuted;
+
+ private final Context mContext;
+ private final CallsManager mCallsManager;
+ private final AudioManager mAudioManager;
+ private final BluetoothManager mBluetoothManager;
+ private final WiredHeadsetManager mWiredHeadsetManager;
+ private final StatusBarNotifier mStatusBarNotifier;
+ private final CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+
+ private HashMap<String, Integer> mStateNameToRouteCode;
+ private HashMap<Integer, AudioState> mRouteCodeToQuiescentState;
+
+ // CallAudioState is used as an interface to communicate with many other system components.
+ // No internal state transitions should depend on this variable.
+ private CallAudioState mCurrentCallAudioState;
+
+ public CallAudioRouteStateMachine(
+ Context context,
+ CallsManager callsManager,
+ BluetoothManager bluetoothManager,
+ WiredHeadsetManager wiredHeadsetManager,
+ StatusBarNotifier statusBarNotifier,
+ CallAudioManager.AudioServiceFactory audioServiceFactory) {
+ super(NAME);
+ addState(mActiveEarpieceRoute);
+ addState(mActiveHeadsetRoute);
+ addState(mActiveBluetoothRoute);
+ addState(mActiveSpeakerRoute);
+ addState(mQuiescentEarpieceRoute);
+ addState(mQuiescentHeadsetRoute);
+ addState(mQuiescentBluetoothRoute);
+ addState(mQuiescentSpeakerRoute);
+
+ mContext = context;
+ mCallsManager = callsManager;
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mBluetoothManager = bluetoothManager;
+ mWiredHeadsetManager = wiredHeadsetManager;
+ mStatusBarNotifier = statusBarNotifier;
+ mAudioServiceFactory = audioServiceFactory;
+
+ mStateNameToRouteCode = new HashMap<>(8);
+ mStateNameToRouteCode.put(mQuiescentEarpieceRoute.getName(), ROUTE_EARPIECE);
+ mStateNameToRouteCode.put(mQuiescentBluetoothRoute.getName(), ROUTE_BLUETOOTH);
+ mStateNameToRouteCode.put(mQuiescentHeadsetRoute.getName(), ROUTE_WIRED_HEADSET);
+ mStateNameToRouteCode.put(mQuiescentSpeakerRoute.getName(), ROUTE_SPEAKER);
+ mStateNameToRouteCode.put(mActiveEarpieceRoute.getName(), ROUTE_EARPIECE);
+ mStateNameToRouteCode.put(mActiveBluetoothRoute.getName(), ROUTE_BLUETOOTH);
+ mStateNameToRouteCode.put(mActiveHeadsetRoute.getName(), ROUTE_WIRED_HEADSET);
+ mStateNameToRouteCode.put(mActiveSpeakerRoute.getName(), ROUTE_SPEAKER);
+
+ mRouteCodeToQuiescentState = new HashMap<>(4);
+ mRouteCodeToQuiescentState.put(ROUTE_EARPIECE, mQuiescentEarpieceRoute);
+ mRouteCodeToQuiescentState.put(ROUTE_BLUETOOTH, mQuiescentBluetoothRoute);
+ mRouteCodeToQuiescentState.put(ROUTE_SPEAKER, mQuiescentSpeakerRoute);
+ mRouteCodeToQuiescentState.put(ROUTE_WIRED_HEADSET, mQuiescentHeadsetRoute);
+ initialize();
+ }
+
+ /**
+ * Initializes the state machine with info on initial audio route, supported audio routes,
+ * and mute status.
+ */
+ public void initialize() {
+ CallAudioState initState = getInitialAudioState();
+ initialize(initState);
+ }
+
+ public void initialize(CallAudioState initState) {
+ mCurrentCallAudioState = initState;
+ mAvailableRoutes = initState.getSupportedRouteMask();
+ mIsMuted = initState.isMuted();
+ mWasOnSpeaker = initState.getRoute() == ROUTE_SPEAKER;
+
+ mStatusBarNotifier.notifyMute(initState.isMuted());
+ mStatusBarNotifier.notifySpeakerphone(initState.getRoute() == CallAudioState.ROUTE_SPEAKER);
+ setInitialState(mRouteCodeToQuiescentState.get(initState.getRoute()));
+ start();
+ }
+
+ /**
+ * Getter for the current CallAudioState object that the state machine is keeping track of.
+ * Used for compatibility purposes.
+ */
+ public CallAudioState getCurrentCallAudioState() {
+ return mCurrentCallAudioState;
+ }
+
+ /**
+ * This is for state-independent changes in audio route (i.e. muting)
+ * @param msg that couldn't be handled.
+ */
+ @Override
+ protected void unhandledMessage(Message msg) {
+ CallAudioState newCallAudioState;
+ switch (msg.what) {
+ case MUTE_ON:
+ setMuteOn(true);
+ newCallAudioState = new CallAudioState(mIsMuted,
+ mCurrentCallAudioState.getRoute(),
+ mAvailableRoutes);
+ setSystemAudioState(mCurrentCallAudioState, newCallAudioState);
+ updateInternalCallAudioState();
+ return;
+ case MUTE_OFF:
+ setMuteOn(false);
+ newCallAudioState = new CallAudioState(mIsMuted,
+ mCurrentCallAudioState.getRoute(),
+ mAvailableRoutes);
+ setSystemAudioState(mCurrentCallAudioState, newCallAudioState);
+ updateInternalCallAudioState();
+ return;
+ case TOGGLE_MUTE:
+ if (mIsMuted) {
+ sendInternalMessage(MUTE_OFF);
+ } else {
+ sendInternalMessage(MUTE_ON);
+ }
+ return;
+ default:
+ Log.e(this, new IllegalStateException(),
+ "Unexpected message code");
+ }
+ }
+
+ private void setSpeakerphoneOn(boolean on) {
+ if (mAudioManager.isSpeakerphoneOn() != on) {
+ Log.i(this, "turning speaker phone %s", on);
+ mAudioManager.setSpeakerphoneOn(on);
+ }
+ }
+
+ private void setBluetoothOn(boolean on) {
+ if (mBluetoothManager.isBluetoothAvailable()) {
+ boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();
+ if (on != isAlreadyOn) {
+ Log.i(this, "connecting bluetooth %s", on);
+ if (on) {
+ mBluetoothManager.connectBluetoothAudio();
+ } else {
+ mBluetoothManager.disconnectBluetoothAudio();
+ }
+ }
+ }
+ }
+
+ private void setMuteOn(boolean mute) {
+ mIsMuted = mute;
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.MUTE,
+ mute ? "on" : "off");
+ if (mute != mAudioManager.isMicrophoneMute() && isInActiveState()) {
+ IAudioService audio = mAudioServiceFactory.getAudioService();
+ Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]",
+ mute, audio == null);
+ if (audio != null) {
+ try {
+ // We use the audio service directly here so that we can specify
+ // the current user. Telecom runs in the system_server process which
+ // may run as a separate user from the foreground user. If we
+ // used AudioManager directly, we would change mute for the system's
+ // user and not the current foreground, which we want to avoid.
+ audio.setMicrophoneMute(
+ mute, mContext.getOpPackageName(), getCurrentUserId());
+
+ } catch (RemoteException e) {
+ Log.e(this, e, "Remote exception while toggling mute.");
+ }
+ // TODO: Check microphone state after attempting to set to ensure that
+ // our state corroborates AudioManager's state.
+ }
+ }
+ }
+
+ /**
+ * Updates the CallAudioState object from current internal state. The result is used for
+ * external communication only.
+ */
+ private void updateInternalCallAudioState() {
+ IState currentState = getCurrentState();
+ if (currentState == null) {
+ Log.e(this, new IllegalStateException(), "Current state should never be null" +
+ " when updateInternalCallAudioState is called.");
+ mCurrentCallAudioState = new CallAudioState(
+ mIsMuted, mCurrentCallAudioState.getRoute(), mAvailableRoutes);
+ return;
+ }
+ int currentRoute = mStateNameToRouteCode.get(currentState.getName());
+ mCurrentCallAudioState = new CallAudioState(mIsMuted, currentRoute, mAvailableRoutes);
+ }
+
+ private void setSystemAudioState(CallAudioState oldCallAudioState,
+ CallAudioState newCallAudioState) {
+ Log.i(this, "setSystemAudioState: changing from %s to %s", oldCallAudioState,
+ newCallAudioState);
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ CallAudioState.audioRouteToString(newCallAudioState.getRoute()));
+
+ if (!oldCallAudioState.equals(newCallAudioState)) {
+ mCallsManager.onCallAudioStateChanged(oldCallAudioState, newCallAudioState);
+ updateAudioForForegroundCall(newCallAudioState);
+ }
+ }
+
+ private void updateAudioForForegroundCall(CallAudioState newCallAudioState) {
+ Call call = mCallsManager.getForegroundCall();
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
+ }
+ }
+
+ private int calculateSupportedRoutes() {
+ int routeMask = CallAudioState.ROUTE_SPEAKER;
+
+ if (mWiredHeadsetManager.isPluggedIn()) {
+ routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+ } else {
+ routeMask |= CallAudioState.ROUTE_EARPIECE;
+ }
+
+ if (mBluetoothManager.isBluetoothAvailable()) {
+ routeMask |= CallAudioState.ROUTE_BLUETOOTH;
+ }
+
+ return routeMask;
+ }
+
+ private void sendInternalMessage(int messageCode) {
+ // Internal messages are messages which the state machine sends to itself in the
+ // course of processing externally-sourced messages. We want to send these messages at
+ // the front of the queue in order to make actions appear atomic to the user and to
+ // prevent scenarios such as these:
+ // 1. State machine handler thread is suspended for some reason.
+ // 2. Headset gets connected (sends CONNECT_HEADSET).
+ // 3. User switches to speakerphone in the UI (sends SWITCH_SPEAKER).
+ // 4. State machine handler is un-suspended.
+ // 5. State machine handler processes the CONNECT_HEADSET message and sends
+ // SWITCH_HEADSET at end of queue.
+ // 6. State machine handler processes SWITCH_SPEAKER.
+ // 7. State machine handler processes SWITCH_HEADSET.
+ sendMessageAtFrontOfQueue(messageCode);
+ }
+
+ private CallAudioState getInitialAudioState() {
+ int supportedRouteMask = calculateSupportedRoutes();
+ int route = (supportedRouteMask & ROUTE_WIRED_HEADSET) != 0
+ ? ROUTE_WIRED_HEADSET : ROUTE_EARPIECE;
+ if ((supportedRouteMask & ROUTE_BLUETOOTH) != 0) {
+ route = ROUTE_BLUETOOTH;
+ }
+
+ return new CallAudioState(false, route, supportedRouteMask);
+ }
+
+ private int getCurrentUserId() {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ UserInfo currentUser = ActivityManagerNative.getDefault().getCurrentUser();
+ return currentUser.id;
+ } catch (RemoteException e) {
+ // Activity manager not running, nothing we can do assume user 0.
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return UserHandle.USER_OWNER;
+ }
+
+ private boolean isInActiveState() {
+ AudioState currentState = (AudioState) getCurrentState();
+ if (currentState == null) {
+ Log.w(this, "Current state is null, assuming inactive state");
+ return false;
+ }
+ return currentState.isActive();
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 12ee98e..c6bd951 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -132,6 +132,7 @@
new ConcurrentHashMap<CallsManagerListener, Boolean>(16, 0.9f, 1));
private final HeadsetMediaButton mHeadsetMediaButton;
private final WiredHeadsetManager mWiredHeadsetManager;
+ private final BluetoothManager mBluetoothManager;
private final DockManager mDockManager;
private final TtyManager mTtyManager;
private final ProximitySensorManager mProximitySensorManager;
@@ -171,7 +172,9 @@
HeadsetMediaButtonFactory headsetMediaButtonFactory,
ProximitySensorManagerFactory proximitySensorManagerFactory,
InCallWakeLockControllerFactory inCallWakeLockControllerFactory,
- CallAudioManager.AudioServiceFactory audioServiceFactory) {
+ CallAudioManager.AudioServiceFactory audioServiceFactory,
+ BluetoothManager bluetoothManager,
+ WiredHeadsetManager wiredHeadsetManager) {
mContext = context;
mLock = lock;
mContactsAsyncHelper = contactsAsyncHelper;
@@ -179,13 +182,29 @@
mPhoneAccountRegistrar = phoneAccountRegistrar;
mMissedCallNotifier = missedCallNotifier;
StatusBarNotifier statusBarNotifier = new StatusBarNotifier(context, this);
- mWiredHeadsetManager = new WiredHeadsetManager(context);
+ mWiredHeadsetManager = wiredHeadsetManager;
+ mBluetoothManager = bluetoothManager;
mDockManager = new DockManager(context);
- mCallAudioManager = new CallAudioManager(
- context, mLock, statusBarNotifier,
- mWiredHeadsetManager, mDockManager, this, audioServiceFactory);
- InCallTonePlayer.Factory playerFactory =
- new InCallTonePlayer.Factory(mCallAudioManager, lock);
+ CallAudioRouteStateMachine callAudioRouteStateMachine = new CallAudioRouteStateMachine(
+ context,
+ this,
+ bluetoothManager,
+ wiredHeadsetManager,
+ statusBarNotifier,
+ audioServiceFactory
+ );
+ CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
+ new CallAudioRoutePeripheralAdapter(
+ callAudioRouteStateMachine,
+ bluetoothManager,
+ wiredHeadsetManager,
+ mDockManager);
+
+ mCallAudioManager = new CallAudioManager(context, mLock, this, callAudioRouteStateMachine);
+
+ InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager,
+ callAudioRoutePeripheralAdapter, lock);
+
RingtoneFactory ringtoneFactory = new RingtoneFactory(context);
SystemVibrator systemVibrator = new SystemVibrator(context);
AsyncRingtonePlayer asyncRingtonePlayer = new AsyncRingtonePlayer();
@@ -193,7 +212,7 @@
mRinger = new Ringer(
mCallAudioManager, this, playerFactory, context, systemSettingsUtil,
asyncRingtonePlayer, ringtoneFactory, systemVibrator);
- mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock);
+ mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock);
mTtyManager = new TtyManager(context, mWiredHeadsetManager);
mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
@@ -846,7 +865,7 @@
call.answer(videoState);
if (VideoProfile.isVideo(videoState) &&
!mWiredHeadsetManager.isPluggedIn() &&
- !mCallAudioManager.isBluetoothDeviceAvailable() &&
+ !mBluetoothManager.isBluetoothAvailable() &&
isSpeakerEnabledForVideoCalls()) {
call.setStartWithSpeakerphoneOn(true);
}
@@ -1034,7 +1053,9 @@
}
/** Called when the audio state changes. */
- void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
+ @VisibleForTesting
+ public void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState
+ newAudioState) {
Log.v(this, "onAudioStateChanged, audioState: %s -> %s", oldAudioState, newAudioState);
for (CallsManagerListener listener : mListeners) {
listener.onCallAudioStateChanged(oldAudioState, newAudioState);
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 619fe01..f7046bc 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -37,6 +37,7 @@
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IConnectionService;
import com.android.internal.telecom.IConnectionServiceAdapter;
import com.android.internal.telecom.IVideoProvider;
@@ -57,7 +58,8 @@
* {@link IConnectionService} directly and instead should use this class to invoke methods of
* {@link IConnectionService}.
*/
-final class ConnectionServiceWrapper extends ServiceBinder {
+@VisibleForTesting
+public class ConnectionServiceWrapper extends ServiceBinder {
private final class Adapter extends IConnectionServiceAdapter.Stub {
@@ -684,7 +686,8 @@
}
/** @see IConnectionService#onCallAudioStateChanged(String,CallAudioState) */
- void onCallAudioStateChanged(Call activeCall, CallAudioState audioState) {
+ @VisibleForTesting
+ public void onCallAudioStateChanged(Call activeCall, CallAudioState audioState) {
final String callId = mCallIdMapper.getCallId(activeCall);
if (callId != null && isServiceValid("onCallAudioStateChanged")) {
try {
diff --git a/src/com/android/server/telecom/DockManager.java b/src/com/android/server/telecom/DockManager.java
index e6ad446..1048e2b 100644
--- a/src/com/android/server/telecom/DockManager.java
+++ b/src/com/android/server/telecom/DockManager.java
@@ -21,6 +21,7 @@
import android.content.Intent;
import android.content.IntentFilter;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.Collections;
@@ -28,8 +29,10 @@
import java.util.concurrent.ConcurrentHashMap;
/** Listens for and caches car dock state. */
-class DockManager {
- interface Listener {
+@VisibleForTesting
+public class DockManager {
+ @VisibleForTesting
+ public interface Listener {
void onDockChanged(boolean isDocked);
}
@@ -65,7 +68,8 @@
context.registerReceiver(mReceiver, intentFilter);
}
- void addListener(Listener listener) {
+ @VisibleForTesting
+ public void addListener(Listener listener) {
mListeners.add(listener);
}
diff --git a/src/com/android/server/telecom/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
index bdebace..4fa4389 100644
--- a/src/com/android/server/telecom/InCallTonePlayer.java
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -35,15 +35,20 @@
*/
public static class Factory {
private final CallAudioManager mCallAudioManager;
+ private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
private final TelecomSystem.SyncRoot mLock;
- Factory(CallAudioManager callAudioManager, TelecomSystem.SyncRoot lock) {
+ Factory(CallAudioManager callAudioManager,
+ CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
+ TelecomSystem.SyncRoot lock) {
mCallAudioManager = callAudioManager;
+ mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
mLock = lock;
}
public InCallTonePlayer createPlayer(int tone) {
- return new InCallTonePlayer(tone, mCallAudioManager, mLock);
+ return new InCallTonePlayer(tone, mCallAudioManager,
+ mCallAudioRoutePeripheralAdapter, mLock);
}
}
@@ -85,6 +90,7 @@
private static int sTonesPlaying = 0;
private final CallAudioManager mCallAudioManager;
+ private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
@@ -105,10 +111,12 @@
private InCallTonePlayer(
int toneId,
CallAudioManager callAudioManager,
+ CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
TelecomSystem.SyncRoot lock) {
mState = STATE_OFF;
mToneId = toneId;
mCallAudioManager = callAudioManager;
+ mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
mLock = lock;
}
@@ -197,7 +205,7 @@
}
int stream = AudioManager.STREAM_VOICE_CALL;
- if (mCallAudioManager.isBluetoothAudioOn()) {
+ if (mCallAudioRoutePeripheralAdapter.isBluetoothAudioOn()) {
stream = AudioManager.STREAM_BLUETOOTH_SCO;
}
diff --git a/src/com/android/server/telecom/StatusBarNotifier.java b/src/com/android/server/telecom/StatusBarNotifier.java
index c8c3c18..d8ede59 100644
--- a/src/com/android/server/telecom/StatusBarNotifier.java
+++ b/src/com/android/server/telecom/StatusBarNotifier.java
@@ -19,12 +19,15 @@
import android.app.StatusBarManager;
import android.content.Context;
+import com.android.internal.annotations.VisibleForTesting;
+
// TODO: Needed for move to system service: import com.android.internal.R;
/**
* Manages the special status bar notifications used by the phone app.
*/
-final class StatusBarNotifier extends CallsManagerListenerBase {
+@VisibleForTesting
+public class StatusBarNotifier extends CallsManagerListenerBase {
private static final String SLOT_MUTE = "mute";
private static final String SLOT_SPEAKERPHONE = "speakerphone";
@@ -50,7 +53,8 @@
}
}
- void notifyMute(boolean isMuted) {
+ @VisibleForTesting
+ public void notifyMute(boolean isMuted) {
// Never display anything if there are no calls.
if (!mCallsManager.hasAnyCalls()) {
isMuted = false;
@@ -74,7 +78,8 @@
mIsShowingMute = isMuted;
}
- void notifySpeakerphone(boolean isSpeakerphone) {
+ @VisibleForTesting
+ public void notifySpeakerphone(boolean isSpeakerphone) {
// Never display anything if there are no calls.
if (!mCallsManager.hasAnyCalls()) {
isSpeakerphone = false;
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 7ebcddc..e7acf52 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -125,6 +125,9 @@
mMissedCallNotifier = missedCallNotifier;
mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext);
mContactsAsyncHelper = new ContactsAsyncHelper(mLock);
+ BluetoothManager bluetoothManager = new BluetoothManager(mContext);
+ WiredHeadsetManager wiredHeadsetManager = new WiredHeadsetManager(mContext);
+
mCallsManager = new CallsManager(
mContext,
@@ -136,7 +139,9 @@
headsetMediaButtonFactory,
proximitySensorManagerFactory,
inCallWakeLockControllerFactory,
- audioServiceFactory);
+ audioServiceFactory,
+ bluetoothManager,
+ wiredHeadsetManager);
mRespondViaSmsManager = new RespondViaSmsManager(mCallsManager, mLock);
mCallsManager.setRespondViaSmsManager(mRespondViaSmsManager);
diff --git a/src/com/android/server/telecom/WiredHeadsetManager.java b/src/com/android/server/telecom/WiredHeadsetManager.java
index ef5f38c..f25e928 100644
--- a/src/com/android/server/telecom/WiredHeadsetManager.java
+++ b/src/com/android/server/telecom/WiredHeadsetManager.java
@@ -22,6 +22,7 @@
import android.content.IntentFilter;
import android.media.AudioManager;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.Collections;
@@ -29,8 +30,10 @@
import java.util.concurrent.ConcurrentHashMap;
/** Listens for and caches headset state. */
-class WiredHeadsetManager {
- interface Listener {
+@VisibleForTesting
+public class WiredHeadsetManager {
+ @VisibleForTesting
+ public interface Listener {
void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn);
}
@@ -58,7 +61,7 @@
private final Set<Listener> mListeners = Collections.newSetFromMap(
new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
- WiredHeadsetManager(Context context) {
+ public WiredHeadsetManager(Context context) {
mReceiver = new WiredHeadsetBroadcastReceiver();
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -69,7 +72,8 @@
context.registerReceiver(mReceiver, intentFilter);
}
- void addListener(Listener listener) {
+ @VisibleForTesting
+ public void addListener(Listener listener) {
mListeners.add(listener);
}
@@ -79,7 +83,8 @@
}
}
- boolean isPluggedIn() {
+ @VisibleForTesting
+ public boolean isPluggedIn() {
return mIsPluggedIn;
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
new file mode 100644
index 0000000..496ad7d
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -0,0 +1,652 @@
+/*
+ * Copyright (C) 2015 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.tests;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.telecom.CallAudioState;
+
+import com.android.server.telecom.BluetoothManager;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallAudioRouteStateMachine;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.WiredHeadsetManager;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+
+public class CallAudioRouteStateMachineTest extends TelecomTestCase {
+ private static final int NONE = 0;
+ private static final int ON = 1;
+ private static final int OFF = 2;
+
+ private class TestParameters {
+ public String name;
+ public int initialRoute;
+ public int availableRoutes; // may excl. speakerphone, because that's always available
+ public int speakerInteraction; // one of NONE, ON, or OFF
+ public int bluetoothInteraction; // one of NONE, ON, or OFF
+ public int action;
+ public int expectedRoute;
+ public int expectedAvailableRoutes; // also may exclude the speakerphone.
+
+ public TestParameters(String name, int initialRoute, int availableRoutes, int
+ speakerInteraction, int bluetoothInteraction, int action, int expectedRoute, int
+ expectedAvailableRoutes) {
+ this.name = name;
+ this.initialRoute = initialRoute;
+ this.availableRoutes = availableRoutes;
+ this.speakerInteraction = speakerInteraction;
+ this.bluetoothInteraction = bluetoothInteraction;
+ this.action = action;
+ this.expectedRoute = expectedRoute;
+ this.expectedAvailableRoutes = expectedAvailableRoutes;
+ }
+
+ @Override
+ public String toString() {
+ return "TestParameters{" +
+ "name='" + name + '\'' +
+ ", initialRoute=" + initialRoute +
+ ", availableRoutes=" + availableRoutes +
+ ", speakerInteraction=" + speakerInteraction +
+ ", bluetoothInteraction=" + bluetoothInteraction +
+ ", action=" + action +
+ ", expectedRoute=" + expectedRoute +
+ ", expectedAvailableRoutes=" + expectedAvailableRoutes +
+ '}';
+ }
+ }
+
+ @Mock CallsManager mockCallsManager;
+ @Mock BluetoothManager mockBluetoothManager;
+ @Mock IAudioService mockAudioService;
+ @Mock ConnectionServiceWrapper mockConnectionServiceWrapper;
+ @Mock WiredHeadsetManager mockWiredHeadsetManager;
+ @Mock StatusBarNotifier mockStatusBarNotifier;
+ @Mock Call fakeCall;
+
+ private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+ private static final int TEST_TIMEOUT = 200;
+ private AudioManager mockAudioManager;
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ mockAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+ mAudioServiceFactory = new CallAudioManager.AudioServiceFactory() {
+ @Override
+ public IAudioService getAudioService() {
+ return mockAudioService;
+ }
+ };
+
+ when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+ when(fakeCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
+ when(fakeCall.isAlive()).thenReturn(true);
+ doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
+ any(CallAudioState.class));
+ }
+
+ public void testStateMachineTransitionsWithFocus() throws Throwable {
+ List<TestParameters> paramList = generateTransitionTests();
+ for (TestParameters params : paramList) {
+ try {
+ runParametrizedTestCaseWithFocus(params);
+ } catch (Throwable e) {
+ String newMessage = "Failed at parameters: \n" + params.toString() + '\n'
+ + e.getMessage();
+ throw(new Throwable(newMessage, e));
+ }
+ }
+ }
+
+ public void testStateMachineTransitionsWithoutFocus() throws Throwable {
+ List<TestParameters> paramList = generateTransitionTests();
+ for (TestParameters params : paramList) {
+ try {
+ runParametrizedTestCaseWithoutFocus(params);
+ } catch (Throwable e) {
+ String newMessage = "Failed at parameters: \n" + params.toString() + '\n'
+ + e.getMessage();
+ throw(new Throwable(newMessage, e));
+ }
+ }
+ }
+
+ public void testSpeakerPersistence() {
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory);
+
+ when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
+ when(mockAudioManager.isSpeakerphoneOn()).thenReturn(true);
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] args = invocation.getArguments();
+ when(mockAudioManager.isSpeakerphoneOn()).thenReturn((Boolean) args[0]);
+ return null;
+ }
+ }).when(mockAudioManager).setSpeakerphoneOn(any(Boolean.class));
+ CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+ stateMachine.initialize(initState);
+
+ stateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.HAS_FOCUS);
+ stateMachine.sendMessage(CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
+ CallAudioState expectedMiddleState = new CallAudioState(false,
+ CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
+ verifyNewSystemCallAudioState(initState, expectedMiddleState);
+ resetMocks();
+
+ stateMachine.sendMessage(CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+ verifyNewSystemCallAudioState(expectedMiddleState, initState);
+ }
+
+ public void testInitializationWithNoHeadsetNoBluetooth() {
+ when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(false);
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(false);
+
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory);
+ stateMachine.initialize();
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
+ public void testInitializationWithHeadsetNoBluetooth() {
+ when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(false);
+
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory);
+ stateMachine.initialize();
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
+ public void testInitializationWithHeadsetAndBluetooth() {
+ when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
+
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory);
+ stateMachine.initialize();
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
+ public void testInitializationWithBluetoothNoHeadset() {
+ when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(false);
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
+
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory);
+ stateMachine.initialize();
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
+ private List<TestParameters> generateTransitionTests() {
+ List<TestParameters> params = new ArrayList<>();
+ params.add(new TestParameters(
+ "Connect headset during earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Connect headset during bluetooth", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH // expectedAvail
+ ));
+
+ params.add(new TestParameters(
+ "Connect headset during speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ OFF, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Disconnect headset during headset", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Disconnect headset during headset with bluetooth available", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+ ));
+
+ params.add(new TestParameters(
+ "Disconnect headset during bluetooth", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+ ));
+
+ params.add(new TestParameters(
+ "Disconnect headset during speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Disconnect headset during speakerphone with bluetooth available", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+ ));
+
+ params.add(new TestParameters(
+ "Connect bluetooth during earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ NONE, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE // expectedAvailableR
+ ));
+
+ params.add(new TestParameters(
+ "Connect bluetooth during wired headset", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+ NONE, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_WIRED_HEADSET // expectedAvail
+ ));
+
+ params.add(new TestParameters(
+ "Connect bluetooth during speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ OFF, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE // expectedAvailableR
+ ));
+
+ params.add(new TestParameters(
+ "Disconnect bluetooth during bluetooth without headset in", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Disconnect bluetooth during bluetooth with headset in", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Disconnect bluetooth during speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Disconnect bluetooth during earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE| CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Switch to speakerphone from earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ ON, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Switch to speakerphone from headset", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+ ON, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Switch to speakerphone from bluetooth", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ ON, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH // expectedAvail
+ ));
+
+ params.add(new TestParameters(
+ "Switch to earpiece from bluetooth", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+ ));
+
+ params.add(new TestParameters(
+ "Switch to earpiece from speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ OFF, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+ ));
+
+ params.add(new TestParameters(
+ "Switch to bluetooth from speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ OFF, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+ ));
+
+ params.add(new TestParameters(
+ "Switch to bluetooth from earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+ ));
+
+ params.add(new TestParameters(
+ "Switch to bluetooth from wired headset", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH // expectedAvail
+ ));
+
+ return params;
+ }
+
+ private void runParametrizedTestCaseWithFocus(TestParameters params) {
+ resetMocks();
+
+ // Construct a fresh state machine on every case
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory);
+
+ // Set up bluetooth and speakerphone state
+ when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(
+ params.initialRoute == CallAudioState.ROUTE_BLUETOOTH);
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(
+ (params.availableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0
+ || (params.expectedAvailableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0);
+ when(mockAudioManager.isSpeakerphoneOn()).thenReturn(
+ params.initialRoute == CallAudioState.ROUTE_SPEAKER);
+
+ // Set the initial CallAudioState object
+ CallAudioState initState = new CallAudioState(false,
+ params.initialRoute, (params.availableRoutes | CallAudioState.ROUTE_SPEAKER));
+ stateMachine.initialize(initState);
+ // Make the state machine have focus so that we actually do something
+ stateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.HAS_FOCUS);
+ stateMachine.sendMessage(params.action);
+
+ // Verify interactions with the speakerphone and bluetooth systems
+ switch(params.bluetoothInteraction) {
+ case NONE:
+ verify(mockBluetoothManager, never()).disconnectBluetoothAudio();
+ verify(mockBluetoothManager, never()).connectBluetoothAudio();
+ break;
+ case ON:
+ verify(mockBluetoothManager, timeout(TEST_TIMEOUT)).connectBluetoothAudio();
+ verify(mockBluetoothManager, never()).disconnectBluetoothAudio();
+ break;
+ case OFF:
+ verify(mockBluetoothManager, never()).connectBluetoothAudio();
+ verify(mockBluetoothManager, timeout(TEST_TIMEOUT)).disconnectBluetoothAudio();
+ }
+
+ switch (params.speakerInteraction) {
+ case NONE:
+ verify(mockAudioManager, never()).setSpeakerphoneOn(any(Boolean.class));
+ break;
+ case ON: // fall through
+ case OFF:
+ verify(mockAudioManager, timeout(TEST_TIMEOUT)).setSpeakerphoneOn(
+ params.speakerInteraction == ON);
+ }
+
+ // Verify the end state
+ CallAudioState expectedState = new CallAudioState(false, params.expectedRoute,
+ params.expectedAvailableRoutes | CallAudioState.ROUTE_SPEAKER);
+ verifyNewSystemCallAudioState(initState, expectedState);
+ }
+
+ private void runParametrizedTestCaseWithoutFocus(TestParameters params) {
+ resetMocks();
+
+ // Construct a fresh state machine on every case
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory);
+
+ // Set up bluetooth and speakerphone state
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(
+ (params.availableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0
+ || (params.expectedAvailableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0);
+ when(mockAudioManager.isSpeakerphoneOn()).thenReturn(
+ params.initialRoute == CallAudioState.ROUTE_SPEAKER);
+
+ // Set the initial CallAudioState object
+ CallAudioState initState = new CallAudioState(false,
+ params.initialRoute, (params.availableRoutes | CallAudioState.ROUTE_SPEAKER));
+ stateMachine.initialize(initState);
+ // Omit the focus-getting statement
+ stateMachine.sendMessage(params.action);
+ try {
+ Thread.sleep(100L);
+ } catch (InterruptedException e) {
+ // Just a pause to make sure the state machine handler thread has a chance to update
+ // its state. Do nothing.
+ }
+
+ // Verify that no substantive interactions have taken place with the rest of the system
+ verify(mockBluetoothManager, never()).disconnectBluetoothAudio();
+ verify(mockBluetoothManager, never()).connectBluetoothAudio();
+ verify(mockAudioManager, never()).setSpeakerphoneOn(any(Boolean.class));
+ verify(mockCallsManager, never()).onCallAudioStateChanged(any(CallAudioState.class),
+ any(CallAudioState.class));
+ verify(mockConnectionServiceWrapper, never()).onCallAudioStateChanged(
+ any(Call.class), any(CallAudioState.class));
+
+ // Verify the end state
+ CallAudioState expectedState = new CallAudioState(false, params.expectedRoute,
+ params.expectedAvailableRoutes | CallAudioState.ROUTE_SPEAKER);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
+ private void verifyNewSystemCallAudioState(CallAudioState expectedOldState,
+ CallAudioState expectedNewState) {
+ ArgumentCaptor<CallAudioState> oldStateCaptor = ArgumentCaptor.forClass(
+ CallAudioState.class);
+ ArgumentCaptor<CallAudioState> newStateCaptor1 = ArgumentCaptor.forClass(
+ CallAudioState.class);
+ ArgumentCaptor<CallAudioState> newStateCaptor2 = ArgumentCaptor.forClass(
+ CallAudioState.class);
+ verify(mockCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+ oldStateCaptor.capture(), newStateCaptor1.capture());
+ verify(mockConnectionServiceWrapper, timeout(TEST_TIMEOUT).atLeastOnce())
+ .onCallAudioStateChanged(same(fakeCall), newStateCaptor2.capture());
+
+ assertTrue(oldStateCaptor.getValue().equals(expectedOldState));
+ assertTrue(newStateCaptor1.getValue().equals(expectedNewState));
+ assertTrue(newStateCaptor2.getValue().equals(expectedNewState));
+ }
+
+ private void resetMocks() {
+ reset(mockAudioManager, mockBluetoothManager, mockCallsManager,
+ mockConnectionServiceWrapper);
+ when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+ doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
+ any(CallAudioState.class));
+ }
+}
\ No newline at end of file