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

Commit 63e690cb authored by Hall Liu's avatar Hall Liu
Browse files

Accept timeout from apps requesting call disconnect

When processing a call disconnected via the ACTION_NEW_OUTGOING_CALL
broadcast, use the timeout specified by the app instead of the default
timeout. This allows apps that may take a bit longer to place a new call
some more time if they need it.

This also changes the default timeout to 500ms and sets a max timeout of
10s for an app-specified timeout.

Bug: 34474757
Test: manual, unit
Change-Id: I1b535f17aefffe423cfbc2cb1d42352e8f4b57f0
parent b29a6fd3
Loading
Loading
Loading
Loading
+14 −11
Original line number Diff line number Diff line
@@ -114,7 +114,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable {
        void onConnectionManagerPhoneAccountChanged(Call call);
        void onPhoneAccountChanged(Call call);
        void onConferenceableCallsChanged(Call call);
        boolean onCanceledViaNewOutgoingCallBroadcast(Call call);
        boolean onCanceledViaNewOutgoingCallBroadcast(Call call, long disconnectionTimeout);
        void onHoldToneRequested(Call call);
        void onConnectionEvent(Call call, String event, Bundle extras);
        void onExternalCallChanged(Call call, boolean isExternalCall);
@@ -176,7 +176,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable {
        @Override
        public void onConferenceableCallsChanged(Call call) {}
        @Override
        public boolean onCanceledViaNewOutgoingCallBroadcast(Call call) {
        public boolean onCanceledViaNewOutgoingCallBroadcast(Call call, long disconnectionTimeout) {
            return false;
        }

@@ -1355,14 +1355,14 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable {

    @VisibleForTesting
    public void disconnect() {
        disconnect(false);
        disconnect(0);
    }

    /**
     * Attempts to disconnect the call through the connection service.
     */
    @VisibleForTesting
    public void disconnect(boolean wasViaNewOutgoingCallBroadcaster) {
    public void disconnect(long disconnectionTimeout) {
        Log.addEvent(this, LogUtils.Events.REQUEST_DISCONNECT);

        // Track that the call is now locally disconnecting.
@@ -1371,7 +1371,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable {
        if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT ||
                mState == CallState.CONNECTING) {
            Log.v(this, "Aborting call %s", this);
            abort(wasViaNewOutgoingCallBroadcaster);
            abort(disconnectionTimeout);
        } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
            if (mConnectionService == null) {
                Log.e(this, new Exception(), "disconnect() request on a call without a"
@@ -1387,22 +1387,25 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable {
        }
    }

    void abort(boolean wasViaNewOutgoingCallBroadcaster) {
    void abort(long disconnectionTimeout) {
        if (mCreateConnectionProcessor != null &&
                !mCreateConnectionProcessor.isProcessingComplete()) {
            mCreateConnectionProcessor.abort();
        } else if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT
                || mState == CallState.CONNECTING) {
            if (wasViaNewOutgoingCallBroadcaster) {
                // If the cancelation was from NEW_OUTGOING_CALL, then we do not automatically
                // destroy the call.  Instead, we announce the cancelation and CallsManager handles
            if (disconnectionTimeout > 0) {
                // If the cancelation was from NEW_OUTGOING_CALL with a timeout of > 0
                // milliseconds, do not destroy the call.
                // Instead, we announce the cancellation and CallsManager handles
                // it through a timer. Since apps often cancel calls through NEW_OUTGOING_CALL and
                // then re-dial them quickly using a gateway, allowing the first call to end
                // causes jank. This timeout allows CallsManager to transition the first call into
                // the second call so that in-call only ever sees a single call...eliminating the
                // jank altogether.
                // jank altogether. The app will also be able to set the timeout via an extra on
                // the ordered broadcast.
                for (Listener listener : mListeners) {
                    if (listener.onCanceledViaNewOutgoingCallBroadcast(this)) {
                    if (listener.onCanceledViaNewOutgoingCallBroadcast(
                            this, disconnectionTimeout)) {
                        // The first listener to handle this wins. A return value of true means that
                        // the listener will handle the disconnection process later and so we
                        // should not continue it here.
+3 −2
Original line number Diff line number Diff line
@@ -533,7 +533,8 @@ public class CallsManager extends Call.ListenerBase
    }

    @Override
    public boolean onCanceledViaNewOutgoingCallBroadcast(final Call call) {
    public boolean onCanceledViaNewOutgoingCallBroadcast(final Call call,
            long disconnectionTimeout) {
        mPendingCallsToDisconnect.add(call);
        mHandler.postDelayed(new Runnable("CM.oCVNOCB", mLock) {
            @Override
@@ -543,7 +544,7 @@ public class CallsManager extends Call.ListenerBase
                    call.disconnect();
                }
            }
        }.prepare(), Timeouts.getNewOutgoingCallCancelMillis(mContext.getContentResolver()));
        }.prepare(), disconnectionTimeout);

        return true;
    }
+21 −1
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.Trace;
import android.os.UserHandle;
import android.telecom.GatewayInfo;
@@ -113,19 +114,24 @@ public class NewOutgoingCallIntentBroadcaster {
                            Log.pii(resultNumber));

                    boolean endEarly = false;
                    long disconnectTimeout =
                            Timeouts.getNewOutgoingCallCancelMillis(mContext.getContentResolver());
                    if (resultNumber == null) {
                        Log.v(this, "Call cancelled (null number), returning...");
                        disconnectTimeout = getDisconnectTimeoutFromApp(
                                getResultExtras(false), disconnectTimeout);
                        endEarly = true;
                    } else if (mPhoneNumberUtilsAdapter.isPotentialLocalEmergencyNumber(
                            mContext, resultNumber)) {
                        Log.w(this, "Cannot modify outgoing call to emergency number %s.",
                                resultNumber);
                        disconnectTimeout = 0;
                        endEarly = true;
                    }

                    if (endEarly) {
                        if (mCall != null) {
                            mCall.disconnect(true /* wasViaNewOutgoingCall */);
                            mCall.disconnect(disconnectTimeout);
                        }
                        return;
                    }
@@ -446,4 +452,18 @@ public class NewOutgoingCallIntentBroadcaster {
            intent.setAction(action);
        }
    }

    private long getDisconnectTimeoutFromApp(Bundle resultExtras, long defaultTimeout) {
        if (resultExtras != null) {
            long disconnectTimeout = resultExtras.getLong(
                    TelecomManager.EXTRA_NEW_OUTGOING_CALL_CANCEL_TIMEOUT, defaultTimeout);
            if (disconnectTimeout < 0) {
                disconnectTimeout = 0;
            }
            return Math.min(disconnectTimeout,
                    Timeouts.getMaxNewOutgoingCallCancelMillis(mContext.getContentResolver()));
        } else {
            return defaultTimeout;
        }
    }
}
+10 −1
Original line number Diff line number Diff line
@@ -71,7 +71,16 @@ public final class Timeouts {
     * in-call UI.
     */
    public static long getNewOutgoingCallCancelMillis(ContentResolver contentResolver) {
        return get(contentResolver, "new_outgoing_call_cancel_ms", 100000L);
        return get(contentResolver, "new_outgoing_call_cancel_ms", 500L);
    }

    /**
     * Returns the maximum amount of time to wait before disconnecting a call that was canceled via
     * NEW_OUTGOING_CALL broadcast. This prevents malicious or poorly configured apps from
     * forever tying up the Telecom stack.
     */
    public static long getMaxNewOutgoingCallCancelMillis(ContentResolver contentResolver) {
        return get(contentResolver, "max_new_outgoing_call_cancel_ms", 10000L);
    }

    /**
+23 −2
Original line number Diff line number Diff line
@@ -313,7 +313,28 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {

        result.receiver.onReceive(mContext, result.intent);
        verifyNoCallPlaced();
        verify(mCall).disconnect(true);
        ArgumentCaptor<Long> timeoutCaptor = ArgumentCaptor.forClass(Long.class);
        verify(mCall).disconnect(timeoutCaptor.capture());
        assertTrue(timeoutCaptor.getValue() > 0);
    }

    @SmallTest
    public void testCallNumberModifiedToNullWithLongCustomTimeout() {
        Uri handle = Uri.parse("tel:6505551234");
        Intent callIntent = buildIntent(handle, Intent.ACTION_CALL, null);
        ReceiverIntentPair result = regularCallTestHelper(callIntent, null);

        long customTimeout = 100000000;
        Bundle bundle = new Bundle();
        bundle.putLong(TelecomManager.EXTRA_NEW_OUTGOING_CALL_CANCEL_TIMEOUT, customTimeout);
        result.receiver.setResultData(null);
        result.receiver.setResultExtras(bundle);

        result.receiver.onReceive(mContext, result.intent);
        verifyNoCallPlaced();
        ArgumentCaptor<Long> timeoutCaptor = ArgumentCaptor.forClass(Long.class);
        verify(mCall).disconnect(timeoutCaptor.capture());
        assertTrue(timeoutCaptor.getValue() < customTimeout);
    }

    @SmallTest
@@ -328,7 +349,7 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
        doReturn(true).when(mPhoneNumberUtilsAdapterSpy).isPotentialLocalEmergencyNumber(
                any(Context.class), eq(newEmergencyNumber));
        result.receiver.onReceive(mContext, result.intent);
        verify(mCall).disconnect(true);
        verify(mCall).disconnect(eq(0L));
    }

    private ReceiverIntentPair regularCallTestHelper(Intent intent,