Merge "Modify CallAudioManager\'s audio speedup functionality" into nyc-dev
am: 0125eab

* commit '0125eab617f07007c327811cf3b55e16419739f2':
  Modify CallAudioManager's audio speedup functionality

Change-Id: I8cccbe5ec32220e150dad4fc6333254b43d26b82
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 1280950..7bc727c 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -97,8 +97,8 @@
         Log.d(LOG_TAG, "Call state changed for TC@%s: %s -> %s", call.getId(),
                 CallState.toString(oldState), CallState.toString(newState));
 
-        if (mCallStateToCalls.get(oldState) != null) {
-            mCallStateToCalls.get(oldState).remove(call);
+        for (int i = 0; i < mCallStateToCalls.size(); i++) {
+            mCallStateToCalls.valueAt(i).remove(call);
         }
         if (mCallStateToCalls.get(newState) != null) {
             mCallStateToCalls.get(newState).add(call);
@@ -157,8 +157,8 @@
         Log.d(LOG_TAG, "Call removed with id TC@%s in state %s", call.getId(),
                 CallState.toString(call.getState()));
 
-        if (mCallStateToCalls.get(call.getState()) != null) {
-            mCallStateToCalls.get(call.getState()).remove(call);
+        for (int i = 0; i < mCallStateToCalls.size(); i++) {
+            mCallStateToCalls.valueAt(i).remove(call);
         }
 
         updateForegroundCall();
@@ -216,7 +216,7 @@
                     mCallStateToCalls.get(call.getState()).remove(call);
                 }
                 mActiveDialingOrConnectingCalls.add(call);
-                mCallAudioModeStateMachine.sendMessage(
+                mCallAudioModeStateMachine.sendMessageWithArgs(
                         CallAudioModeStateMachine.MT_AUDIO_SPEEDUP_FOR_RINGING_CALL,
                         makeArgsForModeStateMachine());
             }
@@ -267,7 +267,7 @@
         if (call != mForegroundCall) {
             return;
         }
-        mCallAudioModeStateMachine.sendMessage(
+        mCallAudioModeStateMachine.sendMessageWithArgs(
                 CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE,
                 makeArgsForModeStateMachine());
     }
@@ -388,7 +388,8 @@
         mRingingCalls.clear();
         mRinger.stopRinging();
         mRinger.stopCallWaiting();
-        mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
+        mCallAudioModeStateMachine.sendMessageWithArgs(
+                CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
                 makeArgsForModeStateMachine());
     }
 
@@ -451,7 +452,7 @@
     @VisibleForTesting
     public void setIsTonePlaying(boolean isTonePlaying) {
         mIsTonePlaying = isTonePlaying;
-        mCallAudioModeStateMachine.sendMessage(
+        mCallAudioModeStateMachine.sendMessageWithArgs(
                 isTonePlaying ? CallAudioModeStateMachine.TONE_STARTED_PLAYING
                         : CallAudioModeStateMachine.TONE_STOPPED_PLAYING,
                 makeArgsForModeStateMachine());
@@ -496,7 +497,7 @@
 
     private void onCallLeavingActiveDialingOrConnecting() {
         if (mActiveDialingOrConnectingCalls.size() == 0) {
-            mCallAudioModeStateMachine.sendMessage(
+            mCallAudioModeStateMachine.sendMessageWithArgs(
                     CallAudioModeStateMachine.NO_MORE_ACTIVE_OR_DIALING_CALLS,
                     makeArgsForModeStateMachine());
         }
@@ -504,21 +505,23 @@
 
     private void onCallLeavingRinging() {
         if (mRingingCalls.size() == 0) {
-            mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
+            mCallAudioModeStateMachine.sendMessageWithArgs(
+                    CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
                     makeArgsForModeStateMachine());
         }
     }
 
     private void onCallLeavingHold() {
         if (mHoldingCalls.size() == 0) {
-            mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NO_MORE_HOLDING_CALLS,
+            mCallAudioModeStateMachine.sendMessageWithArgs(
+                    CallAudioModeStateMachine.NO_MORE_HOLDING_CALLS,
                     makeArgsForModeStateMachine());
         }
     }
 
     private void onCallEnteringActiveDialingOrConnecting() {
         if (mActiveDialingOrConnectingCalls.size() == 1) {
-            mCallAudioModeStateMachine.sendMessage(
+            mCallAudioModeStateMachine.sendMessageWithArgs(
                     CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL,
                     makeArgsForModeStateMachine());
         }
@@ -526,14 +529,16 @@
 
     private void onCallEnteringRinging() {
         if (mRingingCalls.size() == 1) {
-            mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NEW_RINGING_CALL,
+            mCallAudioModeStateMachine.sendMessageWithArgs(
+                    CallAudioModeStateMachine.NEW_RINGING_CALL,
                     makeArgsForModeStateMachine());
         }
     }
 
     private void onCallEnteringHold() {
         if (mHoldingCalls.size() == 1) {
-            mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NEW_HOLDING_CALL,
+            mCallAudioModeStateMachine.sendMessageWithArgs(
+                    CallAudioModeStateMachine.NEW_HOLDING_CALL,
                     makeArgsForModeStateMachine());
         }
     }
@@ -691,4 +696,14 @@
             mRinger.stopCallWaiting();
         }
     }
+
+    @VisibleForTesting
+    public Set<Call> getTrackedCalls() {
+        return mCalls;
+    }
+
+    @VisibleForTesting
+    public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
+        return mCallStateToCalls;
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 76ddbea..92adca6 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -308,6 +308,13 @@
                     if (args.isTonePlaying) {
                         mCallAudioManager.stopCallWaiting();
                     }
+                    // If a MT-audio-speedup call gets disconnected by the connection service
+                    // concurrently with the user answering it, we may get this message
+                    // indicating that a ringing call has disconnected while this state machine
+                    // is in the SimCallFocusState.
+                    if (!args.hasActiveOrDialingCalls) {
+                        transitionTo(destinationStateAfterNoMoreActiveCalls(args));
+                    }
                     return HANDLED;
                 case NO_MORE_HOLDING_CALLS:
                     // Do nothing.
@@ -483,6 +490,10 @@
         return currentState == null ? "no state" : currentState.getName();
     }
 
+    public void sendMessageWithArgs(int messageCode, MessageArgs args) {
+        sendMessage(messageCode, args);
+    }
+
     @Override
     protected void onPreHandleMessage(Message msg) {
         if (msg.obj != null && msg.obj instanceof MessageArgs) {
diff --git a/src/com/android/server/telecom/DtmfLocalTonePlayer.java b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
index 20c9dd8..64c0c55 100644
--- a/src/com/android/server/telecom/DtmfLocalTonePlayer.java
+++ b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
@@ -34,7 +34,7 @@
  * class employs a concept of a call "session" that starts and stops when the foreground call
  * changes.
  */
-class DtmfLocalTonePlayer {
+public class DtmfLocalTonePlayer {
     /** Generator used to actually play the tone. */
     private ToneGenerator mToneGenerator;
 
diff --git a/src/com/android/server/telecom/RingbackPlayer.java b/src/com/android/server/telecom/RingbackPlayer.java
index af60b68..bfaf8a2 100644
--- a/src/com/android/server/telecom/RingbackPlayer.java
+++ b/src/com/android/server/telecom/RingbackPlayer.java
@@ -24,7 +24,7 @@
  * able to turn off and on as the user switches between calls. This is why it is implemented as its
  * own class.
  */
-class RingbackPlayer {
+public class RingbackPlayer {
 
     private final InCallTonePlayer.Factory mPlayerFactory;
 
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 8a2b41c..77b1590 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -31,7 +31,7 @@
  * Controls the ringtone player.
  */
 @VisibleForTesting
-public final class Ringer {
+public class Ringer {
     private static final long[] VIBRATION_PATTERN = new long[] {
         0, // No delay before starting
         1000, // How long to vibrate
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
new file mode 100644
index 0000000..34325f0
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.tests;
+
+import android.media.ToneGenerator;
+import android.telecom.DisconnectCause;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.SparseArray;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioModeStateMachine;
+import com.android.server.telecom.CallAudioRouteStateMachine;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.DtmfLocalTonePlayer;
+import com.android.server.telecom.InCallTonePlayer;
+import com.android.server.telecom.RingbackPlayer;
+import com.android.server.telecom.Ringer;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.LinkedHashSet;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class CallAudioManagerTest extends TelecomTestCase {
+    @Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+    @Mock private CallsManager mCallsManager;
+    @Mock private CallAudioModeStateMachine mCallAudioModeStateMachine;
+    @Mock private InCallTonePlayer.Factory mPlayerFactory;
+    @Mock private Ringer mRinger;
+    @Mock private RingbackPlayer mRingbackPlayer;
+    @Mock private DtmfLocalTonePlayer mDtmfLocalTonePlayer;
+
+    private CallAudioManager mCallAudioManager;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        doAnswer((invocation) -> {
+            InCallTonePlayer mockInCallTonePlayer = mock(InCallTonePlayer.class);
+            doAnswer((invocation2) -> {
+                mCallAudioManager.setIsTonePlaying(true);
+                return null;
+            }).when(mockInCallTonePlayer).startTone();
+            return mockInCallTonePlayer;
+        }).when(mPlayerFactory).createPlayer(anyInt());
+        mCallAudioManager = new CallAudioManager(
+                mCallAudioRouteStateMachine,
+                mCallsManager,
+                mCallAudioModeStateMachine,
+                mPlayerFactory,
+                mRinger,
+                mRingbackPlayer,
+                mDtmfLocalTonePlayer);
+    }
+
+    @MediumTest
+    public void testSingleIncomingCallFlowWithoutMTSpeedUp() {
+        Call call = createIncomingCall();
+        when(call.can(android.telecom.Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO))
+                .thenReturn(false);
+
+        ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
+                ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
+        // Answer the incoming call
+        mCallAudioManager.onIncomingCallAnswered(call);
+        when(call.getState()).thenReturn(CallState.ACTIVE);
+        mCallAudioManager.onCallStateChanged(call, CallState.RINGING, CallState.ACTIVE);
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.NO_MORE_RINGING_CALLS), captor.capture());
+        CallAudioModeStateMachine.MessageArgs correctArgs =
+                new CallAudioModeStateMachine.MessageArgs(
+                        true, // hasActiveOrDialingCalls
+                        false, // hasRingingCalls
+                        false, // hasHoldingCalls
+                        false, // isTonePlaying
+                        false, // foregroundCallIsVoip
+                        null // session
+                );
+        assertMessageArgEquality(correctArgs, captor.getValue());
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
+        assertMessageArgEquality(correctArgs, captor.getValue());
+
+        disconnectCall(call);
+        stopTone();
+
+        mCallAudioManager.onCallRemoved(call);
+        verifyProperCleanup();
+    }
+
+    @MediumTest
+    public void testSingleIncomingCallFlowWithMTSpeedUp() {
+        Call call = createIncomingCall();
+        when(call.can(android.telecom.Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO))
+                .thenReturn(true);
+
+        ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
+                ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
+        // Answer the incoming call
+        mCallAudioManager.onIncomingCallAnswered(call);
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.MT_AUDIO_SPEEDUP_FOR_RINGING_CALL), captor.capture());
+        CallAudioModeStateMachine.MessageArgs correctArgs =
+                new CallAudioModeStateMachine.MessageArgs(
+                        true, // hasActiveOrDialingCalls
+                        false, // hasRingingCalls
+                        false, // hasHoldingCalls
+                        false, // isTonePlaying
+                        false, // foregroundCallIsVoip
+                        null // session
+                );
+        assertMessageArgEquality(correctArgs, captor.getValue());
+        assertMessageArgEquality(correctArgs, captor.getValue());
+        when(call.getState()).thenReturn(CallState.ACTIVE);
+        mCallAudioManager.onCallStateChanged(call, CallState.RINGING, CallState.ACTIVE);
+
+        disconnectCall(call);
+        stopTone();
+
+        mCallAudioManager.onCallRemoved(call);
+        verifyProperCleanup();
+    }
+
+    @MediumTest
+    public void testSingleOutgoingCall() {
+        Call call = mock(Call.class);
+        when(call.getState()).thenReturn(CallState.CONNECTING);
+
+        mCallAudioManager.onCallAdded(call);
+        assertEquals(call, mCallAudioManager.getForegroundCall());
+        ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
+                ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
+        verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
+        CallAudioModeStateMachine.MessageArgs expectedArgs =
+                new CallAudioModeStateMachine.MessageArgs(
+                        true, // hasActiveOrDialingCalls
+                        false, // hasRingingCalls
+                        false, // hasHoldingCalls
+                        false, // isTonePlaying
+                        false, // foregroundCallIsVoip
+                        null // session
+                );
+        assertMessageArgEquality(expectedArgs, captor.getValue());
+
+        when(call.getState()).thenReturn(CallState.DIALING);
+        mCallAudioManager.onCallStateChanged(call, CallState.CONNECTING, CallState.DIALING);
+        verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
+        assertMessageArgEquality(expectedArgs, captor.getValue());
+        verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
+                anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+
+
+        when(call.getState()).thenReturn(CallState.ACTIVE);
+        mCallAudioManager.onCallStateChanged(call, CallState.DIALING, CallState.ACTIVE);
+        verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
+        assertMessageArgEquality(expectedArgs, captor.getValue());
+        verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
+                anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+
+        disconnectCall(call);
+        stopTone();
+
+        mCallAudioManager.onCallRemoved(call);
+        verifyProperCleanup();
+    }
+
+    private Call createIncomingCall() {
+        Call call = mock(Call.class);
+        when(call.getState()).thenReturn(CallState.RINGING);
+
+        mCallAudioManager.onCallAdded(call);
+        assertEquals(call, mCallAudioManager.getForegroundCall());
+        ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
+                ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
+        verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.NEW_RINGING_CALL), captor.capture());
+        assertMessageArgEquality(new CallAudioModeStateMachine.MessageArgs(
+                false, // hasActiveOrDialingCalls
+                true, // hasRingingCalls
+                false, // hasHoldingCalls
+                false, // isTonePlaying
+                false, // foregroundCallIsVoip
+                null // session
+        ), captor.getValue());
+
+        return call;
+    }
+
+    private void disconnectCall(Call call) {
+        ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
+                ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
+        CallAudioModeStateMachine.MessageArgs correctArgs;
+
+        when(call.getState()).thenReturn(CallState.DISCONNECTED);
+        when(call.getDisconnectCause()).thenReturn(new DisconnectCause(DisconnectCause.LOCAL,
+                "", "", "", ToneGenerator.TONE_PROP_PROMPT));
+
+        mCallAudioManager.onCallStateChanged(call, CallState.ACTIVE, CallState.DISCONNECTED);
+        verify(mPlayerFactory).createPlayer(InCallTonePlayer.TONE_CALL_ENDED);
+        correctArgs = new CallAudioModeStateMachine.MessageArgs(
+                false, // hasActiveOrDialingCalls
+                false, // hasRingingCalls
+                false, // hasHoldingCalls
+                true, // isTonePlaying
+                false, // foregroundCallIsVoip
+                null // session
+        );
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.NO_MORE_ACTIVE_OR_DIALING_CALLS), captor.capture());
+        assertMessageArgEquality(correctArgs, captor.getValue());
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.TONE_STARTED_PLAYING), captor.capture());
+        assertMessageArgEquality(correctArgs, captor.getValue());
+    }
+
+    private void stopTone() {
+        ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
+                ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
+        mCallAudioManager.setIsTonePlaying(false);
+        CallAudioModeStateMachine.MessageArgs correctArgs =
+                new CallAudioModeStateMachine.MessageArgs(
+                        false, // hasActiveOrDialingCalls
+                        false, // hasRingingCalls
+                        false, // hasHoldingCalls
+                        false, // isTonePlaying
+                        false, // foregroundCallIsVoip
+                        null // session
+                );
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.TONE_STOPPED_PLAYING), captor.capture());
+        assertMessageArgEquality(correctArgs, captor.getValue());
+    }
+
+    private void verifyProperCleanup() {
+        assertEquals(0, mCallAudioManager.getTrackedCalls().size());
+        SparseArray<LinkedHashSet<Call>> callStateToCalls = mCallAudioManager.getCallStateToCalls();
+        for (int i = 0; i < callStateToCalls.size(); i++) {
+            assertEquals(0, callStateToCalls.valueAt(i).size());
+        }
+    }
+
+    private void assertMessageArgEquality(CallAudioModeStateMachine.MessageArgs expected,
+            CallAudioModeStateMachine.MessageArgs actual) {
+        assertEquals(expected.hasActiveOrDialingCalls, actual.hasActiveOrDialingCalls);
+        assertEquals(expected.hasHoldingCalls, actual.hasHoldingCalls);
+        assertEquals(expected.hasRingingCalls, actual.hasRingingCalls);
+        assertEquals(expected.isTonePlaying, actual.isTonePlaying);
+        assertEquals(expected.foregroundCallIsVoip, actual.foregroundCallIsVoip);
+    }
+}