Merge "Prevent reset of EmergencyCallDiagnosticLogger active call timestamp" into main
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
index 40b75a2..c44bea4 100644
--- a/flags/telecom_api_flags.aconfig
+++ b/flags/telecom_api_flags.aconfig
@@ -30,6 +30,20 @@
}
flag{
+ name: "get_registered_phone_accounts"
+ namespace: "telecom"
+ description: "When set, self-managed clients can get their own phone accounts"
+ bug: "317132586"
+}
+
+flag{
+ name: "transactional_video_state"
+ namespace: "telecom"
+ description: "when set, clients using transactional implementations will be able to set & get the video state"
+ bug: "311265260"
+}
+
+flag{
name: "business_call_composer"
namespace: "telecom"
description: "Enables enriched calling features (e.g. Business name will show for a call)"
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index cdfcc30..de17eee 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -14,3 +14,10 @@
description: "This fix ensures the MO calls won't switch from Active to Quite b/c setDialing was not called"
bug: "309540769"
}
+
+flag {
+ name: "enable_call_sequencing"
+ namespace: "telecom"
+ description: "Enables simultaneous call sequencing for SIM PhoneAccounts"
+ bug: "297446980"
+}
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 912305b..3b5e342 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -26,6 +26,8 @@
import android.os.Message;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.util.Pair;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;
@@ -81,16 +83,17 @@
* If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change
* the volume of the ringtone as it plays.
*
- * @param ringtoneSupplier The {@link Ringtone} factory.
+ * @param ringtoneInfoSupplier The {@link Ringtone} factory.
* @param ringtoneConsumer The {@link Ringtone} post-creation callback (to start the vibration).
* @param isHfpDeviceConnected True if there is a HFP BT device connected, false otherwise.
*/
- public void play(@NonNull Supplier<Ringtone> ringtoneSupplier,
- BiConsumer<Ringtone, Boolean> ringtoneConsumer, boolean isHfpDeviceConnected) {
+ public void play(@NonNull Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier,
+ BiConsumer<Pair<Uri, Ringtone>, Boolean> ringtoneConsumer,
+ boolean isHfpDeviceConnected) {
Log.d(this, "Posting play.");
mIsPlaying = true;
SomeArgs args = SomeArgs.obtain();
- args.arg1 = ringtoneSupplier;
+ args.arg1 = ringtoneInfoSupplier;
args.arg2 = ringtoneConsumer;
args.arg3 = Log.createSubsession();
args.arg4 = prepareRingingReadyLatch(isHfpDeviceConnected);
@@ -209,8 +212,10 @@
* Starts the actual playback of the ringtone. Executes on ringtone-thread.
*/
private void handlePlay(SomeArgs args) {
- Supplier<Ringtone> ringtoneSupplier = (Supplier<Ringtone>) args.arg1;
- BiConsumer<Ringtone, Boolean> ringtoneConsumer = (BiConsumer<Ringtone, Boolean>) args.arg2;
+ Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier =
+ (Supplier<Pair<Uri, Ringtone>>) args.arg1;
+ BiConsumer<Pair<Uri, Ringtone>, Boolean> ringtoneConsumer =
+ (BiConsumer<Pair<Uri, Ringtone>, Boolean>) args.arg2;
Session session = (Session) args.arg3;
CountDownLatch ringingReadyLatch = (CountDownLatch) args.arg4;
args.recycle();
@@ -226,6 +231,7 @@
return;
}
Ringtone ringtone = null;
+ Uri ringtoneUri = null;
boolean hasStopped = false;
try {
try {
@@ -236,7 +242,11 @@
} catch (InterruptedException e) {
Log.w(this, "handlePlay: latch exception: " + e);
}
- ringtone = ringtoneSupplier.get();
+ if (ringtoneInfoSupplier != null && ringtoneInfoSupplier.get() != null) {
+ ringtoneUri = ringtoneInfoSupplier.get().first;
+ ringtone = ringtoneInfoSupplier.get().second;
+ }
+
// Ringtone supply can be slow or stop command could have been issued while waiting
// for BT to move to CONNECTED state. Re-check for stop event.
if (mHandler.hasMessages(EVENT_STOP)) {
@@ -253,8 +263,7 @@
Log.w(this, "No ringtone was found bail out from playing.");
return;
}
- Uri uri = mRingtone.getUri();
- String uriString = (uri != null ? uri.toSafeString() : "");
+ String uriString = ringtoneUri != null ? ringtoneUri.toSafeString() : "";
Log.i(this, "handlePlay: Play ringtone. Uri: " + uriString);
mRingtone.setLooping(true);
if (mRingtone.isPlaying()) {
@@ -265,7 +274,7 @@
Log.i(this, "Play ringtone, looping.");
} finally {
removePendingRingingReadyLatch(ringingReadyLatch);
- ringtoneConsumer.accept(ringtone, hasStopped);
+ ringtoneConsumer.accept(new Pair(ringtoneUri, ringtone), hasStopped);
}
} finally {
Log.cancelSubsession(session);
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index 5037cf5..cdf44a8 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -82,13 +82,9 @@
}
}
if (routeInfo == null) {
- CompletableFuture<Boolean> future = new CompletableFuture<>();
- mScheduledExecutorService.schedule(new Runnable() {
- @Override
- public void run() {
- createRetry(type, bluetoothAddress, audioManager, retryCount - 1);
- }
- }, RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+ mScheduledExecutorService.schedule(
+ () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
+ RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
} else {
mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
}
@@ -105,6 +101,7 @@
public static final int TYPE_BLUETOOTH_SCO = 5;
public static final int TYPE_BLUETOOTH_HA = 6;
public static final int TYPE_BLUETOOTH_LE = 7;
+ public static final int TYPE_STREAMING = 8;
@IntDef(prefix = "TYPE", value = {
TYPE_INVALID,
TYPE_EARPIECE,
@@ -113,7 +110,8 @@
TYPE_DOCK,
TYPE_BLUETOOTH_SCO,
TYPE_BLUETOOTH_HA,
- TYPE_BLUETOOTH_LE
+ TYPE_BLUETOOTH_LE,
+ TYPE_STREAMING
})
@Retention(RetentionPolicy.SOURCE)
public @interface AudioRouteType {}
@@ -145,6 +143,7 @@
DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_SCO, "TYPE_BLUETOOTH_SCO");
DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_HA, "TYPE_BLUETOOTH_HA");
DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_LE, "TYPE_BLUETOOTH_LE");
+ DEVICE_TYPE_STRINGS.put(TYPE_STREAMING, "TYPE_STREAMING");
}
public static final HashMap<Integer, Integer> DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE;
@@ -225,6 +224,7 @@
void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
AudioManager audioManager) {
if (pendingAudioRoute.isActive() && !active) {
+ Log.i(this, "clearCommunicationDevice");
audioManager.clearCommunicationDevice();
} else if (active) {
if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 624399b..f7ad93f 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -19,6 +19,8 @@
import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
import static android.telephony.TelephonyManager.EVENT_DISPLAY_EMERGENCY_MESSAGE;
+import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -652,6 +654,36 @@
private boolean mIsVideoCallingSupportedByPhoneAccount = false;
/**
+ * Indicates whether this individual calls video state can be changed as opposed to be gated
+ * by the {@link PhoneAccount}.
+ *
+ * {@code True} if the call is Transactional && has the CallAttributes.SUPPORTS_VIDEO_CALLING
+ * capability {@code false} otherwise.
+ */
+ private boolean mTransactionalCallSupportsVideoCalling = false;
+
+ public void setTransactionalCallSupportsVideoCalling(CallAttributes callAttributes) {
+ if (!mIsTransactionalCall) {
+ Log.i(this, "setTransactionalCallSupportsVideoCalling: call is not transactional");
+ return;
+ }
+ if (callAttributes == null) {
+ Log.i(this, "setTransactionalCallSupportsVideoCalling: callAttributes is null");
+ return;
+ }
+ if ((callAttributes.getCallCapabilities() & CallAttributes.SUPPORTS_VIDEO_CALLING)
+ == CallAttributes.SUPPORTS_VIDEO_CALLING) {
+ mTransactionalCallSupportsVideoCalling = true;
+ } else {
+ mTransactionalCallSupportsVideoCalling = false;
+ }
+ }
+
+ public boolean isTransactionalCallSupportsVideoCalling() {
+ return mTransactionalCallSupportsVideoCalling;
+ }
+
+ /**
* Indicates whether or not this call can be pulled if it is an external call. If true, respect
* the Connection Capability set by the ConnectionService. If false, override the capability
* set and always remove the ability to pull this external call.
@@ -3176,8 +3208,7 @@
} else if (mConnectionService != null) {
mConnectionService.onExtrasChanged(this, mExtras);
} else {
- Log.e(this, new NullPointerException(),
- "putExtras failed due to null CS callId=%s", getId());
+ Log.w(this, "putExtras failed due to null CS callId=%s", getId());
}
}
}
@@ -4023,6 +4054,15 @@
videoState = VideoProfile.STATE_AUDIO_ONLY;
}
+ // Transactional calls have the ability to change video calling capabilities on a per-call
+ // basis as opposed to ConnectionService calls which are only based on the PhoneAccount.
+ if (mFlags.transactionalVideoState()
+ && mIsTransactionalCall && !mTransactionalCallSupportsVideoCalling) {
+ Log.i(this, "setVideoState: The transactional does NOT support video calling."
+ + " defaulted to audio (video not supported)");
+ videoState = VideoProfile.STATE_AUDIO_ONLY;
+ }
+
// Track Video State history during the duration of the call.
// Only update the history when the call is active or disconnected. This ensures we do
// not include the video state history when:
@@ -4045,6 +4085,12 @@
}
}
+ if (mFlags.transactionalVideoState()
+ && mIsTransactionalCall && mTransactionalService != null) {
+ int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
+ mTransactionalService.onVideoStateChanged(this, transactionalVS);
+ }
+
if (VideoProfile.isVideo(videoState)) {
mAnalytics.setCallIsVideo(true);
}
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index f76d47d..5585d09 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -97,6 +97,9 @@
put(SPEAKER_ON, "SPEAKER_ON");
put(SPEAKER_OFF, "SPEAKER_OFF");
+ put(STREAMING_FORCE_ENABLED, "STREAMING_FORCE_ENABLED");
+ put(STREAMING_FORCE_DISABLED, "STREAMING_FORCE_DISABLED");
+
put(USER_SWITCH_EARPIECE, "USER_SWITCH_EARPIECE");
put(USER_SWITCH_BLUETOOTH, "USER_SWITCH_BLUETOOTH");
put(USER_SWITCH_HEADSET, "USER_SWITCH_HEADSET");
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index c1d7d0c..091c8fc 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -19,24 +19,36 @@
import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
+import android.app.ActivityManager;
import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.UserInfo;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
+import android.media.IAudioService;
import android.media.audiopolicy.AudioProductStrategy;
+import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.util.ArrayMap;
+import androidx.annotation.NonNull;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import java.util.HashSet;
import java.util.List;
@@ -57,20 +69,83 @@
ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_SCO, CallAudioState.ROUTE_BLUETOOTH);
ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_HA, CallAudioState.ROUTE_BLUETOOTH);
ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_LE, CallAudioState.ROUTE_BLUETOOTH);
+ ROUTE_MAP.put(AudioRoute.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING);
}
private final CallsManager mCallsManager;
+ private final Context mContext;
private AudioManager mAudioManager;
+ private CallAudioManager mCallAudioManager;
+ private final BluetoothRouteManager mBluetoothRouteManager;
+ private final CallAudioManager.AudioServiceFactory mAudioServiceFactory;
private final Handler mHandler;
private final WiredHeadsetManager mWiredHeadsetManager;
private Set<AudioRoute> mAvailableRoutes;
private AudioRoute mCurrentRoute;
private AudioRoute mEarpieceWiredRoute;
private AudioRoute mSpeakerDockRoute;
+ private AudioRoute mStreamingRoute;
+ private Set<AudioRoute> mStreamingRoutes;
private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes;
private Map<Integer, AudioRoute> mTypeRoutes;
private PendingAudioRoute mPendingAudioRoute;
private AudioRoute.Factory mAudioRouteFactory;
+ private int mFocusType;
+ private final Object mLock = new Object();
+ private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.startSession("CARC.mSPCR");
+ try {
+ if (AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED.equals(intent.getAction())) {
+ if (mAudioManager != null) {
+ AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
+ if ((info != null) &&
+ (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
+ sendMessageWithSessionInfo(SPEAKER_ON);
+ } else {
+ sendMessageWithSessionInfo(SPEAKER_OFF);
+ }
+ }
+ } else {
+ Log.w(this, "Received non-speakerphone-change intent");
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+ };
+ private final BroadcastReceiver mMuteChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.startSession("CARC.mCR");
+ try {
+ if (AudioManager.ACTION_MICROPHONE_MUTE_CHANGED.equals(intent.getAction())) {
+ if (mCallsManager.isInEmergencyCall()) {
+ Log.i(this, "Mute was externally changed when there's an emergency call. "
+ + "Forcing mute back off.");
+ sendMessageWithSessionInfo(MUTE_OFF);
+ } else {
+ sendMessageWithSessionInfo(MUTE_EXTERNALLY_CHANGED);
+ }
+ } else if (AudioManager.STREAM_MUTE_CHANGED_ACTION.equals(intent.getAction())) {
+ int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
+ boolean isStreamMuted = intent.getBooleanExtra(
+ AudioManager.EXTRA_STREAM_VOLUME_MUTED, false);
+
+ if (streamType == AudioManager.STREAM_RING && !isStreamMuted
+ && mCallAudioManager != null) {
+ Log.i(this, "Ring stream was un-muted.");
+ mCallAudioManager.onRingerModeChange();
+ }
+ } else {
+ Log.w(this, "Received non-mute-change intent");
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+ };
private CallAudioState mCallAudioState;
private boolean mIsMute;
private boolean mIsPending;
@@ -79,59 +154,142 @@
public CallAudioRouteController(
Context context,
CallsManager callsManager,
+ CallAudioManager.AudioServiceFactory audioServiceFactory,
AudioRoute.Factory audioRouteFactory,
- WiredHeadsetManager wiredHeadsetManager) {
+ WiredHeadsetManager wiredHeadsetManager,
+ BluetoothRouteManager bluetoothRouteManager) {
+ mContext = context;
mCallsManager = callsManager;
mAudioManager = context.getSystemService(AudioManager.class);
+ mAudioServiceFactory = audioServiceFactory;
mAudioRouteFactory = audioRouteFactory;
mWiredHeadsetManager = wiredHeadsetManager;
mIsMute = false;
+ mBluetoothRouteManager = bluetoothRouteManager;
+ mFocusType = NO_FOCUS;
HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
handlerThread.start();
+
+ // Register broadcast receivers
+ IntentFilter speakerChangedFilter = new IntentFilter(
+ AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED);
+ speakerChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ context.registerReceiver(mSpeakerPhoneChangeReceiver, speakerChangedFilter);
+
+ IntentFilter micMuteChangedFilter = new IntentFilter(
+ AudioManager.ACTION_MICROPHONE_MUTE_CHANGED);
+ micMuteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ context.registerReceiver(mMuteChangeReceiver, micMuteChangedFilter);
+
+ IntentFilter muteChangedFilter = new IntentFilter(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+ muteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ context.registerReceiver(mMuteChangeReceiver, muteChangedFilter);
+
+ // Create handler
mHandler = new Handler(handlerThread.getLooper()) {
@Override
- public void handleMessage(Message msg) {
- preHandleMessage(msg);
- String address;
- BluetoothDevice bluetoothDevice;
- @AudioRoute.AudioRouteType int type;
- switch (msg.what) {
- case BT_AUDIO_CONNECTED:
- bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
- handleBtAudioActive(bluetoothDevice);
- break;
- case BT_AUDIO_DISCONNECTED:
- bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
- handleBtAudioInactive(bluetoothDevice);
- break;
- case BT_DEVICE_ADDED:
- type = msg.arg1;
- bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
- handleBtConnected(type, bluetoothDevice);
- break;
- case BT_DEVICE_REMOVED:
- type = msg.arg1;
- bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
- handleBtDisconnected(type, bluetoothDevice);
- break;
- case BLUETOOTH_DEVICE_LIST_CHANGED:
- break;
- case BT_ACTIVE_DEVICE_PRESENT:
- type = msg.arg1;
- address = (String) ((SomeArgs) msg.obj).arg2;
- handleBtActiveDevicePresent(type, address);
- break;
- case BT_ACTIVE_DEVICE_GONE:
- type = msg.arg1;
- handleBtActiveDeviceGone(type);
- break;
- case EXIT_PENDING_ROUTE:
- handleExitPendingRoute();
- break;
- default:
- break;
+ public void handleMessage(@NonNull Message msg) {
+ synchronized (this) {
+ preHandleMessage(msg);
+ String address;
+ BluetoothDevice bluetoothDevice;
+ int focus;
+ @AudioRoute.AudioRouteType int type;
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ handleWiredHeadsetConnected();
+ break;
+ case DISCONNECT_WIRED_HEADSET:
+ handleWiredHeadsetDisconnected();
+ break;
+ case CONNECT_DOCK:
+ handleDockConnected();
+ break;
+ case DISCONNECT_DOCK:
+ handleDockDisconnected();
+ break;
+ case BLUETOOTH_DEVICE_LIST_CHANGED:
+ break;
+ case BT_ACTIVE_DEVICE_PRESENT:
+ type = msg.arg1;
+ address = (String) ((SomeArgs) msg.obj).arg2;
+ handleBtActiveDevicePresent(type, address);
+ break;
+ case BT_ACTIVE_DEVICE_GONE:
+ type = msg.arg1;
+ handleBtActiveDeviceGone(type);
+ break;
+ case BT_DEVICE_ADDED:
+ type = msg.arg1;
+ bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+ handleBtConnected(type, bluetoothDevice);
+ break;
+ case BT_DEVICE_REMOVED:
+ type = msg.arg1;
+ bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+ handleBtDisconnected(type, bluetoothDevice);
+ break;
+ case SWITCH_EARPIECE:
+ case USER_SWITCH_EARPIECE:
+ handleSwitchEarpiece();
+ break;
+ case SWITCH_BLUETOOTH:
+ case USER_SWITCH_BLUETOOTH:
+ address = (String) ((SomeArgs) msg.obj).arg2;
+ handleSwitchBluetooth(address);
+ break;
+ case SWITCH_HEADSET:
+ case USER_SWITCH_HEADSET:
+ handleSwitchHeadset();
+ break;
+ case SWITCH_SPEAKER:
+ case USER_SWITCH_SPEAKER:
+ handleSwitchSpeaker();
+ break;
+ case USER_SWITCH_BASELINE_ROUTE:
+ handleSwitchBaselineRoute();
+ break;
+ case SPEAKER_ON:
+ handleSpeakerOn();
+ break;
+ case SPEAKER_OFF:
+ handleSpeakerOff();
+ break;
+ case STREAMING_FORCE_ENABLED:
+ handleStreamingEnabled();
+ break;
+ case STREAMING_FORCE_DISABLED:
+ handleStreamingDisabled();
+ break;
+ case BT_AUDIO_CONNECTED:
+ bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+ handleBtAudioActive(bluetoothDevice);
+ break;
+ case BT_AUDIO_DISCONNECTED:
+ bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+ handleBtAudioInactive(bluetoothDevice);
+ break;
+ case MUTE_ON:
+ handleMuteChanged(true);
+ break;
+ case MUTE_OFF:
+ handleMuteChanged(false);
+ break;
+ case MUTE_EXTERNALLY_CHANGED:
+ handleMuteChanged(mAudioManager.isMasterMute());
+ break;
+ case SWITCH_FOCUS:
+ focus = msg.arg1;
+ handleSwitchFocus(focus);
+ break;
+ case EXIT_PENDING_ROUTE:
+ handleExitPendingRoute();
+ break;
+ default:
+ break;
+ }
+ postHandleMessage(msg);
}
- postHandleMessage(msg);
}
};
}
@@ -140,7 +298,10 @@
mAvailableRoutes = new HashSet<>();
mBluetoothRoutes = new ArrayMap<>();
mTypeRoutes = new ArrayMap<>();
+ mStreamingRoutes = new HashSet<>();
mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
+ mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
+ mStreamingRoutes.add(mStreamingRoute);
int supportMask = calculateSupportedRouteMask();
if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
@@ -225,6 +386,7 @@
@Override
public void setCallAudioManager(CallAudioManager callAudioManager) {
+ mCallAudioManager = callAudioManager;
}
@Override
@@ -271,6 +433,9 @@
}
private void routeTo(boolean active, AudioRoute destRoute) {
+ if (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
+ return;
+ }
if (mIsPending) {
if (destRoute.equals(mPendingAudioRoute.getDestRoute()) && (mIsActive == active)) {
return;
@@ -280,22 +445,24 @@
mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
// override pending route while keep waiting for still pending messages for the
// previous pending route
+ mIsActive = active;
mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
mPendingAudioRoute.setDestRoute(active, destRoute);
} else {
- if (mCurrentRoute.equals(destRoute) && (mIsActive = active)) {
+ if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
return;
}
Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute,
mIsActive, destRoute, active);
// route to pending route
- if (mAvailableRoutes.contains(mCurrentRoute)) {
+ if (getAvailableRoutes().contains(mCurrentRoute)) {
mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
} else {
// Avoid waiting for pending messages for an unavailable route
mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
}
mPendingAudioRoute.setDestRoute(active, destRoute);
+ mIsActive = active;
mIsPending = true;
}
mPendingAudioRoute.evaluatePendingState();
@@ -309,6 +476,103 @@
Message.obtain(mHandler, PENDING_ROUTE_TIMEOUT)), TIMEOUT_LIMIT);
}
+ private void handleWiredHeadsetConnected() {
+ AudioRoute wiredHeadsetRoute = null;
+ try {
+ wiredHeadsetRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null,
+ mAudioManager);
+ } catch (IllegalArgumentException e) {
+ Log.e(this, e, "Can't find available audio device info for route type:"
+ + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED));
+ }
+
+ if (wiredHeadsetRoute != null) {
+ mAvailableRoutes.add(wiredHeadsetRoute);
+ mAvailableRoutes.remove(mEarpieceWiredRoute);
+ mTypeRoutes.put(AudioRoute.TYPE_WIRED, wiredHeadsetRoute);
+ mEarpieceWiredRoute = wiredHeadsetRoute;
+ routeTo(mIsActive, wiredHeadsetRoute);
+ onAvailableRoutesChanged();
+ }
+ }
+
+ public void handleWiredHeadsetDisconnected() {
+ // Update audio route states
+ AudioRoute wiredHeadsetRoute = mTypeRoutes.remove(AudioRoute.TYPE_WIRED);
+ if (wiredHeadsetRoute != null) {
+ mAvailableRoutes.remove(wiredHeadsetRoute);
+ mEarpieceWiredRoute = null;
+ }
+ AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+ if (earpieceRoute != null) {
+ mAvailableRoutes.add(earpieceRoute);
+ mEarpieceWiredRoute = earpieceRoute;
+ }
+ onAvailableRoutesChanged();
+
+ // Route to expected state
+ if (mCurrentRoute.equals(wiredHeadsetRoute)) {
+ routeTo(mIsActive, getBaseRoute(true));
+ }
+ }
+
+ private void handleDockConnected() {
+ AudioRoute dockRoute = null;
+ try {
+ dockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_DOCK, null, mAudioManager);
+ } catch (IllegalArgumentException e) {
+ Log.e(this, e, "Can't find available audio device info for route type:"
+ + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED));
+ }
+
+ if (dockRoute != null) {
+ mAvailableRoutes.add(dockRoute);
+ mAvailableRoutes.remove(mSpeakerDockRoute);
+ mTypeRoutes.put(AudioRoute.TYPE_DOCK, dockRoute);
+ mSpeakerDockRoute = dockRoute;
+ routeTo(mIsActive, dockRoute);
+ onAvailableRoutesChanged();
+ }
+ }
+
+ public void handleDockDisconnected() {
+ // Update audio route states
+ AudioRoute dockRoute = mTypeRoutes.get(AudioRoute.TYPE_DOCK);
+ if (dockRoute != null) {
+ mAvailableRoutes.remove(dockRoute);
+ mSpeakerDockRoute = null;
+ }
+ AudioRoute speakerRoute = mTypeRoutes.get(AudioRoute.TYPE_SPEAKER);
+ if (speakerRoute != null) {
+ mAvailableRoutes.add(speakerRoute);
+ mSpeakerDockRoute = speakerRoute;
+ }
+ onAvailableRoutesChanged();
+
+ // Route to expected state
+ if (mCurrentRoute.equals(dockRoute)) {
+ routeTo(mIsActive, getBaseRoute(true));
+ }
+ }
+
+ private void handleStreamingEnabled() {
+ if (!mCurrentRoute.equals(mStreamingRoute)) {
+ routeTo(mIsActive, mStreamingRoute);
+ } else {
+ Log.i(this, "ignore enable streaming, already in streaming");
+ }
+ }
+
+ private void handleStreamingDisabled() {
+ if (mCurrentRoute.equals(mStreamingRoute)) {
+ mCurrentRoute = DUMMY_ROUTE;
+ onAvailableRoutesChanged();
+ routeTo(mIsActive, getBaseRoute(true));
+ } else {
+ Log.i(this, "ignore disable streaming, not in streaming");
+ }
+ }
+
private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
if (mIsPending) {
if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
@@ -360,13 +624,7 @@
// Fallback to an available route
if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
- // fallback policy
- AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
- if (destRoute != null && mAvailableRoutes.contains(destRoute)) {
- routeTo(mIsActive, destRoute);
- } else {
- routeTo(mIsActive, getPreferredAudioRouteFromDefault(true/* includeBluetooth */));
- }
+ routeTo(mIsActive, getBaseRoute(false));
}
}
@@ -384,11 +642,142 @@
if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
|| (!mIsPending && mCurrentRoute.getType() == type)) {
// Fallback to an available route
- AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
- if (destRoute != null && mAvailableRoutes.contains(destRoute)) {
- routeTo(mIsActive, destRoute);
+ routeTo(mIsActive, getBaseRoute(true));
+ }
+ }
+
+ private void handleMuteChanged(boolean mute) {
+ mIsMute = mute;
+ if (mIsMute != mAudioManager.isMasterMute() && mIsActive) {
+ IAudioService audioService = mAudioServiceFactory.getAudioService();
+ Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
+ audioService == null);
+ if (audioService != null) {
+ try {
+ audioService.setMicrophoneMute(mute, mContext.getOpPackageName(),
+ mCallsManager.getCurrentUserHandle().getIdentifier(),
+ mContext.getAttributionTag());
+ } catch (RemoteException e) {
+ Log.e(this, e, "Remote exception while toggling mute.");
+ return;
+ }
+ }
+ }
+ onMuteStateChanged(mIsMute);
+ }
+
+ private void handleSwitchFocus(int focus) {
+ mFocusType = focus;
+ switch (focus) {
+ case NO_FOCUS -> {
+ if (mIsActive) {
+ handleMuteChanged(false);
+ routeTo(false, mCurrentRoute);
+ }
+ }
+ case ACTIVE_FOCUS -> {
+ if (!mIsActive) {
+ routeTo(true, getBaseRoute(true));
+ }
+ }
+ case RINGING_FOCUS -> {
+ if (!mIsActive) {
+ AudioRoute route = getBaseRoute(true);
+ BluetoothDevice device = mBluetoothRoutes.get(route);
+ if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+ routeTo(false, route);
+ } else {
+ routeTo(true, route);
+ }
+ } else {
+ // active
+ BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
+ if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+ routeTo(false, mCurrentRoute);
+ }
+ }
+ }
+ }
+ }
+
+ public void handleSwitchEarpiece() {
+ AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+ if (earpieceRoute != null && getAvailableRoutes().contains(earpieceRoute)) {
+ routeTo(mIsActive, earpieceRoute);
+ } else {
+ Log.i(this, "ignore switch earpiece request");
+ }
+ }
+
+ private void handleSwitchBluetooth(String address) {
+ Log.i(this, "handle switch to bluetooth with address %s", address);
+ AudioRoute bluetoothRoute = null;
+ BluetoothDevice bluetoothDevice = null;
+ for (AudioRoute route : getAvailableRoutes()) {
+ if (Objects.equals(address, route.getBluetoothAddress())) {
+ bluetoothRoute = route;
+ bluetoothDevice = mBluetoothRoutes.get(route);
+ break;
+ }
+ }
+
+ if (bluetoothRoute != null && bluetoothDevice != null) {
+ if (mFocusType == RINGING_FOCUS) {
+ routeTo(mBluetoothRouteManager.isInbandRingEnabled(bluetoothDevice) && mIsActive,
+ bluetoothRoute);
} else {
- routeTo(mIsActive, getPreferredAudioRouteFromDefault(false/* includeBluetooth */));
+ routeTo(mIsActive, bluetoothRoute);
+ }
+ } else {
+ Log.i(this, "ignore switch bluetooth request");
+ }
+ }
+
+ private void handleSwitchHeadset() {
+ AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED);
+ if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
+ routeTo(mIsActive, headsetRoute);
+ } else {
+ Log.i(this, "ignore switch speaker request");
+ }
+ }
+
+ private void handleSwitchSpeaker() {
+ if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+ routeTo(mIsActive, mSpeakerDockRoute);
+ } else {
+ Log.i(this, "ignore switch speaker request");
+ }
+ }
+
+ private void handleSwitchBaselineRoute() {
+ routeTo(mIsActive, getBaseRoute(true));
+ }
+
+ private void handleSpeakerOn() {
+ if (isPending()) {
+ mPendingAudioRoute.onMessageReceived(SPEAKER_ON);
+ } else {
+ if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+ routeTo(mIsActive, mSpeakerDockRoute);
+ // Since the route switching triggered by this message, we need to manually send it
+ // again so that we won't stuck in the pending route
+ if (mIsActive) {
+ sendMessageWithSessionInfo(SPEAKER_ON);
+ }
+ }
+ }
+ }
+
+ private void handleSpeakerOff() {
+ if (isPending()) {
+ mPendingAudioRoute.onMessageReceived(SPEAKER_OFF);
+ } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
+ routeTo(mIsActive, getBaseRoute(true));
+ // Since the route switching triggered by this message, we need to manually send it
+ // again so that we won't stuck in the pending route
+ if (mIsActive) {
+ sendMessageWithSessionInfo(SPEAKER_OFF);
}
onAvailableRoutesChanged();
}
@@ -397,39 +786,53 @@
public void handleExitPendingRoute() {
if (mIsPending) {
Log.i(this, "Exit pending route and enter %s(active=%b)",
- mPendingAudioRoute.getDestRoute(), mPendingAudioRoute.isActive());
+ mPendingAudioRoute.getDestRoute(), mIsActive);
mCurrentRoute = mPendingAudioRoute.getDestRoute();
- mIsActive = mPendingAudioRoute.isActive();
mIsPending = false;
onCurrentRouteChanged();
}
}
private void onCurrentRouteChanged() {
- BluetoothDevice activeBluetoothDevice = null;
- int route = ROUTE_MAP.get(mCurrentRoute.getType());
- if (route == CallAudioState.ROUTE_BLUETOOTH) {
- activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute);
+ synchronized (mLock) {
+ BluetoothDevice activeBluetoothDevice = null;
+ int route = ROUTE_MAP.get(mCurrentRoute.getType());
+ if (route == CallAudioState.ROUTE_STREAMING) {
+ updateCallAudioState(new CallAudioState(mIsMute, route, route));
+ return;
+ }
+ if (route == CallAudioState.ROUTE_BLUETOOTH) {
+ activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute);
+ }
+ updateCallAudioState(new CallAudioState(mIsMute, route,
+ mCallAudioState.getRawSupportedRouteMask(), activeBluetoothDevice,
+ mCallAudioState.getSupportedBluetoothDevices()));
}
- updateCallAudioState(new CallAudioState(mIsMute, route,
- mCallAudioState.getSupportedRouteMask(), activeBluetoothDevice,
- mCallAudioState.getSupportedBluetoothDevices()));
}
private void onAvailableRoutesChanged() {
- int routeMask = 0;
- Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
- for (AudioRoute route : mAvailableRoutes) {
- routeMask |= ROUTE_MAP.get(route.getType());
- if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
- availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+ synchronized (mLock) {
+ int routeMask = 0;
+ Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
+ for (AudioRoute route : getAvailableRoutes()) {
+ routeMask |= ROUTE_MAP.get(route.getType());
+ if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
+ availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+ }
}
+ updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
+ mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices));
}
- updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
- mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices));
+ }
+
+ private void onMuteStateChanged(boolean mute) {
+ updateCallAudioState(new CallAudioState(mute, mCallAudioState.getRoute(),
+ mCallAudioState.getSupportedRouteMask(), mCallAudioState.getActiveBluetoothDevice(),
+ mCallAudioState.getSupportedBluetoothDevices()));
}
private void updateCallAudioState(CallAudioState callAudioState) {
+ Log.i(this, "updateCallAudioState: " + callAudioState);
CallAudioState oldState = mCallAudioState;
mCallAudioState = callAudioState;
mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
@@ -468,7 +871,7 @@
}
}
- public AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
+ private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
} else {
@@ -495,8 +898,13 @@
return routeMask;
}
+ @VisibleForTesting
public Set<AudioRoute> getAvailableRoutes() {
- return mAvailableRoutes;
+ if (mCurrentRoute.equals(mStreamingRoute)) {
+ return mStreamingRoutes;
+ } else {
+ return mAvailableRoutes;
+ }
}
public AudioRoute getCurrentRoute() {
@@ -513,6 +921,17 @@
return null;
}
+ public AudioRoute getBaseRoute(boolean includeBluetooth) {
+ AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
+ if (destRoute == null) {
+ destRoute = getPreferredAudioRouteFromDefault(includeBluetooth);
+ }
+ if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
+ destRoute = null;
+ }
+ return destRoute;
+ }
+
@VisibleForTesting
public void setAudioManager(AudioManager audioManager) {
mAudioManager = audioManager;
@@ -525,6 +944,11 @@
@VisibleForTesting
public void setActive(boolean active) {
+ if (active) {
+ mFocusType = ACTIVE_FOCUS;
+ } else {
+ mFocusType = NO_FOCUS;
+ }
mIsActive = active;
}
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index de7d84a..ae096af 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -118,6 +118,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IntentForwarderActivity;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
import com.android.server.telecom.callfiltering.BlockCheckerAdapter;
@@ -607,6 +608,7 @@
EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
CallAudioCommunicationDeviceTracker communicationDeviceTracker,
CallStreamingNotification callStreamingNotification,
+ BluetoothDeviceManager bluetoothDeviceManager,
FeatureFlags featureFlags,
IncomingCallFilterGraphProvider incomingCallFilterGraphProvider) {
@@ -632,6 +634,9 @@
mDtmfLocalTonePlayer =
new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
CallAudioRouteAdapter callAudioRouteAdapter;
+ // TODO: add another flag check when
+ // bluetoothDeviceManager.getBluetoothHeadset().isScoManagedByAudio()
+ // available and return true
if (!featureFlags.useRefactoredAudioRouteSwitching()) {
callAudioRouteAdapter = callAudioRouteStateMachineFactory.create(
context,
@@ -646,8 +651,8 @@
featureFlags
);
} else {
- callAudioRouteAdapter = new CallAudioRouteController(
- context, this, new AudioRoute.Factory(), wiredHeadsetManager);
+ callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
+ new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager);
}
callAudioRouteAdapter.initialize();
bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
@@ -700,7 +705,8 @@
mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier,
mAnomalyReporter, featureFlags);
mConnectionServiceRepository =
- new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this);
+ new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this,
+ featureFlags);
mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this);
mClockProxy = clockProxy;
mToastFactory = toastFactory;
@@ -5880,8 +5886,7 @@
return;
}
ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
- phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle(),
- mFeatureFlags);
+ phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle());
if (service == null) {
Log.i(this, "Found no connection service.");
return;
@@ -5906,8 +5911,7 @@
return;
}
ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
- phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle(),
- mFeatureFlags);
+ phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle());
if (service == null) {
Log.i(this, "Found no connection service.");
return;
diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java
index d6a78d0..e4ed220 100644
--- a/src/com/android/server/telecom/ConnectionServiceRepository.java
+++ b/src/com/android/server/telecom/ConnectionServiceRepository.java
@@ -38,6 +38,7 @@
private final Context mContext;
private final TelecomSystem.SyncRoot mLock;
private final CallsManager mCallsManager;
+ private final FeatureFlags mFeatureFlags;
private final ServiceBinder.Listener<ConnectionServiceWrapper> mUnbindListener =
new ServiceBinder.Listener<ConnectionServiceWrapper>() {
@@ -54,18 +55,19 @@
PhoneAccountRegistrar phoneAccountRegistrar,
Context context,
TelecomSystem.SyncRoot lock,
- CallsManager callsManager) {
+ CallsManager callsManager,
+ FeatureFlags featureFlags) {
mPhoneAccountRegistrar = phoneAccountRegistrar;
mContext = context;
mLock = lock;
mCallsManager = callsManager;
+ mFeatureFlags = featureFlags;
}
@VisibleForTesting
public ConnectionServiceWrapper getService(
ComponentName componentName,
- UserHandle userHandle,
- FeatureFlags featureFlags) {
+ UserHandle userHandle) {
Pair<ComponentName, UserHandle> cacheKey = Pair.create(componentName, userHandle);
ConnectionServiceWrapper service = mServiceCache.get(cacheKey);
if (service == null) {
@@ -77,7 +79,7 @@
mContext,
mLock,
userHandle,
- featureFlags);
+ mFeatureFlags);
service.addListener(mUnbindListener);
mServiceCache.put(cacheKey, service);
}
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 43ceff3..53da8ff 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -1351,7 +1351,6 @@
private final CallsManager mCallsManager;
private final AppOpsManager mAppOpsManager;
private final Context mContext;
- private final FeatureFlags mFlags;
private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
@@ -1386,7 +1385,6 @@
mCallsManager = callsManager;
mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mContext = context;
- mFlags = featureFlags;
}
/** See {@link IConnectionService#addConnectionServiceAdapter}. */
@@ -2540,7 +2538,7 @@
isCallerConnectionManager = true;
}
ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
- handle.getComponentName(), handle.getUserHandle(), mFlags);
+ handle.getComponentName(), handle.getUserHandle());
if (service != null && service != this) {
simServices.add(service);
} else {
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index f5b257d..bcb2d2f 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -244,7 +244,7 @@
Log.i(this, "Trying attempt %s", attempt);
PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
mService = mRepository.getService(phoneAccount.getComponentName(),
- phoneAccount.getUserHandle(), mFlags);
+ phoneAccount.getUserHandle());
if (mService == null) {
Log.i(this, "Found no connection service for attempt %s", attempt);
attemptNextPhoneAccount();
@@ -260,7 +260,7 @@
PhoneAccountHandle remotePhoneAccount = attempt.targetPhoneAccount;
ConnectionServiceWrapper mRemoteService =
mRepository.getService(remotePhoneAccount.getComponentName(),
- remotePhoneAccount.getUserHandle(), mFlags);
+ remotePhoneAccount.getUserHandle());
if (mRemoteService == null) {
mCall.setConnectionService(mService);
} else {
diff --git a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
index f0f8bb4..cce8c66 100644
--- a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
+++ b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
@@ -403,7 +403,12 @@
Log.i(this, "skipped dumping diagnostic data");
return;
}
- dumpDiagnosticDataFromDropbox(pw);
+ try {
+ dumpDiagnosticDataFromDropbox(pw);
+ } catch (Exception e) {
+ pw.println("Exception was thrown while dumping diagnostic data from DropBox");
+ e.printStackTrace();
+ }
}
private static class CallEventTimestamps {
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 5fa3048..8de62ed 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -76,9 +76,7 @@
public void onMessageReceived(int message) {
if (message == PENDING_ROUTE_FAILED) {
// Fallback to base route
- //TODO: Replace getPreferredAudioRouteFromDefault by getBaseRoute when available and
- // make the replaced one private
- mDestRoute = mCallAudioRouteController.getPreferredAudioRouteFromDefault(true);
+ mDestRoute = mCallAudioRouteController.getBaseRoute(true);
mCallAudioRouteController.sendMessageWithSessionInfo(
CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
}
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 3ec4ebe..e148ef5 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -44,6 +44,7 @@
import android.os.vibrator.persistence.VibrationXmlParser;
import android.telecom.Log;
import android.telecom.TelecomManager;
+import android.util.Pair;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.annotations.VisibleForTesting;
@@ -413,18 +414,18 @@
isVibratorEnabled, mIsHapticPlaybackSupportedByDevice);
}
// Defer ringtone creation to the async player thread.
- Supplier<Ringtone> ringtoneSupplier;
+ Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier;
final boolean finalHapticChannelsMuted = hapticChannelsMuted;
if (isHapticOnly) {
if (hapticChannelsMuted) {
Log.i(this,
"want haptic only ringtone but haptics are muted, skip ringtone play");
- ringtoneSupplier = null;
+ ringtoneInfoSupplier = null;
} else {
- ringtoneSupplier = mRingtoneFactory::getHapticOnlyRingtone;
+ ringtoneInfoSupplier = mRingtoneFactory::getHapticOnlyRingtone;
}
} else {
- ringtoneSupplier = () -> mRingtoneFactory.getRingtone(
+ ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
}
@@ -447,9 +448,18 @@
// if the loaded ringtone is null. However if a stop event arrives before the ringtone
// creation finishes, then this consumer can be skipped.
final boolean finalUseCustomVibrationEffect = useCustomVibrationEffect;
- BiConsumer<Ringtone, Boolean> afterRingtoneLogic =
- (Ringtone ringtone, Boolean stopped) -> {
+ BiConsumer<Pair<Uri, Ringtone>, Boolean> afterRingtoneLogic =
+ (Pair<Uri, Ringtone> ringtoneInfo, Boolean stopped) -> {
try {
+ Uri ringtoneUri = null;
+ Ringtone ringtone = null;
+ if (ringtoneInfo != null) {
+ ringtoneUri = ringtoneInfo.first;
+ ringtone = ringtoneInfo.second;
+ } else {
+ Log.w(this, "The ringtone could not be loaded.");
+ }
+
if (stopped.booleanValue() || !vibratorReserved) {
// don't start vibration if the ringing is already abandoned, or the
// vibrator wasn't reserved. This still triggers the mBlockOnRingingFuture.
@@ -460,7 +470,7 @@
if (DEBUG_RINGER) {
Log.d(this, "Using ringtone defined vibration effect.");
}
- vibrationEffect = getVibrationEffectForRingtone(ringtone);
+ vibrationEffect = getVibrationEffectForRingtone(ringtoneUri);
} else {
vibrationEffect = mDefaultVibrationEffect;
}
@@ -477,10 +487,10 @@
}
};
deferBlockOnRingingFuture = true; // Run in vibrationLogic.
- if (ringtoneSupplier != null) {
- mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic, isHfpDeviceAttached);
+ if (ringtoneInfoSupplier != null) {
+ mRingtonePlayer.play(ringtoneInfoSupplier, afterRingtoneLogic, isHfpDeviceAttached);
} else {
- afterRingtoneLogic.accept(/* ringtone= */ null, /* stopped= */ false);
+ afterRingtoneLogic.accept(/* ringtoneUri, ringtone = */ null, /* stopped= */ false);
}
// shouldAcquireAudioFocus is meant to be true, but that check is deferred to here
@@ -542,8 +552,7 @@
}
}
- private VibrationEffect getVibrationEffectForRingtone(@NonNull Ringtone ringtone) {
- Uri ringtoneUri = ringtone.getUri();
+ private VibrationEffect getVibrationEffectForRingtone(Uri ringtoneUri) {
if (ringtoneUri == null) {
return mDefaultVibrationEffect;
}
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index 6bcfb4c..0e0b99f 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -34,6 +34,7 @@
import com.android.internal.annotations.VisibleForTesting;
import android.telecom.CallerInfo;
+import android.util.Pair;
import java.util.List;
@@ -53,18 +54,7 @@
mCallsManager = callsManager;
}
- /**
- * Determines if a ringtone has haptic channels.
- * @param ringtone The ringtone URI.
- * @return {@code true} if there is a haptic channel, {@code false} otherwise.
- */
- public boolean hasHapticChannels(Ringtone ringtone) {
- boolean hasHapticChannels = RingtoneManager.hasHapticChannels(ringtone.getUri());
- Log.i(this, "hasHapticChannels %s -> %b", ringtone.getUri(), hasHapticChannels);
- return hasHapticChannels;
- }
-
- public Ringtone getRingtone(Call incomingCall,
+ public Pair<Uri, Ringtone> getRingtone(Call incomingCall,
@Nullable VolumeShaper.Configuration volumeShaperConfig, boolean hapticChannelsMuted) {
// Initializing ringtones on the main thread can deadlock
ThreadUtil.checkNotOnMainThread();
@@ -106,18 +96,19 @@
}
}
- if (defaultRingtoneUri == null) {
+ ringtoneUri = defaultRingtoneUri;
+ if (ringtoneUri == null) {
return null;
}
try {
ringtone = RingtoneManager.getRingtone(
- contextToUse, defaultRingtoneUri, volumeShaperConfig, audioAttrs);
+ contextToUse, ringtoneUri, volumeShaperConfig, audioAttrs);
} catch (Exception e) {
Log.e(this, e, "getRingtone: exception while getting ringtone.");
}
}
- return ringtone;
+ return new Pair(ringtoneUri, ringtone);
}
private AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted) {
@@ -130,7 +121,7 @@
/** Returns a ringtone to be used when ringer is not audible for the incoming call. */
@Nullable
- public Ringtone getHapticOnlyRingtone() {
+ public Pair<Uri, Ringtone> getHapticOnlyRingtone() {
// Initializing ringtones on the main thread can deadlock
ThreadUtil.checkNotOnMainThread();
Uri ringtoneUri = Uri.parse("file://" + mContext.getString(
@@ -138,12 +129,12 @@
AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(
/* hapticChannelsMuted */ false);
Ringtone ringtone = RingtoneManager.getRingtone(
- mContext, ringtoneUri, /* volumeShaperConfig */ null, audioAttrs);
+ mContext, ringtoneUri, /* volumeShaperConfig */ null, audioAttrs);
if (ringtone != null) {
// Make sure the sound is muted.
ringtone.setVolume(0);
}
- return ringtone;
+ return new Pair(ringtoneUri, ringtone);
}
private Context getWorkProfileContextForUser(UserHandle userHandle) {
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 4bda96a..aa721d4 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -77,7 +77,6 @@
import com.android.internal.telecom.ICallControl;
import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
-import com.android.internal.telephony.flags.Flags;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.flags.FeatureFlags;
@@ -239,6 +238,9 @@
callEventCallback, mCallsManager, call);
call.setTransactionServiceWrapper(serviceWrapper);
+ if (mFeatureFlags.transactionalVideoState()) {
+ call.setTransactionalCallSupportsVideoCalling(callAttributes);
+ }
ICallControl clientCallControl = serviceWrapper.getICallControl();
if (clientCallControl == null) {
@@ -600,6 +602,53 @@
}
@Override
+ public ParceledListSlice<PhoneAccount> getRegisteredPhoneAccounts(String callingPackage,
+ String callingFeatureId) {
+ try {
+ Log.startSession("TSI.gRPA", Log.getPackageAbbreviation(callingPackage));
+ try {
+ enforceCallingPackage(callingPackage, "getRegisteredPhoneAccounts");
+ } catch (SecurityException se) {
+ EventLog.writeEvent(0x534e4554, "307609763", Binder.getCallingUid(),
+ "getRegisteredPhoneAccounts: invalid calling package");
+ throw se;
+ }
+
+ boolean hasCrossUserAccess = false;
+ try {
+ enforceInAppCrossUserPermission();
+ hasCrossUserAccess = true;
+ } catch (SecurityException e) {
+ // pass through
+ }
+
+ synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
+ try {
+ return new ParceledListSlice<>(
+ mPhoneAccountRegistrar.getPhoneAccounts(
+ 0 /* capabilities */,
+ 0 /* excludedCapabilities */,
+ null /* UriScheme */,
+ callingPackage,
+ true /* includeDisabledAccounts */,
+ callingUserHandle,
+ hasCrossUserAccess /* crossUserAccess */,
+ false /* includeAll */));
+ } catch (Exception e) {
+ Log.e(this, e, "getRegisteredPhoneAccounts");
+ throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
public int getAllPhoneAccountsCount() {
try {
Log.startSession("TSI.gAPAC");
@@ -2030,6 +2079,11 @@
pw.increaseIndent();
reflectAndPrintFlagConfigs(pw);
pw.decreaseIndent();
+
+ pw.println("TransactionManager: ");
+ pw.increaseIndent();
+ TransactionManager.getInstance().dump(pw);
+ pw.decreaseIndent();
}
if (isTimeLineView) {
Log.dumpEventsTimeline(pw);
@@ -3117,7 +3171,6 @@
+ " does not meet the requirements to access the phone number");
}
-
private boolean canReadPrivilegedPhoneState(String callingPackage, String message) {
// The system/default dialer can always read phone state - so that emergency calls will
// still work.
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 9f6fcba..b4c3a4d 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -425,6 +425,7 @@
emergencyCallDiagnosticLogger,
communicationDeviceTracker,
callStreamingNotification,
+ bluetoothDeviceManager,
featureFlags,
IncomingCallFilterGraph::new);
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 15278e1..793840e 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -61,11 +61,11 @@
return service;
}
- public TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
+ private TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
return mServiceLookupTable.get(pah);
}
- public boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
+ private boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
return mServiceLookupTable.containsKey(pah);
}
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 938ee58..d497c6a 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -43,10 +43,10 @@
import com.android.server.telecom.voip.HoldCallTransaction;
import com.android.server.telecom.voip.EndCallTransaction;
import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
-import com.android.server.telecom.voip.ParallelTransaction;
import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
import com.android.server.telecom.voip.SerialTransaction;
import com.android.server.telecom.voip.SetMuteStateTransaction;
+import com.android.server.telecom.voip.RequestVideoStateTransaction;
import com.android.server.telecom.voip.TransactionManager;
import com.android.server.telecom.voip.VoipCallTransaction;
import com.android.server.telecom.voip.VoipCallTransactionResult;
@@ -71,6 +71,7 @@
public static final String ANSWER = "Answer";
public static final String DISCONNECT = "Disconnect";
public static final String START_STREAMING = "StartStreaming";
+ public static final String REQUEST_VIDEO_STATE = "RequestVideoState";
// CallEventCallback : Telecom --> Client (ex. voip app)
public static final String ON_SET_ACTIVE = "onSetActive";
@@ -130,6 +131,7 @@
return mTransactionManager;
}
+ @VisibleForTesting
public PhoneAccountHandle getPhoneAccountHandle() {
return mPhoneAccountHandle;
}
@@ -166,7 +168,7 @@
return callCount;
}
- public void cleanupTransactionalServiceWrapper() {
+ private void cleanupTransactionalServiceWrapper() {
for (Call call : mTrackedCalls.values()) {
mCallsManager.markCallAsDisconnected(call,
new DisconnectCause(DisconnectCause.ERROR, "process died"));
@@ -179,7 +181,7 @@
** ICallControl: Client --> Server **
**********************************************************************************************
*/
- public final ICallControl mICallControl = new ICallControl.Stub() {
+ private final ICallControl mICallControl = new ICallControl.Stub() {
@Override
public void setActive(String callId, android.os.ResultReceiver callback)
throws RemoteException {
@@ -248,6 +250,17 @@
}
}
+ @Override
+ public void requestVideoState(int videoState, String callId, ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.rVS");
+ createTransactions(callId, callback, REQUEST_VIDEO_STATE, videoState);
+ } finally {
+ Log.endSession();
+ }
+ }
+
private void createTransactions(String callId, ResultReceiver callback, String action,
Object... objects) {
Log.d(TAG, "createTransactions: callId=" + callId);
@@ -274,6 +287,11 @@
addTransactionsToManager(mStreamingController.getStartStreamingTransaction(mCallsManager,
TransactionalServiceWrapper.this, call, mLock), callback);
break;
+ case REQUEST_VIDEO_STATE:
+ addTransactionsToManager(
+ new RequestVideoStateTransaction(mCallsManager, call,
+ (int) objects[0]), callback);
+ break;
}
} else {
Bundle exceptionBundle = new Bundle();
@@ -346,7 +364,7 @@
}
};
- public void addTransactionsToManager(VoipCallTransaction transaction,
+ private void addTransactionsToManager(VoipCallTransaction transaction,
ResultReceiver callback) {
Log.d(TAG, "addTransactionsToManager");
@@ -562,6 +580,15 @@
}
}
+ public void onVideoStateChanged(Call call, int videoState) {
+ if (call != null) {
+ try {
+ mICallEventCallback.onVideoStateChanged(call.getId(), videoState);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
public void removeCallFromWrappers(Call call) {
if (call != null) {
try {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 27e5a7d..a0ffe63 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -25,9 +25,8 @@
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
-import android.media.AudioManager;
import android.media.AudioDeviceInfo;
-import android.media.audio.common.AudioDevice;
+import android.media.AudioManager;
import android.os.Bundle;
import android.telecom.Log;
import android.util.ArraySet;
@@ -41,13 +40,17 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.concurrent.Executor;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
public class BluetoothDeviceManager {
@@ -98,6 +101,9 @@
synchronized (mLock) {
String logString;
if (profile == BluetoothProfile.HEADSET) {
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture.complete((BluetoothHeadset) proxy);
+ }
mBluetoothHeadset = (BluetoothHeadset) proxy;
logString = "Got BluetoothHeadset: " + mBluetoothHeadset;
} else if (profile == BluetoothProfile.HEARING_AID) {
@@ -137,6 +143,9 @@
LinkedHashMap<String, BluetoothDevice> lostServiceDevices;
String logString;
if (profile == BluetoothProfile.HEADSET) {
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture.complete(null);
+ }
mBluetoothHeadset = null;
lostServiceDevices = mHfpDevicesByAddress;
mBluetoothRouteManager.onActiveDeviceChanged(null,
@@ -201,6 +210,7 @@
private BluetoothRouteManager mBluetoothRouteManager;
private BluetoothHeadset mBluetoothHeadset;
+ private CompletableFuture<BluetoothHeadset> mBluetoothHeadsetFuture;
private BluetoothHearingAid mBluetoothHearingAid;
private boolean mLeAudioCallbackRegistered = false;
private BluetoothLeAudio mBluetoothLeAudioService;
@@ -218,8 +228,12 @@
public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
CallAudioCommunicationDeviceTracker communicationDeviceTracker,
FeatureFlags featureFlags) {
+ mFeatureFlags = featureFlags;
if (bluetoothAdapter != null) {
mBluetoothAdapter = bluetoothAdapter;
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture = new CompletableFuture<>();
+ }
bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
BluetoothProfile.HEADSET);
bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
@@ -229,7 +243,6 @@
mAudioManager = context.getSystemService(AudioManager.class);
mExecutor = context.getMainExecutor();
mCommunicationDeviceTracker = communicationDeviceTracker;
- mFeatureFlags = featureFlags;
}
}
@@ -333,7 +346,19 @@
}
public BluetoothHeadset getBluetoothHeadset() {
- return mBluetoothHeadset;
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ try {
+ mBluetoothHeadset = mBluetoothHeadsetFuture.get(500L,
+ TimeUnit.MILLISECONDS);
+ return mBluetoothHeadset;
+ } catch (TimeoutException | InterruptedException | ExecutionException e) {
+ // ignore
+ Log.w(this, "Acquire BluetoothHeadset service failed due to: " + e);
+ return null;
+ }
+ } else {
+ return mBluetoothHeadset;
+ }
}
public BluetoothAdapter getBluetoothAdapter() {
@@ -402,7 +427,7 @@
mHearingAidDeviceSyncIds.put(device, hiSyncId);
targetDeviceMap = mHearingAidDevicesByAddress;
} else if (deviceType == DEVICE_TYPE_HEADSET) {
- if (mBluetoothHeadset == null) {
+ if (getBluetoothHeadset() == null) {
Log.w(this, "Headset service null when receiving device added broadcast");
return;
}
@@ -465,7 +490,7 @@
}
public void disconnectSco() {
- if (mBluetoothHeadset == null) {
+ if (getBluetoothHeadset() == null) {
Log.w(this, "Trying to disconnect audio but no headset service exists.");
} else {
mBluetoothHeadset.disconnectAudio();
@@ -650,7 +675,7 @@
callProfile = BluetoothProfile.HEARING_AID;
} else if (mHfpDevicesByAddress.containsKey(address)) {
Log.i(this, "Telecomm found HFP device for address: " + address);
- if (mBluetoothHeadset == null) {
+ if (getBluetoothHeadset() == null) {
Log.w(this, "Attempting to turn on audio when the headset service is null");
return false;
}
@@ -707,9 +732,15 @@
Log.w(this, "Couldn't set active device to %s", address);
return false;
}
- int scoConnectionRequest = mBluetoothHeadset.connectAudio();
- return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
- scoConnectionRequest == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+ if (getBluetoothHeadset() != null) {
+ int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+ return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+ scoConnectionRequest
+ == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+ } else {
+ Log.w(this, "Couldn't find bluetooth headset service");
+ return false;
+ }
} else {
Log.w(this, "Attempting to turn on audio for a disconnected device");
return false;
@@ -739,16 +770,20 @@
// Get the inband ringing enabled status of expected BT device to route call audio instead
// of using the address of currently connected device.
BluetoothDevice activeDevice = mBluetoothRouteManager.getMostRecentlyReportedActiveDevice();
- Log.i(this, "isInbandRingingEnabled: activeDevice: " + activeDevice);
- if (mBluetoothRouteManager.isCachedLeAudioDevice(activeDevice)) {
+ return isInbandRingEnabled(activeDevice);
+ }
+
+ public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+ Log.i(this, "isInbandRingEnabled: device: " + bluetoothDevice);
+ if (mBluetoothRouteManager.isCachedLeAudioDevice(bluetoothDevice)) {
if (mBluetoothLeAudioService == null) {
Log.i(this, "isInbandRingingEnabled: no leaudio service available.");
return false;
}
- int groupId = mBluetoothLeAudioService.getGroupId(activeDevice);
+ int groupId = mBluetoothLeAudioService.getGroupId(bluetoothDevice);
return mBluetoothLeAudioService.isInbandRingtoneEnabled(groupId);
} else {
- if (mBluetoothHeadset == null) {
+ if (getBluetoothHeadset() == null) {
Log.i(this, "isInbandRingingEnabled: no headset service available.");
return false;
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 235ba56..7da5339 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -957,6 +957,11 @@
return mDeviceManager.isInbandRingingEnabled();
}
+ @VisibleForTesting
+ public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+ return mDeviceManager.isInbandRingEnabled(bluetoothDevice);
+ }
+
private boolean addDevice(String address) {
if (mAudioConnectingStates.containsKey(address)) {
Log.i(this, "Attempting to add device %s twice.", address);
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 6176087..621892a 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -34,6 +34,7 @@
@Override
public void start() {
+ if (mStats != null) mStats.markStarted();
// post timeout work
CompletableFuture<Void> future = new CompletableFuture<>();
mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
@@ -44,7 +45,7 @@
if (mCompleteListener != null) {
mCompleteListener.onTransactionTimeout(mTransactionName);
}
- finish();
+ timeout();
return null;
}, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
+ ".s", mLock));
@@ -68,7 +69,7 @@
transactionName));
mCompleteListener.onTransactionCompleted(mainResult,
mTransactionName);
- finish();
+ finish(mainResult);
return null;
}, new LoggedHandlerExecutor(mHandler,
mTransactionName + "@" + hashCode()
@@ -91,7 +92,7 @@
transactionName));
mCompleteListener.onTransactionCompleted(mainResult,
mTransactionName);
- finish();
+ finish(mainResult);
return null;
}, new LoggedHandlerExecutor(mHandler,
mTransactionName + "@" + hashCode()
diff --git a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
new file mode 100644
index 0000000..64596b1
--- /dev/null
+++ b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 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.voip;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.Call;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class RequestVideoStateTransaction extends VoipCallTransaction {
+
+ private static final String TAG = RequestVideoStateTransaction.class.getSimpleName();
+ private final Call mCall;
+ private final int mVideoProfileState;
+
+ public RequestVideoStateTransaction(CallsManager callsManager, Call call,
+ int transactionalVideoState) {
+ super(callsManager.getLock());
+ mCall = call;
+ mVideoProfileState = TransactionalVideoStateToVideoProfileState(transactionalVideoState);
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+ if (isRequestingVideoTransmission(mVideoProfileState) &&
+ !mCall.isVideoCallingSupportedByPhoneAccount()) {
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ "Video calling is not supported by the target account"));
+ } else if (isRequestingVideoTransmission(mVideoProfileState) &&
+ !mCall.isTransactionalCallSupportsVideoCalling()) {
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ "Video calling is not supported according to the callAttributes"));
+ } else {
+ mCall.setVideoState(mVideoProfileState);
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED,
+ "The Video State was changed successfully"));
+ }
+ return future;
+ }
+
+ private boolean isRequestingVideoTransmission(int targetVideoState) {
+ return targetVideoState != VideoProfile.STATE_AUDIO_ONLY;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index b35b471..7d5a178 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -21,6 +21,7 @@
import java.util.List;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* A VoipCallTransaction implementation that its sub transactions will be executed in serial
@@ -37,6 +38,7 @@
@Override
public void start() {
+ if (mStats != null) mStats.markStarted();
// post timeout work
CompletableFuture<Void> future = new CompletableFuture<>();
mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
@@ -47,7 +49,7 @@
if (mCompleteListener != null) {
mCompleteListener.onTransactionTimeout(mTransactionName);
}
- finish();
+ timeout();
return null;
}, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
+ ".s", mLock));
@@ -55,6 +57,7 @@
if (mSubTransactions != null && mSubTransactions.size() > 0) {
TransactionManager.TransactionCompleteListener subTransactionListener =
new TransactionManager.TransactionCompleteListener() {
+ private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
@Override
public void onTransactionCompleted(VoipCallTransactionResult result,
@@ -71,14 +74,16 @@
transactionName));
mCompleteListener.onTransactionCompleted(mainResult,
mTransactionName);
- finish();
+ finish(mainResult);
return null;
}, new LoggedHandlerExecutor(mHandler,
mTransactionName + "@" + hashCode()
+ ".oTC", mLock));
} else {
- if (mSubTransactions.size() > 0) {
- VoipCallTransaction transaction = mSubTransactions.remove(0);
+ int currTransactionIndex = mTransactionIndex.incrementAndGet();
+ if (currTransactionIndex < mSubTransactions.size()) {
+ VoipCallTransaction transaction = mSubTransactions.get(
+ currTransactionIndex);
transaction.setCompleteListener(this);
transaction.start();
} else {
@@ -99,14 +104,14 @@
transactionName));
mCompleteListener.onTransactionCompleted(mainResult,
mTransactionName);
- finish();
+ finish(mainResult);
return null;
}, new LoggedHandlerExecutor(mHandler,
mTransactionName + "@" + hashCode()
+ ".oTT", mLock));
}
};
- VoipCallTransaction transaction = mSubTransactions.remove(0);
+ VoipCallTransaction transaction = mSubTransactions.get(0);
transaction.setCompleteListener(subTransactionListener);
transaction.start();
} else {
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
index 228bdde..76d83cc 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -21,20 +21,25 @@
import android.os.OutcomeReceiver;
import android.telecom.TelecomManager;
import android.telecom.CallException;
+import android.util.IndentingPrintWriter;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
-
+import com.android.server.telecom.flags.Flags;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
import java.util.List;
+import java.util.Locale;
import java.util.Queue;
public class TransactionManager {
private static final String TAG = "VoipCallTransactionManager";
+ private static final int TRANSACTION_HISTORY_SIZE = 20;
private static TransactionManager INSTANCE = null;
private static final Object sLock = new Object();
- private Queue<VoipCallTransaction> mTransactions;
+ private final Queue<VoipCallTransaction> mTransactions;
+ private final Deque<VoipCallTransaction> mCompletedTransactions;
private VoipCallTransaction mCurrentTransaction;
public interface TransactionCompleteListener {
@@ -45,6 +50,10 @@
private TransactionManager() {
mTransactions = new ArrayDeque<>();
mCurrentTransaction = null;
+ if (Flags.enableCallSequencing()) {
+ mCompletedTransactions = new ArrayDeque<>();
+ } else
+ mCompletedTransactions = null;
}
public static TransactionManager getInstance() {
@@ -69,7 +78,7 @@
transaction.setCompleteListener(new TransactionCompleteListener() {
@Override
public void onTransactionCompleted(VoipCallTransactionResult result,
- String transactionName){
+ String transactionName) {
Log.i(TAG, String.format("transaction %s completed: with result=[%d]",
transactionName, result.getResult()));
if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
@@ -112,7 +121,10 @@
private void finishTransaction() {
synchronized (sLock) {
- mCurrentTransaction = null;
+ if (mCurrentTransaction != null) {
+ addTransactionToHistory(mCurrentTransaction);
+ mCurrentTransaction = null;
+ }
}
startTransactions();
}
@@ -123,8 +135,115 @@
synchronized (sLock) {
pendingTransactions = new ArrayList<>(mTransactions);
}
- for (VoipCallTransaction transaction : pendingTransactions) {
- transaction.finish();
+ for (VoipCallTransaction t : pendingTransactions) {
+ t.finish(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+ "clear called"));
}
}
+
+ private void addTransactionToHistory(VoipCallTransaction t) {
+ if (!Flags.enableCallSequencing()) return;
+
+ mCompletedTransactions.add(t);
+ if (mCompletedTransactions.size() > TRANSACTION_HISTORY_SIZE) {
+ mCompletedTransactions.poll();
+ }
+ }
+
+ /**
+ * Called when the dumpsys is created for telecom to capture the current state.
+ */
+ public void dump(IndentingPrintWriter pw) {
+ if (!Flags.enableCallSequencing()) {
+ pw.println("<<Flag not enabled>>");
+ return;
+ }
+ synchronized (sLock) {
+ pw.println("Pending Transactions:");
+ pw.increaseIndent();
+ for (VoipCallTransaction t : mTransactions) {
+ printPendingTransactionStats(t, pw);
+ }
+ pw.decreaseIndent();
+
+ pw.println("Ongoing Transaction:");
+ pw.increaseIndent();
+ if (mCurrentTransaction != null) {
+ printPendingTransactionStats(mCurrentTransaction, pw);
+ }
+ pw.decreaseIndent();
+
+ pw.println("Completed Transactions:");
+ pw.increaseIndent();
+ for (VoipCallTransaction t : mCompletedTransactions) {
+ printCompleteTransactionStats(t, pw);
+ }
+ pw.decreaseIndent();
+ }
+ }
+
+ /**
+ * Recursively print the pending {@link VoipCallTransaction} stats for logging purposes.
+ * @param t The transaction that stats should be printed for
+ * @param pw The IndentingPrintWriter to print the result to
+ */
+ private void printPendingTransactionStats(VoipCallTransaction t, IndentingPrintWriter pw) {
+ VoipCallTransaction.Stats s = t.getStats();
+ if (s == null) {
+ pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
+ return;
+ }
+ pw.println(String.format(Locale.getDefault(),
+ "[%s] %s: (result=[%s]), (created -> now : [%+d] mS),"
+ + " (created -> started : [%+d] mS),"
+ + " (started -> now : [%+d] mS)",
+ s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s),
+ s.measureTimeSinceCreatedMs(), s.measureCreatedToStartedMs(),
+ s.measureTimeSinceStartedMs()));
+
+ if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) {
+ return;
+ }
+ pw.increaseIndent();
+ for (VoipCallTransaction subTransaction : t.mSubTransactions) {
+ printPendingTransactionStats(subTransaction, pw);
+ }
+ pw.decreaseIndent();
+ }
+
+ /**
+ * Recursively print the complete Transaction stats for logging purposes.
+ * @param t The transaction that stats should be printed for
+ * @param pw The IndentingPrintWriter to print the result to
+ */
+ private void printCompleteTransactionStats(VoipCallTransaction t, IndentingPrintWriter pw) {
+ VoipCallTransaction.Stats s = t.getStats();
+ if (s == null) {
+ pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
+ return;
+ }
+ pw.println(String.format(Locale.getDefault(),
+ "[%s] %s: (result=[%s]), (created -> started : [%+d] mS), "
+ + "(started -> completed : [%+d] mS)",
+ s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s),
+ s.measureCreatedToStartedMs(), s.measureStartedToCompletedMs()));
+
+ if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) {
+ return;
+ }
+ pw.increaseIndent();
+ for (VoipCallTransaction subTransaction : t.mSubTransactions) {
+ printCompleteTransactionStats(subTransaction, pw);
+ }
+ pw.decreaseIndent();
+ }
+
+ private String parseTransactionResult(VoipCallTransaction.Stats s) {
+ if (s.isTimedOut()) return "TIMED OUT";
+ if (s.getTransactionResult() == null) return "PENDING";
+ if (s.getTransactionResult().getResult() == VoipCallTransactionResult.RESULT_SUCCEED) {
+ return "SUCCESS";
+ }
+ return s.getTransactionResult().toString();
+ }
}
diff --git a/src/com/android/server/telecom/voip/VideoStateTranslation.java b/src/com/android/server/telecom/voip/VideoStateTranslation.java
new file mode 100644
index 0000000..615e4bc
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VideoStateTranslation.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 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.voip;
+
+import android.telecom.CallAttributes;
+import android.telecom.Log;
+import android.telecom.VideoProfile;
+
+/**
+ * This remapping class is needed because {@link VideoProfile} has more fine grain levels of video
+ * states as apposed to Transactional video states (defined in {@link CallAttributes.CallType}.
+ * To be more specific, there are 3 video states (rx, tx, and bi-directional).
+ * {@link CallAttributes.CallType} only has 2 states (audio and video).
+ *
+ * The reason why Transactional calls have fewer states is due to the fact that the framework is
+ * only used by VoIP apps and Telecom only cares to know if the call is audio or video.
+ *
+ * Calls that are backed by a {@link android.telecom.ConnectionService} have the ability to be
+ * managed calls (non-VoIP) and Dialer needs more fine grain video states to update the UI. Thus,
+ * {@link VideoProfile} is used for {@link android.telecom.ConnectionService} backed calls.
+ */
+public class VideoStateTranslation {
+ private static final String TAG = VideoStateTranslation.class.getSimpleName();
+
+ /**
+ * Client --> Telecom
+ * This should be used when the client application is signaling they are changing the video
+ * state.
+ */
+ public static int TransactionalVideoStateToVideoProfileState(int transactionalVideo) {
+ if (transactionalVideo == CallAttributes.AUDIO_CALL) {
+ Log.i(TAG, "%s --> VideoProfile.STATE_AUDIO_ONLY",
+ TransactionalVideoState_toString(transactionalVideo));
+ return VideoProfile.STATE_AUDIO_ONLY;
+ } else {
+ Log.i(TAG, "%s --> VideoProfile.STATE_BIDIRECTIONAL",
+ TransactionalVideoState_toString(transactionalVideo));
+ return VideoProfile.STATE_BIDIRECTIONAL;
+ }
+ }
+
+ /**
+ * Telecom --> Client
+ * This should be used when Telecom is informing the client of a video state change.
+ */
+ public static int VideoProfileStateToTransactionalVideoState(int videoProfileState) {
+ if (videoProfileState == VideoProfile.STATE_AUDIO_ONLY) {
+ Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
+ VideoProfileState_toString(videoProfileState));
+ return CallAttributes.AUDIO_CALL;
+ } else {
+ Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
+ VideoProfileState_toString(videoProfileState));
+ return CallAttributes.VIDEO_CALL;
+ }
+ }
+
+ private static String TransactionalVideoState_toString(int transactionalVideoState) {
+ if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
+ return "CallAttributes.AUDIO_CALL";
+ } else {
+ return "CallAttributes.VIDEO_CALL";
+ }
+ }
+
+ private static String VideoProfileState_toString(int videoProfileState) {
+ switch (videoProfileState) {
+ case VideoProfile.STATE_BIDIRECTIONAL -> {
+ return "VideoProfile.STATE_BIDIRECTIONAL";
+ }
+ case VideoProfile.STATE_RX_ENABLED -> {
+ return "VideoProfile.STATE_RX_ENABLED";
+ }
+ case VideoProfile.STATE_TX_ENABLED -> {
+ return "VideoProfile.STATE_TX_ENABLED";
+ }
+ }
+ return "VideoProfile.STATE_AUDIO_ONLY";
+ }
+}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index a952eb1..3c91158 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -22,23 +22,119 @@
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.Flags;
+import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
public class VoipCallTransaction {
//TODO: add log events
protected static final long TIMEOUT_LIMIT = 5000L;
+
+ /**
+ * Tracks stats about a transaction for logging purposes.
+ */
+ public static class Stats {
+ // the logging visible timestamp for ease of debugging
+ public final LocalDateTime addedTimeStamp;
+ // the time in nS that the transaction was first created
+ private final long mCreatedTimeNs;
+ // the time that the transaction was started.
+ private long mStartedTimeNs = -1L;
+ // the time that the transaction was finished.
+ private long mFinishedTimeNs = -1L;
+ // If finished, did this transaction finish because it timed out?
+ private boolean mIsTimedOut = false;
+ private VoipCallTransactionResult mTransactionResult = null;
+
+ public Stats() {
+ addedTimeStamp = LocalDateTime.now();
+ mCreatedTimeNs = System.nanoTime();
+ }
+
+ /**
+ * Mark the transaction as started and record the time.
+ */
+ public void markStarted() {
+ if (mStartedTimeNs > -1) return;
+ mStartedTimeNs = System.nanoTime();
+ }
+
+ /**
+ * Mark the transaction as completed and record the time.
+ */
+ public void markComplete(boolean isTimedOut, VoipCallTransactionResult result) {
+ if (mFinishedTimeNs > -1) return;
+ mFinishedTimeNs = System.nanoTime();
+ mIsTimedOut = isTimedOut;
+ mTransactionResult = result;
+ }
+
+ /**
+ * @return Time in mS since the transaction was created.
+ */
+ public long measureTimeSinceCreatedMs() {
+ return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mCreatedTimeNs);
+ }
+
+ /**
+ * @return Time in mS between when transaction was created and when it was marked as
+ * started. Returns -1 if the transaction was not started yet.
+ */
+ public long measureCreatedToStartedMs() {
+ return mStartedTimeNs > 0 ?
+ TimeUnit.NANOSECONDS.toMillis(mStartedTimeNs - mCreatedTimeNs) : -1;
+ }
+
+ /**
+ * @return Time in mS since the transaction was marked started to the TransactionManager.
+ * Returns -1 if the transaction hasn't been started yet.
+ */
+ public long measureTimeSinceStartedMs() {
+ return mStartedTimeNs > 0 ?
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mStartedTimeNs) : -1;
+ }
+
+ /**
+ * @return Time in mS between when the transaction was marked as started and when it was
+ * marked as completed. Returns -1 if the transaction hasn't started or finished yet.
+ */
+ public long measureStartedToCompletedMs() {
+ return (mStartedTimeNs > 0 && mFinishedTimeNs > 0) ?
+ TimeUnit.NANOSECONDS.toMillis(mFinishedTimeNs - mStartedTimeNs) : -1;
+
+ }
+
+ /**
+ * @return true if this transaction completed due to timing out, false if the transaction
+ * hasn't completed yet or it completed and did not time out.
+ */
+ public boolean isTimedOut() {
+ return mIsTimedOut;
+ }
+
+ /**
+ * @return the result if the transaction completed, null if it timed out or hasn't completed
+ * yet.
+ */
+ public VoipCallTransactionResult getTransactionResult() {
+ return mTransactionResult;
+ }
+ }
+
protected final AtomicBoolean mCompleted = new AtomicBoolean(false);
- protected String mTransactionName = this.getClass().getSimpleName();
+ protected final String mTransactionName = this.getClass().getSimpleName();
private HandlerThread mHandlerThread;
protected Handler mHandler;
protected TransactionManager.TransactionCompleteListener mCompleteListener;
protected List<VoipCallTransaction> mSubTransactions;
protected TelecomSystem.SyncRoot mLock;
+ protected final Stats mStats;
public VoipCallTransaction(
List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock) {
@@ -47,6 +143,7 @@
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mLock = lock;
+ mStats = Flags.enableCallSequencing() ? new Stats() : null;
}
public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
@@ -54,6 +151,7 @@
}
public void start() {
+ if (mStats != null) mStats.markStarted();
// post timeout work
CompletableFuture<Void> future = new CompletableFuture<>();
mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
@@ -64,7 +162,7 @@
if (mCompleteListener != null) {
mCompleteListener.onTransactionTimeout(mTransactionName);
}
- finish();
+ timeout();
return null;
}, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
+ ".s", mLock));
@@ -82,7 +180,7 @@
if (mCompleteListener != null) {
mCompleteListener.onTransactionCompleted(result, mTransactionName);
}
- finish();
+ finish(result);
return null;
}, executor)
.exceptionallyAsync((throwable -> {
@@ -100,11 +198,27 @@
mCompleteListener = listener;
}
- public void finish() {
+ public void timeout() {
+ finish(true, null);
+ }
+
+ public void finish(VoipCallTransactionResult result) {
+ finish(false, result);
+ }
+
+ public void finish(boolean isTimedOut, VoipCallTransactionResult result) {
+ if (mStats != null) mStats.markComplete(isTimedOut, result);
// finish all sub transactions
- if (mSubTransactions != null && mSubTransactions.size() > 0) {
- mSubTransactions.forEach(VoipCallTransaction::finish);
+ if (mSubTransactions != null && !mSubTransactions.isEmpty()) {
+ mSubTransactions.forEach( t -> t.finish(isTimedOut, result));
}
mHandlerThread.quit();
}
+
+ /**
+ * @return Stats related to this transaction if stats are enabled, null otherwise.
+ */
+ public Stats getStats() {
+ return mStats;
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index 648a831..c516c8e 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -403,7 +403,8 @@
when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
mBluetoothDeviceManager.connectAudio(device1.getAddress(), false);
- verify(mAdapter).setActiveDevice(device1, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+ verify(mAdapter).setActiveDevice(eq(device1),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_ALL));
mBluetoothDeviceManager.disconnectAudio();
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 08576fc..0a53eb0 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -16,18 +16,43 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.CallAudioRouteAdapter.ACTIVE_FOCUS;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_DOCK;
+import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_DOCK;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_ON;
+import static com.android.server.telecom.CallAudioRouteAdapter.NO_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.RINGING_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
+import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_DISABLED;
+import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_ENABLED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_SPEAKER;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -35,15 +60,19 @@
import android.bluetooth.BluetoothDevice;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
+import android.media.IAudioService;
import android.media.audiopolicy.AudioProductStrategy;
+import android.os.UserHandle;
import android.telecom.CallAudioState;
import androidx.test.filters.SmallTest;
import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import org.junit.After;
import org.junit.Before;
@@ -62,6 +91,9 @@
@Mock AudioManager mAudioManager;
@Mock AudioDeviceInfo mEarpieceDeviceInfo;
@Mock CallsManager mCallsManager;
+ @Mock CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+ @Mock IAudioService mAudioService;
+ @Mock BluetoothRouteManager mBluetoothRouteManager;
private AudioRoute mEarpieceRoute;
private AudioRoute mSpeakerRoute;
private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -90,10 +122,16 @@
new AudioDeviceInfo[] {
mEarpieceDeviceInfo
});
+ when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
+ .thenReturn(null);
+ when(mAudioServiceFactory.getAudioService()).thenReturn(mAudioService);
+ when(mContext.getAttributionTag()).thenReturn("");
doNothing().when(mCallsManager).onCallAudioStateChanged(any(CallAudioState.class),
any(CallAudioState.class));
- mController = new CallAudioRouteController(mContext, mCallsManager, mAudioRouteFactory,
- mWiredHeadsetManager);
+ when(mCallsManager.getCurrentUserHandle()).thenReturn(
+ new UserHandle(UserHandle.USER_SYSTEM));
+ mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
+ mAudioRouteFactory, mWiredHeadsetManager, mBluetoothRouteManager);
mController.setAudioRouteFactory(mAudioRouteFactory);
mController.setAudioManager(mAudioManager);
mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
@@ -148,6 +186,7 @@
AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
nullable(AudioDeviceInfo.class));
+
expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
| CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
@@ -166,9 +205,6 @@
@SmallTest
@Test
public void testActiveDeactivateBluetoothDevice() {
- when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
- .thenReturn(null);
-
mController.initialize();
mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
BLUETOOTH_DEVICE_1);
@@ -192,7 +228,256 @@
@SmallTest
@Test
- public void testSwitchFocusInBluetoothRoute() {
+ public void testSwitchFocusForBluetoothDeviceSupportInbandRinging() {
+ doAnswer(invocation -> {
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ return true;
+ }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
+ doAnswer(invocation -> {
+ mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0, BLUETOOTH_DEVICE_1);
+ return true;
+ }).when(mAudioManager).clearCommunicationDevice();
+ when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ BLUETOOTH_DEVICE_1);
+
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ assertFalse(mController.isActive());
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS);
+ verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
+ nullable(AudioDeviceInfo.class));
+ assertTrue(mController.isActive());
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ assertTrue(mController.isActive());
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+ verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+ assertFalse(mController.isActive());
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectWiredHeadset() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectDock() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(DISCONNECT_DOCK);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSpeakerToggle() {
+ mController.initialize();
+ mController.setActive(true);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSpeakerToggleWhenDockConnected() {
+ mController.initialize();
+ mController.setActive(true);
+ mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSwitchEarpiece() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_EARPIECE);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSwitchBluetooth() {
+ doAnswer(invocation -> {
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ return true;
+ }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
+
+ mController.initialize();
+ mController.setActive(true);
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ BLUETOOTH_DEVICE_1);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0,
+ BLUETOOTH_DEVICE_1.getAddress());
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void tesetSwitchSpeakerAndHeadset() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_HEADSET);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testEnableAndDisableStreaming() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(STREAMING_FORCE_ENABLED);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_STREAMING,
+ CallAudioState.ROUTE_STREAMING, null, new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(STREAMING_FORCE_DISABLED);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testToggleMute() throws Exception {
+ when(mAudioManager.isMasterMute()).thenReturn(false);
+
+ mController.initialize();
+ mController.setActive(true);
+
+ mController.sendMessageWithSessionInfo(MUTE_ON);
+ CallAudioState expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+ anyInt(), anyString());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ when(mAudioManager.isMasterMute()).thenReturn(true);
+ mController.sendMessageWithSessionInfo(MUTE_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(false), anyString(),
+ anyInt(), anyString());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
}
}
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index a7ccb0a..1529629 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -127,6 +127,7 @@
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
@@ -290,6 +291,7 @@
@Mock private PhoneCapability mPhoneCapability;
@Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
@Mock private CallStreamingNotification mCallStreamingNotification;
+ @Mock private BluetoothDeviceManager mBluetoothDeviceManager;
@Mock private FeatureFlags mFeatureFlags;
@Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
private CallsManager mCallsManager;
@@ -366,6 +368,7 @@
mEmergencyCallDiagnosticLogger,
mCommunicationDeviceTracker,
mCallStreamingNotification,
+ mBluetoothDeviceManager,
mFeatureFlags,
(call, listener, context, timeoutsAdapter, lock) -> mIncomingCallFilterGraph);
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index e973992..2f27bb5 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -838,9 +838,10 @@
private ConnectionServiceWrapper makeConnectionServiceWrapper() {
ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
+
when(mMockConnectionServiceRepository.getService(
- eq(makeQuickConnectionServiceComponentName()),
- any(UserHandle.class), any(FeatureFlags.class))).thenReturn(wrapper);
+ eq(makeQuickConnectionServiceComponentName()), any(UserHandle.class)))
+ .thenReturn(wrapper);
return wrapper;
}
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index a0ec267..1215fd3 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -59,6 +59,7 @@
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
+import android.util.Pair;
import androidx.test.filters.SmallTest;
@@ -83,6 +84,7 @@
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
@RunWith(JUnit4.class)
public class RingerTest extends TelecomTestCase {
@@ -144,7 +146,6 @@
mockNotificationManager =mContext.getSystemService(NotificationManager.class);
when(mockTonePlayer.startTone()).thenReturn(true);
when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true);
- when(mockRingtoneFactory.hasHapticChannels(any(Ringtone.class))).thenReturn(false);
when(mockCall1.getState()).thenReturn(CallState.RINGING);
when(mockCall2.getState()).thenReturn(CallState.RINGING);
when(mockCall1.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle());
@@ -426,7 +427,7 @@
// Pretend we're using audio coupled haptics.
setIsUsingHaptics(mockRingtone, true);
assertTrue(startRingingAndWaitForAsync(mockCall1, false));
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
@@ -468,14 +469,14 @@
mRingerUnderTest.startCallWaiting(mockCall1);
when(mockRingtoneFactory.getRingtone(any(Call.class), eq(null), anyBoolean()))
- .thenReturn(mockRingtone);
+ .thenReturn(new Pair(FAKE_RINGTONE_URI, mockRingtone));
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
enableVibrationWhenRinging();
assertFalse(startRingingAndWaitForAsync(mockCall2, false));
verify(mockTonePlayer).stopTone();
// Try to play a silent haptics ringtone
- verify(mockRingtoneFactory, times(1)).getHapticOnlyRingtone();
+ verify(mockRingtoneFactory, atLeastOnce()).getHapticOnlyRingtone();
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockRingtone).play();
@@ -514,7 +515,7 @@
enableVibrationWhenRinging();
assertFalse(startRingingAndWaitForAsync(mockCall2, false));
- verify(mockRingtoneFactory, times(1)).getHapticOnlyRingtone();
+ verify(mockRingtoneFactory, atLeastOnce()).getHapticOnlyRingtone();
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
// Try to play a silent haptics ringtone
@@ -534,7 +535,7 @@
enableVibrationWhenRinging();
assertTrue(startRingingAndWaitForAsync(mockCall2, false));
verify(mockTonePlayer).stopTone();
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), isNull(), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockRingtone).play();
@@ -551,7 +552,7 @@
ensureRingerIsAudible();
enableVibrationOnlyWhenNotRinging();
assertTrue(startRingingAndWaitForAsync(mockCall2, false));
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
@@ -570,7 +571,7 @@
enableRampingRinger();
enableVibrationWhenRinging();
assertTrue(startRingingAndWaitForAsync(mockCall2, false));
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
@@ -602,7 +603,7 @@
when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(100);
enableVibrationWhenRinging();
assertTrue(startRingingAndWaitForAsync(mockCall2, true));
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), isNull(), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
@@ -623,7 +624,7 @@
asyncRingtonePlayer.updateBtActiveState(true);
mRingCompletionFuture.get();
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class),
anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
@@ -753,7 +754,7 @@
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
- return mockRingtone;
+ return new Pair(FAKE_RINGTONE_URI, mockRingtone);
});
// Start call waiting to make sure that it doesn't stop when we start ringing
enableVibrationWhenRinging();
@@ -832,10 +833,12 @@
private Ringtone ensureRingtoneMocked() {
Ringtone mockRingtone = mock(Ringtone.class);
+ Pair<Uri, Ringtone> ringtoneInfo = new Pair(
+ FAKE_RINGTONE_URI, mockRingtone);
when(mockRingtoneFactory.getRingtone(
any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
- .thenReturn(mockRingtone);
- when(mockRingtoneFactory.getHapticOnlyRingtone()).thenReturn(mockRingtone);
+ .thenReturn(ringtoneInfo);
+ when(mockRingtoneFactory.getHapticOnlyRingtone()).thenReturn(ringtoneInfo);
return mockRingtone;
}