Merge "Add volume to logs when call audio route changes."
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index c17c447..35ba93a 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -92,6 +92,9 @@
     private static final int RTT_PIPE_WRITE_SIDE_INDEX = 1;
 
     private static final int INVALID_RTT_REQUEST_ID = -1;
+
+    private static final char NO_DTMF_TONE = '\0';
+
     /**
      * Listener for events on the call.
      */
@@ -403,6 +406,7 @@
     private final String mId;
     private String mConnectionId;
     private Analytics.CallInfo mAnalytics;
+    private char mPlayingDtmfTone;
 
     private boolean mWasConferencePreviouslyMerged = false;
     private boolean mWasHighDefAudio = false;
@@ -1571,7 +1575,8 @@
     /**
      * Plays the specified DTMF tone.
      */
-    void playDtmfTone(char digit) {
+    @VisibleForTesting
+    public void playDtmfTone(char digit) {
         if (mConnectionService == null) {
             Log.w(this, "playDtmfTone() request on a call without a connection service.");
         } else {
@@ -1579,12 +1584,14 @@
             mConnectionService.playDtmfTone(this, digit);
             Log.addEvent(this, LogUtils.Events.START_DTMF, Log.pii(digit));
         }
+        mPlayingDtmfTone = digit;
     }
 
     /**
      * Stops playing any currently playing DTMF tone.
      */
-    void stopDtmfTone() {
+    @VisibleForTesting
+    public void stopDtmfTone() {
         if (mConnectionService == null) {
             Log.w(this, "stopDtmfTone() request on a call without a connection service.");
         } else {
@@ -1592,6 +1599,15 @@
             Log.addEvent(this, LogUtils.Events.STOP_DTMF);
             mConnectionService.stopDtmfTone(this);
         }
+        mPlayingDtmfTone = NO_DTMF_TONE;
+    }
+
+    /**
+     * @return {@code true} if a DTMF tone has been started via {@link #playDtmfTone(char)} but has
+     * not been stopped via {@link #stopDtmfTone()}, {@code false} otherwise.
+     */
+    boolean isDtmfTonePlaying() {
+        return mPlayingDtmfTone != NO_DTMF_TONE;
     }
 
     /**
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 7485841..34f299d 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -1229,6 +1229,9 @@
             if (targetPhoneAccountHandle != null) {
                 if (!accounts.contains(targetPhoneAccountHandle)) {
                     targetPhoneAccountHandle = null;
+                } else {
+                    // The target phone account is valid and was found.
+                    return Arrays.asList(targetPhoneAccountHandle);
                 }
             }
 
@@ -1489,8 +1492,12 @@
         if (!mCalls.contains(call)) {
             Log.i(this, "Request to play DTMF in a non-existent call %s", call);
         } else {
-            call.playDtmfTone(digit);
-            mDtmfLocalTonePlayer.playTone(call, digit);
+            if (call.getState() != CallState.ON_HOLD) {
+                call.playDtmfTone(digit);
+                mDtmfLocalTonePlayer.playTone(call, digit);
+            } else {
+                Log.i(this, "Request to play DTMF tone for held call %s", call.getId());
+            }
         }
     }
 
@@ -1788,7 +1795,8 @@
         maybeMoveToSpeakerPhone(call);
     }
 
-    void markCallAsOnHold(Call call) {
+    @VisibleForTesting
+    public void markCallAsOnHold(Call call) {
         setCallState(call, CallState.ON_HOLD, "on-hold set explicitly");
     }
 
@@ -2259,6 +2267,12 @@
         Log.i(this, "setCallState %s -> %s, call: %s", CallState.toString(oldState),
                 CallState.toString(newState), call);
         if (newState != oldState) {
+            // If the call switches to held state while a DTMF tone is playing, stop the tone to
+            // ensure that the tone generator stops playing the tone.
+            if (newState == CallState.ON_HOLD && call.isDtmfTonePlaying()) {
+                stopDtmfTone(call);
+            }
+
             // Unfortunately, in the telephony world the radio is king. So if the call notifies
             // us that the call is in a particular state, we allow it even if it doesn't make
             // sense (e.g., STATE_ACTIVE -> STATE_RINGING).
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index fda68a8..a7e21ad 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -30,6 +30,7 @@
 import android.telecom.GatewayInfo;
 import android.telecom.Log;
 import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.telephony.DisconnectCause;
@@ -262,6 +263,21 @@
             return DisconnectCause.INVALID_NUMBER;
         }
 
+        // True for all managed calls, false for self-managed calls.
+        boolean sendNewOutgoingCallBroadcast = true;
+        PhoneAccountHandle targetPhoneAccount = mIntent.getParcelableExtra(
+                TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+        if (targetPhoneAccount != null) {
+            PhoneAccount phoneAccount =
+                    mCallsManager.getPhoneAccountRegistrar().getPhoneAccountUnchecked(
+                            targetPhoneAccount);
+            if (phoneAccount != null && phoneAccount.isSelfManaged()) {
+                callImmediately = true;
+                sendNewOutgoingCallBroadcast = false;
+                Log.i(this, "Skipping NewOutgoingCallBroadcast for self-managed call.");
+            }
+        }
+
         if (callImmediately) {
             String scheme = isUriNumber ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL;
             boolean speakerphoneOn = mIntent.getBooleanExtra(
@@ -278,9 +294,11 @@
             // initiate the call again because of the presence of the EXTRA_ALREADY_CALLED extra.
         }
 
-        UserHandle targetUser = mCall.getInitiatingUser();
-        Log.i(this, "Sending NewOutgoingCallBroadcast for %s to %s", mCall, targetUser);
-        broadcastIntent(intent, number, !callImmediately, targetUser);
+        if (sendNewOutgoingCallBroadcast) {
+            UserHandle targetUser = mCall.getInitiatingUser();
+            Log.i(this, "Sending NewOutgoingCallBroadcast for %s to %s", mCall, targetUser);
+            broadcastIntent(intent, number, !callImmediately, targetUser);
+        }
         return DisconnectCause.NOT_DISCONNECTED;
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index fed3233..4aaa4e6 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -55,6 +55,7 @@
 import static org.mockito.Matchers.same;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.timeout;
@@ -1145,7 +1146,8 @@
 
     private void resetMocks() {
         reset(mockAudioManager, mockBluetoothRouteManager, mockCallsManager,
-                mockConnectionServiceWrapper, fakeCall);
+                mockConnectionServiceWrapper);
+        fakeCall = mock(Call.class);
         when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
         when(fakeCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
         when(fakeCall.isAlive()).thenReturn(true);
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 5e7eb50..714261a 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -18,8 +18,11 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyChar;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.ComponentName;
@@ -37,6 +40,7 @@
 import com.android.server.telecom.CallerInfoAsyncQueryFactory;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.ConnectionServiceWrapper;
 import com.android.server.telecom.ContactsAsyncHelper;
 import com.android.server.telecom.DefaultDialerCache;
 import com.android.server.telecom.EmergencyCallHelper;
@@ -59,6 +63,7 @@
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
@@ -315,6 +320,84 @@
         assertTrue(accounts.contains(SIM_1_HANDLE));
     }
 
+    /**
+     * Tests that we will use the provided target phone account if it exists.
+     * @throws Exception
+     */
+    @MediumTest
+    public void testUseSpecifiedAccount() throws Exception {
+        when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
+                null);
+        when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
+                any(), anyInt())).thenReturn(
+                new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
+
+        List<PhoneAccountHandle> accounts = mCallsManager.findOutgoingCallPhoneAccount(
+                SIM_2_HANDLE, TEST_ADDRESS, false /* isVideo */, null /* userHandle */);
+
+        assertEquals(1, accounts.size());
+        assertTrue(accounts.contains(SIM_2_HANDLE));
+    }
+
+    /**
+     * Verifies that an active call will result in playing a DTMF tone when requested.
+     * @throws Exception
+     */
+    @MediumTest
+    public void testPlayDtmfWhenActive() throws Exception {
+        Call callSpy = addSpyCall();
+        mCallsManager.playDtmfTone(callSpy, '1');
+        verify(callSpy).playDtmfTone(anyChar());
+    }
+
+    /**
+     * Verifies that DTMF requests are suppressed when a call is held.
+     * @throws Exception
+     */
+    @MediumTest
+    public void testSuppessDtmfWhenHeld() throws Exception {
+        Call callSpy = addSpyCall();
+        callSpy.setState(CallState.ON_HOLD, "test");
+
+        mCallsManager.playDtmfTone(callSpy, '1');
+        verify(callSpy, never()).playDtmfTone(anyChar());
+    }
+
+    /**
+     * Verifies that DTMF requests are suppressed when a call is held.
+     * @throws Exception
+     */
+    @MediumTest
+    public void testCancelDtmfWhenHeld() throws Exception {
+        Call callSpy = addSpyCall();
+        mCallsManager.playDtmfTone(callSpy, '1');
+        mCallsManager.markCallAsOnHold(callSpy);
+        verify(callSpy).stopDtmfTone();
+    }
+
+    private Call addSpyCall() {
+        Call ongoingCall = new Call("1", /* callId */
+                mComponentContextFixture.getTestDouble(),
+                mCallsManager,
+                mLock, /* ConnectionServiceRepository */
+                null,
+                mContactsAsyncHelper,
+                mCallerInfoAsyncQueryFactory,
+                mPhoneNumberUtilsAdapter,
+                TEST_ADDRESS,
+                null /* GatewayInfo */,
+                null /* connectionManagerPhoneAccountHandle */,
+                SIM_2_HANDLE,
+                Call.CALL_DIRECTION_INCOMING,
+                false /* shouldAttachToExistingConnection*/,
+                false /* isConference */,
+                mClockProxy);
+        ongoingCall.setState(CallState.ACTIVE, "just cuz");
+        Call callSpy = Mockito.spy(ongoingCall);
+        mCallsManager.addCall(callSpy);
+        return callSpy;
+    }
+
     private void setupMsimAccounts() {
         TelephonyManager mockTelephonyManager = mComponentContextFixture.getTelephonyManager();
         when(mockTelephonyManager.getMultiSimConfiguration()).thenReturn(
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index 7635427..f851162 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -25,7 +25,6 @@
 import android.os.IBinder;
 import android.os.IInterface;
 import android.os.RemoteException;
-import android.telecom.AudioState;
 import android.telecom.CallAudioState;
 import android.telecom.ParcelableCall;
 
@@ -49,7 +48,8 @@
     public boolean mShowDialpad;
     public boolean mCanAddCall;
     public boolean mSilenceRinger;
-    public CountDownLatch mLock = new CountDownLatch(1);
+    public CountDownLatch mUpdateCallLock = new CountDownLatch(1);
+    public CountDownLatch mAddCallLock = new CountDownLatch(1);
 
     public class FakeInCallService extends IInCallService.Stub {
         @Override
@@ -68,8 +68,9 @@
             if (mCallById.containsKey(call.getId())) {
                 throw new RuntimeException("Call " + call.getId() + " already added");
             }
-            mCallById.put(call.getId(), call);
             mLatestCallId = call.getId();
+            mCallById.put(call.getId(), call);
+            mAddCallLock.countDown();
         }
 
         @Override
@@ -77,9 +78,9 @@
             if (!mCallById.containsKey(call.getId())) {
                 throw new RuntimeException("Call " + call.getId() + " not added yet");
             }
-            mCallById.put(call.getId(), call);
             mLatestCallId = call.getId();
-            mLock.countDown();
+            mCallById.put(call.getId(), call);
+            mUpdateCallLock.countDown();
         }
 
         @Override
@@ -159,10 +160,23 @@
 
     public void waitForUpdate() {
         try {
-            mLock.await(5000, TimeUnit.MILLISECONDS);
+            mUpdateCallLock.await(5000, TimeUnit.MILLISECONDS);
         } catch (InterruptedException ie) {
             return;
         }
-        mLock = new CountDownLatch(1);
+        mUpdateCallLock = new CountDownLatch(1);
+    }
+
+    public void waitUntilNumCalls(int numCalls) {
+        if (mCallById.size() == numCalls) {
+            return;
+        }
+        mAddCallLock = new CountDownLatch(1);
+
+        try {
+            mAddCallLock.await(5000, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ie) {
+            return;
+        }
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 475b550..b84f9d5 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -578,13 +578,8 @@
         int startingNumConnections = connectionServiceFixture.mConnectionById.size();
         int startingNumCalls = mInCallServiceFixtureX.mCallById.size();
 
-        String callId = startOutgoingPhoneCallPendingCreateConnection(number, phoneAccountHandle,
+        startOutgoingPhoneCallPendingCreateConnection(number, phoneAccountHandle,
                 connectionServiceFixture, initiatingUser, videoState);
-        // Set the phone account if there is one specified
-        if (phoneAccountHandle != null) {
-            mInCallServiceFixtureX.getInCallAdapter().phoneAccountSelected(
-                    callId, phoneAccountHandle, false);
-        }
 
         return outgoingCallCreateConnectionComplete(startingNumConnections, startingNumCalls,
                 phoneAccountHandle, connectionServiceFixture);
@@ -847,34 +842,35 @@
         // is added, future interactions as triggered by the ConnectionService, through the various
         // test fixtures, will be synchronous.
 
-        if (!hasInCallAdapter
-                && phoneAccountHandle != mPhoneAccountSelfManaged.getAccountHandle()) {
-            verify(mInCallServiceFixtureX.getTestDouble(), timeout(TEST_TIMEOUT))
-                    .setInCallAdapter(any(IInCallAdapter.class));
-            verify(mInCallServiceFixtureY.getTestDouble(), timeout(TEST_TIMEOUT))
-                    .setInCallAdapter(any(IInCallAdapter.class));
+        if (phoneAccountHandle != mPhoneAccountSelfManaged.getAccountHandle()) {
+            if (!hasInCallAdapter) {
+                verify(mInCallServiceFixtureX.getTestDouble(), timeout(TEST_TIMEOUT))
+                        .setInCallAdapter(any(IInCallAdapter.class));
+                verify(mInCallServiceFixtureY.getTestDouble(), timeout(TEST_TIMEOUT))
+                        .setInCallAdapter(any(IInCallAdapter.class));
 
-            // Give the InCallService time to respond
-            assertTrueWithTimeout(new Predicate<Void>() {
-                @Override
-                public boolean apply(Void v) {
-                    return mInCallServiceFixtureX.mInCallAdapter != null;
-                }
-            });
+                // Give the InCallService time to respond
+                assertTrueWithTimeout(new Predicate<Void>() {
+                    @Override
+                    public boolean apply(Void v) {
+                        return mInCallServiceFixtureX.mInCallAdapter != null;
+                    }
+                });
 
-            assertTrueWithTimeout(new Predicate<Void>() {
-                @Override
-                public boolean apply(Void v) {
-                    return mInCallServiceFixtureY.mInCallAdapter != null;
-                }
-            });
+                assertTrueWithTimeout(new Predicate<Void>() {
+                    @Override
+                    public boolean apply(Void v) {
+                        return mInCallServiceFixtureY.mInCallAdapter != null;
+                    }
+                });
 
-            verify(mInCallServiceFixtureX.getTestDouble(), timeout(TEST_TIMEOUT))
-                    .addCall(any(ParcelableCall.class));
-            verify(mInCallServiceFixtureY.getTestDouble(), timeout(TEST_TIMEOUT))
-                    .addCall(any(ParcelableCall.class));
+                verify(mInCallServiceFixtureX.getTestDouble(), timeout(TEST_TIMEOUT))
+                        .addCall(any(ParcelableCall.class));
+                verify(mInCallServiceFixtureY.getTestDouble(), timeout(TEST_TIMEOUT))
+                        .addCall(any(ParcelableCall.class));
 
-            // Give the InCallService time to respond
+                // Give the InCallService time to respond
+            }
 
             assertTrueWithTimeout(new Predicate<Void>() {
                 @Override
@@ -883,18 +879,11 @@
                             connectionServiceFixture.mConnectionById.size();
                 }
             });
-            assertTrueWithTimeout(new Predicate<Void>() {
-                @Override
-                public boolean apply(Void v) {
-                    return startingNumCalls + 1 == mInCallServiceFixtureX.mCallById.size();
-                }
-            });
-            assertTrueWithTimeout(new Predicate<Void>() {
-                @Override
-                public boolean apply(Void v) {
-                    return startingNumCalls + 1 == mInCallServiceFixtureY.mCallById.size();
-                }
-            });
+
+            mInCallServiceFixtureX.waitUntilNumCalls(startingNumCalls + 1);
+            mInCallServiceFixtureY.waitUntilNumCalls(startingNumCalls + 1);
+            assertEquals(startingNumCalls + 1, mInCallServiceFixtureX.mCallById.size());
+            assertEquals(startingNumCalls + 1, mInCallServiceFixtureY.mCallById.size());
 
             assertEquals(mInCallServiceFixtureX.mLatestCallId,
                     mInCallServiceFixtureY.mLatestCallId);