Merge "Propagate associated user into call" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 09ebfe2..1355343 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -47,11 +47,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
- }
- ],
- "presubmit-large": [
+ },
{
- "name": "CtsTelecomTestCases",
+ "name": "CtsTelecomCujTestCases",
"options": [
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
@@ -59,9 +57,9 @@
]
}
],
- "postsubmit": [
+ "presubmit-large": [
{
- "name": "CtsTelecomCujTestCases",
+ "name": "CtsTelecomTestCases",
"options": [
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 411449c..8cd5266 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -2651,7 +2651,7 @@
return;
}
mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
- phoneAccountRegistrar, mContext, mFlags, new Timeouts.Adapter());
+ phoneAccountRegistrar, mCallsManager, mContext, mFlags, new Timeouts.Adapter());
mCreateConnectionProcessor.process();
}
diff --git a/src/com/android/server/telecom/CallAudioWatchdog.java b/src/com/android/server/telecom/CallAudioWatchdog.java
index dcfc80f..4ca237a 100644
--- a/src/com/android/server/telecom/CallAudioWatchdog.java
+++ b/src/com/android/server/telecom/CallAudioWatchdog.java
@@ -28,6 +28,7 @@
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Handler;
+import android.os.Process;
import android.telecom.Log;
import android.telecom.Logging.EventManager;
import android.telecom.PhoneAccountHandle;
@@ -288,6 +289,11 @@
&& config.getAudioAttributes().getUsage()
== AudioAttributes.USAGE_VOICE_COMMUNICATION) {
+ // Skip if the client's pid is same as myself
+ if (config.getClientPid() == Process.myPid()) {
+ continue;
+ }
+
// If an audio session is idle, we don't count it as playing. It must be in a
// started state.
boolean isPlaying = config.getPlayerState() == PLAYER_STATE_STARTED;
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index a2c742d..c2b5da1 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -37,12 +37,13 @@
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -127,6 +128,21 @@
}
};
+ /**
+ * Call states which should be prioritized when sorting phone accounts. The ordering is
+ * intentional and should NOT be modified. Other call states will not have any priority.
+ */
+ private static final int[] PRIORITY_CALL_STATES = new int []
+ {CallState.ACTIVE, CallState.ON_HOLD, CallState.DIALING, CallState.RINGING};
+ private static final int DEFAULT_CALL_STATE_PRIORITY = PRIORITY_CALL_STATES.length;
+ private static final Map<Integer, Integer> mCallStatePriorityMap = new HashMap<>();
+ static {
+ for (int i = 0; i < PRIORITY_CALL_STATES.length; i++) {
+ mCallStatePriorityMap.put(PRIORITY_CALL_STATES[i], i);
+ }
+ }
+
+
private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapterImpl();
private final Call mCall;
@@ -136,6 +152,7 @@
private CreateConnectionResponse mCallResponse;
private DisconnectCause mLastErrorDisconnectCause;
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+ private final CallsManager mCallsManager;
private final Context mContext;
private final FeatureFlags mFlags;
private final Timeouts.Adapter mTimeoutsAdapter;
@@ -148,6 +165,7 @@
ConnectionServiceRepository repository,
CreateConnectionResponse response,
PhoneAccountRegistrar phoneAccountRegistrar,
+ CallsManager callsManager,
Context context,
FeatureFlags featureFlags,
Timeouts.Adapter timeoutsAdapter) {
@@ -156,6 +174,7 @@
mRepository = repository;
mCallResponse = response;
mPhoneAccountRegistrar = phoneAccountRegistrar;
+ mCallsManager = callsManager;
mContext = context;
mConnectionAttempt = 0;
mFlags = featureFlags;
@@ -693,6 +712,23 @@
return retval;
}
+ // Sort accounts by ongoing call states
+ Set<Integer> callStatesAccount1 = mCallsManager.getCalls().stream()
+ .filter(c -> Objects.equals(account1.getAccountHandle(),
+ c.getTargetPhoneAccount()))
+ .map(Call::getState).collect(Collectors.toSet());
+ Set<Integer> callStatesAccount2 = mCallsManager.getCalls().stream()
+ .filter(c -> Objects.equals(account2.getAccountHandle(),
+ c.getTargetPhoneAccount()))
+ .map(Call::getState).collect(Collectors.toSet());
+ int account1Priority = computeCallStatePriority(callStatesAccount1);
+ int account2Priority = computeCallStatePriority(callStatesAccount2);
+ Log.d(this, "account1: %s, call state priority: %s", account1, account1Priority);
+ Log.d(this, "account2: %s, call state priority: %s", account2, account2Priority);
+ if (account1Priority != account2Priority) {
+ return account1Priority < account2Priority ? -1 : 1;
+ }
+
// Prefer the user's choice if all PhoneAccounts are associated with valid logical
// slots.
if (userPreferredAccount != null) {
@@ -731,6 +767,25 @@
});
}
+ /**
+ * Computes the call state priority based on the passed in call states associated with the
+ * calls present on the phone account. The lower the value, the higher the priority (i.e.
+ * ACTIVE (0) < HOLDING (1) < DIALING (2) < RINGING (3) equates to ACTIVE holding the highest
+ * priority).
+ */
+ private int computeCallStatePriority(Set<Integer> callStates) {
+ int priority = DEFAULT_CALL_STATE_PRIORITY;
+ for (int state: callStates) {
+ if (priority == mCallStatePriorityMap.get(CallState.ACTIVE)) {
+ return priority;
+ } else if (mCallStatePriorityMap.containsKey(state)
+ && priority > mCallStatePriorityMap.get(state)) {
+ priority = mCallStatePriorityMap.get(state);
+ }
+ }
+ return priority;
+ }
+
private static String nullToEmpty(String str) {
return str == null ? "" : str;
}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index cc7f6ab..c59cf2c 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -181,7 +181,7 @@
private final TelecomSystem.SyncRoot mLock;
private State mState;
private UserHandle mCurrentUserHandle;
- private String mTestPhoneAccountPackageNameFilter;
+ private final Set<String> mTestPhoneAccountPackageNameFilters;
private interface PhoneAccountRegistrarWriteLock {}
private final PhoneAccountRegistrarWriteLock mWriteLock =
new PhoneAccountRegistrarWriteLock() {};
@@ -215,6 +215,7 @@
mAppLabelProxy = appLabelProxy;
mCurrentUserHandle = Process.myUserHandle();
mTelecomFeatureFlags = telecomFeatureFlags;
+ mTestPhoneAccountPackageNameFilters = new HashSet<>();
if (telephonyFeatureFlags != null) {
mTelephonyFeatureFlags = telephonyFeatureFlags;
@@ -607,23 +608,33 @@
* {@link PhoneAccount}s with the same package name.
*/
public void setTestPhoneAccountPackageNameFilter(String packageNameFilter) {
- mTestPhoneAccountPackageNameFilter = packageNameFilter;
- Log.i(this, "filter set for PhoneAccounts, packageName=" + packageNameFilter);
+ mTestPhoneAccountPackageNameFilters.clear();
+ if (packageNameFilter == null) {
+ return;
+ }
+ String [] pkgNamesFilter = packageNameFilter.split(",");
+ mTestPhoneAccountPackageNameFilters.addAll(Arrays.asList(pkgNamesFilter));
+ StringBuilder pkgNames = new StringBuilder();
+ for (int i = 0; i < pkgNamesFilter.length; i++) {
+ pkgNames.append(pkgNamesFilter[i])
+ .append(i != pkgNamesFilter.length - 1 ? ", " : ".");
+ }
+ Log.i(this, "filter set for PhoneAccounts, packageNames: %s", pkgNames.toString());
}
/**
* Filter the given {@link List<PhoneAccount>} and keep only {@link PhoneAccount}s that have the
- * #mTestPhoneAccountPackageNameFilter.
+ * #mTestPhoneAccountPackageNameFilters.
* @param accounts List of {@link PhoneAccount}s to filter.
* @return new list of filtered {@link PhoneAccount}s.
*/
public List<PhoneAccount> filterRestrictedPhoneAccounts(List<PhoneAccount> accounts) {
- if (TextUtils.isEmpty(mTestPhoneAccountPackageNameFilter)) {
+ if (mTestPhoneAccountPackageNameFilters.isEmpty()) {
return new ArrayList<>(accounts);
}
- // Remove all PhoneAccounts that do not have the same package name as the filter.
- return accounts.stream().filter(account -> mTestPhoneAccountPackageNameFilter.equals(
- account.getAccountHandle().getComponentName().getPackageName()))
+ // Remove all PhoneAccounts that do not have the same package name (prefix) as the filter.
+ return accounts.stream().filter(account -> mTestPhoneAccountPackageNameFilters
+ .contains(account.getAccountHandle().getComponentName().getPackageName()))
.collect(Collectors.toList());
}
@@ -1977,7 +1988,7 @@
}
pw.decreaseIndent();
pw.increaseIndent();
- pw.println("test emergency PhoneAccount filter: " + mTestPhoneAccountPackageNameFilter);
+ pw.println("test emergency PhoneAccount filter: " + mTestPhoneAccountPackageNameFilters);
pw.decreaseIndent();
}
}
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index bfaadf0..d0fd201 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -31,6 +31,7 @@
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.Ringtone;
+import android.media.RingtoneManager;
import android.media.Utils;
import android.media.VolumeShaper;
import android.media.audio.Flags;
@@ -431,6 +432,11 @@
&& isVibratorEnabled) {
Log.i(this, "Muted haptic channels since audio coupled ramping ringer is disabled");
hapticChannelsMuted = true;
+ if (useCustomVibration(foregroundCall)) {
+ Log.i(this,
+ "Not muted haptic channel for customization when apply ramping ringer");
+ hapticChannelsMuted = false;
+ }
} else if (hapticChannelsMuted) {
Log.i(this,
"Muted haptic channels isVibratorEnabled=%s, hapticPlaybackSupported=%s",
@@ -442,7 +448,7 @@
if (!isHapticOnly) {
ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
- } else if (Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported) {
+ } else if (useCustomVibration(foregroundCall)) {
ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
foregroundCall, null, false);
}
@@ -521,6 +527,21 @@
}
}
+ private boolean useCustomVibration(@NonNull Call foregroundCall) {
+ return Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported
+ && hasExplicitVibration(foregroundCall);
+ }
+
+ private boolean hasExplicitVibration(@NonNull Call foregroundCall) {
+ final Uri ringtoneUri = foregroundCall.getRingtone();
+ if (ringtoneUri != null) {
+ // TODO(b/399265235) : Avoid this hidden API access for mainline
+ return Utils.hasVibration(ringtoneUri);
+ }
+ return Utils.hasVibration(RingtoneManager.getActualDefaultRingtoneUri(
+ mContext, RingtoneManager.TYPE_RINGTONE));
+ }
+
/**
* Try to reserve the vibrator for this call, returning false if it's already committed.
* The vibration will be started by AsyncRingtonePlayer to ensure timing is aligned with the
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 9d1a382..7bb041d 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -198,6 +198,7 @@
public void addCall(CallAttributes callAttributes, ICallEventCallback callEventCallback,
String callId, String callingPackage) {
int uid = Binder.getCallingUid();
+ int pid = Binder.getCallingPid();
ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDCALL,
uid, ApiStats.RESULT_PERMISSION);
try {
@@ -217,7 +218,7 @@
// add extras about info used for FGS delegation
Bundle extras = new Bundle();
extras.putInt(CallAttributes.CALLER_UID_KEY, uid);
- extras.putInt(CallAttributes.CALLER_PID_KEY, uid);
+ extras.putInt(CallAttributes.CALLER_PID_KEY, pid);
CompletableFuture<CallTransaction> transactionFuture;
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index d6e02c1..acda1a1 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -44,6 +44,7 @@
import android.telecom.PhoneAccountHandle;
import android.telephony.AnomalyReporter;
import android.telephony.CarrierConfigManager;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.AnomalyReporterAdapter;
@@ -62,8 +63,10 @@
import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.stats.CallFailureCause;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -456,11 +459,26 @@
* made for the emergency call.
*/
private CompletableFuture<Boolean> makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
- // Always disconnect any ringing/incoming calls when an emergency call is placed to minimize
- // distraction. This does not affect live call count.
- CompletableFuture<Boolean> ringingCallFuture = null;
- Call ringingCall = null;
- if (mCallsManager.hasRingingOrSimulatedRingingCall()) {
+ // Disconnect all self-managed + transactional calls. We will never use these accounts for
+ // emergency calling. Disconnect non-holdable calls (in the dual-sim case) as well. For
+ // the single sim case (like Verizon), we should support the existing behavior of
+ // disconnecting the active call; refrain from disconnecting the held call in this case if
+ // it exists.
+ boolean areMultiplePhoneAccountsActive = areMultiplePhoneAccountsActive();
+ Pair<Set<Call>, CompletableFuture<Boolean>> disconnectCallsForEmergencyPair =
+ disconnectCallsForEmergencyCall(emergencyCall, areMultiplePhoneAccountsActive);
+ // The list of calls that were disconnected
+ Set<Call> disconnectedCalls = disconnectCallsForEmergencyPair.first;
+ // The future encompassing the result of the disconnect transaction(s). Because of the
+ // bulk transaction, we will always opt to perform sequencing on this future. Note that this
+ // future will always be completed with true if no disconnects occurred.
+ CompletableFuture<Boolean> transactionFuture = disconnectCallsForEmergencyPair.second;
+
+ Call ringingCall;
+ if (mCallsManager.hasRingingOrSimulatedRingingCall() && !disconnectedCalls
+ .contains(mCallsManager.getRingingOrSimulatedRingingCall())) {
+ // Always disconnect any ringing/incoming calls when an emergency call is placed to
+ // minimize distraction. This does not affect live call count.
ringingCall = mCallsManager.getRingingOrSimulatedRingingCall();
ringingCall.getAnalytics().setCallIsAdditional(true);
ringingCall.getAnalytics().setCallIsInterrupted(true);
@@ -469,39 +487,54 @@
// If this is an incoming call that is currently in SIMULATED_RINGING only
// after a call screen, disconnect to make room and mark as missed, since
// the user didn't get a chance to accept/reject.
- ringingCallFuture = ringingCall.disconnect("emergency call dialed during "
- + "simulated ringing after screen.");
+ transactionFuture = transactionFuture.thenComposeAsync((result) ->
+ ringingCall.disconnect("emergency call dialed during simulated "
+ + "ringing after screen."),
+ new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+ mCallsManager.getLock()));
} else {
// If this is a simulated ringing call after being active and put in
// AUDIO_PROCESSING state again, disconnect normally.
- ringingCallFuture = ringingCall.reject(false, null, "emergency call dialed "
- + "during simulated ringing.");
+ transactionFuture = transactionFuture.thenComposeAsync((result) ->
+ ringingCall.reject(false, null,
+ "emergency call dialed during simulated ringing."),
+ new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+ mCallsManager.getLock()));
}
} else { // normal incoming ringing call.
// Hang up the ringing call to make room for the emergency call and mark as missed,
// since the user did not reject.
ringingCall.setOverrideDisconnectCauseCode(
new DisconnectCause(DisconnectCause.MISSED));
- ringingCallFuture = ringingCall.reject(false, null, "emergency call dialed "
- + "during ringing.");
+ transactionFuture = transactionFuture.thenComposeAsync((result) ->
+ ringingCall.reject(false, null,
+ "emergency call dialed during ringing."),
+ new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+ mCallsManager.getLock()));
}
+ } else {
+ ringingCall = null;
}
// There is already room!
if (!mCallsManager.hasMaximumLiveCalls(emergencyCall)) {
- return CompletableFuture.completedFuture(true);
+ return transactionFuture;
}
Call liveCall = mCallsManager.getFirstCallWithLiveState();
Log.i(this, "makeRoomForOutgoingEmergencyCall: call = " + emergencyCall
+ " livecall = " + liveCall);
- if (emergencyCall == liveCall) {
- // Not likely, but a good correctness check.
- return CompletableFuture.completedFuture(true);
+ // Don't need to proceed further if we already disconnected the live call or if the live
+ // call is the emergency call being placed (not likely).
+ if (emergencyCall == liveCall || disconnectedCalls.contains(liveCall)) {
+ return transactionFuture;
}
- if (mCallsManager.hasMaximumOutgoingCalls(emergencyCall)) {
+ // If we already disconnected the outgoing call, then don't perform any additional ops on
+ // it.
+ if (mCallsManager.hasMaximumOutgoingCalls(emergencyCall) && !disconnectedCalls
+ .contains(mCallsManager.getFirstCallWithState(OUTGOING_CALL_STATES))) {
Call outgoingCall = mCallsManager.getFirstCallWithState(OUTGOING_CALL_STATES);
String disconnectReason = null;
if (!outgoingCall.isEmergencyCall()) {
@@ -520,27 +553,10 @@
+ " of new outgoing call.";
}
if (disconnectReason != null) {
- boolean isSequencingRequiredRingingAndOutgoing = !arePhoneAccountsSame(
- ringingCall, outgoingCall);
- if (ringingCallFuture != null && isSequencingRequiredRingingAndOutgoing) {
- String finalDisconnectReason = disconnectReason;
- return ringingCallFuture.thenComposeAsync((result) -> {
- if (result) {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect"
- + " ringing call succeeded. Attempting to disconnect "
- + "outgoing call.");
- return outgoingCall.disconnect(finalDisconnectReason);
- } else {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect"
- + "ringing call failed. Aborting attempt to disconnect "
- + "outgoing call");
- return CompletableFuture.completedFuture(false);
- }
- }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
- mCallsManager.getLock()));
- } else {
- return outgoingCall.disconnect(disconnectReason);
- }
+ boolean isSequencingRequiredRingingAndOutgoing = ringingCall == null
+ || !arePhoneAccountsSame(ringingCall, outgoingCall);
+ return disconnectOngoingCallForEmergencyCall(transactionFuture, outgoingCall,
+ disconnectReason, isSequencingRequiredRingingAndOutgoing);
}
// If the user tries to make two outgoing calls to different emergency call numbers,
// we will try to connect the first outgoing call and reject the second.
@@ -548,28 +564,14 @@
return CompletableFuture.completedFuture(false);
}
- boolean isSequencingRequiredRingingAndLive = ringingCall != null
- && !arePhoneAccountsSame(ringingCall, liveCall);
+ boolean isSequencingRequiredLive = ringingCall == null
+ || !arePhoneAccountsSame(ringingCall, liveCall);
if (liveCall.getState() == CallState.AUDIO_PROCESSING) {
emergencyCall.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
final String disconnectReason = "disconnecting audio processing call for emergency";
- if (ringingCallFuture != null && isSequencingRequiredRingingAndLive) {
- return ringingCallFuture.thenComposeAsync((result) -> {
- if (result) {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "ringing call succeeded. Attempting to disconnect live call.");
- return liveCall.disconnect(disconnectReason);
- } else {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "ringing call failed. Aborting attempt to disconnect live call.");
- return CompletableFuture.completedFuture(false);
- }
- }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
- mCallsManager.getLock()));
- } else {
- return liveCall.disconnect(disconnectReason);
- }
+ return disconnectOngoingCallForEmergencyCall(transactionFuture, liveCall,
+ disconnectReason, isSequencingRequiredLive);
}
// If the live call is stuck in a connecting state, prompt the user to generate a bugreport.
@@ -582,30 +584,37 @@
// we'll disconnect the ongoing call if it cannot be held. If we have a self-managed call
// that can't be held, then we should disconnect the call in favor of the emergency call.
// Likewise, if there's only one active managed call which can't be held, then it should
- // also be disconnected.
+ // also be disconnected. This will only happen for the single sim scenario to support
+ // backwards compatibility. For dual sim, we should try disconnecting the held call and
+ // hold the active call.
+ Call heldCall = null;
if (mCallsManager.hasMaximumManagedHoldingCalls(emergencyCall)
|| !mCallsManager.canHold(liveCall)) {
- emergencyCall.getAnalytics().setCallIsAdditional(true);
- liveCall.getAnalytics().setCallIsInterrupted(true);
- // Disconnect the active call instead of the holding call because it is historically
- // easier to do, rather than disconnect a held call.
final String disconnectReason = "disconnecting to make room for emergency call "
+ emergencyCall.getId();
- if (ringingCallFuture != null && isSequencingRequiredRingingAndLive) {
- return ringingCallFuture.thenComposeAsync((result) -> {
- if (result) {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "ringing call succeeded. Attempting to disconnect live call.");
- return liveCall.disconnect(disconnectReason);
- } else {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "ringing call failed. Aborting attempt to disconnect live call.");
- return CompletableFuture.completedFuture(false);
- }
- }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
- mCallsManager.getLock()));
- } else {
- return liveCall.disconnect(disconnectReason);
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ // Single sim case
+ if (!areMultiplePhoneAccountsActive) {
+ liveCall.getAnalytics().setCallIsInterrupted(true);
+ // Disconnect the active call instead of the holding call because it is historically
+ // easier to do, rather than disconnect a held call.
+ return disconnectOngoingCallForEmergencyCall(transactionFuture, liveCall,
+ disconnectReason, isSequencingRequiredLive);
+ } else { // Dual sim case
+ // If the live call can't be held, we would've already disconnected it
+ // in disconnectCallsForEmergencyCall. Note at this point, we should always have
+ // a held call then that should be disconnected (over the active call).
+ if (!mCallsManager.canHold(liveCall)) {
+ return transactionFuture;
+ }
+ heldCall = mCallsManager.getFirstCallWithState(CallState.ON_HOLD);
+ boolean isSequencingRequiredRingingAndHeld = ringingCall == null
+ || !arePhoneAccountsSame(ringingCall, heldCall);
+ isSequencingRequiredLive = !arePhoneAccountsSame(heldCall, liveCall);
+ heldCall.getAnalytics().setCallIsInterrupted(true);
+ // Disconnect the held call.
+ transactionFuture = disconnectOngoingCallForEmergencyCall(transactionFuture,
+ heldCall, disconnectReason, isSequencingRequiredRingingAndHeld);
}
}
@@ -638,24 +647,8 @@
DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
final String disconnectReason = "outgoing call does not support emergency calls, "
+ "disconnecting.";
- if (ringingCallFuture != null && isSequencingRequiredRingingAndLive) {
- return ringingCallFuture.thenComposeAsync((result) -> {
- if (result) {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "ringing call succeeded. "
- + "Attempting to disconnect live call.");
- return liveCall.disconnect(disconnectReason);
- } else {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "ringing call failed. "
- + "Aborting attempt to disconnect live call.");
- return CompletableFuture.completedFuture(false);
- }
- }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
- mCallsManager.getLock()));
- } else {
- return liveCall.disconnect(disconnectReason);
- }
+ return disconnectOngoingCallForEmergencyCall(transactionFuture, liveCall,
+ disconnectReason, isSequencingRequiredLive);
}
}
@@ -667,8 +660,8 @@
emergencyCall.getTargetPhoneAccount())) {
Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccounts are from same "
+ "package. Attempting to hold live call before placing emergency call.");
- return maybeHoldLiveCallForEmergency(ringingCallFuture,
- isSequencingRequiredRingingAndLive, liveCall, emergencyCall,
+ return maybeHoldLiveCallForEmergency(transactionFuture,
+ isSequencingRequiredLive, liveCall, emergencyCall,
shouldHoldForEmergencyCall(liveCallPhoneAccount) /* shouldHoldForEmergency */);
} else if (emergencyCall.getTargetPhoneAccount() == null) {
// Without a phone account, we can't say reliably that the call will fail.
@@ -677,20 +670,24 @@
// hold but they still support adding a call by going immediately into conference
// mode). Return true here and we'll run this code again after user chooses an
// account.
- return CompletableFuture.completedFuture(true);
+ return transactionFuture;
}
- // Hold the live call if possible before attempting the new outgoing emergency call.
- if (mCallsManager.canHold(liveCall)) {
+ // Hold the live call if possible before attempting the new outgoing emergency call. Also,
+ // ensure that we try holding if we disconnected a held call and the live call supports
+ // holding.
+ if (mCallsManager.canHold(liveCall) || (heldCall != null
+ && mCallsManager.supportsHold(liveCall))) {
Log.i(this, "makeRoomForOutgoingEmergencyCall: holding live call.");
- return maybeHoldLiveCallForEmergency(ringingCallFuture,
- isSequencingRequiredRingingAndLive, liveCall,
- emergencyCall, true /* shouldHoldForEmergency */);
+ return maybeHoldLiveCallForEmergency(transactionFuture, isSequencingRequiredLive,
+ liveCall, emergencyCall, true /* shouldHoldForEmergency */);
}
- // The live call cannot be held so we're out of luck here. There's no room.
- emergencyCall.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
- return CompletableFuture.completedFuture(false);
+ // Refrain from failing the call in Telecom if possible. Additional processing will be done
+ // in the Telephony layer to hold/disconnect calls (across subs, if needed) and we will fail
+ // there instead. This should be treated as the preprocessing steps required to set up the
+ // ability to place an emergency call.
+ return transactionFuture;
}
/**
@@ -828,25 +825,31 @@
/* makeRoomForOutgoingEmergencyCall helpers */
+ /**
+ * Tries to hold the live call before placing the emergency call. If the hold fails, then we
+ * will instead disconnect the call.
+ *
+ * Note: This only applies when the live call and emergency call are from the same phone
+ * account.
+ */
private CompletableFuture<Boolean> maybeHoldLiveCallForEmergency(
- CompletableFuture<Boolean> ringingCallFuture, boolean isSequencingRequired,
+ CompletableFuture<Boolean> transactionFuture, boolean isSequencingRequired,
Call liveCall, Call emergencyCall, boolean shouldHoldForEmergency) {
emergencyCall.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
final String holdReason = "calling " + emergencyCall.getId();
CompletableFuture<Boolean> holdResultFuture = CompletableFuture.completedFuture(false);
if (shouldHoldForEmergency) {
- if (ringingCallFuture != null && isSequencingRequired) {
- holdResultFuture = ringingCallFuture.thenComposeAsync((result) -> {
+ if (transactionFuture != null && isSequencingRequired) {
+ holdResultFuture = transactionFuture.thenComposeAsync((result) -> {
if (result) {
Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "ringing call succeeded. Attempting to hold live call.");
- return liveCall.hold(holdReason);
- } else {
+ + "previous call succeeded. Attempting to hold live call.");
+ } else { // Log the failure but proceed with hold transaction.
Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "ringing call failed. Aborting attempt to hold live call.");
- return CompletableFuture.completedFuture(false);
+ + "previous call failed. Still attempting to hold live call.");
}
+ return liveCall.hold(holdReason);
}, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
mCallsManager.getLock()));
} else {
@@ -868,6 +871,117 @@
}
/**
+ * Disconnects all VOIP + non-holdable calls as well as those that don't support placing
+ * emergency calls before placing an emergency call.
+ *
+ * Note: If a call can't be held, it will be active to begin with.
+ * @return The list of calls to be disconnected alongside the future keeping track of the
+ * disconnect transaction.
+ */
+ private Pair<Set<Call>, CompletableFuture<Boolean>> disconnectCallsForEmergencyCall(
+ Call emergencyCall, boolean areMultiplePhoneAccountsActive) {
+ Set<Call> callsDisconnected = new HashSet<>();
+ Call previousCall = null;
+ Call ringingCall = mCallsManager.getRingingOrSimulatedRingingCall();
+ CompletableFuture<Boolean> disconnectFuture = CompletableFuture.completedFuture(true);
+ for (Call call: mCallsManager.getCalls()) {
+ // Conditions for checking if call doesn't need to be disconnected immediately.
+ boolean isManaged = !call.isSelfManaged() && !call.isTransactionalCall();
+ boolean callSupportsHold = call.can(Connection.CAPABILITY_SUPPORT_HOLD);
+ boolean callSupportsHoldingEmergencyCall = shouldHoldForEmergencyCall(
+ call.getTargetPhoneAccount());
+
+ // Skip the ringing call; we'll handle the disconnect explicitly later.
+ if (call.equals(ringingCall)) {
+ continue;
+ }
+
+ // If the call is managed and supports holding + capability to place emergency calls,
+ // don't disconnect the call.
+ if (isManaged && callSupportsHoldingEmergencyCall) {
+ // If call supports hold, we can skip. Other condition we check here is if calls
+ // are on single sim, in which case we will refrain from disconnecting a potentially
+ // held call (i.e. Verizon ACTIVE + HOLD case) here and let that be determined later
+ // down in makeRoomForOutgoingEmergencyCall.
+ if (callSupportsHold || (!areMultiplePhoneAccountsActive)) {
+ continue;
+ }
+ }
+
+ Log.i(this, "Disconnecting call (%s). isManaged: %b, call supports hold: %b, call "
+ + "supports holding emergency call: %b", call.getId(), isManaged,
+ callSupportsHold, callSupportsHoldingEmergencyCall);
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ call.getAnalytics().setCallIsInterrupted(true);
+ call.setOverrideDisconnectCauseCode(new DisconnectCause(
+ DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
+
+ Call finalPreviousCall = previousCall;
+ disconnectFuture = disconnectFuture.thenComposeAsync((result) -> {
+ if (!result) {
+ // Log the failure if it happens but proceed with the disconnects.
+ Log.i(this, "Call (%s) failed to be disconnected",
+ finalPreviousCall);
+ }
+ return call.disconnect("Disconnecting call with phone account that does not "
+ + "support emergency call");
+ }, new LoggedHandlerExecutor(mHandler, "CSC.dAVC",
+ mCallsManager.getLock()));
+ previousCall = call;
+ callsDisconnected.add(call);
+ }
+ return new Pair<>(callsDisconnected, disconnectFuture);
+ }
+
+ /**
+ * Waiting on the passed future completion when sequencing is required, this will try to the
+ * disconnect the call passed in.
+ */
+ private CompletableFuture<Boolean> disconnectOngoingCallForEmergencyCall(
+ CompletableFuture<Boolean> transactionFuture, Call callToDisconnect,
+ String disconnectReason, boolean isSequencingRequired) {
+ if (isSequencingRequired) {
+ return transactionFuture.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "previous call succeeded. Attempting to disconnect ongoing call"
+ + " %s.", callToDisconnect);
+ } else {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+ + "previous call failed. Still attempting to disconnect ongoing call"
+ + " %s.", callToDisconnect);
+ }
+ return callToDisconnect.disconnect(disconnectReason);
+ }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+ mCallsManager.getLock()));
+ } else {
+ return callToDisconnect.disconnect(disconnectReason);
+ }
+ }
+
+ /**
+ * Determines if DSDA is being used (i.e. calls present on more than one phone account).
+ */
+ private boolean areMultiplePhoneAccountsActive() {
+ List<Call> calls = mCallsManager.getCalls().stream().toList();
+ PhoneAccountHandle handle1 = null;
+ if (!calls.isEmpty()) {
+ // Find the first handle different from the one retrieved from the first call in
+ // the list.
+ for(int i = 0; i < calls.size(); i++) {
+ if (handle1 == null && calls.get(i).getTargetPhoneAccount() != null) {
+ handle1 = calls.getFirst().getTargetPhoneAccount();
+ }
+ if (handle1 != null && calls.get(i).getTargetPhoneAccount() != null
+ && !handle1.equals(calls.get(i).getTargetPhoneAccount())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
* Checks the carrier config to see if the carrier supports holding emergency calls.
* @param handle The {@code PhoneAccountHandle} to check
* @return {@code true} if the carrier supports holding emergency calls, {@code} false
diff --git a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
index 4adc8d0..37bc065 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
@@ -260,9 +260,7 @@
}
private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
- if (cause.getCode() != DisconnectCause.REJECTED) {
- mCallsManager.markCallAsDisconnected(call, cause);
- }
+ mCallsManager.markCallAsDisconnected(call, cause);
mCallsManager.removeCall(call);
}
diff --git a/tests/src/com/android/server/telecom/tests/CallSequencingTests.java b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
index 9cfc95c..2f511b8 100644
--- a/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
@@ -516,6 +516,25 @@
assertTrue(waitForFutureResult(future, false));
}
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingEmergencyCall_DoesNotSupportHoldingEmergency() {
+ setupMakeRoomForOutgoingEmergencyCallMocks();
+ when(mCallsManager.getCalls()).thenReturn(List.of(mActiveCall, mRingingCall));
+ when(mActiveCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+ // Set the KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL carrier config to false for the active
+ // call's phone account.
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putBoolean(CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, false);
+ when(mCallsManager.getCarrierConfigForPhoneAccount(eq(mHandle1))).thenReturn(bundle);
+ when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+ when(mRingingCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+
+ mController.makeRoomForOutgoingCall(true, mNewCall);
+ // Verify that the active call got disconnected as it doesn't support holding for emergency.
+ verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).disconnect(anyString());
+ }
+
@Test
@SmallTest
public void testMakeRoomForOutgoingCall() {
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index 475133c..406bc8a 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -51,6 +51,7 @@
import com.android.server.telecom.Call;
import com.android.server.telecom.CallIdMapper;
import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
import com.android.server.telecom.ConnectionServiceFocusManager;
import com.android.server.telecom.ConnectionServiceRepository;
import com.android.server.telecom.ConnectionServiceWrapper;
@@ -97,6 +98,8 @@
@Mock
PhoneAccountRegistrar mMockAccountRegistrar;
@Mock
+ CallsManager mCallsManager;
+ @Mock
CreateConnectionResponse mMockCreateConnectionResponse;
@Mock
Call mMockCall;
@@ -136,7 +139,7 @@
mTestCreateConnectionProcessor = new CreateConnectionProcessor(mMockCall,
mMockConnectionServiceRepository, mMockCreateConnectionResponse,
- mMockAccountRegistrar, mContext, mFeatureFlags, mTimeoutsAdapter);
+ mMockAccountRegistrar, mCallsManager, mContext, mFeatureFlags, mTimeoutsAdapter);
mAccountToSub = new HashMap<>();
phoneAccounts = new ArrayList<>();
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index ad62643..d9bf6e1 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -22,6 +22,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -46,6 +47,7 @@
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.Ringtone;
+import android.media.RingtoneManager;
import android.media.VolumeShaper;
import android.media.audio.Flags;
import android.net.Uri;
@@ -65,6 +67,7 @@
import android.telecom.TelecomManager;
import android.util.Pair;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -85,6 +88,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
@@ -846,8 +850,12 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION)
public void testNoVibrateForSilentRingtoneIfRingtoneHasVibration() throws Exception {
+ final Context context = ApplicationProvider.getApplicationContext();
+ Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context,
+ RingtoneManager.TYPE_RINGTONE);
+ assumeNotNull(defaultRingtoneUri);
Uri FAKE_RINGTONE_VIBRATION_URI =
- FAKE_RINGTONE_URI.buildUpon().appendQueryParameter(
+ defaultRingtoneUri.buildUpon().appendQueryParameter(
VIBRATION_PARAM, FAKE_VIBRATION_URI.toString()).build();
Ringtone mockRingtone = mock(Ringtone.class);
Pair<Uri, Ringtone> ringtoneInfo = new Pair(FAKE_RINGTONE_VIBRATION_URI, mockRingtone);
@@ -856,21 +864,61 @@
.thenReturn(ringtoneInfo);
mComponentContextFixture.putBooleanResource(
com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported, true);
- createRingerUnderTest(); // Needed after mock the config.
+ try {
+ RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+ FAKE_RINGTONE_VIBRATION_URI);
+ createRingerUnderTest(); // Needed after mock the config.
- mRingerUnderTest.startCallWaiting(mockCall1);
- when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
- when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
- enableVibrationWhenRinging();
- assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+ mRingerUnderTest.startCallWaiting(mockCall1);
+ when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+ when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
+ enableVibrationWhenRinging();
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
- verify(mockRingtoneFactory, atLeastOnce())
- .getRingtone(any(Call.class), eq(null), eq(false));
- verifyNoMoreInteractions(mockRingtoneFactory);
- verify(mockTonePlayer).stopTone();
- // Skip vibration play in Ringer if a vibration was specified to the ringtone
- verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
- any(VibrationAttributes.class));
+ verify(mockRingtoneFactory, atLeastOnce())
+ .getRingtone(any(Call.class), eq(null), eq(false));
+ verifyNoMoreInteractions(mockRingtoneFactory);
+ verify(mockTonePlayer).stopTone();
+ // Skip vibration play in Ringer if a vibration was specified to the ringtone
+ verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
+ any(VibrationAttributes.class));
+ } finally {
+ // Restore the default ringtone Uri
+ RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+ defaultRingtoneUri);
+ }
+ }
+
+ @SmallTest
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION)
+ public void testNotMuteHapticChannelWithRampingRinger() throws Exception {
+ final Context context = ApplicationProvider.getApplicationContext();
+ Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context,
+ RingtoneManager.TYPE_RINGTONE);
+ assumeNotNull(defaultRingtoneUri);
+ Uri FAKE_RINGTONE_VIBRATION_URI = defaultRingtoneUri.buildUpon().appendQueryParameter(
+ VIBRATION_PARAM, FAKE_VIBRATION_URI.toString()).build();
+ mComponentContextFixture.putBooleanResource(
+ com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported, true);
+ ArgumentCaptor<Boolean> muteHapticChannelCaptor = ArgumentCaptor.forClass(Boolean.class);
+ try {
+ RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+ FAKE_RINGTONE_VIBRATION_URI);
+ createRingerUnderTest(); // Needed after mock the config.
+ mRingerUnderTest.startCallWaiting(mockCall1);
+ ensureRingerIsAudible();
+ enableRampingRinger();
+ enableVibrationWhenRinging();
+ assertTrue(startRingingAndWaitForAsync(mockCall2, false));
+ verify(mockRingtoneFactory, atLeastOnce()).getRingtone(any(Call.class),
+ nullable(VolumeShaper.Configuration.class), muteHapticChannelCaptor.capture());
+ assertFalse(muteHapticChannelCaptor.getValue());
+ } finally {
+ // Restore the default ringtone Uri
+ RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+ defaultRingtoneUri);
+ }
}
/**
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalCallSequencingAdapterTest.java b/tests/src/com/android/server/telecom/tests/TransactionalCallSequencingAdapterTest.java
new file mode 100644
index 0000000..6449ea7
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TransactionalCallSequencingAdapterTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2025 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.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.OutcomeReceiver;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.TransactionalCallSequencingAdapter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+
+/**
+ * Unit tests for {@link TransactionalCallSequencingAdapter}.
+ *
+ * These tests verify the behavior of the TransactionalCallSequencingAdapter, focusing on
+ * how it interacts with the TransactionManager and CallsManager, particularly in the
+ * context of asynchronous operations and feature flag configurations (e.g., setting
+ * rejected calls to a disconnected state).
+ */
+public class TransactionalCallSequencingAdapterTest extends TelecomTestCase {
+
+ private static final String CALL_ID_1 = "1";
+ private static final DisconnectCause REJECTED_DISCONNECT_CAUSE =
+ new DisconnectCause(DisconnectCause.REJECTED);
+
+ @Mock private Call mMockCall1;
+ @Mock private Context mMockContext;
+ @Mock private CallsManager mCallsManager;
+ @Mock private TransactionManager mTransactionManager;
+
+ private TransactionalCallSequencingAdapter mAdapter;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+ when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+ mAdapter = new TransactionalCallSequencingAdapter(
+ mTransactionManager, mCallsManager, true);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Tests the scenario where an incoming call is rejected and the onSetDisconnect is called.
+ * Verifies that {@link CallsManager#markCallAsDisconnected} *is* called and that the
+ * {@link OutcomeReceiver} receives the correct result, handling the asynchronous nature of
+ * the operation.
+ */
+ @Test
+ public void testOnSetDisconnected() {
+ // GIVEN -a new incoming call that is rejected
+
+ // Create a CompletableFuture to control the asynchronous operation.
+ CompletableFuture<Boolean> future = new CompletableFuture<>();
+
+ // Mock the TransactionManager's addTransaction method.
+ setupAddTransactionMock(future);
+
+ // Create a mock OutcomeReceiver to verify interactions.
+ OutcomeReceiver<CallTransactionResult, CallException> resultReceiver =
+ mock(OutcomeReceiver.class);
+
+ // WHEN - Call onSetDisconnected and get the result future.
+ mAdapter.onSetDisconnected(
+ mMockCall1,
+ REJECTED_DISCONNECT_CAUSE,
+ mock(CallTransaction.class),
+ resultReceiver);
+
+ // Simulate the asynchronous operation completing.
+ completeAddTransactionSuccessfully(future);
+
+ // THEN - Verify that markCallAsDisconnected and the receiver's onResult were called.
+ verifyMarkCallAsDisconnectedAndReceiverResult(resultReceiver);
+ }
+ /**
+ * Sets up the mock behavior for {@link TransactionManager#addTransaction}.
+ *
+ * @param future The CompletableFuture to be returned by the mocked method.
+ */
+ private void setupAddTransactionMock(CompletableFuture<Boolean> future) {
+ when(mTransactionManager.addTransaction(any(), any())).thenAnswer(invocation -> {
+ return future; // Return the provided future.
+ });
+ }
+ /**
+ * Simulates the successful completion of the asynchronous operation tracked by the given
+ * future. Captures the {@link OutcomeReceiver} passed to
+ * {@link TransactionManager#addTransaction}, completes the future, and invokes
+ * {@link OutcomeReceiver#onResult} with a successful result.
+ *
+ * @param future The CompletableFuture to complete.
+ */
+ private void completeAddTransactionSuccessfully(CompletableFuture<Boolean> future) {
+ // Capture the OutcomeReceiver passed to addTransaction.
+ ArgumentCaptor<OutcomeReceiver<CallTransactionResult, CallException>> captor =
+ ArgumentCaptor.forClass(OutcomeReceiver.class);
+ verify(mTransactionManager).addTransaction(any(CallTransaction.class), captor.capture());
+
+ // Complete the future to signal the end of the asynchronous operation.
+ future.complete(true);
+
+ // Create a successful CallTransactionResult.
+ CallTransactionResult callTransactionResult = new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED,
+ "EndCallTransaction: RESULT_SUCCEED");
+
+ // Invoke onResult on the captured OutcomeReceiver.
+ captor.getValue().onResult(callTransactionResult);
+
+ }
+ /**
+ * Verifies that {@link CallsManager#markCallAsDisconnected} and the provided
+ * {@link OutcomeReceiver}'s {@code onResult} method were called. Also waits for the future
+ * to complete.
+ *
+ * @param resultReceiver The mock OutcomeReceiver.
+ */
+ private void verifyMarkCallAsDisconnectedAndReceiverResult(
+ OutcomeReceiver<CallTransactionResult, CallException> resultReceiver) {
+ verify(mCallsManager, times(1)).markCallAsDisconnected(
+ mMockCall1,
+ REJECTED_DISCONNECT_CAUSE);
+ verify(resultReceiver).onResult(any());
+ }
+}
\ No newline at end of file