Loading src/com/android/server/telecom/Call.java +14 −11 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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; } Loading Loading @@ -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. Loading @@ -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" Loading @@ -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. Loading src/com/android/server/telecom/CallsManager.java +3 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -543,7 +544,7 @@ public class CallsManager extends Call.ListenerBase call.disconnect(); } } }.prepare(), Timeouts.getNewOutgoingCallCancelMillis(mContext.getContentResolver())); }.prepare(), disconnectionTimeout); return true; } Loading src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java +21 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -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; } } } src/com/android/server/telecom/Timeouts.java +10 −1 Original line number Diff line number Diff line Loading @@ -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); } /** Loading tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java +23 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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, Loading Loading
src/com/android/server/telecom/Call.java +14 −11 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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; } Loading Loading @@ -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. Loading @@ -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" Loading @@ -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. Loading
src/com/android/server/telecom/CallsManager.java +3 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -543,7 +544,7 @@ public class CallsManager extends Call.ListenerBase call.disconnect(); } } }.prepare(), Timeouts.getNewOutgoingCallCancelMillis(mContext.getContentResolver())); }.prepare(), disconnectionTimeout); return true; } Loading
src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java +21 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -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; } } }
src/com/android/server/telecom/Timeouts.java +10 −1 Original line number Diff line number Diff line Loading @@ -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); } /** Loading
tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java +23 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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, Loading