Merge "Ensure calls from profile log under initiating user." 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..6cca8f9 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -80,3 +80,11 @@
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"
+}
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..9375798 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -82,4 +82,10 @@
<!-- 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>
</resources>
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 29eb419..7e5f6de 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;
@@ -1589,7 +1590,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);
}
@@ -3726,6 +3731,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;
}
@@ -4105,14 +4115,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
@@ -4129,17 +4133,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)) {
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/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 621ba36..04dae5f 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -847,6 +847,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 {
@@ -977,9 +985,6 @@
public void enter() {
super.enter();
mHasUserExplicitlyLeftBluetooth = false;
- if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
- setMuteOn(false);
- }
updateInternalCallAudioState();
}
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/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 c3eb3b8..defc9bb 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -5025,6 +5025,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 f6f4889..96305dd 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -1987,6 +1987,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/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index a57360b..230bb09 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. "
@@ -1854,11 +1854,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 7fba62a..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)
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 50476fe..f220648 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -116,15 +116,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 +138,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");
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/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/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/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/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()