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

Commit 79c8c007 authored by Thomas Stuart's avatar Thomas Stuart
Browse files

fix toggling hold for a single transactional call

I discovered in manual testing calls that were set to inactive (held)
were not able to be set active again. This was due to the fact that the
requestFocusTransaction asserts the focusManager has the desired new
active call as the focusCall.  FocusManager will not set the focusCall
to a held call which would error out the transaction.

If there are no active calls, then the requested new active call does
not need to wait for a hold on another call and the focusManager will
update after the call is marked as active.

Fixes: 266621488
Test: atest android.telecom.cts.TransactionalApisTest
                                       #testToggleActiveAndInactive
Change-Id: Ife3d3b3b73ca40237f9a0a22e7c369b2b63e2c3a
parent 9203e483
Loading
Loading
Loading
Loading
+40 −15
Original line number Diff line number Diff line
@@ -6075,34 +6075,53 @@ public class CallsManager extends Call.ListenerBase
    }

    /**
     * Intended for ongoing or new calls that would like to go active/answered and need to
     * update the mConnectionSvrFocusMgr before setting the state
     * This helper mainly requests mConnectionSvrFocusMgr to update the call focus via a
     * {@link TransactionalFocusRequestCallback}.  However, in the case of a held call, the
     * state must be set first and then a request must be made.
     *
     * @param newCallFocus          to set active/answered
     * @param resultCallback        that back propagates the focusManager result
     *
     * Note: This method should only be called if there are no active calls.
     */
    public void transactionRequestNewFocusCall(Call call, int newCallState,
            OutcomeReceiver<Boolean, CallException> callback) {
        Log.d(this, "transactionRequestNewFocusCall");
        PendingAction pendingAction = new ActionSetCallState(call, newCallState,
                "transactional ActionSetCallState");
    public void requestNewCallFocusAndVerify(Call newCallFocus,
            OutcomeReceiver<Boolean, CallException> resultCallback) {
        int currentCallState = newCallFocus.getState();
        PendingAction pendingAction = null;

        // if the current call is in a state that can become the new call focus, we can set the
        // state afterwards...
        if (ConnectionServiceFocusManager.PRIORITY_FOCUS_CALL_STATE.contains(currentCallState)) {
            pendingAction = new ActionSetCallState(newCallFocus, CallState.ACTIVE,
                    "vCFC: pending action set state");
        } else {
            // However, HELD calls need to be set to ACTIVE before requesting call focus.
            setCallState(newCallFocus, CallState.ACTIVE, "vCFC: immediately set active");
        }

        mConnectionSvrFocusMgr
                .requestFocus(call,
                        new TransactionalFocusRequestCallback(pendingAction, call, callback));
                .requestFocus(newCallFocus,
                        new TransactionalFocusRequestCallback(pendingAction, currentCallState,
                                newCallFocus, resultCallback));
    }

    /**
     * Request a new call focus and ensure the request was successful via an OutcomeReceiver. Also,
     * include a PendingAction that will execute if the call focus change is successful.
     * conditionally include a PendingAction that will execute if and only if the call focus change
     * is successful.
     */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public class TransactionalFocusRequestCallback implements
            ConnectionServiceFocusManager.RequestFocusCallback {
        private PendingAction mPendingAction;
        @NonNull
        private Call mTargetCallFocus;
        private int mPreviousCallState;
        @NonNull private Call mTargetCallFocus;
        private OutcomeReceiver<Boolean, CallException> mCallback;

        TransactionalFocusRequestCallback(PendingAction pendingAction, @NonNull Call call,
                OutcomeReceiver<Boolean, CallException> callback) {
        TransactionalFocusRequestCallback(PendingAction pendingAction, int previousState,
                @NonNull Call call, OutcomeReceiver<Boolean, CallException> callback) {
            mPendingAction = pendingAction;
            mPreviousCallState = previousState;
            mTargetCallFocus = call;
            mCallback = callback;
        }
@@ -6115,12 +6134,18 @@ public class CallsManager extends Call.ListenerBase
                    mTargetCallFocus, currentCallFocus);
            if (currentCallFocus == null ||
                    !currentCallFocus.getId().equals(mTargetCallFocus.getId())) {
                // possibly reset the call state
                if (mTargetCallFocus.getState() != mPreviousCallState) {
                    mTargetCallFocus.setState(mPreviousCallState, "resetting call state");
                }
                mCallback.onError(new CallException("failed to switch focus to requested call",
                        CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
                return;
            }
            // at this point, we know the FocusManager is able to update successfully
            if (mPendingAction != null) {
                mPendingAction.performAction(); // set the call state
            }
            mCallback.onResult(true); // complete the transaction
        }
    }
+10 −12
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -153,10 +154,9 @@ public class ConnectionServiceFocusManager {
        void setCallsManagerListener(CallsManager.CallsManagerListener listener);
    }

    private static final int[] PRIORITY_FOCUS_CALL_STATE = new int[] {
            CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING, CallState.AUDIO_PROCESSING,
            CallState.RINGING
    };
    public static final Set<Integer> PRIORITY_FOCUS_CALL_STATE
            = Set.of(CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING,
            CallState.AUDIO_PROCESSING, CallState.RINGING);

    private static final int MSG_REQUEST_FOCUS = 1;
    private static final int MSG_RELEASE_CONNECTION_FOCUS = 2;
@@ -374,17 +374,15 @@ public class ConnectionServiceFocusManager {
                        && call.isFocusable())
                .collect(Collectors.toList());

        for (int i = 0; i < PRIORITY_FOCUS_CALL_STATE.length; i++) {
        for (CallFocus call : calls) {
                if (call.getState() == PRIORITY_FOCUS_CALL_STATE[i]) {
            if (PRIORITY_FOCUS_CALL_STATE.contains(call.getState())) {
                mCurrentFocusCall = call;
                    Log.d(this, "updateCurrentFocusCall %s", mCurrentFocusCall);
                Log.i(this, "updateCurrentFocusCall %s", mCurrentFocusCall);
                return;
            }
        }
        }

        Log.d(this, "updateCurrentFocusCall = null");
        Log.i(this, "updateCurrentFocusCall = null");
    }

    private void onRequestFocusDone(FocusRequest focusRequest) {
+72 −54
Original line number Diff line number Diff line
@@ -38,14 +38,13 @@ import androidx.annotation.VisibleForTesting;

import com.android.internal.telecom.ICallControl;
import com.android.internal.telecom.ICallEventCallback;
import com.android.server.telecom.voip.AnswerCallTransaction;
import com.android.server.telecom.voip.CallEventCallbackAckTransaction;
import com.android.server.telecom.voip.EndpointChangeTransaction;
import com.android.server.telecom.voip.HoldCallTransaction;
import com.android.server.telecom.voip.EndCallTransaction;
import com.android.server.telecom.voip.HoldActiveCallForNewCallTransaction;
import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
import com.android.server.telecom.voip.ParallelTransaction;
import com.android.server.telecom.voip.RequestFocusTransaction;
import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
import com.android.server.telecom.voip.SerialTransaction;
import com.android.server.telecom.voip.TransactionManager;
import com.android.server.telecom.voip.VoipCallTransaction;
@@ -247,11 +246,12 @@ public class TransactionalServiceWrapper implements
            if (call != null) {
                switch (action) {
                    case SET_ACTIVE:
                        addTransactionsToManager(createSetActiveTransactions(call), callback);
                        handleCallControlNewCallFocusTransactions(call, SET_ACTIVE,
                                false /* isAnswer */, 0/*VideoState (ignored)*/, callback);
                        break;
                    case ANSWER:
                        addTransactionsToManager(createSetAnswerTransactions(call,
                                (int) objects[0]), callback);
                        handleCallControlNewCallFocusTransactions(call, ANSWER,
                                true /* isAnswer */, (int) objects[0] /*VideoState*/, callback);
                        break;
                    case DISCONNECT:
                        addTransactionsToManager(new EndCallTransaction(mCallsManager,
@@ -278,6 +278,32 @@ public class TransactionalServiceWrapper implements
            }
        }

        // The client is request their VoIP call state go ACTIVE/ANSWERED.
        // This request is originating from the VoIP application.
        private void handleCallControlNewCallFocusTransactions(Call call, String action,
                boolean isAnswer, int potentiallyNewVideoState, ResultReceiver callback) {
            mTransactionManager.addTransaction(createSetActiveTransactions(call),
                    new OutcomeReceiver<>() {
                        @Override
                        public void onResult(VoipCallTransactionResult result) {
                            Log.i(TAG, String.format(Locale.US,
                                    "%s: onResult: callId=[%s]", action, call.getId()));
                            if (isAnswer) {
                                call.setVideoState(potentiallyNewVideoState);
                            }
                            callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
                        }

                        @Override
                        public void onError(CallException exception) {
                            Bundle extras = new Bundle();
                            extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
                            callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
                                    exception.getCode(), extras);
                        }
                    });
        }

        @Override
        public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
            try {
@@ -299,14 +325,12 @@ public class TransactionalServiceWrapper implements
                Call call = mTrackedCalls.get(callId);
                if (call != null) {
                    call.onConnectionEvent(event, extras);
                }
                else{
                } else {
                    Log.i(TAG,
                            "sendEvent: was called but there is no call with id=[%s] cannot be "
                                    + "found. Most likely the call has been disconnected");
                }
            }
            finally {
            } finally {
                Log.endSession();
            }
        }
@@ -348,7 +372,8 @@ public class TransactionalServiceWrapper implements
        try {
            Log.startSession("TSW.oSA");
            Log.d(TAG, String.format(Locale.US, "onSetActive: callId=[%s]", call.getId()));
            handleNewActiveCallCallbacks(call, ON_SET_ACTIVE, 0);
            handleCallEventCallbackNewFocus(call, ON_SET_ACTIVE, false /*isAnswerRequest*/,
                    0 /*VideoState*/);
        } finally {
            Log.endSession();
        }
@@ -358,35 +383,44 @@ public class TransactionalServiceWrapper implements
        try {
            Log.startSession("TSW.oA");
            Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId()));
            handleNewActiveCallCallbacks(call, ON_ANSWER, videoState);
            handleCallEventCallbackNewFocus(call, ON_ANSWER, true /*isAnswerRequest*/,
                    videoState /*VideoState*/);
        } finally {
            Log.endSession();
        }
    }

    // need to create multiple transactions for onSetActive and onAnswer which both seek to set
    // the call to active
    private void handleNewActiveCallCallbacks(Call call, String action, int videoState) {
    // handle a CallEventCallback to set a call ACTIVE/ANSWERED. Must get ack from client since the
    // request has come from another source (ex. Android Auto is requesting a call to go active)
    private void handleCallEventCallbackNewFocus(Call call, String action, boolean isAnswerRequest,
            int potentiallyNewVideoState) {
        // save CallsManager state before sending client state changes
        Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
        boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();

        // create 3 serial transactions:
        // -- hold active
        // -- set newCall as active
        // -- ack from client
        SerialTransaction serialTransactions = createSetActiveTransactions(call);
        // 3. get ack from client (that the requested call can go active)
        if (isAnswerRequest) {
            serialTransactions.appendTransaction(
                    new CallEventCallbackAckTransaction(mICallEventCallback,
                            action, call.getId(), potentiallyNewVideoState, mLock));
        } else {
            serialTransactions.appendTransaction(
                    new CallEventCallbackAckTransaction(mICallEventCallback,
                        action, call.getId(), videoState, mLock));
                            action, call.getId(), mLock));
        }

        // do CallsManager workload before asking client and
        //   reset CallsManager state if client does NOT ack
        mTransactionManager.addTransaction(serialTransactions, new OutcomeReceiver<>() {
        mTransactionManager.addTransaction(serialTransactions,
                new OutcomeReceiver<>() {
                    @Override
                    public void onResult(VoipCallTransactionResult result) {
                        Log.i(TAG, String.format(Locale.US,
                                "%s: onResult: callId=[%s]", action, call.getId()));
                        if (isAnswerRequest) {
                            call.setVideoState(potentiallyNewVideoState);
                        }
                    }

                    @Override
@@ -553,27 +587,11 @@ public class TransactionalServiceWrapper implements
        // create list for multiple transactions
        List<VoipCallTransaction> transactions = new ArrayList<>();

        // add t1. hold potential active call
        transactions.add(new HoldActiveCallForNewCallTransaction(mCallsManager, call));
        // potentially hold the current active call in order to set a new call (active/answered)
        transactions.add(new MaybeHoldCallForNewCallTransaction(mCallsManager, call));
        // And request a new focus call update
        transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));

        // add t2. send request to set the current call active
        transactions.add(new RequestFocusTransaction(mCallsManager, call));

        // send off to Transaction Manager to process
        return new SerialTransaction(transactions, mLock);
    }

    private SerialTransaction createSetAnswerTransactions(Call call, int videoState) {
        // create list for multiple transactions
        List<VoipCallTransaction> transactions = new ArrayList<>();

        // add t1. hold potential active call
        transactions.add(new HoldActiveCallForNewCallTransaction(mCallsManager, call));

        // add t2. answer current call
        transactions.add(new AnswerCallTransaction(mCallsManager, call, videoState));

        // send off to Transaction Manager to process
        return new SerialTransaction(transactions, mLock);
    }

+0 −76
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.voip;

import android.os.OutcomeReceiver;
import android.telecom.CallException;
import android.util.Log;

import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

/**
 * This transaction should be created for new incoming calls that request to go from
 * CallState.Ringing to CallState.Answered.  Before changing the CallState, the focus manager must
 * be updated. Once the focus manager updates, the call state will be set.  If there is an issue
 * answering the call, the transaction will fail.
 */
public class AnswerCallTransaction extends VoipCallTransaction {

    private static final String TAG = AnswerCallTransaction.class.getSimpleName();
    private final CallsManager mCallsManager;
    private final Call mCall;
    private final int mVideoState;

    public AnswerCallTransaction(CallsManager callsManager, Call call, int videoState) {
        super(callsManager.getLock());
        mCallsManager = callsManager;
        mCall = call;
        mVideoState = videoState;
    }

    @Override
    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
        Log.d(TAG, "processTransaction");
        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();

        mCall.setVideoState(mVideoState);

        mCallsManager.transactionRequestNewFocusCall(mCall, CallState.ANSWERED,
                new OutcomeReceiver<>() {
            @Override
            public void onResult(Boolean result) {
                Log.d(TAG, "processTransaction: onResult");
                future.complete(new VoipCallTransactionResult(
                        VoipCallTransactionResult.RESULT_SUCCEED, null));
            }

            @Override
            public void onError(CallException exception) {
                Log.d(TAG, "processTransaction: onError");
                future.complete(new VoipCallTransactionResult(
                        exception.getCode(), exception.getMessage()));
            }
        });

        return future;
    }
}
 No newline at end of file
+2 −1
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.telecom.CallAttributes;
import android.telecom.DisconnectCause;
import android.util.Log;

@@ -44,7 +45,7 @@ public class CallEventCallbackAckTransaction extends VoipCallTransaction {
    private final String mAction;
    private final String mCallId;
    // optional values
    private int mVideoState = 0;
    private int mVideoState = CallAttributes.AUDIO_CALL;
    private DisconnectCause mDisconnectCause = null;

    private final VoipCallTransactionResult TRANSACTION_FAILED = new VoipCallTransactionResult(
Loading