Merge "VoipCallMonitor updates" into main
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index 5d42b86..bc248c8 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -27,3 +27,11 @@
purpose: PURPOSE_BUGFIX
}
}
+
+# OWNER=tjstuart TARGET=25Q2
+flag {
+ name: "enable_call_exception_anom_reports"
+ namespace: "telecom"
+ description: "When a new CallException is created, generate an anomaly report for metrics"
+ bug: "308932906"
+}
diff --git a/src/com/android/server/telecom/CallAudioWatchdog.java b/src/com/android/server/telecom/CallAudioWatchdog.java
index 3d4214d..4d06b62 100644
--- a/src/com/android/server/telecom/CallAudioWatchdog.java
+++ b/src/com/android/server/telecom/CallAudioWatchdog.java
@@ -395,7 +395,10 @@
@Override
public void onCallRemoved(Call call) {
- // Nothing to do for call removal; sessions get cleaned up when their audio goes away.
+ // Only track for voip calls.
+ if (call.isSelfManaged() || call.isTransactionalCall()) {
+ maybeRemoveCall(call);
+ }
}
@VisibleForTesting
@@ -428,7 +431,7 @@
}
sessions.forEach(pw::println);
pw.decreaseIndent();
- pw.println("Non-Telecom Sessions:");
+ pw.println("Audio sessions Sessions:");
pw.increaseIndent();
mLocalLog.dump(pw);
pw.decreaseIndent();
@@ -544,6 +547,27 @@
}
/**
+ * Given a telecom call, cleanup the session if there are no audio resources remaining for that
+ * session.
+ * @param call The call.
+ */
+ private void maybeRemoveCall(Call call) {
+ int uid = mPhoneAccountRegistrarProxy.getUidForPhoneAccountHandle(
+ call.getTargetPhoneAccount());
+ CommunicationSession session;
+ synchronized (mCommunicationSessionsLock) {
+ session = getSession(uid);
+ if (session == null) {
+ return;
+ }
+ if (!session.hasMediaResources()) {
+ mLocalLog.log(session.toString());
+ mCommunicationSessions.remove(uid);
+ }
+ }
+ }
+
+ /**
* Returns an existing session for a uid, or {@code null} if none exists.
* @param uid the uid,
* @return The session found, or {@code null}.
@@ -621,14 +645,9 @@
// If audio resources are no longer held for a uid, then we'll clean up its
// media session.
- if (!session.hasMediaResources()) {
+ if (!session.hasMediaResources() && session.getTelecomCall() == null) {
Log.i(this, "cleanupAttributeForSessions: removing session %s", session);
- // Only log the audio session if it has no telecom call; we'll correlate to
- // a telecom call if one was present so the logs for a telecom call will be
- // in the calls dumpsys.
- if (session.getTelecomCall() == null) {
- mLocalLog.log(session.toString());
- }
+ mLocalLog.log(session.toString());
iterator.remove();
}
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index ea66524..8425a10 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -329,6 +329,10 @@
UUID.fromString("0a86157c-50ca-11ee-be56-0242ac120002");
public static final String TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG =
"Telephony has a default MO acct but Telecom prompted user for MO";
+ public static final UUID CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID =
+ UUID.fromString("1b6a9b88-5049-4ffa-a52a-134d7c3a40e6");
+ public static final UUID FAILED_TO_SWITCH_FOCUS_ERROR_UUID =
+ UUID.fromString("a1b2c3d4-e5f6-7890-1234-567890abcdef");
public static final int[] OUTGOING_CALL_STATES =
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
@@ -786,8 +790,9 @@
? mContext.getSystemService(BlockedNumbersManager.class)
: null;
mCallSequencingAdapter = new CallsManagerCallSequencingAdapter(this,
- new CallSequencingController(this, mContext,
- mFeatureFlags), mFeatureFlags);
+ new CallSequencingController(this, mContext, mClockProxy,
+ mAnomalyReporter, mTimeoutsAdapter, mMetricsController,
+ mFeatureFlags), mCallAudioManager, mFeatureFlags);
if (mFeatureFlags.useImprovedListenerOrder()) {
mListeners.add(mInCallController);
@@ -3221,9 +3226,9 @@
* CS: Hold any existing calls, request focus, and then set the call state to answered state.
* <p>
* T: Call TransactionalServiceWrapper, which then generates transactions to hold calls
- * {@link #transactionHoldPotentialActiveCallForNewCall} and then move the active call focus
- * {@link #requestNewCallFocusAndVerify} and notify the remote VOIP app of the call state
- * moving to active.
+ * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall} and
+ * then move the active call focus {@link #requestNewCallFocusAndVerify} and notify the remote
+ * VOIP app of the call state moving to active.
* <p>
* Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is false.
*/
@@ -4074,63 +4079,10 @@
}
/**
- * attempt to hold or swap the current active call in favor of a new call request. The
- * OutcomeReceiver will return onResult if the current active call is held or disconnected.
- * Otherwise, the OutcomeReceiver will fail.
+ * Attempt to hold or swap the current active call in favor of a new call request. The old code
+ * path where {@link FeatureFlags#transactionalHoldDisconnectsUnholdable} is enabled but
+ * {@link FeatureFlags#enableCallSequencing()} is disabled.
*/
- public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
- boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
- String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
- Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
- Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
-
- if (activeCall == null || activeCall == newCall) {
- Log.i(this, mTag + "no need to hold activeCall");
- callback.onResult(true);
- return;
- }
-
- if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
- // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
- // notify the user that they need to disconnect the ongoing call before making the
- // new call ACTIVE.
- if (isCallControlRequest && !canHoldOrSwapActiveCall(activeCall, newCall)) {
- Log.i(this, mTag + "CallControlRequest exit");
- callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
- + " request the user disconnect the call.",
- CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
- return;
- }
-
- mCallSequencingAdapter.transactionHoldPotentialActiveCallForNewCall(newCall,
- activeCall, callback);
- } else {
- // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
- // early
- if (!canHold(activeCall) &&
- !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "conditions show the call cannot be held.");
- callback.onError(new CallException("call does not support hold",
- CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
- return;
- }
-
- // attempt to hold the active call
- if (!holdActiveCallForNewCall(newCall)) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "attempted to hold call but failed.");
- callback.onError(new CallException("cannot hold active call failed",
- CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
- return;
- }
-
- // officially mark the activeCall as held
- markCallAsOnHold(activeCall);
- callback.onResult(true);
- }
- }
-
public void transactionHoldPotentialActiveCallForNewCallOld(Call newCall,
Call activeCall, OutcomeReceiver<Boolean, CallException> callback) {
if (holdActiveCallForNewCall(newCall)) {
@@ -4144,16 +4096,57 @@
if (activeCall.isLocallyDisconnecting()) {
callback.onResult(true);
} else {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: active call could "
- + "not be held or disconnected");
+ String msg = "active call could not be held or disconnected";
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: " + msg);
callback.onError(
- new CallException("activeCall could not be held or disconnected",
+ new CallException(msg,
CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+ }
}
}
}
- private boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
+ /**
+ * The transactional unflagged (original) code path to hold or swap the active call in favor of
+ * a new call request. Refer to
+ * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall}.
+ */
+ public void transactionHoldPotentialActiveCallForNewCallUnflagged(Call activeCall, Call newCall,
+ OutcomeReceiver<Boolean, CallException> callback) {
+ // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
+ // early
+ if (!canHold(activeCall) &&
+ !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
+ String msg = "call does not support hold";
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+ callback.onError(new CallException(msg,
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+ }
+ return;
+ }
+
+ // attempt to hold the active call
+ if (!holdActiveCallForNewCall(newCall)) {
+ String msg = "cannot hold active call failed";
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+ callback.onError(new CallException(msg,
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+ }
+ return;
+ }
+
+ // officially mark the activeCall as held
+ markCallAsOnHold(activeCall);
+ callback.onResult(true);
+ }
+
+ public boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
return canHold(activeCall) || sameSourceHoldCase(activeCall, newCall);
}
@@ -4373,54 +4366,7 @@
removeCall(call);
boolean isLocallyDisconnecting = mLocallyDisconnectingCalls.contains(call);
mLocallyDisconnectingCalls.remove(call);
- maybeMoveHeldCallToForeground(call, isLocallyDisconnecting);
- }
-
- /**
- * Move the held call to foreground in the event that there is a held call and the disconnected
- * call was disconnected locally or the held call has no way to auto-unhold because it does not
- * support hold capability.
- *
- * Note: If {@link FeatureFlags#enableCallSequencing()} is enabled, we will verify that the
- * transaction to unhold the call succeeded or failed.
- */
- public void maybeMoveHeldCallToForeground(Call removedCall, boolean isLocallyDisconnecting) {
- CompletableFuture<Boolean> unholdForegroundCallFuture = null;
- Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
- if (isLocallyDisconnecting) {
- boolean isDisconnectingChildCall = removedCall.isDisconnectingChildCall();
- Log.v(this, "maybeMoveHeldCallToForeground: isDisconnectingChildCall = "
- + isDisconnectingChildCall + "call -> %s", removedCall);
- // Auto-unhold the foreground call due to a locally disconnected call, except if the
- // call which was disconnected is a member of a conference (don't want to auto
- // un-hold the conference if we remove a member of the conference).
- // Also, ensure that the call we're removing is from the same ConnectionService as
- // the one we're removing. We don't want to auto-unhold between ConnectionService
- // implementations, especially if one is managed and the other is a VoIP CS.
- if (!isDisconnectingChildCall && foregroundCall != null
- && foregroundCall.getState() == CallState.ON_HOLD
- && areFromSameSource(foregroundCall, removedCall)) {
-
- unholdForegroundCallFuture = foregroundCall.unhold();
- }
- } else if (foregroundCall != null &&
- !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
- foregroundCall.getState() == CallState.ON_HOLD) {
-
- // The new foreground call is on hold, however the carrier does not display the hold
- // button in the UI. Therefore, we need to auto unhold the held call since the user
- // has no means of unholding it themselves.
- Log.i(this, "maybeMoveHeldCallToForeground: Auto-unholding held foreground call (call "
- + "doesn't support hold)");
- unholdForegroundCallFuture = foregroundCall.unhold();
- }
-
- if (mFeatureFlags.enableCallSequencing() && unholdForegroundCallFuture != null) {
- mCallSequencingAdapter.logFutureResultTransaction(unholdForegroundCallFuture,
- "maybeMoveHeldCallToForeground", "CM.mMHCTF",
- "Successfully unheld the foreground call.",
- "Failed to unhold the foreground call.");
- }
+ mCallSequencingAdapter.maybeMoveHeldCallToForeground(call, isLocallyDisconnecting);
}
/**
@@ -5574,12 +5520,41 @@
return true;
}
- CompletableFuture<Boolean> disconnectFuture =
- maybeDisconnectExistingCallForNewOutgoingCall(call, liveCall);
- // If future is instantiated, it will always be completed when call sequencing
- // isn't enabled.
- if (!mFeatureFlags.enableCallSequencing() && disconnectFuture != null) {
- return disconnectFuture.getNow(false);
+ // If the live call is stuck in a connecting state for longer than the transitory timeout,
+ // then we should disconnect it in favor of the new outgoing call and prompt the user to
+ // generate a bugreport.
+ // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
+ // live call stuck in the connecting state. Unfortunately that code will get tripped up by
+ // calls that have a longer than expected new outgoing call broadcast response time. This
+ // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
+ // block outgoing calls. However, if the user dials two calls in quick succession it will
+ // result in both calls getting disconnected, which is not optimal.
+ if (liveCall.getState() == CallState.CONNECTING
+ && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
+ > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_STUCK_CONNECTING);
+ }
+ mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
+ LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
+ liveCall.disconnect("Force disconnect CONNECTING call.");
+ return true;
+ }
+
+ if (hasMaximumOutgoingCalls(call)) {
+ Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
+ if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
+ // If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
+ // state, just disconnect it since the user has explicitly started a new call.
+ call.getAnalytics().setCallIsAdditional(true);
+ outgoingCall.getAnalytics().setCallIsInterrupted(true);
+ outgoingCall.disconnect("Disconnecting call in SELECT_PHONE_ACCOUNT in favor"
+ + " of new outgoing call.");
+ return true;
+ }
+ call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
+ return false;
}
// TODO: Remove once b/23035408 has been corrected.
@@ -5645,64 +5620,6 @@
}
/**
- * Potentially disconnects the live call if it has been stuck in a connecting state for more
- * than the designated timeout or the outgoing call if it's stuck in the
- * {@link CallState#SELECT_PHONE_ACCOUNT} stage.
- *
- * @param call The new outgoing call that is being placed.
- * @param liveCall The first live call that has been detected.
- * @return The {@link CompletableFuture<Boolean>} representing if room for the outgoing call
- * could be made, null if further processing is required.
- */
- public CompletableFuture<Boolean> maybeDisconnectExistingCallForNewOutgoingCall(Call call,
- Call liveCall) {
- // If the live call is stuck in a connecting state for longer than the transitory timeout,
- // then we should disconnect it in favor of the new outgoing call and prompt the user to
- // generate a bugreport.
- // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
- // live call stuck in the connecting state. Unfortunately that code will get tripped up by
- // calls that have a longer than expected new outgoing call broadcast response time. This
- // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
- // block outgoing calls. However, if the user dials two calls in quick succession it will
- // result in both calls getting disconnected, which is not optimal.
- if (liveCall.getState() == CallState.CONNECTING
- && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
- > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
- if (mFeatureFlags.telecomMetricsSupport()) {
- mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
- ErrorStats.ERROR_STUCK_CONNECTING);
- }
- mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
- LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
- CompletableFuture<Boolean> disconnectFuture =
- liveCall.disconnect("Force disconnect CONNECTING call.");
- return mFeatureFlags.enableCallSequencing()
- ? disconnectFuture
- : CompletableFuture.completedFuture(true);
- }
-
- if (hasMaximumOutgoingCalls(call)) {
- Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
- if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
- // If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
- // state, just disconnect it since the user has explicitly started a new call.
- call.getAnalytics().setCallIsAdditional(true);
- outgoingCall.getAnalytics().setCallIsInterrupted(true);
- CompletableFuture<Boolean> disconnectFuture = outgoingCall.disconnect(
- "Disconnecting call in SELECT_PHONE_ACCOUNT in favor of new "
- + "outgoing call.");
- return mFeatureFlags.enableCallSequencing()
- ? disconnectFuture
- : CompletableFuture.completedFuture(true);
- }
- call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
- return CompletableFuture.completedFuture(false);
- }
-
- return null;
- }
-
- /**
* Given a call, find the first non-null phone account handle of its children.
*
* @param parentCall The parent call.
@@ -6796,13 +6713,11 @@
Log.d(this, "perform unhold call for %s", mCall);
CompletableFuture<Boolean> unholdFuture =
mCall.unhold("held " + mPreviouslyHeldCallId);
- if (mFeatureFlags.enableCallSequencing() && unholdFuture != null) {
- mCallSequencingAdapter.logFutureResultTransaction(unholdFuture,
- "performAction", "AUC.pA", "performAction: unhold call transaction "
- + "succeeded. Call state is active.",
- "performAction: unhold call transaction failed. Call state did not "
- + "move to active in designated time.");
- }
+ mCallSequencingAdapter.maybeLogFutureResultTransaction(unholdFuture,
+ "performAction", "AUC.pA", "performAction: unhold call transaction "
+ + "succeeded. Call state is active.",
+ "performAction: unhold call transaction failed. Call state did not "
+ + "move to active in designated time.");
}
}
}
@@ -6845,13 +6760,11 @@
if (isSpeakerphoneAutoEnabledForVideoCalls(mVideoState)) {
mCall.setStartWithSpeakerphoneOn(true);
}
- if (mFeatureFlags.enableCallSequencing() && answerCallFuture != null) {
- mCallSequencingAdapter.logFutureResultTransaction(answerCallFuture,
- "performAction", "AAC.pA", "performAction: answer call transaction "
- + "succeeded. Call state is active.",
- "performAction: answer call transaction failed. Call state did not "
- + "move to active in designated time.");
- }
+ mCallSequencingAdapter.maybeLogFutureResultTransaction(answerCallFuture,
+ "performAction", "AAC.pA", "performAction: answer call transaction "
+ + "succeeded. Call state is active.",
+ "performAction: answer call transaction failed. Call state did not "
+ + "move to active in designated time.");
}
}
}
@@ -6937,8 +6850,12 @@
if (mTargetCallFocus.getState() != mPreviousCallState) {
mTargetCallFocus.setState(mPreviousCallState, "resetting call state");
}
- mCallback.onError(new CallException("failed to switch focus to requested call",
+ String msg = "failed to switch focus to requested call";
+ mCallback.onError(new CallException(msg,
CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(FAILED_TO_SWITCH_FOCUS_ERROR_UUID, msg);
+ }
return;
}
// at this point, we know the FocusManager is able to update successfully
@@ -7165,4 +7082,9 @@
public void addCallBeingSetup(Call call) {
mSelfManagedCallsBeingSetup.add(call);
}
+
+ @VisibleForTesting
+ public CallsManagerCallSequencingAdapter getCallSequencingAdapter() {
+ return mCallSequencingAdapter;
+ }
}
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index af094b7..b9841ba 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -27,8 +27,6 @@
import static android.Manifest.permission.READ_SMS;
import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
-import static android.telecom.CallAttributes.DIRECTION_INCOMING;
-import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
@@ -52,8 +50,6 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.os.OutcomeReceiver;
import android.os.ParcelFileDescriptor;
import android.os.Process;
@@ -84,15 +80,12 @@
import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.metrics.ApiStats;
import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.settings.BlockedNumbersActivity;
-import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
-import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
import com.android.server.telecom.callsequencing.TransactionManager;
import com.android.server.telecom.callsequencing.CallTransaction;
import com.android.server.telecom.callsequencing.CallTransactionResult;
@@ -147,6 +140,13 @@
UUID.fromString("4edf6c8d-1e43-4c94-b0fc-a40c8d80cfe8");
public static final String PLACE_CALL_SECURITY_EXCEPTION_ERROR_MSG =
"Security exception thrown while placing an outgoing call.";
+ public static final UUID CALL_IS_NULL_OR_ID_MISMATCH_UUID =
+ UUID.fromString("b11f3251-474c-4f90-96d6-a256aebc3c19");
+ public static final String CALL_IS_NULL_OR_ID_MISMATCH_MSG =
+ "call is null or id mismatch";
+ public static final UUID ADD_CALL_ON_ERROR_UUID =
+ UUID.fromString("f8e7d6c5-b4a3-9210-8765-432109abcdef");
+
private static final String TAG = "TelecomServiceImpl";
private static final String TIME_LINE_ARG = "timeline";
private static final int DEFAULT_VIDEO_STATE = -1;
@@ -239,6 +239,11 @@
onAddCallControl(callId, callEventCallback, null,
new CallException(ADD_CALL_ERR_MSG,
CODE_ERROR_UNKNOWN));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ CALL_IS_NULL_OR_ID_MISMATCH_UUID,
+ CALL_IS_NULL_OR_ID_MISMATCH_MSG);
+ }
return;
}
@@ -268,6 +273,11 @@
public void onError(@NonNull CallException exception) {
Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
onAddCallControl(callId, callEventCallback, null, exception);
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ ADD_CALL_ON_ERROR_UUID,
+ exception.getMessage());
+ }
}
});
}
@@ -3024,7 +3034,10 @@
});
mTransactionManager = TransactionManager.getInstance();
- mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags);
+ mTransactionManager.setFeatureFlag(mFeatureFlags);
+ mTransactionManager.setAnomalyReporter(mAnomalyReporter);
+ mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags,
+ mAnomalyReporter);
mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
? mContext.getSystemService(BlockedNumbersManager.class)
: null;
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 5ae459e..954307a 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -35,9 +35,13 @@
private static final Map<PhoneAccountHandle, TransactionalServiceWrapper> mServiceLookupTable =
new HashMap<>();
private final FeatureFlags mFlags;
+ private final AnomalyReporterAdapter mAnomalyReporter;
- public TransactionalServiceRepository(FeatureFlags flags) {
+ public TransactionalServiceRepository(
+ FeatureFlags flags,
+ AnomalyReporterAdapter anomalyReporter) {
mFlags = flags;
+ mAnomalyReporter = anomalyReporter;
}
public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
@@ -50,7 +54,8 @@
Log.d(TAG, "creating a new TSW; handle=[%s]", phoneAccountHandle);
service = new TransactionalServiceWrapper(callEventCallback,
callsManager, phoneAccountHandle, call, this,
- TransactionManager.getInstance(), mFlags.enableCallSequencing());
+ TransactionManager.getInstance(), mFlags.enableCallSequencing(),
+ mFlags, mAnomalyReporter);
} else {
Log.d(TAG, "add a new call to an existing TSW; handle=[%s]", phoneAccountHandle);
service = getTransactionalServiceWrapper(phoneAccountHandle);
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index d63a0bd..cc0d547 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -47,9 +47,11 @@
import com.android.server.telecom.callsequencing.TransactionManager;
import com.android.server.telecom.callsequencing.CallTransaction;
import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.Locale;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -92,7 +94,12 @@
private TransactionManager mTransactionManager;
private CallStreamingController mStreamingController;
private final TransactionalCallSequencingAdapter mCallSequencingAdapter;
-
+ private final FeatureFlags mFeatureFlags;
+ private final AnomalyReporterAdapter mAnomalyReporter;
+ public static final UUID CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID =
+ UUID.fromString("8187cd59-97a7-4e9f-a772-638dda4b69bb");
+ public static final String CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG =
+ "A call update was attempted for a call no longer being tracked";
// Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up
// any calls in the event the application crashes or is force stopped.
@@ -108,7 +115,8 @@
public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
TransactionalServiceRepository repo, TransactionManager transactionManager,
- boolean isCallSequencingEnabled) {
+ boolean isCallSequencingEnabled, FeatureFlags featureFlags,
+ AnomalyReporterAdapter anomalyReporterAdapter) {
// passed args
mICallEventCallback = callEventCallback;
mCallsManager = callsManager;
@@ -123,6 +131,8 @@
mCallSequencingAdapter = new TransactionalCallSequencingAdapter(mTransactionManager,
mCallsManager, isCallSequencingEnabled);
setDeathRecipient(callEventCallback);
+ mFeatureFlags = featureFlags;
+ mAnomalyReporter = anomalyReporterAdapter;
}
public TransactionManager getTransactionManager() {
@@ -307,6 +317,11 @@
+ " via TelecomManager#addCall", action, callId),
CODE_CALL_IS_NOT_BEING_TRACKED));
callback.send(CODE_CALL_IS_NOT_BEING_TRACKED, exceptionBundle);
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID,
+ CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG);
+ }
}
}
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index 69740dd..034f02a 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -20,6 +20,8 @@
import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG;
import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID;
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_ERROR_MSG;
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_ERROR_UUID;
import static com.android.server.telecom.CallsManager.OUTGOING_CALL_STATES;
import static com.android.server.telecom.UserUtil.showErrorDialogForRestrictedOutgoingCall;
@@ -40,21 +42,26 @@
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;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ClockProxy;
import com.android.server.telecom.LogUtils;
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.R;
+import com.android.server.telecom.Timeouts;
import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.stats.CallFailureCause;
import java.util.Objects;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
@@ -66,14 +73,28 @@
*/
public class CallSequencingController {
private final CallsManager mCallsManager;
+ private final ClockProxy mClockProxy;
+ private final AnomalyReporterAdapter mAnomalyReporter;
+ private final Timeouts.Adapter mTimeoutsAdapter;
+ private final TelecomMetricsController mMetricsController;
private final Handler mHandler;
private final Context mContext;
private final FeatureFlags mFeatureFlags;
private static String TAG = CallSequencingController.class.getSimpleName();
+ public static final UUID SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID =
+ UUID.fromString("ea094d77-6ea9-4e40-891e-14bff5d485d7");
+ public static final String SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG =
+ "Cannot hold active call";
public CallSequencingController(CallsManager callsManager, Context context,
+ ClockProxy clockProxy, AnomalyReporterAdapter anomalyReporter,
+ Timeouts.Adapter timeoutsAdapter, TelecomMetricsController metricsController,
FeatureFlags featureFlags) {
mCallsManager = callsManager;
+ mClockProxy = clockProxy;
+ mAnomalyReporter = anomalyReporter;
+ mMetricsController = metricsController;
+ mTimeoutsAdapter = timeoutsAdapter;
HandlerThread handlerThread = new HandlerThread(this.toString());
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
@@ -119,16 +140,18 @@
if (callFuture == null) {
Log.d(this, "createTransactionalOutgoingCall: Outgoing call not permitted at the "
+ "current time.");
- return CompletableFuture.completedFuture(null);
+ return CompletableFuture.completedFuture(new OutgoingCallTransactionSequencing(
+ mCallsManager, null, true /* callNotPermitted */, mFeatureFlags));
}
return callFuture.thenComposeAsync((call) -> CompletableFuture.completedFuture(
new OutgoingCallTransactionSequencing(mCallsManager, callFuture,
- mFeatureFlags)),
+ false /* callNotPermitted */, mFeatureFlags)),
new LoggedHandlerExecutor(mHandler, "CSC.aC", mCallsManager.getLock()));
} else {
Log.d(this, "createTransactionalOutgoingCall: outgoing call not permitted at the "
+ "current time.");
- return CompletableFuture.completedFuture(null);
+ return CompletableFuture.completedFuture(new OutgoingCallTransactionSequencing(
+ mCallsManager, null, true /* callNotPermitted */, mFeatureFlags));
}
}
@@ -198,6 +221,12 @@
callback.onError(
new CallException("activeCall could not be held or disconnected",
CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID,
+ SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG
+ );
+ }
}
return CompletableFuture.completedFuture(result);
}, new LoggedHandlerExecutor(mHandler, "CM.mCAA", mCallsManager.getLock()));
@@ -208,11 +237,11 @@
* call sequencing and the resulting future is an indication of whether that request
* has succeeded.
* @param call The call that's waiting to go active.
- * @return The {@code Pair<CompletableFuture, Boolean>} indicating the result of whether the
- * active call was able to be held (if applicable) and also if sequencing would be
- * required between the last and subsequent transaction to be performed on the call.
+ * @return The {@link CompletableFuture} indicating the result of whether the
+ * active call was able to be held (if applicable).
*/
- private CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(
+ @VisibleForTesting
+ public CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(
Call call) {
Call activeCall = (Call) mCallsManager.getConnectionServiceFocusManager()
.getCurrentFocusCall();
@@ -274,15 +303,21 @@
holdFutureHandler = disconnectFutureHandler
.thenComposeAsync((result) -> {
if (result) {
- return activeCall.hold();
+ return activeCall.hold().thenCompose((holdSuccess) -> {
+ if (holdSuccess) {
+ // Increase hold count only if hold succeeds.
+ call.increaseHeldByThisCallCount();
+ }
+ return CompletableFuture.completedFuture(holdSuccess);
+ });
}
return CompletableFuture.completedFuture(false);
}, new LoggedHandlerExecutor(mHandler,
"CSC.hACFNCWS", mCallsManager.getLock()));
} else {
holdFutureHandler = activeCall.hold();
+ call.increaseHeldByThisCallCount();
}
- call.increaseHeldByThisCallCount();
// Next transaction will be performed on the call passed in and the last transaction
// was performed on the active call so ensure that the caller has this information
// to determine if sequencing is required.
@@ -293,37 +328,33 @@
// This call does not support hold. If it is from a different connection
// service or connection manager, then disconnect it, otherwise allow the connection
// service or connection manager to figure out the right states.
- if (isSequencingRequiredActiveAndCall) {
- Log.i(this, "holdActiveCallForNewCallWithSequencing: disconnecting %s "
- + "so that %s can be made active.", activeCall.getId(), call.getId());
- if (!activeCall.isEmergencyCall()) {
- // We don't want to allow VOIP apps to disconnect carrier calls. We are
- // purposely completing the future with false so that the call isn't
- // answered.
- if (call.isSelfManaged() && !activeCall.isSelfManaged()) {
- Log.w(this, "holdActiveCallForNewCallWithSequencing: ignore "
- + "disconnecting carrier call for making VOIP call active");
- return CompletableFuture.completedFuture(false);
- } else {
- return activeCall.disconnect();
- }
- } else {
- // It's not possible to hold the active call, and it's an emergency call so
- // we will silently reject the incoming call instead of answering it.
- Log.w(this, "holdActiveCallForNewCallWithSequencing: rejecting incoming "
- + "call %s as the active call is an emergency call and "
- + "it cannot be held.", call.getId());
- call.reject(false /* rejectWithMessage */, "" /* message */,
- "active emergency call can't be held");
+ Log.i(this, "holdActiveCallForNewCallWithSequencing: disconnecting %s "
+ + "so that %s can be made active.", activeCall.getId(), call.getId());
+ if (!activeCall.isEmergencyCall()) {
+ // We don't want to allow VOIP apps to disconnect carrier calls. We are
+ // purposely completing the future with false so that the call isn't
+ // answered.
+ if (isSequencingRequiredActiveAndCall && call.isSelfManaged()
+ && !activeCall.isSelfManaged()) {
+ Log.w(this, "holdActiveCallForNewCallWithSequencing: ignore "
+ + "disconnecting carrier call for making VOIP call active");
return CompletableFuture.completedFuture(false);
+ } else {
+ CompletableFuture<Boolean> disconnectFuture = activeCall.disconnect(
+ "Active call disconnected in favor of new call.");
+ return isSequencingRequiredActiveAndCall
+ ? disconnectFuture
+ : CompletableFuture.completedFuture(true);
}
} else {
- // Same source case: if the active call cannot be held, then the user has
- // willingly chosen to accept the incoming call knowing that the active call
- // will be disconnected.
- activeCall.disconnect("Active call disconnected in favor "
- + "of accepting incoming call.");
- return CompletableFuture.completedFuture(true);
+ // It's not possible to hold the active call, and it's an emergency call so
+ // we will silently reject the incoming call instead of answering it.
+ Log.w(this, "holdActiveCallForNewCallWithSequencing: rejecting incoming "
+ + "call %s as the active call is an emergency call and "
+ + "it cannot be held.", call.getId());
+ call.reject(false /* rejectWithMessage */, "" /* message */,
+ "active emergency call can't be held");
+ return CompletableFuture.completedFuture(false);
}
}
}
@@ -362,20 +393,15 @@
// is managed, abort the transaction. Otherwise, disconnect the call. We also
// don't want to drop an emergency call.
if (!activeCall.isEmergencyCall()) {
- if (!activeCall.isSelfManaged() && call.isSelfManaged()) {
- Log.w(this, "unholdCall: %s and %s are using different phone accounts. "
- + "Aborting swap to %s", activeCallId, call.getId(),
- call.getId());
- return;
- } else {
- unholdCallFutureHandler = activeCall.disconnect("Swap to "
- + call.getId());
- }
+ Log.w(this, "unholdCall: Unable to hold the active call (%s),"
+ + " aborting swap to %s", activeCallId, call.getId(),
+ call.getId());
+ showErrorDialogForCannotHoldCall(call, false);
} else {
Log.w(this, "unholdCall: %s is an emergency call, aborting swap to %s",
activeCallId, call.getId());
- return;
}
+ return;
} else {
activeCall.hold("Swap to " + call.getId());
}
@@ -593,9 +619,9 @@
// will not be that one and we do not want multiple PhoneAccounts active during an
// emergency call if possible. Disconnect the active call in favor of the emergency call
// instead of trying to hold.
- if (liveCall.getTargetPhoneAccount() != null) {
+ if (liveCallPhoneAccount != null) {
PhoneAccount pa = mCallsManager.getPhoneAccountRegistrar().getPhoneAccountUnchecked(
- liveCall.getTargetPhoneAccount());
+ liveCallPhoneAccount);
if((pa.getCapabilities() & PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) == 0) {
liveCall.setOverrideDisconnectCauseCode(new DisconnectCause(
DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
@@ -619,8 +645,6 @@
} else {
return liveCall.disconnect(disconnectReason);
}
- } else {
- return CompletableFuture.completedFuture(true);
}
}
@@ -685,26 +709,48 @@
return CompletableFuture.completedFuture(true);
}
- CompletableFuture<Boolean> disconnectFuture = mCallsManager
- .maybeDisconnectExistingCallForNewOutgoingCall(call, liveCall);
- if (disconnectFuture != null) {
- return disconnectFuture;
+ // If the live call is stuck in a connecting state for longer than the transitory timeout,
+ // then we should disconnect it in favor of the new outgoing call and prompt the user to
+ // generate a bugreport.
+ // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
+ // live call stuck in the connecting state. Unfortunately that code will get tripped up by
+ // calls that have a longer than expected new outgoing call broadcast response time. This
+ // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
+ // block outgoing calls. However, if the user dials two calls in quick succession it will
+ // result in both calls getting disconnected, which is not optimal.
+ if (liveCall.getState() == CallState.CONNECTING
+ && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
+ > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_STUCK_CONNECTING);
+ }
+ mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
+ LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
+ return liveCall.disconnect("Force disconnect CONNECTING call.");
}
- // TODO: Remove once b/23035408 has been corrected.
- // If the live call is a conference, it will not have a target phone account set. This
- // means the check to see if the live call has the same target phone account as the new
- // call will not cause us to bail early. As a result, we'll end up holding the
- // ongoing conference call. However, the ConnectionService is already doing that. This
- // has caused problems with some carriers. As a workaround until b/23035408 is
- // corrected, we will try and get the target phone account for one of the conference's
- // children and use that instead.
- PhoneAccountHandle liveCallPhoneAccount = liveCall.getTargetPhoneAccount();
- if (liveCallPhoneAccount == null && liveCall.isConference() &&
- !liveCall.getChildCalls().isEmpty()) {
- liveCallPhoneAccount = mCallsManager.getFirstChildPhoneAccount(liveCall);
- Log.i(this, "makeRoomForOutgoingCall: using child call PhoneAccount = " +
- liveCallPhoneAccount);
+ if (mCallsManager.hasMaximumOutgoingCalls(call)) {
+ Call outgoingCall = mCallsManager.getFirstCallWithState(OUTGOING_CALL_STATES);
+ if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
+ // If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
+ // state, just disconnect it since the user has explicitly started a new call.
+ call.getAnalytics().setCallIsAdditional(true);
+ outgoingCall.getAnalytics().setCallIsInterrupted(true);
+ return outgoingCall.disconnect(
+ "Disconnecting call in SELECT_PHONE_ACCOUNT in favor of new "
+ + "outgoing call.");
+ }
+ showErrorDialogForMaxOutgoingCall(call);
+ return CompletableFuture.completedFuture(false);
+ }
+
+ // Early check to see if we already have a held call + live call. It's possible if a device
+ // switches to DSDS with two ongoing calls for the phone account to be null in which case
+ // we will return true from this method and report a different failure cause instead.
+ if (mCallsManager.hasMaximumManagedHoldingCalls(call) && !mCallsManager.canHold(liveCall)) {
+ showErrorDialogForMaxOutgoingCall(call);
+ return CompletableFuture.completedFuture(false);
}
if (call.getTargetPhoneAccount() == null) {
@@ -726,19 +772,7 @@
}
// The live call cannot be held so we're out of luck here. There's no room.
- int stringId;
- String reason;
- if (mCallsManager.hasMaximumManagedHoldingCalls(call)) {
- call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
- stringId = R.string.callFailed_too_many_calls;
- reason = " there are two calls already in progress. Disconnect one of the calls "
- + "or merge the calls.";
- } else {
- call.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
- stringId = R.string.callFailed_unholdable_call;
- reason = " unable to hold live call. Disconnect the unholdable call.";
- }
- showErrorDialogForRestrictedOutgoingCall(mContext, stringId, TAG, reason);
+ showErrorDialogForCannotHoldCall(call, true);
return CompletableFuture.completedFuture(false);
}
@@ -819,14 +853,8 @@
CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true);
}
- /* Miscellaneous helpers/getters */
-
- /**
- * Checks if the phone accounts for any two calls is the same. This is used to determine if
- * call sequencing is required between the two calls. That is if the phone accounts are
- * different, then sequencing is required.
- */
- private boolean arePhoneAccountsSame(Call call1, Call call2) {
+ @VisibleForTesting
+ public boolean arePhoneAccountsSame(Call call1, Call call2) {
if (call1 == null || call2 == null) {
return false;
}
@@ -847,6 +875,42 @@
&& callToUnhold.getState() == CallState.ON_HOLD;
}
+ /**
+ * Generic helper to log the result of the {@link CompletableFuture} containing the transactions
+ * that are being processed in the context of call sequencing.
+ * @param future The {@link CompletableFuture} encompassing the transaction that's being
+ * computed.
+ * @param methodName The method name to describe the type of transaction being processed.
+ * @param sessionName The session name to identify the log.
+ * @param successMsg The message to be logged if the transaction succeeds.
+ * @param failureMsg The message to be logged if the transaction fails.
+ */
+ public void logFutureResultTransaction(CompletableFuture<Boolean> future, String methodName,
+ String sessionName, String successMsg, String failureMsg) {
+ future.thenApplyAsync((result) -> {
+ String msg = methodName + ": " + (result ? successMsg : failureMsg);
+ Log.i(this, msg);
+ return CompletableFuture.completedFuture(result);
+ }, new LoggedHandlerExecutor(mHandler, sessionName, mCallsManager.getLock()));
+ }
+
+ private void showErrorDialogForMaxOutgoingCall(Call call) {
+ call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
+ int stringId = R.string.callFailed_too_many_calls;
+ String reason = " there are two calls already in progress. Disconnect one of the calls "
+ + "or merge the calls.";
+ showErrorDialogForRestrictedOutgoingCall(mContext, stringId, TAG, reason);
+ }
+
+ private void showErrorDialogForCannotHoldCall(Call call, boolean setCallFailure) {
+ if (setCallFailure) {
+ call.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
+ }
+ int stringId = R.string.callFailed_unholdable_call;
+ String reason = " unable to hold live call. Disconnect the unholdable call.";
+ showErrorDialogForRestrictedOutgoingCall(mContext, stringId, TAG, reason);
+ }
+
public Handler getHandler() {
return mHandler;
}
diff --git a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
index df0837d..101f570 100644
--- a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
@@ -21,9 +21,12 @@
import android.os.OutcomeReceiver;
import android.telecom.CallAttributes;
import android.telecom.CallException;
+import android.telecom.Connection;
import android.telecom.Log;
import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
@@ -39,42 +42,23 @@
private final CallsManager mCallsManager;
private final CallSequencingController mSequencingController;
+ private final CallAudioManager mCallAudioManager;
private final Handler mHandler;
private final FeatureFlags mFeatureFlags;
private final boolean mIsCallSequencingEnabled;
public CallsManagerCallSequencingAdapter(CallsManager callsManager,
- CallSequencingController sequencingController,
+ CallSequencingController sequencingController, CallAudioManager callAudioManager,
FeatureFlags featureFlags) {
mCallsManager = callsManager;
mSequencingController = sequencingController;
+ mCallAudioManager = callAudioManager;
mHandler = sequencingController.getHandler();
mFeatureFlags = featureFlags;
mIsCallSequencingEnabled = featureFlags.enableCallSequencing();
}
/**
- * Helps create the transaction representing the outgoing transactional call. For outgoing
- * calls, there can be more than one transaction that will need to complete when
- * mIsCallSequencingEnabled is true. Otherwise, rely on the old behavior of creating an
- * {@link OutgoingCallTransaction}.
- * @param callAttributes The call attributes associated with the call.
- * @param extras The extras that are associated with the call.
- * @param callingPackage The calling package representing where the request was invoked from.
- * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
- * place/receive the transactional call.
- */
- public CompletableFuture<CallTransaction> createTransactionalOutgoingCall(String callId,
- CallAttributes callAttributes, Bundle extras, String callingPackage) {
- return mIsCallSequencingEnabled
- ? mSequencingController.createTransactionalOutgoingCall(callId,
- callAttributes, extras, callingPackage)
- : CompletableFuture.completedFuture(new OutgoingCallTransaction(callId,
- mCallsManager.getContext(), callAttributes, mCallsManager, extras,
- mFeatureFlags));
- }
-
- /**
* Conditionally try to answer the call depending on whether call sequencing
* (mIsCallSequencingEnabled) is enabled.
* @param incomingCall The incoming call that should be answered.
@@ -109,10 +93,8 @@
public void holdCall(Call call) {
// Sequencing already taken care of for CSW/TSW in Call class.
CompletableFuture<Boolean> holdFuture = call.hold();
- if (mIsCallSequencingEnabled) {
- logFutureResultTransaction(holdFuture, "holdCall", "CMCSA.hC",
- "hold call transaction succeeded.", "hold call transaction failed.");
- }
+ maybeLogFutureResultTransaction(holdFuture, "holdCall", "CMCSA.hC",
+ "hold call transaction succeeded.", "hold call transaction failed.");
}
/**
@@ -167,22 +149,115 @@
}
/**
- * Attempts to hold the active call for transactional call cases with call sequencing support
- * if mIsCallSequencingEnabled is true.
+ * Helps create the transaction representing the outgoing transactional call. For outgoing
+ * calls, there can be more than one transaction that will need to complete when
+ * mIsCallSequencingEnabled is true. Otherwise, rely on the old behavior of creating an
+ * {@link OutgoingCallTransaction}.
+ * @param callAttributes The call attributes associated with the call.
+ * @param extras The extras that are associated with the call.
+ * @param callingPackage The calling package representing where the request was invoked from.
+ * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
+ * place/receive the transactional call.
+ */
+ public CompletableFuture<CallTransaction> createTransactionalOutgoingCall(String callId,
+ CallAttributes callAttributes, Bundle extras, String callingPackage) {
+ return mIsCallSequencingEnabled
+ ? mSequencingController.createTransactionalOutgoingCall(callId,
+ callAttributes, extras, callingPackage)
+ : CompletableFuture.completedFuture(new OutgoingCallTransaction(callId,
+ mCallsManager.getContext(), callAttributes, mCallsManager, extras,
+ mFeatureFlags));
+ }
+
+ /**
+ * attempt to hold or swap the current active call in favor of a new call request. The
+ * OutcomeReceiver will return onResult if the current active call is held or disconnected.
+ * Otherwise, the OutcomeReceiver will fail.
* @param newCall The new (transactional) call that's waiting to go active.
- * @param activeCall The currently active call.
- * @param callback The callback to report the result of the aforementioned hold transaction.
- * @return {@code CompletableFuture} indicating the result of holding the active call.
+ * @param isCallControlRequest Indication of whether this is a call control request.
+ * @param callback The callback to report the result of the aforementioned hold
+ * transaction.
*/
public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
- Call activeCall, OutcomeReceiver<Boolean, CallException> callback) {
- if (mIsCallSequencingEnabled) {
- mSequencingController.transactionHoldPotentialActiveCallForNewCallSequencing(
- newCall, callback);
- } else {
- mCallsManager.transactionHoldPotentialActiveCallForNewCallOld(newCall,
- activeCall, callback);
+ boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
+ String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
+ Call activeCall = (Call) mCallsManager.getConnectionServiceFocusManager()
+ .getCurrentFocusCall();
+ Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
+
+ if (activeCall == null || activeCall == newCall) {
+ Log.i(this, mTag + "no need to hold activeCall");
+ callback.onResult(true);
+ return;
}
+
+ if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
+ // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
+ // notify the user that they need to disconnect the ongoing call before making the
+ // new call ACTIVE.
+ if (isCallControlRequest
+ && !mCallsManager.canHoldOrSwapActiveCall(activeCall, newCall)) {
+ Log.i(this, mTag + "CallControlRequest exit");
+ callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
+ + " request the user disconnect the call.",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ return;
+ }
+
+ if (mIsCallSequencingEnabled) {
+ mSequencingController.transactionHoldPotentialActiveCallForNewCallSequencing(
+ newCall, callback);
+ } else {
+ // The code path without sequencing but where transactionalHoldDisconnectsUnholdable
+ // flag is enabled.
+ mCallsManager.transactionHoldPotentialActiveCallForNewCallOld(newCall,
+ activeCall, callback);
+ }
+ } else {
+ // The unflagged path (aka original code with no flags).
+ mCallsManager.transactionHoldPotentialActiveCallForNewCallUnflagged(activeCall,
+ newCall, callback);
+ }
+ }
+
+ /**
+ * Attempts to move the held call to the foreground in cases where we need to auto-unhold the
+ * call.
+ */
+ public void maybeMoveHeldCallToForeground(Call removedCall, boolean isLocallyDisconnecting) {
+ CompletableFuture<Boolean> unholdForegroundCallFuture = null;
+ Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
+ if (isLocallyDisconnecting) {
+ boolean isDisconnectingChildCall = removedCall.isDisconnectingChildCall();
+ Log.v(this, "maybeMoveHeldCallToForeground: isDisconnectingChildCall = "
+ + isDisconnectingChildCall + "call -> %s", removedCall);
+ // Auto-unhold the foreground call due to a locally disconnected call, except if the
+ // call which was disconnected is a member of a conference (don't want to auto
+ // un-hold the conference if we remove a member of the conference).
+ // Also, ensure that the call we're removing is from the same ConnectionService as
+ // the one we're removing. We don't want to auto-unhold between ConnectionService
+ // implementations, especially if one is managed and the other is a VoIP CS.
+ if (!isDisconnectingChildCall && foregroundCall != null
+ && foregroundCall.getState() == CallState.ON_HOLD
+ && CallsManager.areFromSameSource(foregroundCall, removedCall)) {
+
+ unholdForegroundCallFuture = foregroundCall.unhold();
+ }
+ } else if (foregroundCall != null &&
+ !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
+ foregroundCall.getState() == CallState.ON_HOLD) {
+
+ // The new foreground call is on hold, however the carrier does not display the hold
+ // button in the UI. Therefore, we need to auto unhold the held call since the user
+ // has no means of unholding it themselves.
+ Log.i(this, "maybeMoveHeldCallToForeground: Auto-unholding held foreground call (call "
+ + "doesn't support hold)");
+ unholdForegroundCallFuture = foregroundCall.unhold();
+ }
+ maybeLogFutureResultTransaction(unholdForegroundCallFuture,
+ "maybeMoveHeldCallToForeground", "CM.mMHCTF",
+ "Successfully unheld the foreground call.",
+ "Failed to unhold the foreground call.");
}
/**
@@ -195,14 +270,12 @@
* @param successMsg The message to be logged if the transaction succeeds.
* @param failureMsg The message to be logged if the transaction fails.
*/
- public void logFutureResultTransaction(CompletableFuture<Boolean> future, String methodName,
- String sessionName, String successMsg, String failureMsg) {
- future.thenApplyAsync((result) -> {
- StringBuilder msg = new StringBuilder(methodName).append(": ");
- msg.append(result ? successMsg : failureMsg);
- Log.i(this, String.valueOf(msg));
- return CompletableFuture.completedFuture(result);
- }, new LoggedHandlerExecutor(mHandler, sessionName, mCallsManager.getLock()));
+ public void maybeLogFutureResultTransaction(CompletableFuture<Boolean> future,
+ String methodName, String sessionName, String successMsg, String failureMsg) {
+ if (mFeatureFlags.enableCallSequencing() && future != null) {
+ mSequencingController.logFutureResultTransaction(future, methodName, sessionName,
+ successMsg, failureMsg);
+ }
}
public Handler getHandler() {
diff --git a/src/com/android/server/telecom/callsequencing/TransactionManager.java b/src/com/android/server/telecom/callsequencing/TransactionManager.java
index 2a6431b..98d54da 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionManager.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionManager.java
@@ -25,6 +25,8 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.flags.Flags;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -32,6 +34,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Queue;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class TransactionManager {
@@ -43,6 +46,12 @@
private final Deque<CallTransaction> mCompletedTransactions;
private CallTransaction mCurrentTransaction;
private boolean mProcessingCallSequencing;
+ private AnomalyReporterAdapter mAnomalyReporter;
+ private FeatureFlags mFeatureFlags;
+ public static final UUID TRANSACTION_MANAGER_TIMEOUT_UUID =
+ UUID.fromString("9ccce52e-6694-4357-9e5e-516a9531b062");
+ public static final String TRANSACTION_MANAGER_TIMEOUT_MSG =
+ "TransactionManager hit a timeout while processing a transaction";
public interface TransactionCompleteListener {
void onTransactionCompleted(CallTransactionResult result, String transactionName);
@@ -67,6 +76,14 @@
return INSTANCE;
}
+ public void setFeatureFlag(FeatureFlags flag){
+ mFeatureFlags = flag;
+ }
+
+ public void setAnomalyReporter(AnomalyReporterAdapter callAnomalyReporter){
+ mAnomalyReporter = callAnomalyReporter;
+ }
+
@VisibleForTesting
public static TransactionManager getTestInstance() {
return new TransactionManager();
@@ -109,6 +126,12 @@
receiver.onError(new CallException(transactionName + " timeout",
CODE_OPERATION_TIMED_OUT));
transactionCompleteFuture.complete(false);
+ if (mFeatureFlags != null && mAnomalyReporter != null &&
+ mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ TRANSACTION_MANAGER_TIMEOUT_UUID,
+ TRANSACTION_MANAGER_TIMEOUT_MSG);
+ }
} catch (Exception e) {
Log.e(TAG, String.format("onTransactionTimeout: Notifying transaction "
+ " %s resulted in an Exception.", transactionName), e);
diff --git a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
index 570c2cc..4adc8d0 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
@@ -254,7 +254,7 @@
}
};
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(call,
+ mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(call,
isCallControlRequest, maybePerformHoldCallback);
return createSetActiveFuture[0];
}
diff --git a/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
index 32062b5..cb839dc 100644
--- a/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
@@ -52,8 +52,8 @@
Log.d(TAG, "processTransaction");
CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, mIsCallControlRequest,
- new OutcomeReceiver<>() {
+ mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(
+ mCall, mIsCallControlRequest, new OutcomeReceiver<>() {
@Override
public void onResult(Boolean result) {
Log.d(TAG, "processTransaction: onResult");
diff --git a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
index c38b55d..af6af34 100644
--- a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
+++ b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
@@ -20,6 +20,7 @@
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;
@@ -35,20 +36,23 @@
private static final String TAG = OutgoingCallTransactionSequencing.class.getSimpleName();
private final CompletableFuture<Call> mCallFuture;
private final CallsManager mCallsManager;
+ private final boolean mCallNotPermitted;
private FeatureFlags mFeatureFlags;
public OutgoingCallTransactionSequencing(CallsManager callsManager,
- CompletableFuture<Call> callFuture, FeatureFlags featureFlags) {
+ CompletableFuture<Call> callFuture, boolean callNotPermitted,
+ FeatureFlags featureFlags) {
super(callsManager.getLock());
mCallsManager = callsManager;
mCallFuture = callFuture;
+ mCallNotPermitted = callNotPermitted;
mFeatureFlags = featureFlags;
}
@Override
public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.d(TAG, "processTransaction");
- if (mCallFuture == null) {
+ if (mCallNotPermitted) {
return CompletableFuture.completedFuture(
new CallTransactionResult(
CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
@@ -60,4 +64,9 @@
mCallsManager, mFeatureFlags)
, new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
}
+
+ @VisibleForTesting
+ public boolean getCallNotPermitted() {
+ return mCallNotPermitted;
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallSequencingTests.java b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
new file mode 100644
index 0000000..22427cb
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
@@ -0,0 +1,669 @@
+/*
+ * 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 com.android.server.telecom.UserUtil.showErrorDialogForRestrictedOutgoingCall;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.TestCase.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.telecom.Connection;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.Analytics;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.callsequencing.CallSequencingController;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+import com.android.server.telecom.stats.CallFailureCause;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class CallSequencingTests extends TelecomTestCase {
+ private static final long SEQUENCING_TIMEOUT_MS = 2000L;
+ private static final PhoneAccountHandle mHandle1 = new PhoneAccountHandle(
+ new ComponentName("foo", "bar"), "1");
+ private static final PhoneAccountHandle mHandle2 = new PhoneAccountHandle(
+ new ComponentName("bar", "foo"), "2");
+ private static final String TEST_NAME = "Alan Turing";
+ private static final Uri TEST_URI = Uri.fromParts("tel", "abc", "123");
+ private static final String ACTIVE_CALL_ID = "TC@1";
+ private static final String NEW_CALL_ID = "TC@2";
+
+ private CallSequencingController mController;
+ @Mock
+ private CallsManager mCallsManager;
+ @Mock Context mContext;
+ @Mock ClockProxy mClockProxy;
+ @Mock AnomalyReporterAdapter mAnomalyReporter;
+ @Mock Timeouts.Adapter mTimeoutsAdapter;
+ @Mock TelecomMetricsController mMetricsController;
+ @Mock
+ ConnectionServiceFocusManager mConnectionServiceFocusManager;
+ @Mock Call mActiveCall;
+ @Mock Call mHeldCall;
+ @Mock Call mNewCall;
+ @Mock Call mRingingCall;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ when(mFeatureFlags.enableCallSequencing()).thenReturn(true);
+ mController = new CallSequencingController(mCallsManager, mContext, mClockProxy,
+ mAnomalyReporter, mTimeoutsAdapter, mMetricsController, mFeatureFlags);
+
+ when(mActiveCall.getState()).thenReturn(CallState.ACTIVE);
+ when(mRingingCall.getState()).thenReturn(CallState.RINGING);
+ when(mHeldCall.getState()).thenReturn(CallState.ON_HOLD);
+
+ when(mActiveCall.getId()).thenReturn(ACTIVE_CALL_ID);
+ when(mNewCall.getId()).thenReturn(NEW_CALL_ID);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+
+ @Test
+ @SmallTest
+ public void testTransactionOutgoingCall_CallNotPermitted() {
+ String callingPkg = "testPkg";
+ CallAttributes outgoingCallAttributes = getOutgoingCallAttributes();
+
+ // Outgoing call is not permitted
+ when(mCallsManager.isOutgoingCallPermitted(mHandle1)).thenReturn(false);
+ CompletableFuture<CallTransaction> transactionFuture = mController
+ .createTransactionalOutgoingCall("callId", outgoingCallAttributes,
+ new Bundle(), callingPkg);
+ OutgoingCallTransactionSequencing transaction = (OutgoingCallTransactionSequencing)
+ transactionFuture.getNow(null);
+ assertNotNull(transaction);
+ assertTrue(transaction.getCallNotPermitted());
+
+ // Call future is null
+ when(mCallsManager.isOutgoingCallPermitted(mHandle1)).thenReturn(true);
+ when(mCallsManager.startOutgoingCall(any(Uri.class), any(PhoneAccountHandle.class),
+ any(Bundle.class), any(UserHandle.class), any(Intent.class), anyString()))
+ .thenReturn(null);
+ transactionFuture = mController
+ .createTransactionalOutgoingCall("callId", outgoingCallAttributes,
+ new Bundle(), callingPkg);
+ transaction = (OutgoingCallTransactionSequencing) transactionFuture
+ .getNow(null);
+ assertNotNull(transaction);
+ assertTrue(transaction.getCallNotPermitted());
+ }
+
+ @Test
+ @SmallTest
+ public void testTransactionOutgoingCall() {
+ String callingPkg = "testPkg";
+ CallAttributes outgoingCallAttributes = getOutgoingCallAttributes();
+
+ when(mCallsManager.isOutgoingCallPermitted(mHandle1)).thenReturn(true);
+ when(mCallsManager.startOutgoingCall(any(Uri.class), any(PhoneAccountHandle.class),
+ any(Bundle.class), any(UserHandle.class), any(Intent.class), anyString()))
+ .thenReturn(CompletableFuture.completedFuture(mNewCall));
+ CompletableFuture<CallTransaction> transactionFuture = mController
+ .createTransactionalOutgoingCall("callId", outgoingCallAttributes,
+ new Bundle(), callingPkg);
+ try {
+ OutgoingCallTransactionSequencing transaction = (OutgoingCallTransactionSequencing)
+ transactionFuture.get(SEQUENCING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ assertNotNull(transaction);
+ assertFalse(transaction.getCallNotPermitted());
+ } catch (Exception e) {
+ fail("Failed to retrieve future in allocated time (" + SEQUENCING_TIMEOUT_MS + ").");
+ }
+ }
+
+ @SmallTest
+ @Test
+ public void testAnswerCall() {
+ // This will allow holdActiveCallForNewCallWithSequencing to immediately return true
+ setActiveCallFocus(null);
+ mController.answerCall(mNewCall, 0);
+ verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+ .requestFocusActionAnswerCall(eq(mNewCall), eq(0));
+ }
+
+ @SmallTest
+ @Test
+ public void testAnswerCallFail() {
+ setupHoldActiveCallForNewCallFailMocks();
+ mController.answerCall(mNewCall, 0);
+ verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+ .requestFocusActionAnswerCall(eq(mNewCall), eq(0));
+ }
+
+ @SmallTest
+ @Test
+ public void testSetSelfManagedCallActive() {
+ // This will allow holdActiveCallForNewCallWithSequencing to immediately return true
+ setActiveCallFocus(null);
+ mController.handleSetSelfManagedCallActive(mNewCall);
+ verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+ .requestActionSetActiveCall(eq(mNewCall), anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testSetSelfManagedCallActiveFail() {
+ setupHoldActiveCallForNewCallFailMocks();
+ mController.handleSetSelfManagedCallActive(mNewCall);
+ verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+ .requestActionSetActiveCall(eq(mNewCall), anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testTransactionHoldActiveCallForNewCall() throws InterruptedException {
+ // This will allow holdActiveCallForNewCallWithSequencing to immediately return true
+ setActiveCallFocus(null);
+ CountDownLatch latch = new CountDownLatch(1);
+ OutcomeReceiver<Boolean, CallException> callback = new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Boolean result) {
+ // Expected result
+ latch.countDown();
+ }
+ @Override
+ public void onError(CallException exception) {
+ }
+ };
+ verifyTransactionHoldActiveCallForNewCall(callback, latch);
+ }
+
+ @SmallTest
+ @Test
+ public void testTransactionHoldActiveCallForNewCallFail() {
+ setupHoldActiveCallForNewCallFailMocks();
+ CountDownLatch latch = new CountDownLatch(1);
+ OutcomeReceiver<Boolean, CallException> callback = new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Boolean result) {
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ // Expected result
+ latch.countDown();
+ }
+ };
+ verifyTransactionHoldActiveCallForNewCall(callback, latch);
+ }
+
+ @Test
+ @SmallTest
+ public void testHoldCallForNewCall_NoActiveCall() {
+ setActiveCallFocus(null);
+ CompletableFuture<Boolean> resultFuture = mController
+ .holdActiveCallForNewCallWithSequencing(mNewCall);
+ assertTrue(waitForFutureResult(resultFuture, false));
+ }
+
+ @Test
+ @SmallTest
+ public void testHoldCallForNewCall_CanHold() {
+ setPhoneAccounts(mNewCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+ when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(true));
+
+ // Cross phone account case (sequencing enabled)
+ assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+ CompletableFuture<Boolean> resultFuture = mController
+ .holdActiveCallForNewCallWithSequencing(mNewCall);
+ assertTrue(waitForFutureResult(resultFuture, false));
+
+ // Same phone account case
+ setPhoneAccounts(mNewCall, mActiveCall, true);
+ assertTrue(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+ resultFuture = mController.holdActiveCallForNewCallWithSequencing(mNewCall);
+ assertTrue(waitForFutureResult(resultFuture, false));
+ }
+
+ @Test
+ @SmallTest
+ public void testHoldCallForNewCall_SupportsHold() {
+ setPhoneAccounts(mNewCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+ when(mCallsManager.supportsHold(mActiveCall)).thenReturn(true);
+ when(mCallsManager.getFirstCallWithState(anyInt())).thenReturn(mHeldCall);
+ when(mHeldCall.isSelfManaged()).thenReturn(true);
+ when(mNewCall.isSelfManaged()).thenReturn(false);
+ when(mHeldCall.disconnect()).thenReturn(CompletableFuture.completedFuture(true));
+ when(mActiveCall.hold()).thenReturn(CompletableFuture.completedFuture(true));
+
+ // Verify that we abort transaction when there's a new (VOIP) call and we're trying to
+ // disconnect the active (carrier) call.
+ assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+ CompletableFuture<Boolean> resultFuture = mController
+ .holdActiveCallForNewCallWithSequencing(mNewCall);
+ verify(mHeldCall, timeout(SEQUENCING_TIMEOUT_MS)).disconnect();
+ verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold();
+ verify(mNewCall).increaseHeldByThisCallCount();
+ assertTrue(waitForFutureResult(resultFuture, false));
+ }
+
+ @Test
+ @SmallTest
+ public void testHoldCallForNewCall_SupportsHold_NoHeldCall() {
+ setPhoneAccounts(mNewCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+ when(mCallsManager.supportsHold(mActiveCall)).thenReturn(true);
+ when(mCallsManager.getFirstCallWithState(anyInt())).thenReturn(null);
+ when(mActiveCall.hold()).thenReturn(CompletableFuture.completedFuture(true));
+
+ // Cross phone account case (sequencing enabled)
+ assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+ CompletableFuture<Boolean> resultFuture = mController
+ .holdActiveCallForNewCallWithSequencing(mNewCall);
+ verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold();
+ verify(mNewCall).increaseHeldByThisCallCount();
+ assertTrue(waitForFutureResult(resultFuture, false));
+ }
+
+ @Test
+ @SmallTest
+ public void testHoldCallForNewCall_DoesNotSupportHold_Disconnect() {
+ setPhoneAccounts(mNewCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+ when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+ when(mActiveCall.disconnect(anyString())).thenReturn(
+ CompletableFuture.completedFuture(true));
+ when(mActiveCall.isEmergencyCall()).thenReturn(false);
+
+ assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+ CompletableFuture<Boolean> resultFuture = mController
+ .holdActiveCallForNewCallWithSequencing(mNewCall);
+ verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).disconnect(anyString());
+ assertTrue(waitForFutureResult(resultFuture, false));
+ }
+
+ @Test
+ @SmallTest
+ public void testHoldCallForNewCallFail_SupportsHold_VoipPstn() {
+ setPhoneAccounts(mNewCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+ when(mCallsManager.supportsHold(mActiveCall)).thenReturn(true);
+ when(mCallsManager.getFirstCallWithState(anyInt())).thenReturn(mHeldCall);
+ when(mHeldCall.isSelfManaged()).thenReturn(false);
+ when(mNewCall.isSelfManaged()).thenReturn(true);
+
+ // Verify that we abort transaction when there's a new (VOIP) call and we're trying to
+ // disconnect the active (carrier) call.
+ assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+ CompletableFuture<Boolean> resultFuture = mController
+ .holdActiveCallForNewCallWithSequencing(mNewCall);
+ assertFalse(waitForFutureResult(resultFuture, true));
+ }
+
+ @Test
+ @SmallTest
+ public void testHoldCallForNewCallFail_DoesNotSupportHold_Reject() {
+ setPhoneAccounts(mNewCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+ when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+ when(mNewCall.reject(anyBoolean(), anyString(), anyString()))
+ .thenReturn(CompletableFuture.completedFuture(true));
+ when(mActiveCall.isEmergencyCall()).thenReturn(true);
+
+ assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+ CompletableFuture<Boolean> resultFuture = mController
+ .holdActiveCallForNewCallWithSequencing(mNewCall);
+ verify(mNewCall, timeout(SEQUENCING_TIMEOUT_MS)).reject(
+ anyBoolean(), anyString(), anyString());
+ assertFalse(waitForFutureResult(resultFuture, true));
+ }
+
+ @Test
+ @SmallTest
+ public void testHoldCallForNewCallFail_DoesNotSupportHold_Abort() {
+ setPhoneAccounts(mNewCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+ when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+ when(mActiveCall.isEmergencyCall()).thenReturn(false);
+ when(mActiveCall.isSelfManaged()).thenReturn(false);
+ when(mNewCall.isSelfManaged()).thenReturn(true);
+
+ assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+ CompletableFuture<Boolean> resultFuture = mController
+ .holdActiveCallForNewCallWithSequencing(mNewCall);
+ assertFalse(waitForFutureResult(resultFuture, true));
+ }
+
+ @Test
+ @SmallTest
+ public void testUnholdCallNoActiveCall() {
+ setActiveCallFocus(null);
+ mController.unholdCall(mHeldCall);
+ verify(mCallsManager).requestActionUnholdCall(eq(mHeldCall), eq(null));
+ }
+
+ @Test
+ @SmallTest
+ public void testUnholdCallSwapCase() {
+ when(mActiveCall.can(eq(Connection.CAPABILITY_SUPPORT_HOLD))).thenReturn(true);
+ when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(true));
+ when(mActiveCall.isLocallyDisconnecting()).thenReturn(false);
+ setPhoneAccounts(mHeldCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+
+ mController.unholdCall(mHeldCall);
+ assertFalse(mController.arePhoneAccountsSame(mActiveCall, mHeldCall));
+ verify(mActiveCall).hold(anyString());
+ verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+ .requestActionUnholdCall(eq(mHeldCall), eq(ACTIVE_CALL_ID));
+ }
+
+ @Test
+ @SmallTest
+ public void testUnholdCallFail_DoesNotSupportHold() {
+ when(mActiveCall.can(eq(Connection.CAPABILITY_SUPPORT_HOLD))).thenReturn(false);
+ when(mActiveCall.isEmergencyCall()).thenReturn(true);
+ when(mActiveCall.isLocallyDisconnecting()).thenReturn(false);
+ setPhoneAccounts(mHeldCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+
+ // Emergency call case
+ mController.unholdCall(mHeldCall);
+ assertFalse(mController.arePhoneAccountsSame(mActiveCall, mHeldCall));
+ verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+ .requestActionUnholdCall(eq(mHeldCall), anyString());
+ }
+
+ @Test
+ @SmallTest
+ public void testUnholdFail() {
+ // Fail the hold.
+ when(mActiveCall.can(eq(Connection.CAPABILITY_SUPPORT_HOLD))).thenReturn(true);
+ when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(false));
+ when(mActiveCall.isLocallyDisconnecting()).thenReturn(false);
+ // Use different phone accounts so that the sequencing code path is hit.
+ setPhoneAccounts(mHeldCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+
+ mController.unholdCall(mHeldCall);
+ assertFalse(mController.arePhoneAccountsSame(mActiveCall, mHeldCall));
+ verify(mActiveCall).hold(anyString());
+ // Verify unhold is never reached.
+ verify(mCallsManager, never())
+ .requestActionUnholdCall(eq(mHeldCall), anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingEmergencyCall_SamePkg() {
+ // Ensure that the live call and emergency call are from the same pkg.
+ when(mActiveCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+ when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+ when(mRingingCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+ setupMakeRoomForOutgoingEmergencyCallMocks();
+
+ CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(true, mNewCall);
+ verify(mRingingCall)
+ .reject(anyBoolean(), eq(null), anyString());
+ verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold(anyString());
+ assertTrue(waitForFutureResult(future, false));
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingEmergencyCall_CanHold() {
+ // Ensure that the live call and emergency call are from different pkgs.
+ when(mActiveCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+ when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+ when(mRingingCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+ setupMakeRoomForOutgoingEmergencyCallMocks();
+
+ CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(true, mNewCall);
+ verify(mRingingCall)
+ .reject(anyBoolean(), eq(null), anyString());
+ verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold(anyString());
+ assertTrue(waitForFutureResult(future, false));
+ }
+
+ @Test
+ @SmallTest
+ public void testMakeRoomForOutgoingCall() {
+ setupMakeRoomForOutgoingCallMocks();
+ when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(true));
+ Analytics.CallInfo newCallAnalytics = mock(Analytics.CallInfo.class);
+ Analytics.CallInfo activeCallAnalytics = mock(Analytics.CallInfo.class);
+ when(mNewCall.getAnalytics()).thenReturn(newCallAnalytics);
+ when(mActiveCall.getAnalytics()).thenReturn(activeCallAnalytics);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+
+ CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+ verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold(anyString());
+ verify(newCallAnalytics).setCallIsAdditional(eq(true));
+ verify(activeCallAnalytics).setCallIsInterrupted(eq(true));
+ assertTrue(waitForFutureResult(future, false));
+ }
+
+ @Test
+ @SmallTest
+ public void testMakeRoomForOutgoingCallFail_MaxCalls() {
+ setupMakeRoomForOutgoingCallMocks();
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+ when(mCallsManager.hasMaximumManagedHoldingCalls(mNewCall)).thenReturn(true);
+
+ CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+ verify(mNewCall).setStartFailCause(eq(CallFailureCause.MAX_OUTGOING_CALLS));
+ assertFalse(waitForFutureResult(future, true));
+ }
+
+ @Test
+ @SmallTest
+ public void testMakeRoomForOutgoingCallFail_CannotHold() {
+ setupMakeRoomForOutgoingCallMocks();
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+ when(mCallsManager.hasMaximumManagedHoldingCalls(mNewCall)).thenReturn(false);
+
+ CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+ verify(mNewCall).setStartFailCause(eq(CallFailureCause.CANNOT_HOLD_CALL));
+ assertFalse(waitForFutureResult(future, true));
+ }
+
+ @Test
+ @SmallTest
+ public void testDisconnectCallSuccess() {
+ when(mActiveCall.disconnect()).thenReturn(CompletableFuture.completedFuture(true));
+ int previousState = CallState.ACTIVE;
+ mController.disconnectCall(mActiveCall, previousState);
+ verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+ .processDisconnectCallAndCleanup(eq(mActiveCall), eq(previousState));
+ }
+
+ @Test
+ @SmallTest
+ public void testDisconnectCallFail() {
+ when(mActiveCall.disconnect()).thenReturn(CompletableFuture.completedFuture(false));
+ int previousState = CallState.ACTIVE;
+ mController.disconnectCall(mActiveCall, previousState);
+ verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+ .processDisconnectCallAndCleanup(eq(mActiveCall), eq(previousState));
+ }
+
+ /* Helpers */
+ private void setPhoneAccounts(Call call1, Call call2, boolean useSamePhoneAccount) {
+ when(call1.getTargetPhoneAccount()).thenReturn(mHandle1);
+ when(call2.getTargetPhoneAccount()).thenReturn(useSamePhoneAccount ? mHandle1 : mHandle2);
+ }
+
+ private void setActiveCallFocus(Call call) {
+ when(mCallsManager.getConnectionServiceFocusManager())
+ .thenReturn(mConnectionServiceFocusManager);
+ when(mConnectionServiceFocusManager.getCurrentFocusCall()).thenReturn(call);
+ }
+
+ private void setupMakeRoomForOutgoingEmergencyCallMocks() {
+ when(mNewCall.isEmergencyCall()).thenReturn(true);
+ when(mCallsManager.hasRingingOrSimulatedRingingCall()).thenReturn(true);
+ when(mCallsManager.getRingingOrSimulatedRingingCall()).thenReturn(mRingingCall);
+ when(mCallsManager.hasMaximumLiveCalls(mNewCall)).thenReturn(true);
+ when(mCallsManager.getFirstCallWithLiveState()).thenReturn(mActiveCall);
+ when(mCallsManager.hasMaximumOutgoingCalls(mNewCall)).thenReturn(false);
+ when(mCallsManager.hasMaximumManagedHoldingCalls(mNewCall)).thenReturn(false);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+
+ // Setup analytics mocks
+ setupCallAnalytics(Arrays.asList(mNewCall, mActiveCall, mRingingCall));
+
+ // Setup ecall related checks
+ setupEmergencyCallPaCapabilities();
+ setupCarrierConfigAllowEmergencyCallHold();
+
+ // Setup CompletableFuture mocking for call actions
+ when(mRingingCall.reject(anyBoolean(), eq(null), anyString()))
+ .thenReturn(CompletableFuture.completedFuture(true));
+ when(mActiveCall.hold(anyString())).thenReturn(
+ CompletableFuture.completedFuture(true));
+ }
+
+ private void setupEmergencyCallPaCapabilities() {
+ PhoneAccount pa = mock(PhoneAccount.class);
+ PhoneAccountRegistrar paRegistrar = mock(PhoneAccountRegistrar.class);
+ when(mCallsManager.getPhoneAccountRegistrar()).thenReturn(paRegistrar);
+ when(paRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class))).thenReturn(pa);
+ when(pa.getCapabilities()).thenReturn(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+ }
+
+ private void setupCarrierConfigAllowEmergencyCallHold() {
+ PersistableBundle bundle = mock(PersistableBundle.class);
+ when(mCallsManager.getCarrierConfigForPhoneAccount(any(PhoneAccountHandle.class)))
+ .thenReturn(bundle);
+ when(bundle.getBoolean(
+ CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true))
+ .thenReturn(true);
+ }
+
+ private void setupMakeRoomForOutgoingCallMocks() {
+ when(mCallsManager.hasMaximumLiveCalls(mNewCall)).thenReturn(true);
+ when(mCallsManager.getFirstCallWithLiveState()).thenReturn(mActiveCall);
+ setPhoneAccounts(mActiveCall, mNewCall, false);
+ when(mActiveCall.isConference()).thenReturn(false);
+ when(mCallsManager.hasMaximumOutgoingCalls(mNewCall)).thenReturn(false);
+ }
+
+ private void setupHoldActiveCallForNewCallFailMocks() {
+ // Setup holdActiveCallForNewCallWithSequencing to fail.
+ setPhoneAccounts(mNewCall, mActiveCall, false);
+ setActiveCallFocus(mActiveCall);
+ when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+ when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(false));
+ }
+
+ private void verifyTransactionHoldActiveCallForNewCall(
+ OutcomeReceiver<Boolean, CallException> callback, CountDownLatch latch) {
+ mController.transactionHoldPotentialActiveCallForNewCallSequencing(mNewCall, callback);
+ while (latch.getCount() > 0) {
+ try {
+ latch.await(SEQUENCING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ assertEquals(latch.getCount(), 0);
+ }
+
+ private CallAttributes getOutgoingCallAttributes() {
+ return new CallAttributes.Builder(mHandle1,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+ .build();
+ }
+
+ private void setupCallAnalytics(List<Call> calls) {
+ for (Call call: calls) {
+ Analytics.CallInfo analyticsInfo = mock(Analytics.CallInfo.class);
+ when(call.getAnalytics()).thenReturn(analyticsInfo);
+ }
+ }
+
+ private boolean waitForFutureResult(CompletableFuture<Boolean> future, boolean defaultValue) {
+ boolean result = defaultValue;
+ try {
+ result = future.get(SEQUENCING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ // Pass through
+ }
+ return result;
+ }
+}
+
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 34d8830..126e4cc 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -103,6 +103,7 @@
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
import com.android.server.telecom.ClockProxy;
import com.android.server.telecom.ConnectionServiceFocusManager;
import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory;
@@ -3044,9 +3045,9 @@
/**
* Verify that
- * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
- * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to place
- * on hold.
+ * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+ * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to
+ * place on hold.
*/
@MediumTest
@Test
@@ -3068,8 +3069,8 @@
/**
* Verify that
- * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
- * OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
+ * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+ * boolean, OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
* cannot be held, and it's a CallControlRequest.
*/
@MediumTest
@@ -3086,9 +3087,9 @@
/**
* Verify that
- * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
- * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call and
- * it's a CallControlRequest.
+ * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+ * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call
+ * and it's a CallControlRequest.
*/
@MediumTest
@Test
@@ -3105,9 +3106,9 @@
/**
* Verify that
- * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
- * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
- * supports hold, and it's a CallControlRequest.
+ * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+ * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call
+ * that supports hold, and it's a CallControlRequest.
*/
@MediumTest
@Test
@@ -3124,9 +3125,9 @@
/**
* Verify that
- * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
- * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
- * supports hold + can hold, and it's a CallControlRequest.
+ * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+ * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call
+ * that supports hold + can hold, and it's a CallControlRequest.
*/
@MediumTest
@Test
@@ -3145,9 +3146,9 @@
/**
* Verify that
- * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
- * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
- * supports hold + can hold, and it's a CallControlCallbackRequest.
+ * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+ * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call
+ * that supports hold + can hold, and it's a CallControlCallbackRequest.
*/
@MediumTest
@Test
@@ -3165,9 +3166,9 @@
/**
* Verify that
- * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
- * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active unholdable call,
- * and it's a CallControlCallbackRequest.
+ * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+ * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active
+ * unholdable call, and it's a CallControlCallbackRequest.
*/
@MediumTest
@Test
@@ -3940,7 +3941,7 @@
CountDownLatch latch = new CountDownLatch(1);
when(mFeatureFlags.transactionalHoldDisconnectsUnholdable()).thenReturn(true);
when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(activeCall);
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(
+ mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(
newCall,
isCallControlRequest,
new LatchedOutcomeReceiver(latch, expectOnResult));
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 0a23913..6c049f6 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -63,6 +63,7 @@
import com.android.server.telecom.PhoneNumberUtilsAdapter;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
import com.android.server.telecom.callsequencing.TransactionManager;
import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
import com.android.server.telecom.callsequencing.voip.EndCallTransaction;
@@ -97,6 +98,7 @@
@Mock private Call mMockCall1;
@Mock private Context mMockContext;
@Mock private CallsManager mCallsManager;
+ @Mock private CallsManagerCallSequencingAdapter mCallSequencingAdapter;
@Mock private ToastFactory mToastFactory;
@Mock private ClockProxy mClockProxy;
@Mock private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
@@ -113,6 +115,7 @@
MockitoAnnotations.initMocks(this);
Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
Mockito.when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+ when(mCallsManager.getCallSequencingAdapter()).thenReturn(mCallSequencingAdapter);
}
@Override
@@ -220,7 +223,7 @@
transaction.processTransaction(null);
// THEN
- verify(mCallsManager, times(1))
+ verify(mCallsManager.getCallSequencingAdapter(), times(1))
.transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1), eq(false),
isA(OutcomeReceiver.class));
}
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
index fea6135..16b6e44 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -34,6 +34,7 @@
import com.android.internal.telecom.ICallControl;
import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.AnomalyReporterAdapter;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.TelecomSystem;
@@ -70,6 +71,7 @@
@Mock private TransactionManager mTransactionManager;
@Mock private ICallEventCallback mCallEventCallback;
@Mock private TransactionalServiceRepository mRepository;
+ @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
@Mock private IBinder mIBinder;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {};
@@ -84,7 +86,7 @@
Mockito.when(mCallEventCallback.asBinder()).thenReturn(mIBinder);
mTransactionalServiceWrapper = new TransactionalServiceWrapper(mCallEventCallback,
mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
- false /*call sequencing*/);
+ false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
}
@Override
@@ -98,7 +100,7 @@
TransactionalServiceWrapper service =
new TransactionalServiceWrapper(mCallEventCallback,
mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
- false /*call sequencing*/);
+ false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
assertEquals(SERVICE_HANDLE, service.getPhoneAccountHandle());
assertEquals(1, service.getNumberOfTrackedCalls());
@@ -109,7 +111,7 @@
TransactionalServiceWrapper service =
new TransactionalServiceWrapper(mCallEventCallback,
mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
- false /*call sequencing*/);
+ false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
assertEquals(1, service.getNumberOfTrackedCalls());
service.trackCall(mMockCall2);