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
Merged-In: I3121dcaf973a10043ffcaa0c8251aa21e18d027e
Change-Id: I1b535f17aefffe423cfbc2cb1d42352e8f4b57f0
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 18c9c05..99a1c8c 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -110,7 +110,7 @@
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);
@@ -172,7 +172,7 @@
@Override
public void onConferenceableCallsChanged(Call call) {}
@Override
- public boolean onCanceledViaNewOutgoingCallBroadcast(Call call) {
+ public boolean onCanceledViaNewOutgoingCallBroadcast(Call call, long disconnectionTimeout) {
return false;
}
@@ -1314,14 +1314,14 @@
@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.event(this, Log.Events.REQUEST_DISCONNECT);
// Track that the call is now locally disconnecting.
@@ -1330,7 +1330,7 @@
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"
@@ -1346,22 +1346,25 @@
}
}
- 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.
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index d6462d7..cd2aa85 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -552,7 +552,8 @@
}
@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
@@ -562,7 +563,7 @@
call.disconnect();
}
}
- }.prepare(), Timeouts.getNewOutgoingCallCancelMillis(mContext.getContentResolver()));
+ }.prepare(), disconnectionTimeout);
return true;
}
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 8b5604b..b63baaf 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -24,6 +24,7 @@
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;
@@ -112,19 +113,24 @@
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;
}
@@ -445,4 +451,18 @@
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;
+ }
+ }
}
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 7be59c3..b2f4b6d 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -63,7 +63,16 @@
* 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);
}
/**
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
index 54613d4..9b7dc37 100644
--- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -313,7 +313,28 @@
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 @@
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,