Merge "Cache call removal future to enable canceling it when we retry" into main
diff --git a/flags/Android.bp b/flags/Android.bp
index 25a7a8c..d60a5f5 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -41,5 +41,6 @@
"telecom_connection_service_wrapper_flags.aconfig",
"telecom_remote_connection_service.aconfig",
"telecom_profile_user_flags.aconfig",
+ "telecom_bluetoothdevicemanager_flags.aconfig",
],
}
diff --git a/flags/telecom_bluetoothdevicemanager_flags.aconfig b/flags/telecom_bluetoothdevicemanager_flags.aconfig
new file mode 100644
index 0000000..4c91491
--- /dev/null
+++ b/flags/telecom_bluetoothdevicemanager_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tgunn TARGET=24Q4
+flag {
+ name: "postpone_register_to_leaudio"
+ namespace: "telecom"
+ description: "Fix for Log.wtf in the BinderProxy"
+ bug: "333417369"
+}
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index 1608869..33bccba 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -80,3 +80,22 @@
description: "Clear the requested communication device after the audio operations are completed."
bug: "315865533"
}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "resolve_switching_bt_devices_computation"
+ namespace: "telecom"
+ description: "Update switching bt devices based on arbitrary device chosen if no device is specified."
+ bug: "333751408"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "early_update_internal_call_audio_state"
+ namespace: "telecom"
+ description: "Update internal call audio state before sending updated state to ICS"
+ bug: "335538831"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 44645dc..df2c70c 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -27,7 +27,7 @@
<string name="notification_missedCall_call_back" msgid="7900333283939789732">"फेरि कल गर्नुहोस्"</string>
<string name="notification_missedCall_message" msgid="4054698824390076431">"सन्देश"</string>
<string name="notification_disconnectedCall_title" msgid="1790131923692416928">"विच्छेद गरिएको कल"</string>
- <string name="notification_disconnectedCall_body" msgid="600491714584417536">"आपत्कालीन कल गरिएको हुनाले <xliff:g id="CALLER">%s</xliff:g> लाई गरिएको कल विच्छेद गरियो।"</string>
+ <string name="notification_disconnectedCall_body" msgid="600491714584417536">"आपत्कालीन कल गरिएको हुनाले <xliff:g id="CALLER">%s</xliff:g> लाई गरिएको कल डिस्कनेक्ट गरियो।"</string>
<string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"आपत्कालीन कल जारी रहेको हुनाले तपाईंको कल विच्छेद गरिएको छ।"</string>
<string name="notification_audioProcessing_title" msgid="1619035039880584575">"पृष्ठभूमिको कल"</string>
<string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> ले ब्याकग्राउन्डमा कुनै कल प्रोसेस गर्दै छ। यो एपले तपाईंको कलको अडियो प्रयोग गरिरहेको र सोही अडियो प्ले गरिरहेको हुन सक्छ।"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 5ae2e79..70f9bfc 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -133,5 +133,5 @@
<string name="callendpoint_name_unknown" msgid="2199074708477193852">"Không xác định"</string>
<string name="call_streaming_notification_body" msgid="502216105683378263">"Đang truyền trực tuyến âm thanh tới thiết bị khác"</string>
<string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Kết thúc"</string>
- <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển đổi tại đây"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển qua thiết bị này"</string>
</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index bf30720..ae5d88e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -80,6 +80,16 @@
callers are combined into a single toggle. -->
<bool name="combine_options_to_block_unavailable_and_unknown_callers">true</bool>
- <!-- System bluetooth stack package name -->
- <string name="system_bluetooth_stack">com.android.bluetooth</string>
+ <!-- When true, skip fetching quick reply response -->
+ <bool name="skip_loading_canned_text_response">false</bool>
+
+ <!-- When true, skip fetching incoming caller info -->
+ <bool name="skip_incoming_caller_info_query">false</bool>
+
+ <string-array name="system_bluetooth_stack_package_name" translatable="false">
+ <!-- AOSP -->
+ <item>com.android.bluetooth</item>
+ <!-- Used for internal targets -->
+ <item>com.google.android.bluetooth</item>
+ </string-array>
</resources>
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index 7b593d7..8a5e858 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -28,6 +28,7 @@
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.telecom.Log;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
@@ -226,7 +227,7 @@
AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_LE, bluetoothLeDeviceInfoTypes);
}
- int getType() {
+ public int getType() {
return mAudioRouteType;
}
@@ -237,7 +238,7 @@
// Invoked when entered pending route whose dest route is this route
void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
BluetoothDevice device, AudioManager audioManager,
- BluetoothRouteManager bluetoothRouteManager) {
+ BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
if (pendingAudioRoute.isActive() && !active) {
clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
@@ -251,20 +252,19 @@
// Check if the communication device was set for the device, even if
// BluetoothHeadset#connectAudio reports that the SCO connection wasn't
// successfully established.
- boolean scoConnected = audioManager.getCommunicationDevice().equals(mInfo);
- if (connectedBtAudio || scoConnected) {
+ if (connectedBtAudio || isScoAudioConnected) {
pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
- }
- if (connectedBtAudio) {
- pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
- } else if (!scoConnected) {
- pendingAudioRoute.onMessageReceived(
- PENDING_ROUTE_FAILED, mBluetoothAddress);
+ if (!isScoAudioConnected) {
+ pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
+ }
+ } else {
+ pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+ mBluetoothAddress), mBluetoothAddress);
}
return;
}
} else if (mAudioRouteType == TYPE_SPEAKER) {
- pendingAudioRoute.addMessage(SPEAKER_ON);
+ pendingAudioRoute.addMessage(SPEAKER_ON, null);
}
boolean result = false;
@@ -291,7 +291,7 @@
// before being able to successfully set the communication device. Refrain from sending
// pending route failed message for BT route until the second attempt fails.
if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
- pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED, null);
+ pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, null), null);
}
}
}
@@ -303,13 +303,13 @@
Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
if (active) {
if (mAudioRouteType == TYPE_SPEAKER) {
- pendingAudioRoute.addMessage(SPEAKER_OFF);
+ pendingAudioRoute.addMessage(SPEAKER_OFF, null);
}
int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
audioManager);
// Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
- pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
+ pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
}
}
}
@@ -370,7 +370,7 @@
return success;
}
- private int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
+ int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
// Try to see if there's a previously set device for communication that should be cleared.
// This only serves to help in the SCO case to ensure that we disconnect the headset.
diff --git a/src/com/android/server/telecom/CachedVideoStateChange.java b/src/com/android/server/telecom/CachedVideoStateChange.java
new file mode 100644
index 0000000..0892c33
--- /dev/null
+++ b/src/com/android/server/telecom/CachedVideoStateChange.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
+
+import android.telecom.Log;
+
+public class CachedVideoStateChange implements CachedCallback {
+ public static final String ID = CachedVideoStateChange.class.getSimpleName();
+ int mCurrentVideoState;
+
+ public int getCurrentCallEndpoint() {
+ return mCurrentVideoState;
+ }
+
+ public CachedVideoStateChange(int videoState) {
+ mCurrentVideoState = videoState;
+ }
+
+ @Override
+ public void executeCallback(CallSourceService service, Call call) {
+ service.onVideoStateChanged(call, mCurrentVideoState);
+ Log.addEvent(call, LogUtils.Events.VIDEO_STATE_CHANGED,
+ TransactionalVideoStateToString(mCurrentVideoState));
+ }
+
+ @Override
+ public String getCallbackId() {
+ return ID;
+ }
+
+ @Override
+ public int hashCode() {
+ return mCurrentVideoState;
+ }
+
+ @Override
+ public boolean equals(Object obj){
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof CachedVideoStateChange other)) {
+ return false;
+ }
+ return mCurrentVideoState == other.mCurrentVideoState;
+ }
+}
+
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 8fbe082..760028d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -19,6 +19,7 @@
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.TransactionalVideoStateToString;
import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
import android.annotation.NonNull;
@@ -1610,7 +1611,11 @@
mIsTestEmergencyCall = mHandle != null &&
isTestEmergencyCall(mHandle.getSchemeSpecificPart());
}
- startCallerInfoLookup();
+ if (!mContext.getResources().getBoolean(R.bool.skip_incoming_caller_info_query)) {
+ startCallerInfoLookup();
+ } else {
+ Log.i(this, "skip incoming caller info lookup");
+ }
for (Listener l : mListeners) {
l.onHandleChanged(this);
}
@@ -3747,6 +3752,11 @@
* SMSes to that number will silently fail.
*/
public boolean isRespondViaSmsCapable() {
+ if (mContext.getResources().getBoolean(R.bool.skip_loading_canned_text_response)) {
+ Log.d(this, "maybeLoadCannedSmsResponses: skip loading due to setting");
+ return false;
+ }
+
if (mState != CallState.RINGING) {
return false;
}
@@ -4126,14 +4136,8 @@
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;
- }
+ // TODO:: b/338280297. If a transactional call does not have the
+ // CallAttributes.SUPPORTS_VIDEO_CALLING capability, the videoState should be set to audio
// 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
@@ -4150,17 +4154,24 @@
int previousVideoState = mVideoState;
mVideoState = videoState;
if (mVideoState != previousVideoState) {
- Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
- VideoProfile.videoStateToString(videoState));
+ if (!mIsTransactionalCall) {
+ Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+ VideoProfile.videoStateToString(videoState));
+ }
for (Listener l : mListeners) {
l.onVideoStateChanged(this, previousVideoState, mVideoState);
}
}
- if (mFlags.transactionalVideoState()
- && mIsTransactionalCall && mTransactionalService != null) {
+ if (mFlags.transactionalVideoState() && mIsTransactionalCall) {
int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
- mTransactionalService.onVideoStateChanged(this, transactionalVS);
+ if (mTransactionalService != null) {
+ Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+ TransactionalVideoStateToString(transactionalVS));
+ mTransactionalService.onVideoStateChanged(this, transactionalVS);
+ } else {
+ cacheServiceCallback(new CachedVideoStateChange(transactionalVS));
+ }
}
if (VideoProfile.isVideo(videoState)) {
@@ -4855,12 +4866,20 @@
}
/**
+ * @return The binding {@link CompletableFuture} for the BT ICS.
+ */
+ public CompletableFuture<Boolean> getBtIcsFuture() {
+ return mBtIcsFuture;
+ }
+
+ /**
* Wait for bluetooth {@link android.telecom.InCallService} binding completion or timeout. Used
* for audio routing operations for a ringing call.
*/
public void waitForBtIcs() {
if (mBtIcsFuture != null) {
try {
+ Log.i(this, "waitForBtIcs: waiting for BT service to bind");
mBtIcsFuture.get();
} catch (InterruptedException | ExecutionException e) {
// ignore
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
index 3a05eb5..8130685 100644
--- a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -31,6 +31,8 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Semaphore;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
/**
* Helper class used to keep track of the requested communication device within Telecom for audio
@@ -47,7 +49,7 @@
private int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
// Keep track of the locally requested BT audio device if set
private String mBtAudioDevice = null;
- private final Semaphore mLock = new Semaphore(1);
+ private final Lock mLock = new ReentrantLock();
public CallAudioCommunicationDeviceTracker(Context context) {
mAudioManager = context.getSystemService(AudioManager.class);
@@ -58,11 +60,29 @@
}
public boolean isAudioDeviceSetForType(int audioDeviceType) {
- return mAudioDeviceType == audioDeviceType;
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
+ }
+ try {
+ return mAudioDeviceType == audioDeviceType;
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
}
public int getCurrentLocallyRequestedCommunicationDevice() {
- return mAudioDeviceType;
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
+ }
+ try {
+ return mAudioDeviceType;
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
}
@VisibleForTesting
@@ -71,13 +91,22 @@
}
public void clearBtCommunicationDevice() {
- if (mBtAudioDevice == null) {
- Log.i(this, "No bluetooth device was set for communication that can be cleared.");
- return;
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
}
- // If mBtAudioDevice is set, we know a BT audio device was set for communication so
- // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
- clearCommunicationDevice(mAudioDeviceType);
+ try {
+ if (mBtAudioDevice == null) {
+ Log.i(this, "No bluetooth device was set for communication that can be cleared.");
+ } else {
+ // If mBtAudioDevice is set, we know a BT audio device was set for communication so
+ // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
+ processClearCommunicationDevice(mAudioDeviceType);
+ }
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
}
/*
@@ -93,8 +122,19 @@
public boolean setCommunicationDevice(int audioDeviceType,
BluetoothDevice btDevice) {
if (Flags.communicationDeviceProtectedByLock()) {
- mLock.tryAcquire();
+ mLock.lock();
}
+ try {
+ return processSetCommunicationDevice(audioDeviceType, btDevice);
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
+ }
+
+ private boolean processSetCommunicationDevice(int audioDeviceType,
+ BluetoothDevice btDevice) {
// There is only one audio device type associated with each type of BT device.
boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s",
@@ -132,14 +172,14 @@
Log.i(this, "No active device of type(s) %s available",
audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
- AudioDeviceInfo.TYPE_USB_HEADSET)
+ AudioDeviceInfo.TYPE_USB_HEADSET)
: audioDeviceType);
return false;
}
// Force clear previous communication device, if one was set, before setting the new device.
if (mAudioDeviceType != sAUDIO_DEVICE_TYPE_INVALID) {
- clearCommunicationDevice(mAudioDeviceType);
+ processClearCommunicationDevice(mAudioDeviceType);
}
// Turn activeDevice ON.
@@ -161,12 +201,8 @@
mBtAudioDevice = null;
}
}
- if (Flags.communicationDeviceProtectedByLock()) {
- mLock.release();
- }
return result;
}
-
/*
* Clears the communication device for the passed in audio device types, given that the device
* has previously been set for communication.
@@ -174,8 +210,23 @@
*/
public void clearCommunicationDevice(int audioDeviceType) {
if (Flags.communicationDeviceProtectedByLock()) {
- mLock.tryAcquire();
+ mLock.lock();
}
+ try {
+ processClearCommunicationDevice(audioDeviceType);
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
+ }
+
+ public void processClearCommunicationDevice(int audioDeviceType) {
+ if (audioDeviceType == sAUDIO_DEVICE_TYPE_INVALID) {
+ Log.i(this, "clearCommunicationDevice: Skip clearing communication device"
+ + "for invalid audio type (-1).");
+ }
+
// There is only one audio device type associated with each type of BT device.
boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s",
@@ -184,10 +235,10 @@
if (audioDeviceType != mAudioDeviceType
&& !isUsbHeadsetType(audioDeviceType, mAudioDeviceType)) {
Log.i(this, "Unable to clear communication device of type(s), %s. "
- + "Device does not correspond to the locally requested device type.",
+ + "Device does not correspond to the locally requested device type.",
audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
- AudioDeviceInfo.TYPE_USB_HEADSET)
+ AudioDeviceInfo.TYPE_USB_HEADSET)
: audioDeviceType
);
return;
@@ -207,13 +258,10 @@
mBluetoothRouteManager.onAudioLost(mBtAudioDevice);
mBtAudioDevice = null;
}
- if (Flags.communicationDeviceProtectedByLock()) {
- mLock.release();
- }
}
private boolean isUsbHeadsetType(int audioDeviceType, int sourceType) {
- return audioDeviceType != AudioDeviceInfo.TYPE_WIRED_HEADSET
- ? false : sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
+ return audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+ && sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
}
}
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index e5678a0..1f1ca9d 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -20,6 +20,8 @@
import android.content.Context;
import android.media.IAudioService;
import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.Log;
@@ -36,6 +38,7 @@
import java.util.HashSet;
import java.util.Set;
import java.util.LinkedHashSet;
+import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@@ -66,9 +69,13 @@
private Call mStreamingCall;
private Call mForegroundCall;
+ private CompletableFuture<Boolean> mCallRingingFuture;
+ private Thread mBtIcsBindingThread;
private boolean mIsTonePlaying = false;
private boolean mIsDisconnectedTonePlaying = false;
private InCallTonePlayer mHoldTonePlayer;
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
public CallAudioManager(CallAudioRouteAdapter callAudioRouteAdapter,
CallsManager callsManager,
@@ -105,6 +112,9 @@
mBluetoothStateReceiver = bluetoothStateReceiver;
mDtmfLocalTonePlayer = dtmfLocalTonePlayer;
mFeatureFlags = featureFlags;
+ mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
mPlayerFactory.setCallAudioManager(this);
mCallAudioModeStateMachine.setCallAudioManager(this);
@@ -750,14 +760,42 @@
private void onCallEnteringRinging() {
if (mRingingCalls.size() == 1) {
- // Wait until the BT ICS binding completed to request further audio route change
- for (Call ringingCall: mRingingCalls) {
- ringingCall.waitForBtIcs();
+ Log.i(this, "onCallEnteringRinging: mFeatureFlags.separatelyBindToBtIncallService() ? %s",
+ mFeatureFlags.separatelyBindToBtIncallService());
+ Log.i(this, "onCallEnteringRinging: mRingingCalls.getFirst().getBtIcsFuture() = %s",
+ mRingingCalls.getFirst().getBtIcsFuture());
+ if (mFeatureFlags.separatelyBindToBtIncallService()
+ && mRingingCalls.getFirst().getBtIcsFuture() != null) {
+ mCallRingingFuture = mRingingCalls.getFirst().getBtIcsFuture()
+ .thenComposeAsync((completed) -> {
+ mCallAudioModeStateMachine.sendMessageWithArgs(
+ CallAudioModeStateMachine.NEW_RINGING_CALL,
+ makeArgsForModeStateMachine());
+ return CompletableFuture.completedFuture(completed);
+ }, new LoggedHandlerExecutor(mHandler, "CAM.oCER", mCallsManager.getLock()))
+ .exceptionally((throwable) -> {
+ Log.e(this, throwable, "Error while executing BT ICS future");
+ // Fallback on performing computation on a separate thread.
+ handleBtBindingWaitFallback();
+ return null;
+ });
+ } else {
+ mCallAudioModeStateMachine.sendMessageWithArgs(
+ CallAudioModeStateMachine.NEW_RINGING_CALL,
+ makeArgsForModeStateMachine());
}
+ }
+ }
+
+ private void handleBtBindingWaitFallback() {
+ // Wait until the BT ICS binding completed to request further audio route change
+ mBtIcsBindingThread = new Thread(() -> {
+ mRingingCalls.getFirst().waitForBtIcs();
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NEW_RINGING_CALL,
makeArgsForModeStateMachine());
- }
+ });
+ mBtIcsBindingThread.start();
}
private void onCallEnteringHold() {
@@ -889,12 +927,14 @@
// we will not play a disconnect tone.
if (call.isHandoverInProgress()) {
Log.i(LOG_TAG, "Omitting tone because %s is being handed over.", call);
+ completeDisconnectToneFuture(call);
return;
}
if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) {
Log.v(LOG_TAG, "Omitting tone because we are not foreground" +
" and there is another call.");
+ completeDisconnectToneFuture(call);
return;
}
@@ -935,6 +975,8 @@
mCallsManager.onDisconnectedTonePlaying(call, true);
mIsDisconnectedTonePlaying = true;
}
+ } else {
+ completeDisconnectToneFuture(call);
}
}
}
@@ -1022,6 +1064,14 @@
oldState == CallState.ON_HOLD;
}
+ private void completeDisconnectToneFuture(Call call) {
+ CompletableFuture<Void> disconnectedToneFuture = mCallsManager.getInCallController()
+ .getDisconnectedToneBtFutures().get(call.getId());
+ if (disconnectedToneFuture != null) {
+ disconnectedToneFuture.complete(null);
+ }
+ }
+
@VisibleForTesting
public Set<Call> getTrackedCalls() {
return mCalls;
@@ -1031,4 +1081,9 @@
public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
return mCallStateToCalls;
}
+
+ @VisibleForTesting
+ public CompletableFuture<Boolean> getCallRingingFuture() {
+ return mCallRingingFuture;
+ }
}
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 7b29fc8..820219d 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -18,6 +18,7 @@
import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -40,7 +41,9 @@
import android.telecom.CallAudioState;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.telecom.VideoProfile;
import android.util.ArrayMap;
+import android.util.Pair;
import androidx.annotation.NonNull;
@@ -94,7 +97,9 @@
private StatusBarNotifier mStatusBarNotifier;
private FeatureFlags mFeatureFlags;
private int mFocusType;
+ private boolean mIsScoAudioConnected;
private final Object mLock = new Object();
+ private final TelecomSystem.SyncRoot mTelecomLock;
private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -105,7 +110,9 @@
AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
if ((info != null) &&
(info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
- sendMessageWithSessionInfo(SPEAKER_ON);
+ if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
+ sendMessageWithSessionInfo(SPEAKER_ON);
+ }
} else {
sendMessageWithSessionInfo(SPEAKER_OFF);
}
@@ -171,6 +178,8 @@
mStatusBarNotifier = statusBarNotifier;
mFeatureFlags = featureFlags;
mFocusType = NO_FOCUS;
+ mIsScoAudioConnected = false;
+ mTelecomLock = callsManager.getLock();
HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
handlerThread.start();
@@ -282,7 +291,7 @@
handleMuteChanged(false);
break;
case MUTE_EXTERNALLY_CHANGED:
- handleMuteChanged(mAudioManager.isMasterMute());
+ handleMuteChanged(mAudioManager.isMicrophoneMute());
break;
case SWITCH_FOCUS:
focus = msg.arg1;
@@ -455,11 +464,13 @@
Log.i(this, "Override current pending route destination from %s(active=%b) to "
+ "%s(active=%b)",
mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
+ // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
+ if (active && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
+ mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
+ }
// override pending route while keep waiting for still pending messages for the
// previous pending route
mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
- mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
- mIsActive = active;
} else {
if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
return;
@@ -473,10 +484,11 @@
// Avoid waiting for pending messages for an unavailable route
mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
}
- mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
- mIsActive = active;
mIsPending = true;
}
+ mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
+ mIsScoAudioConnected);
+ mIsActive = active;
mPendingAudioRoute.evaluatePendingState();
postTimeoutMessage();
}
@@ -599,7 +611,8 @@
Log.i(this, "handleBtAudioActive: is pending path");
if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
- mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED, null);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_CONNECTED,
+ bluetoothDevice.getAddress()), null);
}
} else {
// ignore, not triggered by telecom
@@ -620,7 +633,8 @@
Log.i(this, "handleBtAudioInactive: is pending path");
if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
- mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED, null);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED,
+ bluetoothDevice.getAddress()), null);
}
} else {
// ignore, not triggered by telecom
@@ -719,7 +733,7 @@
private void handleMuteChanged(boolean mute) {
mIsMute = mute;
- if (mIsMute != mAudioManager.isMasterMute() && mIsActive) {
+ if (mIsMute != mAudioManager.isMicrophoneMute() && mIsActive) {
IAudioService audioService = mAudioServiceFactory.getAudioService();
Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
audioService == null);
@@ -752,10 +766,9 @@
}
}
case ACTIVE_FOCUS -> {
- // Route to active baseline route, otherwise ignore if route is already active.
- if (!mIsActive) {
- routeTo(true, getBaseRoute(true, null));
- }
+ // Route to active baseline route (we may need to change audio route in the case
+ // when a video call is put on hold).
+ routeTo(true, getBaseRoute(true, null));
}
case RINGING_FOCUS -> {
if (!mIsActive) {
@@ -836,7 +849,7 @@
private void handleSpeakerOn() {
if (isPending()) {
Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
- mPendingAudioRoute.onMessageReceived(SPEAKER_ON, null);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null);
// Update status bar notification if we are in a call.
mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
} else {
@@ -854,7 +867,7 @@
private void handleSpeakerOff() {
if (isPending()) {
Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
- mPendingAudioRoute.onMessageReceived(SPEAKER_OFF, null);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null);
// Update status bar notification
mStatusBarNotifier.notifySpeakerphone(false);
} else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
@@ -878,6 +891,7 @@
Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
"Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
mIsPending = false;
+ mPendingAudioRoute.clearPendingMessages();
onCurrentRouteChanged();
}
}
@@ -909,7 +923,8 @@
BluetoothDevice deviceToAdd = mBluetoothRoutes.get(route);
// Only include the lead device for LE audio (otherwise, the routes will show
// two separate devices in the UI).
- if (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE) {
+ if (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE
+ && getLeAudioService() != null) {
int groupId = getLeAudioService().getGroupId(deviceToAdd);
if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId);
@@ -988,6 +1003,12 @@
private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
String btAddressToExclude) {
+ boolean skipEarpiece;
+ Call foregroundCall = mCallAudioManager.getForegroundCall();
+ synchronized (mTelecomLock) {
+ skipEarpiece = foregroundCall != null
+ && VideoProfile.isVideo(foregroundCall.getVideoState());
+ }
// Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there
// are only wearables available.
AudioRoute activeWatchOrNonWatchDeviceRoute =
@@ -996,7 +1017,17 @@
|| activeWatchOrNonWatchDeviceRoute == null) {
Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+ "available non-BT route.");
- return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
+ AudioRoute defaultRoute = mEarpieceWiredRoute != null
+ ? mEarpieceWiredRoute
+ : mSpeakerDockRoute;
+ // Ensure that we default to speaker route if we're in a video call, but disregard it if
+ // a wired headset is plugged in.
+ if (skipEarpiece && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) {
+ Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+ + "speaker route for video call.");
+ defaultRoute = mSpeakerDockRoute;
+ }
+ return defaultRoute;
} else {
// Most recent active route will always be the last in the array (ensure that we don't
// auto route to a wearable device unless it's already active).
@@ -1042,8 +1073,8 @@
return mCurrentRoute;
}
- private AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
- String address) {
+ public AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
+ String address) {
for (AudioRoute route : mBluetoothRoutes.keySet()) {
if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) {
return route;
@@ -1129,7 +1160,7 @@
BluetoothDevice device = mBluetoothRoutes.get(route);
// Skip excluded BT address and LE audio if it's not the lead device.
if (route.getBluetoothAddress().equals(btAddressToExclude)
- || isLeAudioNonLeadDevice(route.getType(), device)) {
+ || isLeAudioNonLeadDeviceOrServiceUnavailable(route.getType(), device)) {
continue;
}
// Check if the most recently active device is a watch device.
@@ -1158,7 +1189,8 @@
for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
AudioRoute route = bluetoothRoutes.get(i);
// Skip LE route if it's not the lead device.
- if (isLeAudioNonLeadDevice(route.getType(), mBluetoothRoutes.get(route))) {
+ if (isLeAudioNonLeadDeviceOrServiceUnavailable(
+ route.getType(), mBluetoothRoutes.get(route))) {
continue;
}
if (!route.getBluetoothAddress().equals(btAddressToExclude)) {
@@ -1168,15 +1200,19 @@
return null;
}
- private boolean isLeAudioNonLeadDevice(@AudioRoute.AudioRouteType int type,
+ private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
BluetoothDevice device) {
if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
return false;
+ } else if (getLeAudioService() == null) {
+ return true;
}
+
int groupId = getLeAudioService().getGroupId(device);
if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
- return !device.getAddress().equals(
- getLeAudioService().getConnectedGroupLeadDevice(groupId).getAddress());
+ BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+ Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
+ return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
}
return false;
}
@@ -1195,6 +1231,18 @@
mAudioRouteFactory = audioRouteFactory;
}
+ public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() {
+ return mBluetoothRoutes;
+ }
+
+ public void overrideIsPending(boolean isPending) {
+ mIsPending = isPending;
+ }
+
+ public void setIsScoAudioConnected(boolean value) {
+ mIsScoAudioConnected = value;
+ }
+
@VisibleForTesting
public void setActive(boolean active) {
if (active) {
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 621ba36..74d23a9 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -288,8 +288,13 @@
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
mAvailableRoutes, null,
mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -511,8 +516,13 @@
}
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -749,8 +759,13 @@
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
// Do not send RINGER_MODE_CHANGE if no Bluetooth SCO audio device is available
if (mBluetoothRouteManager.getBluetoothAudioConnectedDevice() != null) {
mCallAudioManager.onRingerModeChange();
@@ -847,6 +862,14 @@
if (msg.arg1 == NO_FOCUS) {
// Only disconnect audio here instead of routing away from BT entirely.
if (mFeatureFlags.transitRouteBeforeAudioDisconnectBt()) {
+ // Note: We have to turn off mute here rather than when entering the
+ // QuiescentBluetooth route because setMuteOn will only work when there the
+ // current state is active.
+ // We don't need to do this in the unflagged path since reinitialize
+ // will turn off mute.
+ if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
+ setMuteOn(false);
+ }
transitionTo(mQuiescentBluetoothRoute);
mBluetoothRouteManager.disconnectAudio();
} else {
@@ -890,8 +913,13 @@
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -977,9 +1005,6 @@
public void enter() {
super.enter();
mHasUserExplicitlyLeftBluetooth = false;
- if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
- setMuteOn(false);
- }
updateInternalCallAudioState();
}
@@ -1117,8 +1142,13 @@
mWasOnSpeaker = true;
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER,
mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index a100185..8e1f754 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -1,10 +1,16 @@
package com.android.server.telecom;
+import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
+
+import com.android.internal.app.IntentForwarderActivity;
import com.android.server.telecom.components.ErrorDialogActivity;
import com.android.server.telecom.flags.FeatureFlags;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
@@ -20,6 +26,7 @@
import android.telecom.VideoProfile;
import android.telephony.DisconnectCause;
import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
import android.widget.Toast;
import java.util.concurrent.CompletableFuture;
@@ -193,6 +200,18 @@
boolean isPrivilegedDialer = defaultDialerCache.isDefaultOrSystemDialer(callingPackage,
initiatingUser.getIdentifier());
+ if (privateSpaceFlagsEnabled()) {
+ if (!callsManager.isSelfManaged(phoneAccountHandle, initiatingUser)
+ && !TelephonyUtil.shouldProcessAsEmergency(context, handle)
+ && UserUtil.isPrivateProfile(initiatingUser, context)) {
+ boolean dialogShown = maybeRedirectToIntentForwarderForPrivate(context, intent,
+ initiatingUser);
+ if (dialogShown) {
+ return;
+ }
+ }
+ }
+
NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
context, callsManager, intent, callsManager.getPhoneNumberUtilsAdapter(),
isPrivilegedDialer, defaultDialerCache, new MmiUtils(), featureFlags);
@@ -310,4 +329,43 @@
context.startActivityAsUser(errorIntent, UserHandle.CURRENT);
}
}
+
+ private static boolean privateSpaceFlagsEnabled() {
+ return android.multiuser.Flags.enablePrivateSpaceFeatures()
+ && android.multiuser.Flags.enablePrivateSpaceIntentRedirection();
+ }
+
+ private static boolean maybeRedirectToIntentForwarderForPrivate(
+ Context context,
+ Intent forwardCallIntent,
+ UserHandle initiatingUser) {
+
+ // If CALL intent filters are set to SKIP_CURRENT_PROFILE, PM will resolve this to an
+ // intent forwarder activity.
+ forwardCallIntent.setComponent(null);
+ forwardCallIntent.setPackage(null);
+ ResolveInfo resolveInfos =
+ context.getPackageManager()
+ .resolveActivityAsUser(
+ forwardCallIntent,
+ PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY),
+ initiatingUser.getIdentifier());
+
+ if (resolveInfos == null
+ || !resolveInfos
+ .getComponentInfo()
+ .getComponentName()
+ .getShortClassName()
+ .equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)) {
+ return false;
+ }
+
+ try {
+ context.startActivityAsUser(forwardCallIntent, initiatingUser);
+ return true;
+ } catch (ActivityNotFoundException e) {
+ Log.e(CallIntentProcessor.class, e, "Unable to start call intent in the main user");
+ return false;
+ }
+ }
}
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index dc9e2ea..27535c0 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -368,7 +368,7 @@
if (phoneAccount != null &&
phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
if (initiatingUser != null &&
- UserUtil.isManagedProfile(mContext, initiatingUser, mFeatureFlags)) {
+ UserUtil.isProfile(mContext, initiatingUser, mFeatureFlags)) {
paramBuilder.setUserToBeInsertedTo(initiatingUser);
paramBuilder.setAddForAllUsers(false);
} else {
diff --git a/src/com/android/server/telecom/CallSourceService.java b/src/com/android/server/telecom/CallSourceService.java
index 132118b..d579542 100644
--- a/src/com/android/server/telecom/CallSourceService.java
+++ b/src/com/android/server/telecom/CallSourceService.java
@@ -35,4 +35,6 @@
void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint);
void onAvailableCallEndpointsChanged(Call activeCall, Set<CallEndpoint> availableCallEndpoints);
+
+ void onVideoStateChanged(Call activeCall, int videoState);
}
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
index 1323633..efd458e 100644
--- a/src/com/android/server/telecom/CallStreamingController.java
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -127,7 +127,7 @@
if (mCallsManager.getCallStreamingController().isStreaming()) {
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
"STREAMING_FAILED_ALREADY_STREAMING"));
} else {
future.complete(new VoipCallTransactionResult(
@@ -196,7 +196,8 @@
if (roleManager == null || packageManager == null) {
Log.w(this, "processTransaction: Can't find system service");
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
@@ -205,7 +206,8 @@
if (holders.isEmpty()) {
Log.w(this, "processTransaction: Can't find streaming app");
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
@@ -216,7 +218,8 @@
if (infos.isEmpty()) {
Log.w(this, "processTransaction: Can't find streaming service");
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
@@ -227,7 +230,8 @@
Log.w(this, "Must require BIND_CALL_STREAMING_SERVICE: " +
serviceInfo.packageName);
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
@@ -239,7 +243,7 @@
| Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
Log.w(this, "Can't bind to streaming service");
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
"STREAMING_FAILED_SENDER_BINDING_ERROR"));
}
return future;
@@ -379,7 +383,8 @@
VoipCallTransactionResult.RESULT_SUCCEED, null));
} catch (RemoteException e) {
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, "Exception when request "
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ "Exception when request "
+ "setting state to streaming app."));
}
return future;
@@ -409,7 +414,7 @@
} catch (RemoteException e) {
resetController();
mFuture.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
StreamingServiceTransaction.MESSAGE));
}
}
@@ -433,7 +438,7 @@
resetController();
if (!mFuture.isDone()) {
mFuture.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
"STREAMING_FAILED_SENDER_BINDING_ERROR"));
} else {
mWrapper.onCallStreamingFailed(mCall, STREAMING_FAILED_SENDER_BINDING_ERROR);
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index f0d07d0..de53a3d 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -659,6 +659,7 @@
}
callAudioRouteAdapter.initialize();
bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
+ bluetoothDeviceManager.setCallAudioRouteAdapter(callAudioRouteAdapter);
CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
new CallAudioRoutePeripheralAdapter(
@@ -1034,7 +1035,8 @@
if (result.shouldAllowCall) {
if (mFeatureFlags.separatelyBindToBtIncallService()) {
- incomingCall.setBtIcsFuture(mInCallController.bindToBTService(incomingCall));
+ mInCallController.bindToBTService(incomingCall, null);
+ incomingCall.setBtIcsFuture(mInCallController.getBtBindingFuture(incomingCall));
setCallState(incomingCall, CallState.RINGING, "successful incoming call");
}
incomingCall.setPostCallPackageName(
@@ -5061,6 +5063,7 @@
// change what an "active call" is so that the call in SELECT_PHONE_ACCOUNT state
// will be properly cancelled.
call.getTargetPhoneAccount() != null
+ && phoneAccountHandle != null
&& !phoneAccountHandle.getComponentName().equals(
call.getTargetPhoneAccount().getComponentName())
&& call.getParentCall() == null
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 7dadcdc..6449d0e 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -1992,6 +1992,11 @@
}
}
+ @Override
+ public void onVideoStateChanged(Call call, int videoState){
+ // pass through. ConnectionService does not implement this method.
+ }
+
/** @see IConnectionService#onMuteStateChanged(String, boolean, Session.Info) */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@Override
diff --git a/src/com/android/server/telecom/DefaultDialerCache.java b/src/com/android/server/telecom/DefaultDialerCache.java
index d819780..44b426a 100644
--- a/src/com/android/server/telecom/DefaultDialerCache.java
+++ b/src/com/android/server/telecom/DefaultDialerCache.java
@@ -176,7 +176,7 @@
UserHandle.USER_ALL);
}
- public String getBTInCallServicePackage() {
+ public String[] getBTInCallServicePackages() {
return mRoleManagerAdapter.getBTInCallService();
}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index e625bbe..f3c84ba 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -370,7 +370,8 @@
// not be running (handled in getUserFromCall).
UserHandle userToBind = isManagedProfile ? userFromCall : UserHandle.CURRENT;
if ((mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_NON_UI
- || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI) && (
+ || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
+ || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_BLUETOOTH) && (
mUserHandleToUseForBinding != null)) {
//guarding change for non-UI/carmode-UI services which may not be present for
// work profile.
@@ -1080,6 +1081,7 @@
if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
synchronized (mLock) {
int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+ String changedPackage = intent.getData().getSchemeSpecificPart();
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
boolean isManagedProfile = um.isManagedProfile(userHandle.getIdentifier());
@@ -1109,12 +1111,36 @@
childManagedProfileUser);
List<InCallServiceBindingConnection> componentsToBindForUser = null;
List<InCallServiceBindingConnection> componentsToBindForChild = null;
+ // Separate binding for BT logic.
+ boolean isBluetoothPkg = isBluetoothPackage(changedPackage);
+ Call callToConnectWith = mCallIdMapper.getCalls().isEmpty()
+ ? null
+ : mCallIdMapper.getCalls().iterator().next();
+
+ // Bind to BT service if there's an available call. When the flag isn't
+ // enabled, the service will be included as part of
+ // getNonUiInCallServiceBindingConnectionList.
+ if (mFeatureFlags.separatelyBindToBtIncallService()
+ && isBluetoothPkg && callToConnectWith != null) {
+ // mNonUIInCallServiceConnections will always contain a key for
+ // userHandle and/or the child user if there is an ongoing call with
+ // that user, regardless if there aren't any non-UI ICS bound.
+ if (isUserKeyPresent) {
+ bindToBTService(callToConnectWith, userHandle);
+ }
+ if (isChildUserKeyPresent) {
+ // This will try to use the ICS found in the parent if one isn't
+ // available for the child.
+ bindToBTService(callToConnectWith, childManagedProfileUser);
+ }
+ }
if(isUserKeyPresent) {
componentsToBindForUser =
getNonUiInCallServiceBindingConnectionList(intent,
userHandle, null);
}
+
if (isChildUserKeyPresent) {
componentsToBindForChild =
getNonUiInCallServiceBindingConnectionList(intent,
@@ -1127,11 +1153,11 @@
isUserKeyPresent, isChildUserKeyPresent, isManagedProfile,
userHandle.getIdentifier());
- if (isUserKeyPresent && componentsToBindForUser != null) {
+ if (isUserKeyPresent && !componentsToBindForUser.isEmpty()) {
mNonUIInCallServiceConnections.get(userHandle).
addConnections(componentsToBindForUser);
}
- if (isChildUserKeyPresent && componentsToBindForChild != null) {
+ if (isChildUserKeyPresent && !componentsToBindForChild.isEmpty()) {
mNonUIInCallServiceConnections.get(childManagedProfileUser).
addConnections(componentsToBindForChild);
}
@@ -1186,6 +1212,10 @@
private static final int IN_CALL_SERVICE_TYPE_COMPANION = 5;
private static final int IN_CALL_SERVICE_TYPE_BLUETOOTH = 6;
+ // Timeout value to be used to ensure future completion for mDisconnectedToneBtFutures. This is
+ // set to 4 seconds to account for the exceptional case (TONE_CONGESTION).
+ private static final int DISCONNECTED_TONE_TIMEOUT = 4000;
+
private static final int[] LIVE_CALL_STATES = { CallState.ACTIVE, CallState.PULLING,
CallState.DISCONNECTING };
@@ -1233,7 +1263,10 @@
// in-call service.
// The future will complete with true if bluetooth in-call service succeeds, false if it timed
// out.
- private CompletableFuture<Boolean> mBtBindingFuture = CompletableFuture.completedFuture(true);
+ private Map<UserHandle, CompletableFuture<Boolean>> mBtBindingFuture = new ArrayMap<>();
+ // Future used to delay terminating the BT InCallService before the call disconnect tone
+ // finishes playing.
+ private Map<String, CompletableFuture<Void>> mDisconnectedToneBtFutures = new ArrayMap<>();
private final CarModeTracker mCarModeTracker;
@@ -1256,6 +1289,8 @@
private boolean mIsStartCallDelayScheduled = false;
+ private boolean mDisconnectedToneStartedPlaying = false;
+
/**
* A list of call IDs which are currently using the camera.
*/
@@ -1375,27 +1410,29 @@
addCall(call);
if (mFeatureFlags.separatelyBindToBtIncallService()) {
- boolean bindBTService = false;
- boolean bindOtherServices = false;
+ boolean bindingToBtRequired = false;
+ boolean bindingToOtherServicesRequired = false;
if (!isBoundAndConnectedToBTService(userFromCall)) {
Log.i(this, "onCallAdded: %s; not bound or connected to BT ICS.", call);
- bindBTService = true;
- bindToBTService(call);
+ bindingToBtRequired = true;
+ bindToBTService(call, null);
}
if (!isBoundAndConnectedToServices(userFromCall)) {
Log.i(this, "onCallAdded: %s; not bound or connected to other ICS.", call);
// We are not bound, or we're not connected.
- bindOtherServices = true;
- bindToOtherServices(call);
+ bindingToOtherServicesRequired = true;
+ bindToServices(call);
}
- if (!bindBTService || !bindOtherServices) {
+ // If either BT service are already bound or other services are already bound, attempt
+ // to add the new call to the connected incall services.
+ if (!bindingToBtRequired || !bindingToOtherServicesRequired) {
addCallToConnectedServices(call, userFromCall);
}
} else {
if (!isBoundAndConnectedToServices(userFromCall)) {
Log.i(this, "onCallAdded: %s; not bound or connected.", call);
// We are not bound, or we're not connected.
- bindToServices(call, false);
+ bindToServices(call);
} else {
addCallToConnectedServices(call, userFromCall);
}
@@ -1507,17 +1544,31 @@
@Override
public void onDisconnectedTonePlaying(Call call, boolean isTonePlaying) {
Log.i(this, "onDisconnectedTonePlaying: %s -> %b", call, isTonePlaying);
-
if (mFeatureFlags.separatelyBindToBtIncallService()) {
synchronized (mLock) {
- mPendingEndToneCall.remove(call);
- if (!mPendingEndToneCall.isEmpty()) {
- return;
- }
- UserHandle userHandle = getUserFromCall(call);
- if (mBTInCallServiceConnections.containsKey(userHandle)) {
- mBTInCallServiceConnections.get(userHandle).disconnect();
- mBTInCallServiceConnections.remove(userHandle);
+ if (isTonePlaying) {
+ mDisconnectedToneStartedPlaying = true;
+ } else if (mDisconnectedToneStartedPlaying) {
+ mDisconnectedToneStartedPlaying = false;
+ if (mDisconnectedToneBtFutures.containsKey(call.getId())) {
+ Log.i(this, "onDisconnectedTonePlaying: completing BT "
+ + "disconnected tone future");
+ mDisconnectedToneBtFutures.get(call.getId()).complete(null);
+ }
+ mPendingEndToneCall.remove(call);
+ if (!mPendingEndToneCall.isEmpty()) {
+ return;
+ }
+ UserHandle userHandle = getUserFromCall(call);
+ if (mBTInCallServiceConnections.containsKey(userHandle)) {
+ Log.i(this, "onDisconnectedTonePlaying: Unbinding BT service");
+ mBTInCallServiceConnections.get(userHandle).disconnect();
+ mBTInCallServiceConnections.remove(userHandle);
+ }
+ // Ensure that BT ICS instance is cleaned up
+ if (mBTInCallServices.remove(userHandle) != null) {
+ updateCombinedInCallServiceMap(userHandle);
+ }
}
}
}
@@ -1969,6 +2020,8 @@
}
getCombinedInCallServiceMap().remove(userHandle);
if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ // Note that the BT ICS will be repopulated as part of the combined map if the
+ // BT ICS is still bound (disconnected tone hasn't finished playing).
updateCombinedInCallServiceMap(userHandle);
}
}
@@ -1979,34 +2032,49 @@
*
* @param call The newly added call that triggered the binding to the in-call services.
*/
- public CompletableFuture<Boolean> bindToBTService(Call call) {
+ public void bindToBTService(Call call, UserHandle userHandle) {
+ Log.i(this, "bindToBtService");
+ UserHandle userToBind = userHandle == null
+ ? getUserFromCall(call)
+ : userHandle;
+ UserManager um = mContext.getSystemService(UserManager.class);
+ UserHandle parentUser = mFeatureFlags.profileUserSupport()
+ ? um.getProfileParent(userToBind) : null;
+
+ if (!mFeatureFlags.profileUserSupport()
+ && um.isManagedProfile(userToBind.getIdentifier())) {
+ parentUser = um.getProfileParent(userToBind);
+ }
+
// Track the call if we don't already know about it.
addCall(call);
- UserHandle userFromCall = getUserFromCall(call);
-
- List<InCallServiceInfo> infos = getInCallServiceComponents(userFromCall,
+ List<InCallServiceInfo> infos = getInCallServiceComponents(userToBind,
IN_CALL_SERVICE_TYPE_BLUETOOTH);
+ boolean serviceUnavailableForUser = false;
if (infos.size() == 0 || infos.get(0) == null) {
- Log.w(this, "No available BT service");
- mBtBindingFuture = CompletableFuture.completedFuture(false);
- return mBtBindingFuture;
+ Log.i(this, "No available BT ICS for user (%s). Trying with parent instead.",
+ userToBind);
+ serviceUnavailableForUser = true;
+ // Check if the service is available under the parent user instead.
+ if (parentUser != null) {
+ infos = getInCallServiceComponents(parentUser, IN_CALL_SERVICE_TYPE_BLUETOOTH);
+ }
+ if (infos.size() == 0 || infos.get(0) == null) {
+ Log.w(this, "No available BT ICS to bind to for user %s or its parent %s.",
+ userToBind, parentUser);
+ mBtBindingFuture.put(userToBind, CompletableFuture.completedFuture(false));
+ return;
+ }
}
- mBtBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
- mTimeoutsAdapter.getCallBindBluetoothInCallServicesDelay(
- mContext.getContentResolver()), TimeUnit.MILLISECONDS);
- new InCallServiceBindingConnection(infos.get(0)).connect(call);
- return mBtBindingFuture;
- }
- /**
- * Binds to all the UI-providing InCallService as well as system-implemented non-UI
- * InCallServices except BT InCallServices. Method-invoker must check
- * {@link #isBoundAndConnectedToServices(UserHandle)} before invoking.
- *
- * @param call The newly added call that triggered the binding to the in-call services.
- */
- public void bindToOtherServices(Call call) {
- bindToServices(call, true);
+ mBtBindingFuture.put(userToBind, new CompletableFuture<Boolean>().completeOnTimeout(false,
+ mTimeoutsAdapter.getCallBindBluetoothInCallServicesDelay(
+ mContext.getContentResolver()), TimeUnit.MILLISECONDS));
+ InCallServiceBindingConnection btIcsBindingConnection =
+ new InCallServiceBindingConnection(infos.get(0),
+ serviceUnavailableForUser ? parentUser : userToBind);
+ mBTInCallServiceConnections.put(userToBind, btIcsBindingConnection);
+ btIcsBindingConnection.connect(call);
}
/**
@@ -2016,11 +2084,9 @@
*
* @param call The newly added call that triggered the binding to the in-call
* services.
- * @param skipBTServices Boolean variable to specify if the binding to BT InCallService should
- * be skipped
*/
@VisibleForTesting
- public void bindToServices(Call call, boolean skipBTServices) {
+ public void bindToServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
UserManager um = mContext.getSystemService(UserManager.class);
UserHandle parentUser = mFeatureFlags.profileUserSupport()
@@ -2085,7 +2151,7 @@
// Only connect to the non-ui InCallServices if we actually connected to the main UI
// one, or if the call is self-managed (in which case we'd still want to keep Wear, BT,
// etc. informed.
- connectToNonUiInCallServices(call, skipBTServices);
+ connectToNonUiInCallServices(call);
mBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
mContext.getContentResolver()),
@@ -2100,7 +2166,7 @@
packageChangedFilter, null, null);
}
- private void updateNonUiInCallServices(Call call, boolean skipBTService) {
+ private void updateNonUiInCallServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
UserManager um = mContext.getSystemService(UserManager.class);
@@ -2155,10 +2221,10 @@
nonUIInCalls));
}
- private void connectToNonUiInCallServices(Call call, boolean skipBTService) {
+ private void connectToNonUiInCallServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
if (!mNonUIInCallServiceConnections.containsKey(userFromCall)) {
- updateNonUiInCallServices(call, skipBTService);
+ updateNonUiInCallServices(call);
}
mNonUIInCallServiceConnections.get(userFromCall).connect(call);
}
@@ -2427,10 +2493,8 @@
return IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
}
- String bluetoothPackage = mDefaultDialerCache.getBTInCallServicePackage();
- if (mFeatureFlags.separatelyBindToBtIncallService()
- && serviceInfo.packageName != null
- && serviceInfo.packageName.equals(bluetoothPackage)
+ boolean processingBluetoothPackage = isBluetoothPackage(serviceInfo.packageName);
+ if (mFeatureFlags.separatelyBindToBtIncallService() && processingBluetoothPackage
&& (hasControlInCallPermission || hasAppOpsPermittedManageOngoingCalls)) {
return IN_CALL_SERVICE_TYPE_BLUETOOTH;
}
@@ -2477,11 +2541,13 @@
IInCallService inCallService = IInCallService.Stub.asInterface(service);
if (mFeatureFlags.separatelyBindToBtIncallService()
&& info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH) {
- if (mBtBindingFuture.isDone()) {
+ if (!mBtBindingFuture.containsKey(userHandle)
+ || mBtBindingFuture.get(userHandle).isDone()) {
+ Log.i(this, "onConnected: BT binding future timed out.");
// Binding completed after the timeout. Clean up this binding
return false;
} else {
- mBtBindingFuture.complete(true);
+ mBtBindingFuture.get(userHandle).complete(true);
}
mBTInCallServices.put(userHandle, new Pair<>(info, inCallService));
} else {
@@ -2652,12 +2718,20 @@
IInCallService inCallService = entry.getValue();
componentsUpdated.add(componentName);
- try {
- inCallService.updateCall(
- sanitizeParcelableCallForService(info, parcelableCall));
- } catch (RemoteException exception) {
- Log.w(this, "Call status update did not send to: "
- + componentName +" successfully with error " + exception);
+ if (info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH
+ && call.getState() == CallState.DISCONNECTED
+ && !mDisconnectedToneBtFutures.containsKey(call.getId())) {
+ CompletableFuture<Void> disconnectedToneFuture = new CompletableFuture<Void>()
+ .completeOnTimeout(null, DISCONNECTED_TONE_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+ mDisconnectedToneBtFutures.put(call.getId(), disconnectedToneFuture);
+ mDisconnectedToneBtFutures.get(call.getId()).thenRunAsync(() -> {
+ Log.i(this, "updateCall: Sending call disconnected update to BT ICS.");
+ updateCallToIcs(inCallService, info, parcelableCall, componentName);
+ mDisconnectedToneBtFutures.remove(call.getId());
+ }, new LoggedHandlerExecutor(mHandler, "ICC.uC", mLock));
+ } else {
+ updateCallToIcs(inCallService, info, parcelableCall, componentName);
}
}
Log.i(this, "Components updated: %s", componentsUpdated);
@@ -2667,12 +2741,27 @@
}
}
+ private void updateCallToIcs(IInCallService inCallService, InCallServiceInfo info,
+ ParcelableCall parcelableCall, ComponentName componentName) {
+ try {
+ inCallService.updateCall(
+ sanitizeParcelableCallForService(info, parcelableCall));
+ } catch (RemoteException exception) {
+ Log.w(this, "Call status update did not send to: "
+ + componentName + " successfully with error " + exception);
+ }
+ }
+
/**
* Adds the call to the list of calls tracked by the {@link InCallController}.
* @param call The call to add.
*/
@VisibleForTesting
public void addCall(Call call) {
+ if (call == null) {
+ return;
+ }
+
if (mCallIdMapper.getCalls().size() == 0) {
mAppOpsManager.startWatchingActive(new String[] { OPSTR_RECORD_AUDIO },
java.lang.Runnable::run, this);
@@ -2683,12 +2772,12 @@
if (mCallIdMapper.getCallId(call) == null) {
mCallIdMapper.addCall(call);
call.addListener(mCallListener);
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ mPendingEndToneCall.add(call);
+ }
}
maybeTrackMicrophoneUse(isMuted());
- if (mFeatureFlags.separatelyBindToBtIncallService()) {
- mPendingEndToneCall.add(call);
- }
}
/**
@@ -2718,6 +2807,23 @@
}
/**
+ * @return A future that is pending whenever we are in the middle of binding to the BT
+ * incall service.
+ */
+ public CompletableFuture<Boolean> getBtBindingFuture(Call call) {
+ UserHandle userHandle = getUserFromCall(call);
+ return mBtBindingFuture.get(userHandle);
+ }
+
+ /**
+ * @return A future that is pending whenever we are in the process of sending the call
+ * disconnected state to the BT ICS so that the disconnect tone can finish playing.
+ */
+ public Map<String, CompletableFuture<Void>> getDisconnectedToneBtFutures() {
+ return mDisconnectedToneBtFutures;
+ }
+
+ /**
* Dumps the state of the {@link InCallController}.
*
* @param pw The {@code IndentingPrintWriter} to write the state to.
@@ -3174,7 +3280,10 @@
}
}
}
- return false;
+ // If early binding for BT ICS is enabled, ensure that it is included into consideration as
+ // a bound non-UI ICS.
+ return mFeatureFlags.separatelyBindToBtIncallService() && !mBTInCallServices.isEmpty()
+ && isBluetoothPackage(packageName);
}
private void updateCombinedInCallServiceMap(UserHandle user) {
@@ -3208,4 +3317,13 @@
}
}
}
+
+ private boolean isBluetoothPackage(String packageName) {
+ for (String pkgName : mDefaultDialerCache.getBTInCallServicePackages()) {
+ if (pkgName.equals(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 6ba09a5..f9cdc35 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -22,10 +22,12 @@
import android.bluetooth.BluetoothDevice;
import android.media.AudioManager;
import android.telecom.Log;
+import android.util.ArraySet;
+import android.util.Pair;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
-import java.util.ArrayList;
+import java.util.Set;
/**
* Used to represent the intermediate state during audio route switching.
@@ -47,7 +49,7 @@
* by new switching request during the ongoing switching
*/
private AudioRoute mDestRoute;
- private ArrayList<Integer> mPendingMessages;
+ private Set<Pair<Integer, String>> mPendingMessages;
private boolean mActive;
/**
* The device that has been set for communication by Telecom
@@ -59,7 +61,7 @@
mCallAudioRouteController = controller;
mAudioManager = audioManager;
mBluetoothRouteManager = bluetoothRouteManager;
- mPendingMessages = new ArrayList<>();
+ mPendingMessages = new ArraySet<>();
mActive = false;
mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
}
@@ -73,24 +75,25 @@
return mOrigRoute;
}
- void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device) {
+ void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device,
+ boolean isScoAudioConnected) {
destRoute.onDestRouteAsPendingRoute(active, this, device,
- mAudioManager, mBluetoothRouteManager);
+ mAudioManager, mBluetoothRouteManager, isScoAudioConnected);
mActive = active;
mDestRoute = destRoute;
}
- AudioRoute getDestRoute() {
+ public AudioRoute getDestRoute() {
return mDestRoute;
}
- public void addMessage(int message) {
- mPendingMessages.add(message);
+ public void addMessage(int message, String bluetoothDevice) {
+ mPendingMessages.add(new Pair<>(message, bluetoothDevice));
}
- public void onMessageReceived(int message, String btAddressToExclude) {
+ public void onMessageReceived(Pair<Integer, String> message, String btAddressToExclude) {
Log.i(this, "onMessageReceived: message - %s", message);
- if (message == PENDING_ROUTE_FAILED) {
+ if (message.first == PENDING_ROUTE_FAILED) {
// Fallback to base route
mCallAudioRouteController.sendMessageWithSessionInfo(
SWITCH_BASELINE_ROUTE, 0, btAddressToExclude);
@@ -98,7 +101,7 @@
}
// Removes the first occurrence of the specified message from this list, if it is present.
- mPendingMessages.remove((Object) message);
+ mPendingMessages.remove(message);
evaluatePendingState();
}
@@ -107,9 +110,7 @@
mCallAudioRouteController.sendMessageWithSessionInfo(
CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
} else {
- for(Integer i: mPendingMessages) {
- Log.d(this, "evaluatePendingState: pending Messages - %d", i);
- }
+ Log.i(this, "evaluatePendingState: mPendingMessages - %s", mPendingMessages);
}
}
@@ -117,6 +118,10 @@
mPendingMessages.clear();
}
+ public void clearPendingMessage(Pair<Integer, String> message) {
+ mPendingMessages.remove(message);
+ }
+
public boolean isActive() {
return mActive;
}
@@ -129,4 +134,8 @@
@AudioRoute.AudioRouteType int communicationDeviceType) {
mCommunicationDeviceType = communicationDeviceType;
}
+
+ public void overrideDestRoute(AudioRoute route) {
+ mDestRoute = route;
+ }
}
diff --git a/src/com/android/server/telecom/RoleManagerAdapter.java b/src/com/android/server/telecom/RoleManagerAdapter.java
index 9f515e6..1b5c71b 100644
--- a/src/com/android/server/telecom/RoleManagerAdapter.java
+++ b/src/com/android/server/telecom/RoleManagerAdapter.java
@@ -71,7 +71,7 @@
* bt in-call service role.
* @return the package name of the package filling the role, {@code null} otherwise.
*/
- String getBTInCallService();
+ String[] getBTInCallService();
/**
* Override the {@link android.app.role.RoleManager} bt in-call service package with another
diff --git a/src/com/android/server/telecom/RoleManagerAdapterImpl.java b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
index 33ec466..ded4d9c 100644
--- a/src/com/android/server/telecom/RoleManagerAdapterImpl.java
+++ b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
@@ -78,9 +78,9 @@
}
@Override
- public String getBTInCallService() {
+ public String[] getBTInCallService() {
if (mOverrideBTInCallService != null) {
- return mOverrideBTInCallService;
+ return new String [] {mOverrideBTInCallService};
}
return getBluetoothInCallServicePackageName();
}
@@ -166,8 +166,8 @@
return roleHolders.get(0);
}
- private String getBluetoothInCallServicePackageName() {
- return mContext.getResources().getString(R.string.system_bluetooth_stack);
+ private String[] getBluetoothInCallServicePackageName() {
+ return mContext.getResources().getStringArray(R.array.system_bluetooth_stack_package_name);
}
/**
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index a57360b..20320f2 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -215,11 +215,11 @@
switch (callAttributes.getDirection()) {
case DIRECTION_OUTGOING:
transaction = new OutgoingCallTransaction(callId, mContext, callAttributes,
- mCallsManager, extras);
+ mCallsManager, extras, mFeatureFlags);
break;
case DIRECTION_INCOMING:
transaction = new IncomingCallTransaction(callId, callAttributes,
- mCallsManager, extras);
+ mCallsManager, extras, mFeatureFlags);
break;
default:
throw new IllegalArgumentException(String.format("Invalid Call Direction. "
@@ -1672,7 +1672,13 @@
&& accountExtra != null && accountExtra.getBoolean(
PhoneAccount.EXTRA_SKIP_CALL_FILTERING,
false)) {
- mCallsManager.getInCallController().bindToServices(null, false);
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ mCallsManager.getInCallController().bindToBTService(
+ null, null);
+ }
+ // Should be able to run this as is even if above flag is
+ // enabled (BT binding should be skipped automatically).
+ mCallsManager.getInCallController().bindToServices(null);
}
}
} finally {
@@ -1854,11 +1860,12 @@
throw new SecurityException("Package " + callingPackage + " is not allowed"
+ " to start conference call");
}
-
+ // Binder is clearing the identity, so we need to keep the store the handle
+ UserHandle currentUserHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
try {
mCallsManager.startConference(participants, extras, callingPackage,
- Binder.getCallingUserHandle());
+ currentUserHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 32cb896..fe4b1b7 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -602,6 +602,7 @@
}
}
+ @Override
public void onVideoStateChanged(Call call, int videoState) {
if (call != null) {
try {
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
index e159c04..57906d4 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -50,6 +50,12 @@
: userInfo != null && userInfo.isManagedProfile();
}
+ public static boolean isPrivateProfile(UserHandle userHandle, Context context) {
+ UserManager um = context.createContextAsUser(userHandle, 0).getSystemService(
+ UserManager.class);
+ return um != null && um.isPrivateProfile();
+ }
+
public static boolean isProfile(Context context, UserHandle userHandle,
FeatureFlags featureFlags) {
UserManager userManager = context.createContextAsUser(userHandle, 0)
@@ -57,7 +63,8 @@
UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
return featureFlags.telecomResolveHiddenDependencies()
? userManager != null && userManager.isProfile()
- : userInfo != null && userInfo.profileGroupId != userInfo.id;
+ : userInfo != null && userInfo.profileGroupId != userInfo.id
+ && userInfo.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID;
}
public static void showErrorDialogForRestrictedOutgoingCall(Context context,
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 50476fe..0a8ce5a 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -18,6 +18,8 @@
import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA;
import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -34,20 +36,25 @@
import android.telecom.Log;
import android.util.ArraySet;
import android.util.LocalLog;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.AudioRoute;
import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.flags.FeatureFlags;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -62,6 +69,16 @@
public static final int DEVICE_TYPE_HEARING_AID = 1;
public static final int DEVICE_TYPE_LE_AUDIO = 2;
+ private static final Map<Integer, Integer> PROFILE_TO_AUDIO_ROUTE_MAP = new HashMap<>();
+ static {
+ PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEADSET,
+ AudioRoute.TYPE_BLUETOOTH_SCO);
+ PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.LE_AUDIO,
+ AudioRoute.TYPE_BLUETOOTH_LE);
+ PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEARING_AID,
+ TYPE_BLUETOOTH_HA);
+ }
+
private BluetoothLeAudio.Callback mLeAudioCallbacks =
new BluetoothLeAudio.Callback() {
@Override
@@ -116,15 +133,14 @@
+ mBluetoothHearingAid;
} else if (profile == BluetoothProfile.LE_AUDIO) {
mBluetoothLeAudioService = (BluetoothLeAudio) proxy;
- logString = "Got BluetoothLeAudio: "
- + mBluetoothLeAudioService;
+ logString = ("Got BluetoothLeAudio: " + mBluetoothLeAudioService )
+ + (", mLeAudioCallbackRegistered: "
+ + mLeAudioCallbackRegistered);
if (!mLeAudioCallbackRegistered) {
- try {
- mBluetoothLeAudioService.registerCallback(
- mExecutor, mLeAudioCallbacks);
- mLeAudioCallbackRegistered = true;
- } catch (IllegalStateException e) {
- logString += ", but Bluetooth is down";
+ if (mFeatureFlags.postponeRegisterToLeaudio()) {
+ mExecutor.execute(this::registerToLeAudio);
+ } else {
+ registerToLeAudio();
}
}
} else {
@@ -139,6 +155,29 @@
}
}
+ private void registerToLeAudio() {
+ synchronized (mLock) {
+ String logString = "Register to leAudio";
+
+ if (mLeAudioCallbackRegistered) {
+ logString += ", but already registered";
+ Log.i(BluetoothDeviceManager.this, logString);
+ mLocalLog.log(logString);
+ return;
+ }
+ try {
+ mLeAudioCallbackRegistered = true;
+ mBluetoothLeAudioService.registerCallback(
+ mExecutor, mLeAudioCallbacks);
+ } catch (IllegalStateException e) {
+ mLeAudioCallbackRegistered = false;
+ logString += ", but failed: " + e;
+ }
+ Log.i(BluetoothDeviceManager.this, logString);
+ mLocalLog.log(logString);
+ }
+ }
+
@Override
public void onServiceDisconnected(int profile) {
Log.startSession("BPSL.oSD");
@@ -176,11 +215,15 @@
Log.i(BluetoothDeviceManager.this, logString);
mLocalLog.log(logString);
- List<BluetoothDevice> devicesToRemove = new LinkedList<>(
- lostServiceDevices.values());
- lostServiceDevices.clear();
- for (BluetoothDevice device : devicesToRemove) {
- mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ handleAudioRefactoringServiceDisconnected(profile);
+ } else {
+ List<BluetoothDevice> devicesToRemove = new LinkedList<>(
+ lostServiceDevices.values());
+ lostServiceDevices.clear();
+ for (BluetoothDevice device : devicesToRemove) {
+ mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ }
}
}
} finally {
@@ -189,6 +232,34 @@
}
};
+ private void handleAudioRefactoringServiceDisconnected(int profile) {
+ CallAudioRouteController controller = (CallAudioRouteController)
+ mCallAudioRouteAdapter;
+ Map<AudioRoute, BluetoothDevice> btRoutes = controller
+ .getBluetoothRoutes();
+ List<Pair<AudioRoute, BluetoothDevice>> btRoutesToRemove =
+ new ArrayList<>();
+ for (AudioRoute route: btRoutes.keySet()) {
+ if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
+ continue;
+ }
+ BluetoothDevice device = btRoutes.get(route);
+ // Prevent concurrent modification exception by just iterating through keys instead of
+ // simultaneously removing them.
+ btRoutesToRemove.add(new Pair<>(route, device));
+ }
+
+ for (Pair<AudioRoute, BluetoothDevice> routeToRemove:
+ btRoutesToRemove) {
+ AudioRoute route = routeToRemove.first;
+ BluetoothDevice device = routeToRemove.second;
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ BT_DEVICE_REMOVED, route.getType(), device);
+ }
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ SWITCH_BASELINE_ROUTE, 0, (String) null);
+ }
+
private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
new LinkedHashMap<>();
private final LinkedHashMap<String, BluetoothDevice> mHearingAidDevicesByAddress =
@@ -227,6 +298,7 @@
private AudioManager mAudioManager;
private Executor mExecutor;
private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ private CallAudioRouteAdapter mCallAudioRouteAdapter;
private FeatureFlags mFeatureFlags;
public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
@@ -867,6 +939,10 @@
}
}
+ public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+ mCallAudioRouteAdapter = adapter;
+ }
+
public void dump(IndentingPrintWriter pw) {
mLocalLog.dump(pw);
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index d2686e7..5a44041 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -28,6 +28,7 @@
import android.os.Message;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -165,10 +166,23 @@
removeDevice((String) args.arg2);
break;
case CONNECT_BT:
- String actualAddress = connectBtAudio((String) args.arg2,
- false /* switchingBtDevices*/);
+ String actualAddress;
+ boolean connected;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> addressInfo = computeAddressToConnectTo(
+ (String) args.arg2, false, null);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ addressInfo = handleDeviceAlreadyConnected(addressInfo);
+ actualAddress = addressInfo.first;
+ connected = connectBtAudio(actualAddress, 0,
+ false /* switchingBtDevices*/);
+ } else {
+ actualAddress = connectBtAudioLegacy((String) args.arg2, false);
+ connected = actualAddress != null;
+ }
- if (actualAddress != null) {
+ if (connected) {
transitionTo(getConnectingStateForAddress(actualAddress,
"AudioOff/CONNECT_BT"));
} else {
@@ -181,10 +195,24 @@
break;
case RETRY_BT_CONNECTION:
Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2);
- String retryAddress = connectBtAudio((String) args.arg2, args.argi1,
- false /* switchingBtDevices*/);
+ String retryAddress;
+ boolean retrySuccessful;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+ (String) args.arg2, false, null);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+ retryAddress = retryAddressInfo.first;
+ retrySuccessful = connectBtAudio(retryAddress, args.argi1,
+ false /* switchingBtDevices*/);
+ } else {
+ retryAddress = connectBtAudioLegacy((String) args.arg2, args.argi1,
+ false /* switchingBtDevices*/);
+ retrySuccessful = retryAddress != null;
+ }
- if (retryAddress != null) {
+ if (retrySuccessful) {
transitionTo(getConnectingStateForAddress(retryAddress,
"AudioOff/RETRY_BT_CONNECTION"));
} else {
@@ -255,7 +283,7 @@
String address = (String) args.arg2;
boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
- if (switchingBtDevices == true) { // check if it is an hearing aid pair
+ if (switchingBtDevices) { // check if it is an hearing aid pair
BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter();
if (bluetoothAdapter != null) {
List<BluetoothDevice> activeHearingAids =
@@ -272,7 +300,9 @@
}
}
}
-
+ }
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ switchingBtDevices &= (mDeviceAddress != null);
}
}
try {
@@ -288,14 +318,30 @@
}
break;
case CONNECT_BT:
+ String actualAddress = null;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+ switchingBtDevices, mDeviceAddress);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ addressInfo = handleDeviceAlreadyConnected(addressInfo);
+ actualAddress = addressInfo.first;
+ switchingBtDevices = addressInfo.second;
+ }
+
if (!switchingBtDevices) {
// Ignore repeated connection attempts to the same device
break;
}
- String actualAddress = connectBtAudio(address,
- true /* switchingBtDevices*/);
- if (actualAddress != null) {
+ if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ actualAddress = connectBtAudioLegacy(address,
+ true /* switchingBtDevices*/);
+ }
+ boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+ ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+ : actualAddress != null;
+ if (connected) {
transitionTo(getConnectingStateForAddress(actualAddress,
"AudioConnecting/CONNECT_BT"));
} else {
@@ -307,14 +353,32 @@
mDeviceManager.disconnectAudio();
break;
case RETRY_BT_CONNECTION:
+ String retryAddress = null;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+ address, switchingBtDevices, mDeviceAddress);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+ retryAddress = retryAddressInfo.first;
+ switchingBtDevices = retryAddressInfo.second;
+ }
+
if (!switchingBtDevices) {
Log.d(LOG_TAG, "Retry message came through while connecting.");
break;
}
- String retryAddress = connectBtAudio(address, args.argi1,
- true /* switchingBtDevices*/);
- if (retryAddress != null) {
+ if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ retryAddress = connectBtAudioLegacy(address, args.argi1,
+ true /* switchingBtDevices*/);
+ }
+ boolean retrySuccessful = mFeatureFlags
+ .resolveSwitchingBtDevicesComputation()
+ ? connectBtAudio(retryAddress, args.argi1,
+ true /* switchingBtDevices*/)
+ : retryAddress != null;
+ if (retrySuccessful) {
transitionTo(getConnectingStateForAddress(retryAddress,
"AudioConnecting/RETRY_BT_CONNECTION"));
} else {
@@ -393,6 +457,10 @@
SomeArgs args = (SomeArgs) msg.obj;
String address = (String) args.arg2;
boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ switchingBtDevices &= (mDeviceAddress != null);
+ }
+
try {
switch (msg.what) {
case NEW_DEVICE_CONNECTED:
@@ -405,6 +473,17 @@
}
break;
case CONNECT_BT:
+ String actualAddress = null;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+ switchingBtDevices, mDeviceAddress);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ addressInfo = handleDeviceAlreadyConnected(addressInfo);
+ actualAddress = addressInfo.first;
+ switchingBtDevices = addressInfo.second;
+ }
+
if (!switchingBtDevices) {
// Ignore connection to already connected device but still notify
// CallAudioRouteStateMachine since this might be a switch from other
@@ -413,9 +492,14 @@
break;
}
- String actualAddress = connectBtAudio(address,
- true /* switchingBtDevices*/);
- if (actualAddress != null) {
+ if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ actualAddress = connectBtAudioLegacy(address,
+ true /* switchingBtDevices*/);
+ }
+ boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+ ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+ : actualAddress != null;
+ if (connected) {
if (mFeatureFlags.useActualAddressToEnterConnectingState()) {
transitionTo(getConnectingStateForAddress(actualAddress,
"AudioConnected/CONNECT_BT"));
@@ -432,14 +516,32 @@
mDeviceManager.disconnectAudio();
break;
case RETRY_BT_CONNECTION:
+ String retryAddress = null;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+ address, switchingBtDevices, mDeviceAddress);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+ retryAddress = retryAddressInfo.first;
+ switchingBtDevices = retryAddressInfo.second;
+ }
+
if (!switchingBtDevices) {
Log.d(LOG_TAG, "Retry message came through while connected.");
break;
}
- String retryAddress = connectBtAudio(address, args.argi1,
- true /* switchingBtDevices*/);
- if (retryAddress != null) {
+ if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ retryAddress = connectBtAudioLegacy(address, args.argi1,
+ true /* switchingBtDevices*/);
+ }
+ boolean retrySuccessful = mFeatureFlags
+ .resolveSwitchingBtDevicesComputation()
+ ? connectBtAudio(retryAddress, args.argi1,
+ true /* switchingBtDevices*/)
+ : retryAddress != null;
+ if (retrySuccessful) {
transitionTo(getConnectingStateForAddress(retryAddress,
"AudioConnected/RETRY_BT_CONNECTION"));
} else {
@@ -740,8 +842,124 @@
return false;
}
- private String connectBtAudio(String address, boolean switchingBtDevices) {
- return connectBtAudio(address, 0, switchingBtDevices);
+ /**
+ * Determines the address that should be used for the connection attempt. In the case that the
+ * specified address to be used is null, Telecom will try to find an arbitrary address to
+ * connect instead.
+ *
+ * @param address The address that should be prioritized for the connection attempt
+ * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+ * @param stateAddress The address stored in the state that indicates the connecting/connected
+ * device.
+ * @return {@link Pair} containing the address to connect to and whether an existing BT audio
+ * connection for a different device exists.
+ */
+ private Pair<String, Boolean> computeAddressToConnectTo(
+ String address, boolean switchingBtDevices, String stateAddress) {
+ Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
+ Optional<BluetoothDevice> matchingDevice = deviceList.stream()
+ .filter(d -> Objects.equals(d.getAddress(), address))
+ .findAny();
+
+ String actualAddress = matchingDevice.isPresent()
+ ? address : getActiveDeviceAddress();
+ if (actualAddress == null) {
+ Log.i(this, "No device specified and BT stack has no active device."
+ + " Using arbitrary device - except watch");
+ if (deviceList.size() > 0) {
+ for (BluetoothDevice device : deviceList) {
+ if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) {
+ Log.i(this, "Skipping a watch device: " + device);
+ continue;
+ }
+ actualAddress = device.getAddress();
+ break;
+ }
+ }
+
+ if (actualAddress == null) {
+ Log.i(this, "No devices available at all. Not connecting.");
+ return new Pair<>(null, false);
+ }
+ if (switchingBtDevices && actualAddress.equals(stateAddress)) {
+ switchingBtDevices = false;
+ }
+ }
+ if (!matchingDevice.isPresent()) {
+ Log.i(this, "No device with address %s available. Using %s instead.",
+ address, actualAddress);
+ }
+ return new Pair<>(actualAddress, switchingBtDevices);
+ }
+
+ /**
+ * Handles route switching to the connected state for a device. This currently handles the case
+ * for hearing aids when the route manager reports AudioOff since Telecom doesn't treat HA as
+ * the active device outside of a call.
+ *
+ * @param addressInfo A {@link Pair} containing the BT address to connect to as well as if we're
+ * handling a switch of BT devices.
+ * @return {@link Pair} indicating the address to connect to as well as if we're handling a
+ * switch of BT devices. If the device is already connected, then the
+ * return value will be {null, false} to indicate that a connection attempt
+ * is not required.
+ */
+ private Pair<String, Boolean> handleDeviceAlreadyConnected(Pair<String, Boolean> addressInfo) {
+ String address = addressInfo.first;
+ BluetoothDevice alreadyConnectedDevice = getBluetoothAudioConnectedDevice();
+ if (alreadyConnectedDevice != null && alreadyConnectedDevice.getAddress().equals(
+ address)) {
+ Log.i(this, "trying to connect to already connected device -- skipping connection"
+ + " and going into the actual connected state.");
+ transitionToActualState();
+ return new Pair<>(null, false);
+ }
+ return addressInfo;
+ }
+
+ /**
+ * Initiates a connection to the BT address specified.
+ * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
+ * Telecom from within it.
+ * @param address The address that should be tried first. May be null.
+ * @param retryCount The number of times this connection attempt has been retried.
+ * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+ * @return {@code true} if the connection to the address was successful, otherwise {@code false}
+ * if the connection fails.
+ *
+ * Note: This should only be used in par with the resolveSwitchingBtDevicesComputation flag.
+ */
+ private boolean connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+ if (address == null) {
+ return false;
+ }
+
+ if (switchingBtDevices) {
+ /* When new Bluetooth connects audio, make sure previous one has disconnected audio. */
+ mDeviceManager.disconnectAudio();
+ }
+
+ if (!mDeviceManager.connectAudio(address, switchingBtDevices)) {
+ boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
+ Log.w(LOG_TAG, "Could not connect to %s. Will %s", address,
+ shouldRetry ? "retry" : "not retry");
+ if (shouldRetry) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = Log.createSubsession();
+ args.arg2 = address;
+ args.argi1 = retryCount + 1;
+ sendMessageDelayed(RETRY_BT_CONNECTION, args,
+ mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
+ mContext.getContentResolver()));
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ private String connectBtAudioLegacy(String address, boolean switchingBtDevices) {
+ return connectBtAudioLegacy(address, 0, switchingBtDevices);
}
/**
@@ -754,7 +972,8 @@
* @return The address of the device that's actually being connected to, or null if no
* connection was successful.
*/
- private String connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+ private String connectBtAudioLegacy(String address, int retryCount,
+ boolean switchingBtDevices) {
Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
Optional<BluetoothDevice> matchingDevice = deviceList.stream()
.filter(d -> Objects.equals(d.getAddress(), address))
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index af9e07b..9168388 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -23,7 +23,6 @@
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.PENDING_ROUTE_FAILED;
-import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BLUETOOTH;
import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
@@ -41,6 +40,7 @@
import android.os.Bundle;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.util.Pair;
import com.android.internal.os.SomeArgs;
import com.android.server.telecom.AudioRoute;
@@ -50,7 +50,6 @@
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.flags.Flags;
-
public class BluetoothStateReceiver extends BroadcastReceiver {
private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName();
public static final IntentFilter INTENT_FILTER;
@@ -119,9 +118,28 @@
args.arg2 = device.getAddress();
switch (bluetoothHeadsetAudioState) {
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
- if (Flags.useRefactoredAudioRouteSwitching()) {
- mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
- device);
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ CallAudioRouteController audioRouteController =
+ (CallAudioRouteController) mCallAudioRouteAdapter;
+ audioRouteController.setIsScoAudioConnected(true);
+ if (audioRouteController.isPending()) {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+ device);
+ } else {
+ // It's possible that the initial BT connection fails but BT_AUDIO_CONNECTED
+ // is sent later, indicating that SCO audio is on. We should route
+ // appropriately in order for the UI to reflect this state.
+ AudioRoute btRoute = audioRouteController.getBluetoothRoute(
+ AudioRoute.TYPE_BLUETOOTH_SCO, device.getAddress());
+ if (btRoute != null) {
+ audioRouteController.getPendingAudioRoute().overrideDestRoute(btRoute);
+ audioRouteController.overrideIsPending(true);
+ audioRouteController.getPendingAudioRoute()
+ .setCommunicationDeviceType(AudioRoute.TYPE_BLUETOOTH_SCO);
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+ }
+ }
} else {
if (!mIsInCall) {
Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
@@ -132,6 +150,9 @@
break;
case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
if (Flags.useRefactoredAudioRouteSwitching()) {
+ CallAudioRouteController audioRouteController =
+ (CallAudioRouteController) mCallAudioRouteAdapter;
+ audioRouteController.setIsScoAudioConnected(false);
mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
device);
} else {
@@ -229,7 +250,8 @@
+ "communication device for %s. Sending PENDING_ROUTE_FAILED to "
+ "pending audio route.", device);
mCallAudioRouteAdapter.getPendingAudioRoute()
- .onMessageReceived(PENDING_ROUTE_FAILED, device.getAddress());
+ .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+ device.getAddress()), device.getAddress());
} else {
// Track the currently set communication device.
int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
diff --git a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
index e037a79..6841fcf 100644
--- a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
+++ b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
@@ -19,6 +19,7 @@
import android.os.Bundle;
import android.os.ResultReceiver;
import android.telecom.CallEndpoint;
+import android.telecom.CallException;
import android.util.Log;
import com.android.server.telecom.CallsManager;
@@ -49,8 +50,9 @@
future.complete(new VoipCallTransactionResult(
VoipCallTransactionResult.RESULT_SUCCEED, null));
} else {
+ // TODO:: define errors in CallException class. b/335703584
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, null));
+ CallException.CODE_ERROR_UNKNOWN, null));
}
}
});
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
index d35030c..ed0c7d6 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -19,14 +19,18 @@
import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
import android.os.Bundle;
import android.telecom.CallAttributes;
import android.telecom.CallException;
import android.telecom.TelecomManager;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -38,19 +42,25 @@
private final CallAttributes mCallAttributes;
private final CallsManager mCallsManager;
private final Bundle mExtras;
+ private FeatureFlags mFeatureFlags;
+
+ public void setFeatureFlags(FeatureFlags featureFlags) {
+ mFeatureFlags = featureFlags;
+ }
public IncomingCallTransaction(String callId, CallAttributes callAttributes,
- CallsManager callsManager, Bundle extras) {
+ CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
super(callsManager.getLock());
mExtras = extras;
mCallId = callId;
mCallAttributes = callAttributes;
mCallsManager = callsManager;
+ mFeatureFlags = featureFlags;
}
public IncomingCallTransaction(String callId, CallAttributes callAttributes,
- CallsManager callsManager) {
- this(callId, callAttributes, callsManager, new Bundle());
+ CallsManager callsManager, FeatureFlags featureFlags) {
+ this(callId, callAttributes, callsManager, new Bundle(), featureFlags);
}
@Override
@@ -77,10 +87,19 @@
}
}
- private Bundle generateExtras(CallAttributes callAttributes) {
+ @VisibleForTesting
+ public Bundle generateExtras(CallAttributes callAttributes) {
mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
- mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+ if (mFeatureFlags.transactionalVideoState()) {
+ // Transactional calls need to remap the CallAttributes video state to the existing
+ // VideoProfile for consistency.
+ mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+ TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+ } else {
+ mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+ callAttributes.getCallType());
+ }
mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
mExtras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
callAttributes.getAddress());
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
index b2625e6..8c970db 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -21,6 +21,8 @@
import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -29,9 +31,11 @@
import android.telecom.TelecomManager;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -45,9 +49,14 @@
private final CallAttributes mCallAttributes;
private final CallsManager mCallsManager;
private final Bundle mExtras;
+ private FeatureFlags mFeatureFlags;
+
+ public void setFeatureFlags(FeatureFlags featureFlags) {
+ mFeatureFlags = featureFlags;
+ }
public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
- CallsManager callsManager, Bundle extras) {
+ CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
super(callsManager.getLock());
mCallId = callId;
mContext = context;
@@ -55,11 +64,12 @@
mCallsManager = callsManager;
mExtras = extras;
mCallingPackage = mContext.getOpPackageName();
+ mFeatureFlags = featureFlags;
}
public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
- CallsManager callsManager) {
- this(callId, context, callAttributes, callsManager, new Bundle());
+ CallsManager callsManager, FeatureFlags featureFlags) {
+ this(callId, context, callAttributes, callsManager, new Bundle(), featureFlags);
}
@Override
@@ -121,12 +131,20 @@
}
}
- private Bundle generateExtras(CallAttributes callAttributes) {
+ @VisibleForTesting
+ public Bundle generateExtras(CallAttributes callAttributes) {
mExtras.setDefusable(true);
mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
- mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
- callAttributes.getCallType());
+ if (mFeatureFlags.transactionalVideoState()) {
+ // Transactional calls need to remap the CallAttributes video state to the existing
+ // VideoProfile for consistency.
+ mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+ } else {
+ mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ callAttributes.getCallType());
+ }
mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
return mExtras;
}
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 79a940b..e235ead 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.voip;
+import android.telecom.CallException;
+
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
@@ -48,14 +50,8 @@
if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
CompletableFuture.completedFuture(null).thenApplyAsync(
(x) -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format(
- "sub transaction %s failed",
- transactionName));
- finish(mainResult);
- mCompleteListener.onTransactionCompleted(mainResult,
+ finish(result);
+ mCompleteListener.onTransactionCompleted(result,
mTransactionName);
return null;
}, new LoggedHandlerExecutor(mHandler,
@@ -74,7 +70,7 @@
(x) -> {
VoipCallTransactionResult mainResult =
new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_OPERATION_TIMED_OUT,
String.format("sub transaction %s timed out",
transactionName));
finish(mainResult);
diff --git a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
index 64596b1..c1bc343 100644
--- a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
+++ b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
@@ -18,6 +18,7 @@
import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import android.telecom.CallException;
import android.telecom.VideoProfile;
import android.util.Log;
@@ -48,13 +49,8 @@
if (isRequestingVideoTransmission(mVideoProfileState) &&
!mCall.isVideoCallingSupportedByPhoneAccount()) {
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /*TODO:: define error code. b/335703584 */,
"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(
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index 55d2065..748f285 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.voip;
+import android.telecom.CallException;
+
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
@@ -53,14 +55,8 @@
handleTransactionFailure();
CompletableFuture.completedFuture(null).thenApplyAsync(
(x) -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format(
- "sub transaction %s failed",
- transactionName));
- finish(mainResult);
- mCompleteListener.onTransactionCompleted(mainResult,
+ finish(result);
+ mCompleteListener.onTransactionCompleted(result,
mTransactionName);
return null;
}, new LoggedHandlerExecutor(mHandler,
@@ -86,7 +82,7 @@
(x) -> {
VoipCallTransactionResult mainResult =
new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_OPERATION_TIMED_OUT,
String.format("sub transaction %s timed out",
transactionName));
finish(mainResult);
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
index 299bcc3..0086d07 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -146,8 +146,8 @@
pendingTransactions = new ArrayList<>(mTransactions);
}
for (VoipCallTransaction t : pendingTransactions) {
- t.finish(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
- "clear called"));
+ t.finish(new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN
+ /* TODO:: define error b/335703584 */, "clear called"));
}
}
diff --git a/src/com/android/server/telecom/voip/VideoStateTranslation.java b/src/com/android/server/telecom/voip/VideoStateTranslation.java
index 615e4bc..3812d15 100644
--- a/src/com/android/server/telecom/voip/VideoStateTranslation.java
+++ b/src/com/android/server/telecom/voip/VideoStateTranslation.java
@@ -20,6 +20,11 @@
import android.telecom.Log;
import android.telecom.VideoProfile;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.AnomalyReporterAdapterImpl;
+
+import java.util.UUID;
+
/**
* 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}.
@@ -41,15 +46,16 @@
* 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));
+ public static int TransactionalVideoStateToVideoProfileState(int callType) {
+ if (callType == CallAttributes.AUDIO_CALL) {
+ Log.i(TAG, "CallAttributes.AUDIO_CALL --> VideoProfile.STATE_AUDIO_ONLY");
return VideoProfile.STATE_AUDIO_ONLY;
- } else {
- Log.i(TAG, "%s --> VideoProfile.STATE_BIDIRECTIONAL",
- TransactionalVideoState_toString(transactionalVideo));
+ } else if (callType == CallAttributes.VIDEO_CALL) {
+ Log.i(TAG, "CallAttributes.VIDEO_CALL--> VideoProfile.STATE_BIDIRECTIONAL");
return VideoProfile.STATE_BIDIRECTIONAL;
+ } else {
+ Log.w(TAG, "CallType=[%d] does not have a VideoProfile mapping", callType);
+ return VideoProfile.STATE_AUDIO_ONLY;
}
}
@@ -58,26 +64,36 @@
* 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;
+ switch (videoProfileState) {
+ case VideoProfile.STATE_AUDIO_ONLY -> {
+ Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
+ VideoProfileStateToString(videoProfileState));
+ return CallAttributes.AUDIO_CALL;
+ }
+ case VideoProfile.STATE_BIDIRECTIONAL, VideoProfile.STATE_TX_ENABLED,
+ VideoProfile.STATE_RX_ENABLED -> {
+ Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
+ VideoProfileStateToString(videoProfileState));
+ return CallAttributes.VIDEO_CALL;
+ }
+ default -> {
+ Log.w(TAG, "VideoProfile=[%d] does not have a CallType mapping", videoProfileState);
+ return CallAttributes.AUDIO_CALL;
+ }
}
}
- private static String TransactionalVideoState_toString(int transactionalVideoState) {
+ public static String TransactionalVideoStateToString(int transactionalVideoState) {
if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
return "CallAttributes.AUDIO_CALL";
- } else {
+ } else if (transactionalVideoState == CallAttributes.VIDEO_CALL) {
return "CallAttributes.VIDEO_CALL";
+ } else {
+ return "CallAttributes.UNKNOWN";
}
}
- private static String VideoProfileState_toString(int videoProfileState) {
+ private static String VideoProfileStateToString(int videoProfileState) {
switch (videoProfileState) {
case VideoProfile.STATE_BIDIRECTIONAL -> {
return "VideoProfile.STATE_BIDIRECTIONAL";
@@ -88,7 +104,12 @@
case VideoProfile.STATE_TX_ENABLED -> {
return "VideoProfile.STATE_TX_ENABLED";
}
+ case VideoProfile.STATE_AUDIO_ONLY -> {
+ return "VideoProfile.STATE_AUDIO_ONLY";
+ }
+ default -> {
+ return "VideoProfile.UNKNOWN";
+ }
}
- 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 ceb8d55..a589a6d 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -18,6 +18,7 @@
import android.os.Handler;
import android.os.HandlerThread;
+import android.telecom.CallException;
import android.telecom.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -195,21 +196,31 @@
protected final void scheduleTransaction() {
LoggedHandlerExecutor executor = new LoggedHandlerExecutor(mHandler,
- mTransactionName + "@" + hashCode() + ".pT", mLock);
+ mTransactionName + "@" + hashCode() + ".sT", mLock);
CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
future.thenComposeAsync(this::processTransaction, executor)
.thenApplyAsync((Function<VoipCallTransactionResult, Void>) result -> {
- mCompleted.set(true);
- finish(result);
- if (mCompleteListener != null) {
- mCompleteListener.onTransactionCompleted(result, mTransactionName);
- }
+ notifyListenersOfResult(result);
return null;
- }, executor)
- .exceptionallyAsync((throwable -> {
+ }, executor)
+ .exceptionally((throwable -> {
+ // Do NOT wait for the timeout in order to finish this failed transaction.
+ // Instead, propagate the failure to the other transactions immediately!
+ String errorMessage = throwable != null ? throwable.getMessage() :
+ "encountered an exception while processing " + mTransactionName;
+ notifyListenersOfResult(new VoipCallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN, errorMessage));
Log.e(this, throwable, "Error while executing transaction.");
return null;
- }), executor);
+ }));
+ }
+
+ protected void notifyListenersOfResult(VoipCallTransactionResult result){
+ mCompleted.set(true);
+ finish(result);
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionCompleted(result, mTransactionName);
+ }
}
protected CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
@@ -248,7 +259,7 @@
if (mSubTransactions != null && !mSubTransactions.isEmpty()) {
mSubTransactions.forEach( t -> t.finish(isTimedOut, result));
}
- mHandlerThread.quit();
+ mHandlerThread.quitSafely();
}
/**
diff --git a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
index ffc0255..50871f2 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
@@ -22,7 +22,10 @@
public class VoipCallTransactionResult {
public static final int RESULT_SUCCEED = 0;
- public static final int RESULT_FAILED = 1;
+
+ // NOTE: if the VoipCallTransactionResult should not use the RESULT_SUCCEED to represent a
+ // successful transaction, use an error code defined in the
+ // {@link android.telecom.CallException} class
private final int mResult;
private final String mMessage;
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index c516c8e..d5e903b 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -60,9 +60,11 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import static org.mockito.Mockito.reset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.Executor;
@RunWith(JUnit4.class)
public class BluetoothDeviceManagerTest extends TelecomTestCase {
@@ -75,6 +77,7 @@
@Mock BluetoothLeAudio mBluetoothLeAudio;
@Mock AudioManager mockAudioManager;
@Mock AudioDeviceInfo mSpeakerInfo;
+ @Mock Executor mExecutor;
BluetoothDeviceManager mBluetoothDeviceManager;
BluetoothProfile.ServiceListener serviceListenerUnderTest;
@@ -114,6 +117,7 @@
mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager);
mockAudioManager = mContext.getSystemService(AudioManager.class);
+ mExecutor = mContext.getMainExecutor();
ArgumentCaptor<BluetoothProfile.ServiceListener> serviceCaptor =
ArgumentCaptor.forClass(BluetoothProfile.ServiceListener.class);
@@ -750,6 +754,31 @@
assertTrue(mBluetoothDeviceManager.isInbandRingingEnabled());
}
+ @SmallTest
+ @Test
+ public void testRegisterLeAudioCallbackNoPostpone() {
+ reset(mBluetoothLeAudio);
+ when(mFeatureFlags.postponeRegisterToLeaudio()).thenReturn(false);
+ serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+ (BluetoothProfile) mBluetoothLeAudio);
+ // Second time on purpose
+ serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+ (BluetoothProfile) mBluetoothLeAudio);
+ verify(mExecutor, times(0)).execute(any());
+ verify(mBluetoothLeAudio, times(1)).registerCallback(any(Executor.class),
+ any(BluetoothLeAudio.Callback.class));
+ }
+
+ @SmallTest
+ @Test
+ public void testRegisterLeAudioCallbackWithPostpone() {
+ reset(mBluetoothLeAudio);
+ when(mFeatureFlags.postponeRegisterToLeaudio()).thenReturn(true);
+ serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+ (BluetoothProfile) mBluetoothLeAudio);
+ verify(mExecutor, times(1)).execute(any());
+ }
+
private void assertClearHearingAidOrLeCommunicationDevice(
boolean flagEnabled, int device_type
) {
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index 07dd350..1c885c1 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -21,6 +21,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -249,7 +250,18 @@
@SmallTest
@Test
- public void testConnectBtWithoutAddress() {
+ public void testConnectBtWithoutAddress_SwitchingBtDeviceFlag() {
+ when(mFeatureFlags.resolveSwitchingBtDevicesComputation()).thenReturn(true);
+ verifyConnectBtWithoutAddress();
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectBtWithoutAddress_SwitchingBtDeviceFlagDisabled() {
+ verifyConnectBtWithoutAddress();
+ }
+
+ private void verifyConnectBtWithoutAddress() {
when(mFeatureFlags.useActualAddressToEnterConnectingState()).thenReturn(true);
BluetoothRouteManager sm = setupStateMachine(
BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
@@ -266,7 +278,15 @@
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
- verifyConnectionAttempt(DEVICE1, 1);
+ // We should not expect explicit connection attempt (BluetoothDeviceManager#connectAudio)
+ // as the device is already "connected" as per how the state machine was initialized.
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ verify(mDeviceManager, never()).disconnectAudio();
+ } else {
+ // Legacy behavior
+ verifyConnectionAttempt(DEVICE1, 1);
+ verify(mDeviceManager, times(1)).disconnectAudio();
+ }
assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
+ ":" + DEVICE1.getAddress(),
sm.getCurrentState().getName());
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
index 97405a3..1d641ba 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.tests.TelecomSystemTest.TEST_TIMEOUT;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@@ -34,6 +36,8 @@
import static org.mockito.Mockito.when;
import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Looper;
import android.telecom.DisconnectCause;
import android.util.SparseArray;
@@ -67,6 +71,7 @@
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@RunWith(JUnit4.class)
@@ -423,9 +428,12 @@
Call call = mock(Call.class);
ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
when(call.getState()).thenReturn(CallState.RINGING);
+ handleWaitForBtIcsBinding(call);
// Make sure appropriate messages are sent when we add a RINGING call
mCallAudioManager.onCallAdded(call);
+ mCallAudioManager.getCallRingingFuture().join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
assertEquals(call, mCallAudioManager.getForegroundCall());
verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
@@ -556,10 +564,14 @@
Call call = createAudioProcessingCall();
+
when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
+ handleWaitForBtIcsBinding(call);
mCallAudioManager.onCallStateChanged(call, CallState.AUDIO_PROCESSING,
CallState.SIMULATED_RINGING);
+ mCallAudioManager.getCallRingingFuture().join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
CallAudioModeStateMachine.MessageArgs expectedArgs = new Builder()
.setHasActiveOrDialingCalls(false)
@@ -810,9 +822,12 @@
private Call createSimulatedRingingCall() {
Call call = mock(Call.class);
when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
+ handleWaitForBtIcsBinding(call);
ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
mCallAudioManager.onCallAdded(call);
+ mCallAudioManager.getCallRingingFuture().join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
assertEquals(call, mCallAudioManager.getForegroundCall());
@@ -838,8 +853,11 @@
private Call createIncomingCall() {
Call call = mock(Call.class);
when(call.getState()).thenReturn(CallState.RINGING);
+ handleWaitForBtIcsBinding(call);
mCallAudioManager.onCallAdded(call);
+ mCallAudioManager.getCallRingingFuture().join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
assertEquals(call, mCallAudioManager.getForegroundCall());
ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
@@ -924,4 +942,10 @@
assertEquals(expected.isTonePlaying, actual.isTonePlaying);
assertEquals(expected.foregroundCallIsVoip, actual.foregroundCallIsVoip);
}
+
+ private void handleWaitForBtIcsBinding(Call call) {
+ when(mFlags.separatelyBindToBtIncallService()).thenReturn(true);
+ CompletableFuture<Boolean> btBindingFuture = CompletableFuture.completedFuture(true);
+ when(call.getBtIcsFuture()).thenReturn(btBindingFuture);
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index f770b6a..59473bd 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -34,7 +34,7 @@
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_BLUETOOTH;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
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;
@@ -64,14 +64,18 @@
import android.media.audiopolicy.AudioProductStrategy;
import android.os.UserHandle;
import android.telecom.CallAudioState;
+import android.telecom.VideoProfile;
import androidx.test.filters.SmallTest;
import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.Call;
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PendingAudioRoute;
import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.WiredHeadsetManager;
import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
@@ -102,6 +106,9 @@
@Mock StatusBarNotifier mockStatusBarNotifier;
@Mock AudioDeviceInfo mAudioDeviceInfo;
@Mock BluetoothLeAudio mBluetoothLeAudio;
+ @Mock CallAudioManager mCallAudioManager;
+ @Mock Call mCall;
+ @Mock private TelecomSystem.SyncRoot mLock;
private AudioRoute mEarpieceRoute;
private AudioRoute mSpeakerRoute;
private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -143,6 +150,7 @@
any(CallAudioState.class));
when(mCallsManager.getCurrentUserHandle()).thenReturn(
new UserHandle(UserHandle.USER_SYSTEM));
+ when(mCallsManager.getLock()).thenReturn(mLock);
when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
.thenReturn(true);
@@ -160,6 +168,9 @@
mController.setAudioManager(mAudioManager);
mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+ mController.setCallAudioManager(mCallAudioManager);
+ when(mCallAudioManager.getForegroundCall()).thenReturn(mCall);
+ when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
}
@@ -191,6 +202,78 @@
@SmallTest
@Test
+ public void testInitializeWithWiredHeadset() {
+ AudioRoute wiredHeadsetRoute = new AudioRoute(AudioRoute.TYPE_WIRED, null, null);
+ when(mWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+ mController.initialize();
+ assertEquals(wiredHeadsetRoute, mController.getCurrentRoute());
+ assertEquals(2, mController.getAvailableRoutes().size());
+ assertTrue(mController.getAvailableRoutes().contains(mSpeakerRoute));
+ }
+
+ @SmallTest
+ @Test
+ public void testNormalCallRouteToEarpiece() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ // Verify that pending audio destination route is set to speaker. This will trigger pending
+ // message to wait for SPEAKER_ON message once communication device is set before routing.
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+
+ CallAudioState 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 testVideoCallHoldRouteToEarpiece() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ // Verify that pending audio destination route is not defaulted to speaker when a video call
+ // is not the foreground call.
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+ }
+
+ @SmallTest
+ @Test
+ public void testVideoCallRouteToSpeaker() {
+ when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ // Verify that pending audio destination route is set to speaker. This will trigger pending
+ // message to wait for SPEAKER_ON message once communication device is set before routing.
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_SPEAKER, pendingRoute.getDestRoute().getType());
+
+ // Mock SPEAKER_ON message received by controller.
+ 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));
+
+ // Verify that audio is routed to wired headset if it's present.
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
public void testActiveDeactivateBluetoothDevice() {
mController.initialize();
mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
@@ -435,8 +518,7 @@
@SmallTest
@Test
public void testToggleMute() throws Exception {
- when(mAudioManager.isMasterMute()).thenReturn(false);
-
+ when(mAudioManager.isMicrophoneMute()).thenReturn(false);
mController.initialize();
mController.setActive(true);
@@ -449,7 +531,7 @@
verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
any(CallAudioState.class), eq(expectedState));
- when(mAudioManager.isMasterMute()).thenReturn(true);
+ when(mAudioManager.isMicrophoneMute()).thenReturn(true);
mController.sendMessageWithSessionInfo(MUTE_OFF);
expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
@@ -462,6 +544,34 @@
@SmallTest
@Test
+ public void testMuteOffAfterCallEnds() throws Exception {
+ when(mAudioManager.isMicrophoneMute()).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));
+
+ // Switch to NO_FOCUS to indicate call termination and verify mute is reset.
+ when(mAudioManager.isMicrophoneMute()).thenReturn(true);
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+ 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).atLeastOnce()).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
public void testIgnoreAutoRouteToWatch() {
when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
@@ -591,6 +701,23 @@
BLUETOOTH_DEVICES.remove(scoDevice);
}
+ @SmallTest
+ @Test
+ public void testIgnoreLeRouteWhenServiceUnavailable() {
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+
+ when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(null);
+ // Switch baseline to verify that we don't route back to LE audio this time.
+ mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, 0, (String) null);
+ 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).atLeastOnce()).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
private void verifyConnectBluetoothDevice(int audioType) {
mController.initialize();
mController.setActive(true);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index d2da505..e97de2e 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
@@ -1230,8 +1231,9 @@
@SmallTest
@Test
- public void testQuiescentBluetoothRouteResetMute() {
+ public void testQuiescentBluetoothRouteResetMute() throws Exception {
when(mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()).thenReturn(true);
+ when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(true);
CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
mContext,
mockCallsManager,
@@ -1264,6 +1266,7 @@
CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
| CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ when(mockAudioManager.isMicrophoneMute()).thenReturn(true);
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
CallAudioRouteStateMachine.NO_FOCUS);
@@ -1272,9 +1275,8 @@
expectedState = new CallAudioState(false,
CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
| CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
- // TODO: Re-enable this part of the test; this is now failing because we have to
- // revert ag/23783145.
- // assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ verify(mockAudioService).setMicrophoneMute(eq(false), anyString(), anyInt(), eq(null));
}
@SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java
new file mode 100644
index 0000000..6deade4
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.tests;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.app.IntentForwarderActivity;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallIntentProcessor;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.TelephonyUtil;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+
+import java.util.concurrent.CompletableFuture;
+
+/** Unit tests for CollIntentProcessor class. */
+@RunWith(JUnit4.class)
+public class CallIntentProcessorTest extends TelecomTestCase {
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ @Mock
+ private CallsManager mCallsManager;
+ @Mock
+ private DefaultDialerCache mDefaultDialerCache;
+ @Mock
+ private Context mMockCreateContextAsUser;
+ @Mock
+ private UserManager mMockCurrentUserManager;
+ @Mock
+ private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private ResolveInfo mResolveInfo;
+ @Mock
+ private ComponentName mComponentName;
+ @Mock
+ private ComponentInfo mComponentInfo;
+ @Mock
+ private CompletableFuture<Call> mCall;
+ private CallIntentProcessor mCallIntentProcessor;
+ private static final UserHandle PRIVATE_SPACE_USERHANDLE = new UserHandle(12);
+ private static final String TEST_PACKAGE_NAME = "testPackageName";
+ private static final Uri TEST_PHONE_NUMBER = Uri.parse("tel:1234567890");
+ private static final Uri TEST_EMERGENCY_PHONE_NUMBER = Uri.parse("tel:911");
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ when(mContext.createContextAsUser(any(UserHandle.class), eq(0))).thenReturn(
+ mMockCreateContextAsUser);
+ when(mMockCreateContextAsUser.getSystemService(UserManager.class)).thenReturn(
+ mMockCurrentUserManager);
+ mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager, mDefaultDialerCache,
+ mFeatureFlags);
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
+ when(mCallsManager.getPhoneNumberUtilsAdapter()).thenReturn(mPhoneNumberUtilsAdapter);
+ when(mPhoneNumberUtilsAdapter.isUriNumber(anyString())).thenReturn(true);
+ when(mCallsManager.startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+ any(UserHandle.class), any(Intent.class), anyString())).thenReturn(mCall);
+ when(mCall.thenAccept(any())).thenReturn(null);
+ }
+
+ @Test
+ public void testNonPrivateSpaceCall_noConsentDialogShown() {
+ setPrivateSpaceFlagsEnabled();
+
+ Intent intent = new Intent(Intent.ACTION_CALL);
+ intent.setData(TEST_PHONE_NUMBER);
+ intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, UserHandle.CURRENT);
+ when(mCallsManager.isSelfManaged(any(), eq(UserHandle.CURRENT))).thenReturn(false);
+
+ mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+ verify(mContext, never()).startActivityAsUser(any(Intent.class), any(UserHandle.class));
+
+ // Verify that the call proceeds as normal since the dialog was not shown
+ verify(mCallsManager).startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+ eq(UserHandle.CURRENT), eq(intent), eq(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testPrivateSpaceCall_isSelfManaged_noDialogShown() {
+ setPrivateSpaceFlagsEnabled();
+ markInitiatingUserAsPrivateProfile();
+ resolveAsIntentForwarderActivity();
+
+ Intent intent = new Intent(Intent.ACTION_CALL);
+ intent.setData(TEST_PHONE_NUMBER);
+ intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+ when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(true);
+
+ mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+ verify(mContext, never()).startActivityAsUser(any(Intent.class),
+ eq(PRIVATE_SPACE_USERHANDLE));
+
+ // Verify that the call proceeds as normal since the dialog was not shown
+ verify(mCallsManager).startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+ eq(PRIVATE_SPACE_USERHANDLE), eq(intent), eq(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testPrivateSpaceCall_isEmergency_noDialogShown() {
+ MockitoSession session = ExtendedMockito.mockitoSession().mockStatic(
+ TelephonyUtil.class).startMocking();
+ ExtendedMockito.doReturn(true).when(
+ () -> TelephonyUtil.shouldProcessAsEmergency(any(), any()));
+
+ setPrivateSpaceFlagsEnabled();
+ markInitiatingUserAsPrivateProfile();
+ resolveAsIntentForwarderActivity();
+
+ Intent intent = new Intent(Intent.ACTION_CALL);
+ intent.setData(TEST_EMERGENCY_PHONE_NUMBER);
+ intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+ when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(false);
+
+ mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+ verify(mContext, never()).startActivityAsUser(any(Intent.class),
+ eq(PRIVATE_SPACE_USERHANDLE));
+ session.finishMocking();
+ }
+
+ @Test
+ public void testPrivateSpaceCall_showConsentDialog() {
+ setPrivateSpaceFlagsEnabled();
+ markInitiatingUserAsPrivateProfile();
+ resolveAsIntentForwarderActivity();
+
+ Intent intent = new Intent(Intent.ACTION_CALL);
+ intent.setData(TEST_PHONE_NUMBER);
+ intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+ when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(false);
+
+ mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+ // Consent dialog should be shown
+ verify(mContext).startActivityAsUser(any(Intent.class), eq(PRIVATE_SPACE_USERHANDLE));
+
+ /// Verify that the call does not proceeds as normal since the dialog was shown
+ verify(mCallsManager, never()).startOutgoingCall(any(), any(), any(), any(), any(),
+ anyString());
+ }
+
+ private void setPrivateSpaceFlagsEnabled() {
+ mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+ android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_INTENT_REDIRECTION);
+ }
+
+ private void markInitiatingUserAsPrivateProfile() {
+ when(mMockCurrentUserManager.isPrivateProfile()).thenReturn(true);
+ }
+
+ private void resolveAsIntentForwarderActivity() {
+ when(mComponentName.getShortClassName()).thenReturn(
+ IntentForwarderActivity.FORWARD_INTENT_TO_PARENT);
+ when(mComponentInfo.getComponentName()).thenReturn(mComponentName);
+ when(mResolveInfo.getComponentInfo()).thenReturn(mComponentInfo);
+
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+
+ when(mPackageManager.resolveActivityAsUser(any(Intent.class),
+ any(PackageManager.ResolveInfoFlags.class),
+ eq(PRIVATE_SPACE_USERHANDLE.getIdentifier()))).thenReturn(mResolveInfo);
+ }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
index 4e57a3a..cb04dc3 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -24,6 +24,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
@@ -122,6 +123,7 @@
private static final int CURRENT_USER_ID = 0;
private static final int OTHER_USER_ID = 10;
private static final int MANAGED_USER_ID = 11;
+ private static final int PRIVATE_USER_ID = 12;
private static final String TEST_ISO = "KR";
private static final String TEST_ISO_2 = "JP";
@@ -175,9 +177,22 @@
UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
UserInfo userInfo = new UserInfo(CURRENT_USER_ID, "test", 0);
+ userInfo.profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+ userInfo.userType = UserManager.USER_TYPE_FULL_SYSTEM;
+
UserInfo otherUserInfo = new UserInfo(OTHER_USER_ID, "test2", 0);
+ otherUserInfo.profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+ otherUserInfo.userType = UserManager.USER_TYPE_FULL_SECONDARY;
+
UserInfo managedProfileUserInfo = new UserInfo(MANAGED_USER_ID, "test3",
- UserInfo.FLAG_MANAGED_PROFILE);
+ UserInfo.FLAG_MANAGED_PROFILE | userInfo.FLAG_PROFILE);
+ managedProfileUserInfo.profileGroupId = 90210;
+ managedProfileUserInfo.userType = UserManager.USER_TYPE_PROFILE_MANAGED;
+
+ UserInfo privateProfileUserInfo = new UserInfo(PRIVATE_USER_ID, "private",
+ UserInfo.FLAG_PROFILE);
+ privateProfileUserInfo.profileGroupId = 90210;
+ privateProfileUserInfo.userType = UserManager.USER_TYPE_PROFILE_PRIVATE;
doAnswer(new Answer<Uri>() {
@Override
@@ -192,15 +207,42 @@
.thenReturn(false);
when(userManager.getAliveUsers())
.thenReturn(Arrays.asList(userInfo, otherUserInfo, managedProfileUserInfo));
+ configureContextForUser(CURRENT_USER_ID, userInfo);
when(userManager.getUserInfo(eq(CURRENT_USER_ID))).thenReturn(userInfo);
+
+ configureContextForUser(OTHER_USER_ID, otherUserInfo);
when(userManager.getUserInfo(eq(OTHER_USER_ID))).thenReturn(otherUserInfo);
+
+ configureContextForUser(MANAGED_USER_ID, managedProfileUserInfo);
when(userManager.getUserInfo(eq(MANAGED_USER_ID))).thenReturn(managedProfileUserInfo);
+
+ configureContextForUser(PRIVATE_USER_ID, privateProfileUserInfo);
+ when(userManager.getUserInfo(eq(PRIVATE_USER_ID))).thenReturn(privateProfileUserInfo);
+
PackageManager packageManager = mContext.getPackageManager();
when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(false);
when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
}
+ /**
+ * Yuck; this is absolutely wretched that we have to mock things out in this way.
+ * Because the preferred way to get info about a user is to first user
+ * {@link Context#createContextAsUser(UserHandle, int)} to first get a user-specific context,
+ * and to then query the {@link UserManager} instance to see if it's a profile, we need to do
+ * all of this really gross mocking.
+ * @param userId The userid.
+ * @param info The associated userinfo.
+ */
+ private void configureContextForUser(int userId, UserInfo info) {
+ Context mockContext = mock(Context.class);
+ mComponentContextFixture.addContextForUser(UserHandle.of(userId), mockContext);
+ UserManager mockUserManager = mock(UserManager.class);
+ when(mockUserManager.getUserInfo(eq(userId))).thenReturn(info);
+ when(mockUserManager.isProfile()).thenReturn(info.isProfile());
+ when(mockContext.getSystemService(eq(UserManager.class))).thenReturn(mockUserManager);
+ }
+
@Override
@After
public void tearDown() throws Exception {
@@ -233,7 +275,7 @@
@Test
public void testDontLogChoosingAccountCall() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -336,7 +378,7 @@
@Test
public void testLogCallDirectionOutgoing() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeOutgoingCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -361,7 +403,7 @@
@Test
public void testLogCallDirectionIncoming() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeIncomingCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -387,7 +429,7 @@
@Test
public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOff() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeMissedCall = makeFakeCall(
DisconnectCause.MISSED, // disconnectCauseCode
false, // isConference
@@ -418,7 +460,7 @@
@Test
public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOn() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeMissedCall = makeFakeCall(
DisconnectCause.MISSED, // disconnectCauseCode
false, // isConference
@@ -449,7 +491,7 @@
@Test
public void testLogCallDirectionRejected() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeMissedCall = makeFakeCall(
DisconnectCause.REJECTED, // disconnectCauseCode
false, // isConference
@@ -475,7 +517,7 @@
@Test
public void testCreationTimeAndAge() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
long currentTime = System.currentTimeMillis();
long duration = 1000L;
Call fakeCall = makeFakeCall(
@@ -503,7 +545,7 @@
@Test
public void testLogPhoneAccountId() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -527,7 +569,7 @@
@Test
public void testLogCorrectPhoneNumber() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -554,7 +596,7 @@
@Test
public void testLogCallVideoFeatures() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeVideoCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -612,7 +654,8 @@
ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, OTHER_USER_ID)));
assertFalse(uris.getAllValues().contains(
ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, MANAGED_USER_ID)));
-
+ assertFalse(uris.getAllValues().contains(
+ ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, PRIVATE_USER_ID)));
for (ContentValues v : values.getAllValues()) {
assertEquals(v.getAsInteger(CallLog.Calls.TYPE),
Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
@@ -657,7 +700,8 @@
ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, OTHER_USER_ID)));
assertFalse(uris.getAllValues().contains(
ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, MANAGED_USER_ID)));
-
+ assertFalse(uris.getAllValues().contains(
+ ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, PRIVATE_USER_ID)));
for (ContentValues v : values.getAllValues()) {
assertEquals(v.getAsInteger(CallLog.Calls.TYPE),
Integer.valueOf(CallLog.Calls.INCOMING_TYPE));
@@ -697,6 +741,72 @@
}
@MediumTest
+ @Test
+ public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromPrivateProfile() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle,
+ PhoneAccount.CAPABILITY_MULTI_USER));
+ Call fakeOutgoingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ VIA_NUMBER_STRING, // viaNumber
+ UserHandle.of(PRIVATE_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+
+ // Outgoing call placed through private space should only show up in the private space
+ // call logs.
+ verifyNoInsertionInUser(CURRENT_USER_ID);
+ verifyNoInsertionInUser(OTHER_USER_ID);
+ verifyNoInsertionInUser(MANAGED_USER_ID);
+ ContentValues insertedValues = verifyInsertionWithCapture(PRIVATE_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+ }
+
+ @MediumTest
+ @Test
+ public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromPrivateProfileNoRefactor() {
+ // Same as the above test, but turns off the hidden deps refactor; there are some minor
+ // differences in how we detect profiles, so we want to ensure this works both ways.
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle,
+ PhoneAccount.CAPABILITY_MULTI_USER));
+ Call fakeOutgoingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ VIA_NUMBER_STRING, // viaNumber
+ UserHandle.of(PRIVATE_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+
+ // Outgoing call placed through work dialer should be inserted to managed profile only.
+ verifyNoInsertionInUser(CURRENT_USER_ID);
+ verifyNoInsertionInUser(OTHER_USER_ID);
+ verifyNoInsertionInUser(MANAGED_USER_ID);
+ ContentValues insertedValues = verifyInsertionWithCapture(PRIVATE_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+ }
+
+ @MediumTest
@FlakyTest
@Test
public void testLogCallDirectionOutgoingFromManagedProfile() {
@@ -722,6 +832,7 @@
// profile only.
verifyNoInsertionInUser(CURRENT_USER_ID);
verifyNoInsertionInUser(OTHER_USER_ID);
+ verifyNoInsertionInUser(PRIVATE_USER_ID);
ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
@@ -752,6 +863,7 @@
// profile only.
verifyNoInsertionInUser(CURRENT_USER_ID);
verifyNoInsertionInUser(OTHER_USER_ID);
+ verifyNoInsertionInUser(PRIVATE_USER_ID);
ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
Integer.valueOf(Calls.INCOMING_TYPE));
@@ -764,7 +876,7 @@
@Test
public void testLogCallDataUsageSet() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeVideoCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -791,7 +903,7 @@
@Test
public void testLogCallDataUsageNotSet() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeVideoCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -845,7 +957,7 @@
@Test
public void testLogCallWhenExternalCallOnWatch() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
PackageManager packageManager = mContext.getPackageManager();
when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(true);
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index 58d3302..a22d2ca 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -37,6 +37,7 @@
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
@@ -126,6 +127,11 @@
doReturn(new ComponentName(mContext, CallTest.class))
.when(mMockConnectionService).getComponentName();
doReturn(UserHandle.CURRENT).when(mMockCallsManager).getCurrentUserHandle();
+ Resources mockResources = mContext.getResources();
+ when(mockResources.getBoolean(R.bool.skip_loading_canned_text_response))
+ .thenReturn(false);
+ when(mockResources.getBoolean(R.bool.skip_incoming_caller_info_query))
+ .thenReturn(false);
EmergencyCallHelper helper = mock(EmergencyCallHelper.class);
doReturn(helper).when(mMockCallsManager).getEmergencyCallHelper();
}
@@ -146,6 +152,69 @@
assertTrue(call.hasGoneActiveBefore());
}
+ /**
+ * Verify Call#setVideoState will only upgrade to video if the PhoneAccount supports video
+ * state capabilities
+ */
+ @Test
+ @SmallTest
+ public void testSetVideoStateForTransactionalCalls() {
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ call.setIsTransactionalCall(true);
+ call.setTransactionServiceWrapper(tsw);
+ assertTrue(call.isTransactionalCall());
+ assertNotNull(call.getTransactionServiceWrapper());
+ when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+ // VoIP apps using transactional APIs must register a PhoneAccount that supports
+ // video calling capabilities or the video state will be defaulted to audio
+ assertFalse(call.isVideoCallingSupportedByPhoneAccount());
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+
+ call.setVideoCallingSupportedByPhoneAccount(true);
+ assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+
+ // After the PhoneAccount signals it supports video calling, video state changes can occur
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+ }
+
+ /**
+ * Verify all video state changes are echoed out to the TransactionalServiceWrapper
+ */
+ @Test
+ @SmallTest
+ public void testToggleTransactionalVideoState() {
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ call.setIsTransactionalCall(true);
+ call.setTransactionServiceWrapper(tsw);
+ call.setVideoCallingSupportedByPhoneAccount(true);
+ assertTrue(call.isTransactionalCall());
+ assertNotNull(call.getTransactionServiceWrapper());
+ assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+ when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ verify(tsw, times(2)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+ call.setVideoState(VideoProfile.STATE_AUDIO_ONLY);
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+ verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.AUDIO_CALL);
+
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ verify(tsw, times(3)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+ }
+
@Test
public void testMultipleCachedMuteStateChanges() {
when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
@@ -623,6 +692,18 @@
@Test
@SmallTest
+ public void testGetFromCallerInfo_skipLookup() {
+ Resources mockResources = mContext.getResources();
+ when(mockResources.getBoolean(R.bool.skip_incoming_caller_info_query))
+ .thenReturn(true);
+
+ createCall("1");
+
+ verify(mMockCallerInfoLookupHelper, never()).startLookup(any(), any());
+ }
+
+ @Test
+ @SmallTest
public void testOriginalCallIntent() {
Call call = createCall("1");
@@ -948,6 +1029,18 @@
assertTrue(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
}
+ @Test
+ @SmallTest
+ public void testSkipLoadingCannedTextResponse() {
+ Call call = createCall("any");
+ Resources mockResources = mContext.getResources();
+ when(mockResources.getBoolean(R.bool.skip_loading_canned_text_response))
+ .thenReturn(true);
+
+
+ assertFalse(call.isRespondViaSmsCapable());
+ }
+
private Call createCall(String id) {
return createCall(id, Call.CALL_DIRECTION_UNDEFINED);
}
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index e7f2d83..6f5b4e7 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -3610,8 +3610,6 @@
.setShouldAllowCall(true)
.setShouldReject(false)
.build();
- when(mInCallController.bindToBTService(eq(call))).thenReturn(
- CompletableFuture.completedFuture(true));
when(mInCallController.isBoundAndConnectedToBTService(any(UserHandle.class)))
.thenReturn(false);
@@ -3619,7 +3617,7 @@
InOrder inOrder = inOrder(mInCallController, call, mInCallController);
- inOrder.verify(mInCallController).bindToBTService(eq(call));
+ inOrder.verify(mInCallController).bindToBTService(eq(call), eq(null));
inOrder.verify(call).setState(eq(CallState.RINGING), anyString());
}
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 79b4cc8..25f94c6 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -122,6 +122,7 @@
*/
public class ComponentContextFixture implements TestFixture<Context> {
private HandlerThread mHandlerThread;
+ private Map<UserHandle, Context> mContextsByUser = new HashMap<>();
public class FakeApplicationContext extends MockContext {
@Override
@@ -138,6 +139,9 @@
@Override
public Context createContextAsUser(UserHandle userHandle, int flags) {
+ if (mContextsByUser.containsKey(userHandle)) {
+ return mContextsByUser.get(userHandle);
+ }
return this;
}
@@ -871,6 +875,15 @@
return mTelephonyRegistryManager;
}
+ /**
+ * For testing purposes, add a context for a specific user.
+ * @param userHandle the userhandle
+ * @param context the context
+ */
+ public void addContextForUser(UserHandle userHandle, Context context) {
+ mContextsByUser.put(userHandle, context);
+ }
+
private void addService(String action, ComponentName name, IInterface service) {
mComponentNamesByAction.put(action, name);
mServiceByComponentName.put(name, service);
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 6c07c79..6af31ae 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -225,7 +225,7 @@
when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYS_PKG);
when(mDefaultDialerCache.getSystemDialerComponent()).thenReturn(
new ComponentName(SYS_PKG, SYS_CLASS));
- when(mDefaultDialerCache.getBTInCallServicePackage()).thenReturn(BT_PKG);
+ when(mDefaultDialerCache.getBTInCallServicePackages()).thenReturn(new String[] {BT_PKG});
mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, mDefaultDialerCache,
mTimeoutsAdapter);
when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
@@ -409,7 +409,7 @@
.thenReturn(300_000L);
setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext).bindServiceAsUser(
@@ -444,7 +444,7 @@
Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext).bindServiceAsUser(
@@ -483,7 +483,7 @@
anyInt(), eq(mUserHandle))).thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -546,7 +546,7 @@
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -608,7 +608,7 @@
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
@@ -639,7 +639,7 @@
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
@@ -672,7 +672,7 @@
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
@@ -701,7 +701,7 @@
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
@@ -751,7 +751,7 @@
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -833,7 +833,7 @@
.thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -914,7 +914,7 @@
when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -961,7 +961,7 @@
mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
// Now bind; we should only bind to one app.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1044,7 +1044,7 @@
.thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
mInCallController.addCall(mMockCall);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// There will be 4 calls for the various types of ICS.
verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
@@ -1213,7 +1213,7 @@
public void testBindToService_IncludeExternal() throws Exception {
setupMocks(true /* isExternalCall */);
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1264,7 +1264,7 @@
when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall));
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -1313,7 +1313,7 @@
mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
// Now bind; we should only bind to one app.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1332,7 +1332,7 @@
public void testNoBindToInvalidService_CarModeUI() throws Exception {
setupMocks(true /* isExternalCall */);
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
when(mMockPackageManager.checkPermission(
matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
@@ -1384,7 +1384,7 @@
anyInt(), any(AttributionSource.class), nullable(String.class)));
// Now bind; we should bind to the system dialer and app op non ui app.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1428,7 +1428,7 @@
when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(null);
// we should bind to only the non ui app.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1461,7 +1461,7 @@
matches(DEF_PKG))).thenReturn(PackageManager.PERMISSION_DENIED);
when(mMockCall.getName()).thenReturn("evil");
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1498,7 +1498,7 @@
setupMocks(true /* isExternalCall */);
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
// Bind to default dialer.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Uninstall an unrelated app.
mSystemStateListener.onPackageUninstalled("com.joe.stuff");
@@ -1522,7 +1522,7 @@
setupMocks(true /* isExternalCall */);
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
// Bind to default dialer.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Enable car mode and enter car mode at default priority.
when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
@@ -1590,7 +1590,7 @@
setupMockPackageManager(true /* default */, true /* nonui */, false /* appop_nonui */ ,
true /* system */, false /* external calls */,
false /* self mgd in default*/, false /* self mgd in car*/);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -1659,7 +1659,7 @@
// Bind; we should not bind to anything right now; the dialer does not support self
// managed calls.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices; make sure no binding took place. InCallController handles not
// binding initially, but the rebind (see next test case) will always happen.
@@ -1698,7 +1698,7 @@
// Bind; we should not bind to anything right now; the dialer does not support self
// managed calls.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices; make sure no binding took place.
verify(mMockContext, never()).bindServiceAsUser(
@@ -1800,7 +1800,7 @@
assertFalse(mUserHandle.equals(UserHandle.USER_CURRENT));
when(mMockCurrentUserManager.isManagedProfile()).thenReturn(false);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallService on UserHandle.CURRENT and not the user from the call (mUserHandle)
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1821,7 +1821,7 @@
when(mMockCall.getAssociatedUser()).thenReturn(testUser);
// Bind to ICS. The mapping should've been inserted with the testUser as the key.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
// Set the target phone account. Simulates the flow when the user has chosen which sim to
@@ -1849,7 +1849,7 @@
when(mMockCall.isIncoming()).thenReturn(true);
// Bind to ICS. The mapping should've been inserted with the testUser as the key.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
// Remove the call. This invokes getUserFromCall to remove the ICS mapping.
@@ -1951,7 +1951,7 @@
true /*includeSelfManagedCallsInNonUi*/);
//pass in call by child/profile user
- mInCallController.bindToServices(mMockChildUserCall, false);
+ mInCallController.bindToServices(mMockChildUserCall);
// Verify that queryIntentServicesAsUser is also called with parent handle
// Query for the different InCallServices
ArgumentCaptor<Integer> userIdCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1974,7 +1974,7 @@
setupMocks(false /* isExternalCall */);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
Intent expectedIntent = new Intent(InCallService.SERVICE_INTERFACE);
- expectedIntent.setPackage(mDefaultDialerCache.getBTInCallServicePackage());
+ expectedIntent.setPackage(mDefaultDialerCache.getBTInCallServicePackages()[0]);
LinkedList<ResolveInfo> resolveInfo = new LinkedList<ResolveInfo>();
resolveInfo.add(getBluetoothResolveinfo());
when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(true);
@@ -1990,7 +1990,7 @@
}).when(mMockPackageManager).queryIntentServicesAsUser(any(Intent.class), anyInt(),
anyInt());
- mInCallController.bindToBTService(mMockCall);
+ mInCallController.bindToBTService(mMockCall, null);
ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext).bindServiceAsUser(captor.capture(), any(ServiceConnection.class),
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index d1dd20c..dc5f325 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -1167,7 +1167,7 @@
verify(mFakePhoneAccountRegistrar).getPhoneAccount(
TEL_PA_HANDLE_16, TEL_PA_HANDLE_16.getUserHandle());
- verify(mInCallController, never()).bindToServices(any(), anyBoolean());
+ verify(mInCallController, never()).bindToServices(any());
addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL,
CallIntentProcessor.KEY_IS_INCOMING_CALL, extras,
TEL_PA_HANDLE_16, false);
@@ -1189,7 +1189,7 @@
mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
- verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
+ verify(mInCallController, never()).bindToServices(eq(null));
}
@SmallTest
@@ -1207,7 +1207,7 @@
mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
- verify(mInCallController).bindToServices(eq(null), anyBoolean());
+ verify(mInCallController).bindToServices(eq(null));
}
@SmallTest
@@ -1225,7 +1225,7 @@
mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
- verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
+ verify(mInCallController, never()).bindToServices(eq(null));
}
@SmallTest
@@ -1244,7 +1244,7 @@
mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
- verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
+ verify(mInCallController, never()).bindToServices(eq(null));
}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index a8663d6..4463d65 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -516,6 +516,7 @@
when(mRoleManagerAdapter.getCallCompanionApps()).thenReturn(Collections.emptyList());
when(mRoleManagerAdapter.getDefaultCallScreeningApp(any(UserHandle.class)))
.thenReturn(null);
+ when(mRoleManagerAdapter.getBTInCallService()).thenReturn(new String[] {"bt_pkg"});
when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(false);
mTelecomSystem = new TelecomSystem(
mComponentContextFixture.getTestDouble(),
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index b5a0c26..707ed9f 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -16,6 +16,9 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -37,6 +40,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.OutcomeReceiver;
@@ -44,6 +48,8 @@
import android.telecom.CallAttributes;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
import androidx.test.filters.SmallTest;
@@ -65,6 +71,7 @@
import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
import com.android.server.telecom.voip.TransactionManager;
import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.voip.VideoStateTranslation;
import com.android.server.telecom.voip.VoipCallTransactionResult;
import org.junit.After;
@@ -108,6 +115,7 @@
super.setUp();
MockitoAnnotations.initMocks(this);
Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+ Mockito.when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
}
@Override
@@ -227,7 +235,8 @@
CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
OutgoingCallTransaction transaction =
- new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager);
+ new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager,
+ mFeatureFlags);
// WHEN
when(mMockContext.getOpPackageName()).thenReturn("testPackage");
@@ -254,7 +263,8 @@
CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI).build();
IncomingCallTransaction transaction =
- new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager);
+ new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager,
+ mFeatureFlags);
// WHEN
when(mCallsManager.isIncomingCallPermitted(callAttributes.getPhoneAccountHandle()))
@@ -269,6 +279,100 @@
}
/**
+ * Verify that transactional OUTGOING calls are re-mapping the CallAttributes video state to
+ * VideoProfile states when starting the call via CallsManager#startOugoingCall.
+ */
+ @Test
+ public void testOutgoingCallTransactionRemapsVideoState() {
+ // GIVEN
+ CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .build();
+
+ CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.VIDEO_CALL)
+ .build();
+
+ OutgoingCallTransaction t = new OutgoingCallTransaction(null,
+ mContext, null, mCallsManager, new Bundle(), mFeatureFlags);
+
+ // WHEN
+ when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+ t.setFeatureFlags(mFeatureFlags);
+
+ // THEN
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+ .generateExtras(audioOnlyAttributes)
+ .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+ .generateExtras(videoAttributes)
+ .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+ }
+
+ /**
+ * Verify that transactional INCOMING calls are re-mapping the CallAttributes video state to
+ * VideoProfile states when starting the call in CallsManager#processIncomingCallIntent.
+ */
+ @Test
+ public void testIncomingCallTransactionRemapsVideoState() {
+ // GIVEN
+ CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .build();
+
+ CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.VIDEO_CALL)
+ .build();
+
+ IncomingCallTransaction t = new IncomingCallTransaction(null, null,
+ mCallsManager, new Bundle(), mFeatureFlags);
+
+ // WHEN
+ when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+ t.setFeatureFlags(mFeatureFlags);
+
+ // THEN
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+ .generateExtras(audioOnlyAttributes)
+ .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+ .generateExtras(videoAttributes)
+ .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+ }
+
+ @Test
+ public void testTransactionalVideoStateToVideoProfileState() {
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+ TransactionalVideoStateToVideoProfileState(CallAttributes.AUDIO_CALL));
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL,
+ TransactionalVideoStateToVideoProfileState(CallAttributes.VIDEO_CALL));
+ // ensure non-defined values default to audio
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+ TransactionalVideoStateToVideoProfileState(-1));
+ }
+
+ @Test
+ public void testVideoProfileStateToTransactionalVideoState() {
+ assertEquals(CallAttributes.AUDIO_CALL,
+ VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_AUDIO_ONLY));
+ assertEquals(CallAttributes.VIDEO_CALL,
+ VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_RX_ENABLED));
+ assertEquals(CallAttributes.VIDEO_CALL,
+ VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_TX_ENABLED));
+ assertEquals(CallAttributes.VIDEO_CALL,
+ VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_BIDIRECTIONAL));
+ // ensure non-defined values default to audio
+ assertEquals(CallAttributes.AUDIO_CALL,
+ VideoProfileStateToTransactionalVideoState(-1));
+ }
+
+ /**
* This test verifies if the ConnectionService call is NOT transitioned to the desired call
* state (within timeout period), Telecom will disconnect the call.
*/
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index 30cfc2e..c5be130 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -21,7 +21,6 @@
import android.os.OutcomeReceiver;
import android.telecom.CallException;
-import android.util.Log;
import androidx.test.filters.SmallTest;
@@ -86,12 +85,12 @@
} else if (mType == FAILED) {
mLog.append(mName).append(" failed;\n");
resultFuture.complete(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+ new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
null));
} else {
mLog.append(mName).append(" timeout;\n");
resultFuture.complete(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+ new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
"timeout"));
}
}, mSleepTime);
@@ -303,6 +302,40 @@
verifyTransactionsFinished(t1, t2);
}
+ /**
+ * This test verifies that if a transaction encounters an exception while processing it,
+ * the exception finishes the transaction immediately instead of waiting for the timeout.
+ */
+ @SmallTest
+ @Test
+ public void testTransactionHitsException()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ // GIVEN - a transaction that throws an exception when processing
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction(
+ "t1",
+ 100L,
+ TestVoipCallTransaction.EXCEPTION);
+ // verify the TransactionManager informs the client of the failed transaction
+ CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
+ OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ }
+
+ @Override
+ public void onError(CallException e) {
+ exceptionFuture.complete(e.getMessage());
+ }
+ };
+ // WHEN - add and process the transaction
+ mTransactionManager.addTransaction(t1, outcomeExceptionReceiver);
+ exceptionFuture.get(200L, TimeUnit.MILLISECONDS);
+ // THEN - assert the transaction finished and failed
+ assertTrue(mLog.toString().contains("t1 exception;\n"));
+ verifyTransactionsFinished(t1);
+ }
+
@SmallTest
@Test
public void testTransactionResultException()