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

Commit 5af368bf authored by Tyler Gunn's avatar Tyler Gunn
Browse files

Handle providing disconnect message through CallRedirectionService.

If a CDS is bound, then we will pass the disconnect cause on to the CDS
and wait up to 2 sec for it to potentially return an override disconnect
message.  If it does we override the telephony-provided disconnect cause
so that the message provided showed up in the Dialer app.

Test: Added CTS tests for these cases.
Test: Manual test with telecom test app.
Bug: 163085177
Change-Id: I8705c3b912e5277727a8dfca9e321b3856176ee9
parent 0bcd8701
Loading
Loading
Loading
Loading
+103 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import android.provider.CallLog;
import android.provider.ContactsContract.Contacts;
import android.telecom.BluetoothCallQualityReport;
import android.telecom.CallAudioState;
import android.telecom.CallDiagnosticService;
import android.telecom.CallerInfo;
import android.telecom.Conference;
import android.telecom.Connection;
@@ -60,6 +61,7 @@ import android.telecom.VideoProfile;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.telephony.emergency.EmergencyNumber;
import android.telephony.ims.ImsReasonInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.widget.Toast;
@@ -81,7 +83,9 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 *  Encapsulates all aspects of a given phone call throughout its lifecycle, starting
@@ -661,6 +665,22 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
     */
    private boolean mIsSimCall;

    /**
     * Set to {@code true} if we received a valid response ({@code null} or otherwise) from
     * the {@link DiagnosticCall#onCallDisconnected(ImsReasonInfo)} or
     * {@link DiagnosticCall#onCallDisconnected(int, int)} calls.  This is used to detect a timeout
     * when awaiting a response from the call diagnostic service.
     */
    private boolean mReceivedCallDiagnosticPostCallResponse = false;

    /**
     * {@link CompletableFuture} used to delay posting disconnection and removal to a call until
     * after a {@link CallDiagnosticService} is able to handle the disconnection and provide a
     * disconnect message via {@link DiagnosticCall#onCallDisconnected(ImsReasonInfo)} or
     * {@link DiagnosticCall#onCallDisconnected(int, int)}.
     */
    private CompletableFuture<Boolean> mDisconnectFuture;

    /**
     * Persists the specified parameters and initializes the new instance.
     * @param context The context.
@@ -1092,8 +1112,29 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
        }
    }

    /**
     * Handles an incoming overridden disconnect message for this call.
     *
     * We only care if the disconnect is handled via a future.
     * @param message the overridden disconnect message.
     */
    public void handleOverrideDisconnectMessage(@Nullable CharSequence message) {
        Log.i(this, "handleOverrideDisconnectMessage; callid=%s, msg=%s", getId(), message);

        if (isDisconnectHandledViaFuture()) {
            mReceivedCallDiagnosticPostCallResponse = true;
            if (message != null) {
                Log.addEvent(this, LogUtils.Events.OVERRIDE_DISCONNECT_MESSAGE, message);
                // Replace the existing disconnect cause in this call
                setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.ERROR, message,
                        message, null));
            }

            mDisconnectFuture.complete(true);
        } else {
            Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound",
                    getId());
        }
    }

    /**
@@ -4088,4 +4129,66 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
    public boolean isSimCall() {
        return mIsSimCall;
    }

    /**
     * Sets whether this is a sim call or not.
     * @param isSimCall {@code true} if this is a SIM call, {@code false} otherwise.
     */
    public void setIsSimCall(boolean isSimCall) {
        mIsSimCall = isSimCall;
    }

    /**
     * Initializes a disconnect future which is used to chain up pending operations which take
     * place when the {@link CallDiagnosticService} returns the result of the
     * {@link DiagnosticCall#onCallDisconnected(int, int)} or
     * {@link DiagnosticCall#onCallDisconnected(ImsReasonInfo)} invocation via
     * {@link CallDiagnosticServiceAdapter}.  If no {@link CallDiagnosticService} is in use, we
     * would not try to make a disconnect future.
     * @param timeoutMillis Timeout we use for waiting for the response.
     * @return the {@link CompletableFuture}.
     */
    public CompletableFuture<Boolean> initializeDisconnectFuture(long timeoutMillis) {
        if (mDisconnectFuture == null) {
            mDisconnectFuture = new CompletableFuture<Boolean>()
                    .completeOnTimeout(false, timeoutMillis, TimeUnit.MILLISECONDS);
            // After all the chained stuff we will report where the CDS timed out.
            mDisconnectFuture.thenRunAsync(() -> {
                if (!mReceivedCallDiagnosticPostCallResponse) {
                    Log.addEvent(this, LogUtils.Events.CALL_DIAGNOSTIC_SERVICE_TIMEOUT);
                }},
                new LoggedHandlerExecutor(mHandler, "C.iDF", mLock))
                    .exceptionally((throwable) -> {
                        Log.e(this, throwable, "Error while executing disconnect future");
                        return null;
                    });
        }
        return mDisconnectFuture;
    }

    /**
     * @return the disconnect future, if initialized.  Used for chaining operations after creation.
     */
    public CompletableFuture<Boolean> getDisconnectFuture() {
        return mDisconnectFuture;
    }

    /**
     * @return {@code true} if disconnection and removal is handled via a future, or {@code false}
     * if this is handled immediately.
     */
    public boolean isDisconnectHandledViaFuture() {
        return mDisconnectFuture != null && !mDisconnectFuture.isDone();
    }

    /**
     * Perform any cleanup on this call as a result of a {@link TelecomServiceImpl}
     * {@code cleanupStuckCalls} request.
     */
    public void cleanup() {
        if (mDisconnectFuture != null) {
            mDisconnectFuture.complete(false);
            mDisconnectFuture = null;
        }
    }
}
+28 −1
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import android.telecom.CallAudioState;
import android.telecom.CallDiagnosticService;
import android.telecom.ConnectionService;
import android.telecom.DiagnosticCall;
import android.telecom.DisconnectCause;
import android.telecom.InCallService;
import android.telecom.Log;
import android.telecom.ParcelableCall;
@@ -254,6 +255,32 @@ public class CallDiagnosticServiceController extends CallsManagerListenerBase {
        }
    }

    /**
     * Handles a newly disconnected call signalled from {@link CallsManager}.
     * @param call The call
     * @param disconnectCause The disconnect cause
     * @return {@code true} if the {@link CallDiagnosticService} was sent the call, {@code false}
     * if the call was not applicable to the CDS or if there was an issue sending it.
     */
    public boolean onCallDisconnected(@NonNull Call call,
            @NonNull DisconnectCause disconnectCause) {
        if (!call.isSimCall() || call.isExternalCall()) {
            Log.i(this, "onCallDisconnected: skipping call %s as non-sim or external.",
                    call.getId());
            return false;
        }
        String callId = mCallIdMapper.getCallId(call);
        try {
            if (isConnected()) {
                mCallDiagnosticService.notifyCallDisconnected(callId, disconnectCause);
                return true;
            }
        } catch (RemoteException e) {
            Log.w(this, "onCallDisconnected: callId=%s, exception=%s", call.getId(), e);
        }
        return false;
    }

    /**
     * Handles Telecom removal of calls; will remove the call from the bound service and if the
     * number of tracked calls falls to zero, unbind from the service.
@@ -569,7 +596,7 @@ public class CallDiagnosticServiceController extends CallsManagerListenerBase {
    /**
     * @return {@code true} if the call diagnostic service is bound/connected.
     */
    private boolean isConnected() {
    public boolean isConnected() {
        return mCallDiagnosticService != null;
    }

+63 −7
Original line number Diff line number Diff line
@@ -3099,27 +3099,82 @@ public class CallsManager extends Call.ListenerBase
            // be marked as missed.
            call.setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
        }

        // If a call diagnostic service is in use, we will log the original telephony-provided
        // disconnect cause, inform the CDS of the disconnection, and then chain the update of the
        // call state until AFTER the CDS reports it's result back.
        if (oldState == CallState.ACTIVE && disconnectCause.getCode() != DisconnectCause.MISSED
                && mCallDiagnosticServiceController.isConnected()
                && mCallDiagnosticServiceController.onCallDisconnected(call, disconnectCause)) {
            Log.i(this, "markCallAsDisconnected; callid=%s, postingToFuture.", call.getId());

            // Log the original disconnect reason prior to calling into the
            // CallDiagnosticService.
            Log.addEvent(call, LogUtils.Events.SET_DISCONNECTED_ORIG, disconnectCause);

            // Setup the future with a timeout so that the CDS is time boxed.
            CompletableFuture<Boolean> future = call.initializeDisconnectFuture(
                    mTimeoutsAdapter.getCallDiagnosticServiceTimeoutMillis(
                            mContext.getContentResolver()));

            // Post the disconnection updates to the future for completion once the CDS returns
            // with it's overridden disconnect message.
            future.thenRunAsync(() -> {
                call.setDisconnectCause(disconnectCause);
                setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
            }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock))
                    .exceptionally((throwable) -> {
                        Log.e(TAG, throwable, "Error while executing disconnect future.");
                        return null;
                    });
        } else {
            // No CallDiagnosticService, or it doesn't handle this call, so just do this
            // synchronously as always.
            call.setDisconnectCause(disconnectCause);
            setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
        }

        if (oldState == CallState.NEW && disconnectCause.getCode() == DisconnectCause.MISSED) {
            Log.i(this, "markCallAsDisconnected: logging missed call ");
            mCallLogManager.logCall(call, Calls.MISSED_TYPE, true, null);
        }

    }

    /**
     * Removes an existing disconnected call, and notifies the in-call app.
     */
    void markCallAsRemoved(Call call) {
        if (call.isDisconnectHandledViaFuture()) {
            Log.i(this, "markCallAsRemoved; callid=%s, postingToFuture.", call.getId());
            // A future is being used due to a CallDiagnosticService handling the call.  We will
            // chain the removal operation to the end of any outstanding disconnect work.
            call.getDisconnectFuture().thenRunAsync(() -> {
                performRemoval(call);
            }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock))
                    .exceptionally((throwable) -> {
                        Log.e(TAG, throwable, "Error while executing disconnect future");
                        return null;
                    });

        } else {
            Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId());
            performRemoval(call);
        }
    }

    /**
     * Work which is completed when a call is to be removed. Can either be be run synchronously or
     * posted to a {@link Call#getDisconnectFuture()}.
     * @param call The call.
     */
    private void performRemoval(Call call) {
        mInCallController.getBindingFuture().thenRunAsync(() -> {
            call.maybeCleanupHandover();
            removeCall(call);
            Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
            if (mLocallyDisconnectingCalls.contains(call)) {
                boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
                Log.v(this, "markCallAsRemoved: isDisconnectingChildCall = "
                Log.v(this, "performRemoval: isDisconnectingChildCall = "
                        + isDisconnectingChildCall + "call -> %s", call);
                mLocallyDisconnectingCalls.remove(call);
                // Auto-unhold the foreground call due to a locally disconnected call, except if the
@@ -3136,10 +3191,11 @@ public class CallsManager extends Call.ListenerBase
                // 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, "Auto-unholding held foreground call (call doesn't support hold)");
                Log.i(this, "performRemoval: Auto-unholding held foreground call (call doesn't "
                        + "support hold)");
                foregroundCall.unhold();
            }
        }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock))
        }, new LoggedHandlerExecutor(mHandler, "CM.pR", mLock))
                .exceptionally((throwable) -> {
                    Log.e(TAG, throwable, "Error while executing call removal");
                    return null;
+1 −1
Original line number Diff line number Diff line
@@ -352,7 +352,7 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
                    logIncoming("removeCall %s", callId);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        if (call.isAlive()) {
                        if (call.isAlive() && !call.isDisconnectHandledViaFuture()) {
                            mCallsManager.markCallAsDisconnected(
                                    call, new DisconnectCause(DisconnectCause.REMOTE));
                        } else {
+4 −0
Original line number Diff line number Diff line
@@ -197,6 +197,10 @@ public class LogUtils {
        public static final String REDIRECTION_USER_CONFIRMED = "REDIRECTION_USER_CONFIRMED";
        public static final String REDIRECTION_USER_CANCELLED = "REDIRECTION_USER_CANCELLED";
        public static final String BT_QUALITY_REPORT = "BT_QUALITY_REPORT";
        public static final String SET_DISCONNECTED_ORIG = "SET_DISCONNECTED_ORIG";
        public static final String OVERRIDE_DISCONNECT_MESSAGE = "OVERRIDE_DISCONNECT_MSG";
        public static final String CALL_DIAGNOSTIC_SERVICE_TIMEOUT =
                "CALL_DIAGNOSTIC_SERVICE_TIMEOUT";

        public static class Timings {
            public static final String ACCEPT_TIMING = "accept";
Loading