Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 83e93186 authored by Pranav Madapurmath's avatar Pranav Madapurmath Committed by Android (Google) Code Review
Browse files

Merge "Call Sequencing: Internal cleanup and unit tests" into main

parents fae32401 1ab80971
Loading
Loading
Loading
Loading
+93 −188
Original line number Diff line number Diff line
@@ -783,8 +783,9 @@ public class CallsManager extends Call.ListenerBase
                ? 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);
@@ -3218,9 +3219,9 @@ public class CallsManager extends Call.ListenerBase
     * 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.
     */
@@ -4071,37 +4072,39 @@ public class CallsManager extends Call.ListenerBase
    }

    /**
     * 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");
    public void transactionHoldPotentialActiveCallForNewCallOld(Call newCall,
            Call activeCall, OutcomeReceiver<Boolean, CallException> callback) {
        if (holdActiveCallForNewCall(newCall)) {
            // Transactional clients do not call setHold but the request was sent to set the
            // call as inactive and it has already been acked by this point.
            markCallAsOnHold(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.",
        } else {
            // It's possible that holdActiveCallForNewCall disconnected the activeCall.
            // Therefore, the activeCalls state should be checked before failing.
            if (activeCall.isLocallyDisconnecting()) {
                callback.onResult(true);
            } else {
                Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: active call could "
                        + "not be held or disconnected");
                callback.onError(
                        new CallException("activeCall could not be held or disconnected",
                                CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
                return;
            }
        }
    }

            mCallSequencingAdapter.transactionHoldPotentialActiveCallForNewCall(newCall,
                    activeCall, callback);
        } else {
    /**
     * 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) &&
@@ -4126,31 +4129,8 @@ public class CallsManager extends Call.ListenerBase
        markCallAsOnHold(activeCall);
        callback.onResult(true);
    }
    }

    public void transactionHoldPotentialActiveCallForNewCallOld(Call newCall,
            Call activeCall, OutcomeReceiver<Boolean, CallException> callback) {
        if (holdActiveCallForNewCall(newCall)) {
            // Transactional clients do not call setHold but the request was sent to set the
            // call as inactive and it has already been acked by this point.
            markCallAsOnHold(activeCall);
            callback.onResult(true);
        } else {
            // It's possible that holdActiveCallForNewCall disconnected the activeCall.
            // Therefore, the activeCalls state should be checked before failing.
            if (activeCall.isLocallyDisconnecting()) {
                callback.onResult(true);
            } else {
                Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: active call could "
                        + "not be held or disconnected");
                callback.onError(
                        new CallException("activeCall could not be held or disconnected",
                                CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
            }
        }
    }

    private boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
    public boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
        return canHold(activeCall) || sameSourceHoldCase(activeCall, newCall);
    }

@@ -4370,54 +4350,7 @@ public class CallsManager extends Call.ListenerBase
        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);
    }

    /**
@@ -5571,12 +5504,41 @@ public class CallsManager extends Call.ListenerBase
            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.
@@ -5641,64 +5603,6 @@ public class CallsManager extends Call.ListenerBase
        return false;
    }

    /**
     * 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.
     *
@@ -6793,8 +6697,7 @@ public class CallsManager extends Call.ListenerBase
                Log.d(this, "perform unhold call for %s", mCall);
                CompletableFuture<Boolean> unholdFuture =
                        mCall.unhold("held " + mPreviouslyHeldCallId);
                if (mFeatureFlags.enableCallSequencing() && unholdFuture != null) {
                    mCallSequencingAdapter.logFutureResultTransaction(unholdFuture,
                mCallSequencingAdapter.maybeLogFutureResultTransaction(unholdFuture,
                        "performAction", "AUC.pA", "performAction: unhold call transaction "
                                + "succeeded. Call state is active.",
                        "performAction: unhold call transaction failed. Call state did not "
@@ -6802,7 +6705,6 @@ public class CallsManager extends Call.ListenerBase
            }
        }
    }
    }

    private final class ActionAnswerCall implements PendingAction {
        private final Call mCall;
@@ -6842,8 +6744,7 @@ public class CallsManager extends Call.ListenerBase
                if (isSpeakerphoneAutoEnabledForVideoCalls(mVideoState)) {
                    mCall.setStartWithSpeakerphoneOn(true);
                }
                if (mFeatureFlags.enableCallSequencing() && answerCallFuture != null) {
                    mCallSequencingAdapter.logFutureResultTransaction(answerCallFuture,
                mCallSequencingAdapter.maybeLogFutureResultTransaction(answerCallFuture,
                        "performAction", "AAC.pA", "performAction: answer call transaction "
                                + "succeeded. Call state is active.",
                        "performAction: answer call transaction failed. Call state did not "
@@ -6851,7 +6752,6 @@ public class CallsManager extends Call.ListenerBase
            }
        }
    }
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public static final class RequestCallback implements
@@ -7162,4 +7062,9 @@ public class CallsManager extends Call.ListenerBase
    public void addCallBeingSetup(Call call) {
        mSelfManagedCallsBeingSetup.add(call);
    }

    @VisibleForTesting
    public CallsManagerCallSequencingAdapter getCallSequencingAdapter() {
        return mCallSequencingAdapter;
    }
}
+115 −53

File changed.

Preview size limit exceeded, changes collapsed.

+118 −45

File changed.

Preview size limit exceeded, changes collapsed.

+1 −1
Original line number Diff line number Diff line
@@ -254,7 +254,7 @@ public class TransactionalCallSequencingAdapter {
            }
        };

        mCallsManager.transactionHoldPotentialActiveCallForNewCall(call,
        mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(call,
                isCallControlRequest, maybePerformHoldCallback);
        return createSetActiveFuture[0];
    }
+2 −2
Original line number Diff line number Diff line
@@ -52,8 +52,8 @@ public class MaybeHoldCallForNewCallTransaction extends CallTransaction {
        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");
Loading