Snap for 13256841 from b0df908717e5e7b1cf27dc84bf1d4f522e84072c to 25Q2-release
Change-Id: Ib13fb5529dc5b8eb0ac0fd35c546baac7c6e8408
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
index 8e15910..0000f32 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -77,3 +77,11 @@
purpose: PURPOSE_BUGFIX
}
}
+
+# OWNER=pmadapurmath TARGET=25Q4
+flag {
+ name: "call_sequencing_call_resume_failed"
+ namespace: "telecom"
+ description: "Connection event received when a call resume fails"
+ bug: "390116261"
+}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 6fe476d..a54a3b6 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -205,6 +205,7 @@
default void onHoldToneRequested(Call call) {};
default void onCallHoldFailed(Call call) {};
default void onCallSwitchFailed(Call call) {};
+ default void onCallResumeFailed(Call call) {};
default void onConnectionEvent(Call call, String event, Bundle extras) {};
default void onCallStreamingStateChanged(Call call, boolean isStreaming) {}
default void onExternalCallChanged(Call call, boolean isExternalCall) {};
@@ -295,6 +296,8 @@
@Override
public void onCallSwitchFailed(Call call) {}
@Override
+ public void onCallResumeFailed(Call call) {}
+ @Override
public void onConnectionEvent(Call call, String event, Bundle extras) {}
@Override
public void onCallStreamingStateChanged(Call call, boolean isStreaming) {}
@@ -644,6 +647,7 @@
private boolean mIsTransactionalCall = false;
private CallingPackageIdentity mCallingPackageIdentity = new CallingPackageIdentity();
+ private boolean mSkipAutoUnhold = false;
/**
* CallingPackageIdentity is responsible for storing properties about the calling package that
@@ -4544,6 +4548,10 @@
for (Listener l : mListeners) {
l.onCallSwitchFailed(this);
}
+ } else if (Connection.EVENT_CALL_RESUME_FAILED.equals(event)) {
+ for (Listener l : mListeners) {
+ l.onCallResumeFailed(this);
+ }
} else if (Connection.EVENT_DEVICE_TO_DEVICE_MESSAGE.equals(event)
&& extras != null && extras.containsKey(
Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE)
@@ -5108,4 +5116,21 @@
public boolean hasVideoCall() {
return mHasVideoCall;
}
+
+ /**
+ * Used only for call sequencing for cases when we may end up auto-unholding the held call while
+ * processing an outgoing (emergency) call. We want to refrain from unholding the held call so
+ * that we don't end up with two active calls. Once the outgoing call is disconnected (either
+ * from a successful disconnect by the user or a failed call), the auto-unhold logic will be
+ * triggered again and successfully unhold the held call at that point. Note, that this only
+ * applies to non-holdable phone accounts (i.e. Verizon). Refer to
+ * {@link CallsManagerCallSequencingAdapter#maybeMoveHeldCallToForeground} for details.
+ */
+ public void setSkipAutoUnhold(boolean result) {
+ mSkipAutoUnhold = result;
+ }
+
+ public boolean getSkipAutoUnhold() {
+ return mSkipAutoUnhold;
+ }
}
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 8416533..ce06d55 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -849,13 +849,15 @@
private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type,
String deviceAddress) {
AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress);
- if (bluetoothRoute != null) {
+ boolean isBtDeviceCurrentActive = Objects.equals(bluetoothRoute,
+ getArbitraryBluetoothDevice());
+ if (bluetoothRoute != null && isBtDeviceCurrentActive) {
Log.i(this, "request to route to bluetooth route: %s (active=%b)", bluetoothRoute,
mIsActive);
routeTo(mIsActive, bluetoothRoute);
} else {
- Log.i(this, "request to route to unavailable bluetooth route - type (%s), address (%s)",
- type, deviceAddress);
+ Log.i(this, "request to route to unavailable bluetooth route or the route isn't the "
+ + "currently active device - type (%s), address (%s)", type, deviceAddress);
}
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 247fd0b..4d59fc8 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -1477,6 +1477,14 @@
markAllAnsweredCallAsRinging(call, "switch");
}
+ @Override
+ public void onCallResumeFailed(Call call) {
+ Call heldCall = getFirstCallWithState(call, true /* skipSelfManaged */, CallState.ON_HOLD);
+ if (heldCall != null) {
+ mCallSequencingAdapter.handleCallResumeFailed(call, heldCall);
+ }
+ }
+
private void markAllAnsweredCallAsRinging(Call call, String actionName) {
// Normally, we don't care whether a call hold or switch has failed.
// However, if a call was held or switched in order to answer an incoming call, that
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 3f8f579..6cfa4fd 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -44,6 +44,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.PackageTagsList;
+import android.os.Parcel;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
@@ -1723,7 +1724,8 @@
try {
inCallService.updateCall(
- sanitizeParcelableCallForService(info, parcelableCall));
+ copyIfLocal(sanitizeParcelableCallForService(info, parcelableCall),
+ inCallService));
} catch (RemoteException ignored) {
}
}
@@ -2854,7 +2856,8 @@
ParcelableCall parcelableCall, ComponentName componentName) {
try {
inCallService.updateCall(
- sanitizeParcelableCallForService(info, parcelableCall));
+ copyIfLocal(sanitizeParcelableCallForService(info, parcelableCall),
+ inCallService));
} catch (RemoteException exception) {
Log.w(this, "Call status update did not send to: "
+ componentName + " successfully with error " + exception);
@@ -3435,4 +3438,43 @@
}
return false;
}
+
+ /**
+ * Given a {@link ParcelableCall} and a {@link IInCallService}, determines if the ICS binder is
+ * local or remote. If the binder is remote, we just return the parcelable call instance
+ * already constructed.
+ * If the binder if local, as will be the case for
+ * {@code EnhancedConfirmationCallTrackerService} (or any other ICS in the system server, the
+ * underlying Binder implementation is NOT going to parcel and unparcel the
+ * {@link ParcelableCall} instance automatically. This means that the parcelable call instance
+ * is passed by reference and that the ICS in the system server could potentially try to access
+ * internals in the {@link ParcelableCall} in an unsafe manner. As a workaround, we will
+ * manually parcel and unparcel the {@link ParcelableCall} instance so that they get a fresh
+ * copy that they can use safely.
+ *
+ * @param parcelableCall The ParcelableCall instance we want to maybe copy.
+ * @param remote the binder the call is going out over.
+ * @return either the original {@link ParcelableCall} or a deep copy of it if the destination
+ * binder is local.
+ */
+ private ParcelableCall copyIfLocal(ParcelableCall parcelableCall, IInCallService remote) {
+ // We care more about parceling than local (though they should be the same); so, use
+ // queryLocalInterface since that's what Binder uses to decide if it needs to parcel.
+ if (remote.asBinder().queryLocalInterface(IInCallService.Stub.DESCRIPTOR) == null) {
+ // No local interface, so binder itself will parcel and thus we don't need to.
+ return parcelableCall;
+ }
+ // Binder won't be parceling; however, the remotes assume they have their own native
+ // objects (and don't know if caller is local or not), so we need to make a COPY here so
+ // that the remote can clean it up without clearing the original transaction.
+ // Since there's no direct `copy` for Transaction, we have to parcel/unparcel instead.
+ final Parcel p = Parcel.obtain();
+ try {
+ parcelableCall.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ return ParcelableCall.CREATOR.createFromParcel(p);
+ } finally {
+ p.recycle();
+ }
+ }
}
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index 8122787..d418cff 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -465,14 +465,12 @@
* made for the emergency call.
*/
private CompletableFuture<Boolean> makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
- // 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();
+ // Disconnect all self-managed + transactional calls + calls that don't support holding for
+ // emergency. We will never use these accounts for emergency calling. 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.
Pair<Set<Call>, CompletableFuture<Boolean>> disconnectCallsForEmergencyPair =
- disconnectCallsForEmergencyCall(emergencyCall, areMultiplePhoneAccountsActive);
+ disconnectCallsForEmergencyCall(emergencyCall);
// 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
@@ -518,6 +516,7 @@
new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
mCallsManager.getLock()));
}
+ disconnectedCalls.add(ringingCall);
} else {
ringingCall = null;
}
@@ -537,6 +536,25 @@
return transactionFuture;
}
+ // After having rejected any potential ringing call as well as calls that aren't supported
+ // during emergency calls (refer to disconnectCallsForEmergencyCall logic), we can
+ // re-evaluate whether we still have multiple phone accounts in use in order to disconnect
+ // non-holdable calls:
+ // If (yes) - disconnect call the non-holdable calls (this would be just the active call)
+ // If (no) - skip the disconnect and instead let the logic be handled explicitly for the
+ // single sim behavior.
+ boolean areMultiplePhoneAccountsActive = areMultiplePhoneAccountsActive(disconnectedCalls);
+ if (areMultiplePhoneAccountsActive && !liveCall.can(Connection.CAPABILITY_SUPPORT_HOLD)) {
+ // After disconnecting, we should be able to place the ECC now (we either have no calls
+ // or a held call after this point).
+ String disconnectReason = "disconnecting non-holdable call to make room "
+ + "for emergency call";
+ emergencyCall.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
+ return disconnectOngoingCallForEmergencyCall(transactionFuture, liveCall,
+ disconnectReason);
+ }
+
// If we already disconnected the outgoing call, then don't perform any additional ops on
// it.
if (mCallsManager.hasMaximumOutgoingCalls(emergencyCall) && !disconnectedCalls
@@ -559,10 +577,16 @@
+ " of new outgoing call.";
}
if (disconnectReason != null) {
+ // Skip auto-unhold for when the outgoing call is disconnected. Consider a scenario
+ // where we have a held non-holdable call (VZW) and the dialing call (also VZW). If
+ // we auto unhold the VZW while placing the emergency call, then we may end up with
+ // two active calls. The auto-unholding logic really only applies for the
+ // non-holdable phone account.
+ outgoingCall.setSkipAutoUnhold(true);
boolean isSequencingRequiredRingingAndOutgoing = ringingCall == null
|| !arePhoneAccountsSame(ringingCall, outgoingCall);
return disconnectOngoingCallForEmergencyCall(transactionFuture, outgoingCall,
- disconnectReason, isSequencingRequiredRingingAndOutgoing);
+ disconnectReason);
}
// 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.
@@ -570,14 +594,18 @@
return CompletableFuture.completedFuture(false);
}
- boolean isSequencingRequiredLive = ringingCall == null
- || !arePhoneAccountsSame(ringingCall, liveCall);
if (liveCall.getState() == CallState.AUDIO_PROCESSING) {
emergencyCall.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
+ // Skip auto-unhold for when the live call is disconnected. Consider a scenario where
+ // we have a held non-holdable call (VZW) and the live call (also VZW) is stuck in
+ // audio processing. If we auto unhold the VZW while placing the emergency call, then we
+ // may end up with two active calls. The auto-unholding logic really only applies for
+ // the non-holdable phone account.
+ liveCall.setSkipAutoUnhold(true);
final String disconnectReason = "disconnecting audio processing call for emergency";
return disconnectOngoingCallForEmergencyCall(transactionFuture, liveCall,
- disconnectReason, isSequencingRequiredLive);
+ disconnectReason);
}
// If the live call is stuck in a connecting state, prompt the user to generate a bugreport.
@@ -587,40 +615,41 @@
}
// If we have the max number of held managed calls and we're placing an emergency call,
- // we'll disconnect the ongoing call if it cannot be held. If we have a self-managed call
+ // we'll disconnect the active 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. 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;
+ // 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. Also
+ // note that in a scenario where we don't have any held calls and the live call can't be
+ // held (only applies for single sim case), we should try holding the active call (and
+ // disconnect on fail) before placing the ECC (i.e. Verizon swap case). The latter is being
+ // handled further down in this method.
+ Call heldCall = mCallsManager.getFirstCallWithState(CallState.ON_HOLD);
if (mCallsManager.hasMaximumManagedHoldingCalls(emergencyCall)
- || !mCallsManager.canHold(liveCall)) {
+ && !disconnectedCalls.contains(heldCall)) {
final String disconnectReason = "disconnecting to make room for emergency call "
+ emergencyCall.getId();
emergencyCall.getAnalytics().setCallIsAdditional(true);
// Single sim case
if (!areMultiplePhoneAccountsActive) {
liveCall.getAnalytics().setCallIsInterrupted(true);
+ // Skip auto-unhold for when the live call is disconnected. Consider a scenario
+ // where we have a held non-holdable call (VZW) and an active call (also VZW). If
+ // we auto unhold the VZW while placing the emergency call, then we may end up with
+ // two active calls. The auto-unholding logic really only applies for the
+ // non-holdable phone account.
+ liveCall.setSkipAutoUnhold(true);
// Disconnect the active call instead of the holding call because it is historically
- // easier to do, rather than disconnect a held call.
+ // easier to do, rather than disconnecting a held call and holding the active 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);
+ disconnectReason);
+ } else if (heldCall != null) { // Dual sim case
+ // Note at this point, we should always have a held call then that should
+ // be disconnected (over the active call) but still enforce with a null check and
+ // ensure we haven't disconnected it already.
heldCall.getAnalytics().setCallIsInterrupted(true);
// Disconnect the held call.
transactionFuture = disconnectOngoingCallForEmergencyCall(transactionFuture,
- heldCall, disconnectReason, isSequencingRequiredRingingAndHeld);
+ heldCall, disconnectReason);
}
}
@@ -654,46 +683,26 @@
final String disconnectReason = "outgoing call does not support emergency calls, "
+ "disconnecting.";
return disconnectOngoingCallForEmergencyCall(transactionFuture, liveCall,
- disconnectReason, isSequencingRequiredLive);
+ disconnectReason);
}
}
- // If we are trying to make an emergency call with the same package name as
- // the live call, then attempt to hold the call if the carrier config supports holding
- // emergency calls. Otherwise, disconnect the live call in order to make room for the
- // emergency call.
- if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
- emergencyCall.getTargetPhoneAccount())) {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccounts are from same "
- + "package. Attempting to hold live call before placing emergency call.");
- 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.
- // If the user chooses the same phone account as the live call, then it's
- // still possible that the call can be made (like with CDMA calls not supporting
- // 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 transactionFuture;
- }
+ // At this point, if we still have an active call, then it supports holding for emergency
+ // and is a managed call. It may not support holding but we will still try to hold anyway
+ // (i.e. swap for Verizon). Note that there will only be one call at this stage which is
+ // the active call so that means that we will attempt to place the emergency call on the
+ // same phone account unless it's not using a Telephony phone account (Fi wifi call), in
+ // which case, we would want to verify holding happened. For cases like backup calling, the
+ // shared data call will be over Telephony as well as the emergency call, so the shared
+ // data call would get disconnected by the CS.
- // 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(transactionFuture, isSequencingRequiredLive,
- liveCall, emergencyCall, true /* shouldHoldForEmergency */);
- }
-
- // 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;
+ // We want to verify if the live call was placed via the connection manager. Don't use
+ // the manipulated liveCallPhoneAccount since the delegate would pull directly from the
+ // target phone account.
+ boolean isLiveUsingConnectionManager = !Objects.equals(liveCall.getTargetPhoneAccount(),
+ liveCall.getDelegatePhoneAccountHandle());
+ return maybeHoldLiveCallForEmergency(transactionFuture, liveCall,
+ emergencyCall, isLiveUsingConnectionManager);
}
/**
@@ -747,6 +756,12 @@
}
mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
+ // Skip auto-unhold for when the live call is disconnected. Consider a scenario where
+ // we have a held non-holdable call (VZW) and the live call (also VZW) is stuck in
+ // connecting. If we auto unhold the VZW while placing the emergency call, then we may
+ // end up with two active calls. The auto-unholding logic really only applies for
+ // the non-holdable phone account.
+ liveCall.setSkipAutoUnhold(true);
return liveCall.disconnect("Force disconnect CONNECTING call.");
}
@@ -757,6 +772,12 @@
// state, just disconnect it since the user has explicitly started a new call.
call.getAnalytics().setCallIsAdditional(true);
outgoingCall.getAnalytics().setCallIsInterrupted(true);
+ // Skip auto-unhold for when the outgoing call is disconnected. Consider a scenario
+ // where we have a held non-holdable call (VZW) and a dialing call (also VZW). If we
+ // auto unhold the VZW while placing the emergency call, then we may end up with
+ // two active calls. The auto-unholding logic really only applies for the
+ // non-holdable phone account.
+ outgoingCall.setSkipAutoUnhold(true);
return outgoingCall.disconnect(
"Disconnecting call in SELECT_PHONE_ACCOUNT in favor of new "
+ "outgoing call.");
@@ -844,35 +865,41 @@
/**
* Tries to hold the live call before placing the emergency call. If the hold fails, then we
- * will instead disconnect the call.
+ * will instead disconnect the call. This only applies for when the emergency call and live call
+ * are from the same phone account or there's only one ongoing call, in which case, we should
+ * place the emergency call on the ongoing call's phone account.
*
* Note: This only applies when the live call and emergency call are from the same phone
* account.
*/
private CompletableFuture<Boolean> maybeHoldLiveCallForEmergency(
- CompletableFuture<Boolean> transactionFuture, boolean isSequencingRequired,
- Call liveCall, Call emergencyCall, boolean shouldHoldForEmergency) {
+ CompletableFuture<Boolean> transactionFuture,
+ Call liveCall, Call emergencyCall, boolean isLiveUsingConnectionManager) {
emergencyCall.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
final String holdReason = "calling " + emergencyCall.getId();
- CompletableFuture<Boolean> holdResultFuture = CompletableFuture.completedFuture(false);
- if (shouldHoldForEmergency) {
- if (transactionFuture != null && isSequencingRequired) {
- holdResultFuture = transactionFuture.thenComposeAsync((result) -> {
- if (result) {
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "previous call succeeded. Attempting to hold live call.");
- } else { // Log the failure but proceed with hold transaction.
- Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
- + "previous call failed. Still attempting to hold live call.");
- }
- return liveCall.hold(holdReason);
- }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
- mCallsManager.getLock()));
- } else {
- holdResultFuture = liveCall.hold(holdReason);
+ CompletableFuture<Boolean> holdResultFuture;
+ holdResultFuture = transactionFuture.thenComposeAsync((result) -> {
+ if (result) {
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Previous transaction "
+ + "succeeded. Attempting to hold live call.");
+ } else { // Log the failure but proceed with hold transaction.
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Previous transaction "
+ + "failed. Still attempting to hold live call.");
}
+ Log.i(this, "makeRoomForOutgoingEmergencyCall: Attempt to hold live call. "
+ + "Verifying hold: %b", isLiveUsingConnectionManager);
+ return liveCall.hold(holdReason);
+ }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC", mCallsManager.getLock()));
+
+ // If the live call was placed using a connection manager, we should verify that holding
+ // happened before placing the emergency call. We should disconnect the call if hold fails.
+ // Otherwise, let Telephony handle additional sequencing that may be required.
+ if (!isLiveUsingConnectionManager) {
+ return transactionFuture;
}
+
+ // Otherwise, verify hold succeeded and if it didn't, then hangup the call.
return holdResultFuture.thenComposeAsync((result) -> {
if (!result) {
Log.i(this, "makeRoomForOutgoingEmergencyCall: Attempt to hold live call "
@@ -888,7 +915,7 @@
}
/**
- * Disconnects all VOIP + non-holdable calls as well as those that don't support placing
+ * Disconnects all VOIP (SM + Transactional) 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.
@@ -896,38 +923,15 @@
* disconnect transaction.
*/
private Pair<Set<Call>, CompletableFuture<Boolean>> disconnectCallsForEmergencyCall(
- Call emergencyCall, boolean areMultiplePhoneAccountsActive) {
+ Call emergencyCall) {
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 isVoip = isVoipCall(call);
- 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)) {
+ if (skipDisconnectForEmergencyCall(call, ringingCall)) {
continue;
}
-
- // If the call is not VOIP and supports holding + capability to place emergency calls,
- // don't disconnect the call.
- if (!isVoip && 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(), !isVoip,
- callSupportsHold, callSupportsHoldingEmergencyCall);
emergencyCall.getAnalytics().setCallIsAdditional(true);
call.getAnalytics().setCallIsInterrupted(true);
call.setOverrideDisconnectCauseCode(new DisconnectCause(
@@ -950,37 +954,64 @@
return new Pair<>(callsDisconnected, disconnectFuture);
}
+ private boolean skipDisconnectForEmergencyCall(Call call, Call ringingCall) {
+ // Conditions for checking if call doesn't need to be disconnected immediately.
+ boolean isVoip = isVoipCall(call);
+ boolean callSupportsHoldingEmergencyCall = shouldHoldForEmergencyCall(
+ call.getTargetPhoneAccount());
+
+ // Skip the ringing call; we'll handle the disconnect explicitly later. Also, if we have
+ // a conference call, only disconnect the host call.
+ if (call.equals(ringingCall) || call.getParentCall() != null) {
+ return true;
+ }
+
+ // If the call is managed and supports holding for emergency calls, don't disconnect the
+ // call.
+ if (!isVoip && callSupportsHoldingEmergencyCall) {
+ return true;
+ }
+ // Otherwise, we will disconnect the call because it doesn't meet one of the conditions
+ // above.
+ Log.i(this, "Disconnecting call (%s). isManaged: %b, call "
+ + "supports holding emergency call: %b", call.getId(), !isVoip,
+ callSupportsHoldingEmergencyCall);
+ return false;
+ }
+
/**
* 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 {
+ String disconnectReason) {
+ 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()));
}
/**
* Determines if DSDA is being used (i.e. calls present on more than one phone account).
+ * @param callsToExclude The list of calls to exclude (these will be calls that have been
+ * disconnected but may still be being tracked by CallsManager depending
+ * on timing).
*/
- private boolean areMultiplePhoneAccountsActive() {
- List<Call> calls = mCallsManager.getCalls().stream().toList();
+ private boolean areMultiplePhoneAccountsActive(Set<Call> callsToExclude) {
+ for (Call excludedCall: callsToExclude) {
+ Log.i(this, "Calls to exclude: %s", excludedCall);
+ }
+ List<Call> calls = mCallsManager.getCalls().stream()
+ .filter(c -> !callsToExclude.contains(c)).toList();
PhoneAccountHandle handle1 = null;
if (!calls.isEmpty()) {
// Find the first handle different from the one retrieved from the first call in
@@ -1034,8 +1065,11 @@
private CompletableFuture<Boolean> disconnectAllCallsWithPhoneAccount(
PhoneAccountHandle handle, boolean excludeAccount) {
CompletableFuture<Boolean> disconnectFuture = CompletableFuture.completedFuture(true);
+ // Filter out the corresponding phone account and ensure that we don't consider conference
+ // participants as part of the bulk disconnect (we'll just disconnect the host directly).
List<Call> calls = mCallsManager.getCalls().stream()
- .filter(c -> excludeAccount != c.getTargetPhoneAccount().equals(handle)).toList();
+ .filter(c -> excludeAccount != c.getTargetPhoneAccount().equals(handle)
+ && c.getParentCall() == null).toList();
for (Call call: calls) {
// Wait for all disconnects before we accept the new call.
disconnectFuture = disconnectFuture.thenComposeAsync((result) -> {
diff --git a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
index 2de3b22..611bb9e 100644
--- a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
@@ -234,6 +234,16 @@
public void maybeMoveHeldCallToForeground(Call removedCall, boolean isLocallyDisconnecting) {
CompletableFuture<Boolean> unholdForegroundCallFuture = null;
Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
+ // There are some cases (non-holdable calls) where we may want to skip auto-unholding when
+ // we're processing a new outgoing call and waiting for it to go active. Skip the
+ // auto-unholding in this case so that we don't end up with two active calls. If the new
+ // call fails, we will auto-unhold on that removed call. This is only set in
+ // CallSequencingController because the legacy code doesn't wait for disconnects to occur
+ // in order to place an outgoing (emergency) call, so we don't see this issue.
+ if (removedCall.getSkipAutoUnhold()) {
+ return;
+ }
+
if (isLocallyDisconnecting) {
boolean isDisconnectingChildCall = removedCall.isDisconnectingChildCall();
Log.v(this, "maybeMoveHeldCallToForeground: isDisconnectingChildCall = "
@@ -247,7 +257,6 @@
if (!isDisconnectingChildCall && foregroundCall != null
&& foregroundCall.getState() == CallState.ON_HOLD
&& CallsManager.areFromSameSource(foregroundCall, removedCall)) {
-
unholdForegroundCallFuture = foregroundCall.unhold();
}
} else if (foregroundCall != null &&
@@ -355,6 +364,20 @@
});
}
+ /**
+ * Upon a call resume failure, we will auto-unhold the foreground call that was held. Note that
+ * this should only apply for calls across phone accounts as the ImsPhoneCallTracker handles
+ * this for a single phone.
+ * @param callResumeFailed The call that failed to resume.
+ * @param callToUnhold The fg call that was held.
+ */
+ public void handleCallResumeFailed(Call callResumeFailed, Call callToUnhold) {
+ if (mIsCallSequencingEnabled && !mSequencingController.arePhoneAccountsSame(
+ callResumeFailed, callToUnhold)) {
+ unholdCall(callToUnhold);
+ }
+ }
+
public Handler getHandler() {
return mHandler;
}
diff --git a/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
index 7bebb55..b7e4f04 100644
--- a/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
@@ -18,8 +18,10 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
import com.android.server.telecom.TelecomSystem;
+import android.telecom.CallException;
import android.telecom.Log;
import java.util.Set;
@@ -56,6 +58,26 @@
}
};
+ private final Call.ListenerBase mCallListenerImpl = new Call.ListenerBase() {
+ @Override
+ public void onCallHoldFailed(Call call) {
+ if (call.equals(mCall) && mTargetCallStates.contains(CallState.ON_HOLD)) {
+ // Fail the transaction if a call hold failure is received.
+ mTransactionResult.complete(new CallTransactionResult(
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL, "error holding call"));
+ }
+ }
+ @Override
+ public void onCallResumeFailed(Call call) {
+ if (call.equals(mCall) && mTargetCallStates.contains(CallState.ACTIVE)) {
+ // Fail the transaction if a call resume failure is received (this means that the
+ // current call could not be unheld).
+ mTransactionResult.complete(new CallTransactionResult(
+ CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE, "error unholding call"));
+ }
+ }
+ };
+
public VerifyCallStateChangeTransaction(TelecomSystem.SyncRoot lock, Call call,
int... targetCallStates) {
super(lock, CALL_STATE_TIMEOUT_MILLISECONDS);
@@ -73,12 +95,14 @@
return mTransactionResult;
}
mCall.addCallStateListener(mCallStateListenerImpl);
+ mCall.addListener(mCallListenerImpl);
return mTransactionResult;
}
@Override
public void finishTransaction() {
mCall.removeCallStateListener(mCallStateListenerImpl);
+ mCall.removeListener(mCallListenerImpl);
}
private boolean isNewCallStateTargetCallState() {
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 9daa7cf..1b1ca56 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -1357,6 +1357,56 @@
verify(mCallAudioManager, timeout(TEST_TIMEOUT)).notifyAudioOperationsComplete();
}
+ @Test
+ @SmallTest
+ public void testActiveDevicePresentRoutesOnCurrentActive() {
+ when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+ // Connect first BT device.
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ // Connect another BT device.
+ String scoDeviceAddress = "00:00:00:00:00:03";
+ BluetoothDevice scoDevice2 =
+ BluetoothRouteManagerTest.makeBluetoothDevice(scoDeviceAddress);
+ BLUETOOTH_DEVICES.add(scoDevice2);
+
+ // Signal second BT device added in controller and verify routing to that device upon
+ // receiving active focus.
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ scoDevice2);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Update the currently tracked active device to be BLUETOOTH_DEVICE_1.
+ mController.updateActiveBluetoothDevice(
+ new Pair<>(AudioRoute.TYPE_BLUETOOTH_SCO, BLUETOOTH_DEVICE_1.getAddress()));
+ // Verify that sending BT_ACTIVE_DEVICE_PRESENT when BLUETOOTH_DEVICE_1 isn't the currently
+ // tracked active device, that we ignore routing.
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice2.getAddress());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Now update the active device so that it's scoDevice2 and verify that
+ // BT_ACTIVE_DEVICE_PRESENT is properly processed and that we route into the device.
+ mController.updateActiveBluetoothDevice(
+ new Pair<>(AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice2.getAddress()));
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice2.getAddress());
+ mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+ BLUETOOTH_DEVICE_1);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
+ 0, scoDevice2);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, scoDevice2, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
private void verifyConnectBluetoothDevice(int audioType) {
mController.initialize();
mController.setActive(true);
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index 3a7a822..b2cdd7d 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -999,6 +999,7 @@
@Test
@SmallTest
public void testOnConnectionEventNotifiesListener() {
+ when(mFeatureFlags.enableCallSequencing()).thenReturn(true);
Call.Listener listener = mock(Call.Listener.class);
Call call = createCall("1");
call.addListener(listener);
@@ -1017,6 +1018,9 @@
call.onConnectionEvent(Connection.EVENT_CALL_SWITCH_FAILED, null);
verify(listener).onCallSwitchFailed(call);
+ call.onConnectionEvent(Connection.EVENT_CALL_RESUME_FAILED, null);
+ verify(listener).onCallResumeFailed(call);
+
final int d2dType = 1;
final int d2dValue = 2;
final Bundle d2dExtras = new Bundle();