Revert "Revert "Ensure MediaSession is ONLY made active when routed to wired headset.""

This reverts commit 3beead23d884c4fdbcd5e19c7b0d048fcefbe0ae.

Reason for revert: Re-landing with special case fix for accessibility

Change-Id: I04038fe9fc586e7d5995629aebf8b193d929a445
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
index 7458f54..8e9caff 100644
--- a/src/com/android/server/telecom/HeadsetMediaButton.java
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -23,11 +23,16 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.Log;
+import android.util.ArraySet;
 import android.view.KeyEvent;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Set;
+
 /**
  * Static class to handle listening to the headset media buttons.
  */
@@ -149,8 +154,10 @@
     private final Context mContext;
     private final CallsManager mCallsManager;
     private final TelecomSystem.SyncRoot mLock;
+    private final Set<Call> mCalls = new ArraySet<>();
     private MediaSessionAdapter mSession;
     private KeyEvent mLastHookEvent;
+    private @CallEndpoint.EndpointType int mCurrentEndpointType;
 
     /**
      * Constructor used for testing purposes to initialize a {@link HeadsetMediaButton} with a
@@ -212,7 +219,7 @@
             return mCallsManager.onMediaButton(LONG_PRESS);
         } else if (event.getAction() == KeyEvent.ACTION_UP) {
             // We should not judge SHORT_PRESS by ACTION_UP event repeatCount, because it always
-            // return 0.
+            // returns 0.
             // Actually ACTION_DOWN event repeatCount only increases when LONG_PRESS performed.
             if (mLastHookEvent != null && mLastHookEvent.getRepeatCount() == 0) {
                 return mCallsManager.onMediaButton(SHORT_PRESS);
@@ -226,52 +233,72 @@
         return true;
     }
 
+    @Override
+    public void onCallEndpointChanged(CallEndpoint callEndpoint) {
+        mCurrentEndpointType = callEndpoint.getEndpointType();
+        Log.i(this, "onCallEndpointChanged: endPoint=%s", callEndpoint);
+        maybeChangeSessionState();
+    }
+
     /** ${inheritDoc} */
     @Override
     public void onCallAdded(Call call) {
-        if (call.isExternalCall()) {
-            return;
-        }
-        handleCallAddition();
+        handleCallAddition(call);
     }
 
     /**
      * Triggers session activation due to call addition.
      */
-    private void handleCallAddition() {
-        mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget();
-    }
-
-    /** ${inheritDoc} */
-    @Override
-    public void onCallRemoved(Call call) {
-        if (call.isExternalCall()) {
-            return;
-        }
-        handleCallRemoval();
+    private void handleCallAddition(Call call) {
+        mCalls.add(call);
+        maybeChangeSessionState();
     }
 
     /**
-     * Triggers session deactivation due to call removal.
+     * Based on whether there are tracked calls and the audio is routed to a wired headset,
+     * potentially activate or deactive the media session.
      */
-    private void handleCallRemoval() {
-        if (!mCallsManager.hasAnyCalls()) {
+    private void maybeChangeSessionState() {
+        boolean hasNonExternalCalls = !mCalls.isEmpty()
+                && mCalls.stream().anyMatch(c -> !c.isExternalCall());
+        if (hasNonExternalCalls && mCurrentEndpointType == CallEndpoint.TYPE_WIRED_HEADSET) {
+            Log.i(this, "maybeChangeSessionState: hasCalls=%b, currentEndpointType=%s, ACTIVATE",
+                    hasNonExternalCalls, CallEndpoint.endpointTypeToString(mCurrentEndpointType));
+            mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget();
+        } else {
+            Log.i(this, "maybeChangeSessionState: hasCalls=%b, currentEndpointType=%s, DEACTIVATE",
+                    hasNonExternalCalls, CallEndpoint.endpointTypeToString(mCurrentEndpointType));
             mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget();
         }
     }
 
     /** ${inheritDoc} */
     @Override
-    public void onExternalCallChanged(Call call, boolean isExternalCall) {
-        // Note: We don't use the onCallAdded/onCallRemoved methods here since they do checks to see
-        // if the call is external or not and would skip the session activation/deactivation.
-        if (isExternalCall) {
-            handleCallRemoval();
-        } else {
-            handleCallAddition();
+    public void onCallRemoved(Call call) {
+        handleCallRemoval(call);
+    }
+
+    /**
+     * Triggers session deactivation due to call removal.
+     */
+    private void handleCallRemoval(Call call) {
+        // If we were tracking the call, potentially change session state.
+        if (mCalls.remove(call)) {
+            if (mCalls.isEmpty()) {
+                // When there are no calls, don't cache that we previously had a wired headset
+                // connected; we'll be updated on the next call.
+                mCurrentEndpointType = CallEndpoint.TYPE_UNKNOWN;
+            }
+            maybeChangeSessionState();
         }
     }
 
+    /** ${inheritDoc} */
+    @Override
+    public void onExternalCallChanged(Call call, boolean isExternalCall) {
+        maybeChangeSessionState();
+    }
+
     @VisibleForTesting
     /**
      * @return the handler this class instance uses for operation; used for unit testing.
diff --git a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
index 0bfa987..ce23724 100644
--- a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
+++ b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
@@ -18,6 +18,7 @@
 
 import android.content.Intent;
 import android.media.session.MediaSession;
+import android.telecom.CallEndpoint;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.KeyEvent;
@@ -80,7 +81,7 @@
     }
 
     /**
-     * Nominal case; just add a call and remove it.
+     * Nominal case; just add a call and remove it; this happens when the audio state is earpiece.
      */
     @SmallTest
     @Test
@@ -90,14 +91,95 @@
         when(mMockCallsManager.hasAnyCalls()).thenReturn(true);
         mHeadsetMediaButton.onCallAdded(regularCall);
         waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter, never()).setActive(eq(true));
+
+        // Report that the endpoint is earpiece and other routes that don't matter
+        mHeadsetMediaButton.onCallEndpointChanged(
+                new CallEndpoint("Earpiece", CallEndpoint.TYPE_EARPIECE));
+        mHeadsetMediaButton.onCallEndpointChanged(
+                new CallEndpoint("Speaker", CallEndpoint.TYPE_SPEAKER));
+        mHeadsetMediaButton.onCallEndpointChanged(
+                new CallEndpoint("BT", CallEndpoint.TYPE_BLUETOOTH));
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter, never()).setActive(eq(true));
+
+        // ... and thus we see how the original code isn't amenable to tests.
+        when(mMediaSessionAdapter.isActive()).thenReturn(false);
+
+        // Still should not have done anything; we never hit wired headset
+        mHeadsetMediaButton.onCallRemoved(regularCall);
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter, never()).setActive(eq(false));
+    }
+
+    /**
+     * Call is added and then routed to headset after call start
+     */
+    @SmallTest
+    @Test
+    public void testAddCallThenRouteToHeadset() {
+        Call regularCall = getRegularCall();
+
+        mHeadsetMediaButton.onCallAdded(regularCall);
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter, never()).setActive(eq(true));
+
+        mHeadsetMediaButton.onCallEndpointChanged(
+                new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
         verify(mMediaSessionAdapter).setActive(eq(true));
+
         // ... and thus we see how the original code isn't amenable to tests.
         when(mMediaSessionAdapter.isActive()).thenReturn(true);
 
-        when(mMockCallsManager.hasAnyCalls()).thenReturn(false);
+        // Remove the one call; we should release the session.
         mHeadsetMediaButton.onCallRemoved(regularCall);
         waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
         verify(mMediaSessionAdapter).setActive(eq(false));
+        when(mMediaSessionAdapter.isActive()).thenReturn(false);
+
+        // Add a new call; make sure we go active once more.
+        mHeadsetMediaButton.onCallAdded(regularCall);
+        mHeadsetMediaButton.onCallEndpointChanged(
+                new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter, times(2)).setActive(eq(true));
+    }
+
+    /**
+     * Call is added and then routed to headset after call start
+     */
+    @SmallTest
+    @Test
+    public void testAddCallThenRouteToHeadsetAndBack() {
+        Call regularCall = getRegularCall();
+
+        when(mMockCallsManager.hasAnyCalls()).thenReturn(true);
+        mHeadsetMediaButton.onCallAdded(regularCall);
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter, never()).setActive(eq(true));
+
+        mHeadsetMediaButton.onCallEndpointChanged(
+                new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter).setActive(eq(true));
+
+        // ... and thus we see how the original code isn't amenable to tests.
+        when(mMediaSessionAdapter.isActive()).thenReturn(true);
+
+        mHeadsetMediaButton.onCallEndpointChanged(
+                new CallEndpoint("Earpiece", CallEndpoint.TYPE_EARPIECE));
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter).setActive(eq(false));
+        when(mMediaSessionAdapter.isActive()).thenReturn(false);
+
+        // Remove the one call; we should not release again.
+        mHeadsetMediaButton.onCallRemoved(regularCall);
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        // Remember, mockito counts total invocations; we should have went active once and then
+        // inactive again when we hit earpiece.
+        verify(mMediaSessionAdapter, times(1)).setActive(eq(true));
+        verify(mMediaSessionAdapter, times(1)).setActive(eq(false));
     }
 
     /**
@@ -111,6 +193,8 @@
         // Start with a regular old call.
         when(mMockCallsManager.hasAnyCalls()).thenReturn(true);
         mHeadsetMediaButton.onCallAdded(regularCall);
+        mHeadsetMediaButton.onCallEndpointChanged(
+                new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
         waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
         verify(mMediaSessionAdapter).setActive(eq(true));
         when(mMediaSessionAdapter.isActive()).thenReturn(true);
@@ -122,6 +206,7 @@
         // Expect to set session inactive.
         waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
         verify(mMediaSessionAdapter).setActive(eq(false));
+        when(mMediaSessionAdapter.isActive()).thenReturn(false);
 
         // For good measure lets make it non-external again.
         when(regularCall.isExternalCall()).thenReturn(false);
@@ -129,7 +214,7 @@
         mHeadsetMediaButton.onExternalCallChanged(regularCall, false);
         // Expect to set session active.
         waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
-        verify(mMediaSessionAdapter).setActive(eq(true));
+        verify(mMediaSessionAdapter, times(2)).setActive(eq(true));
     }
 
     @MediumTest
@@ -139,6 +224,8 @@
         when(externalCall.isExternalCall()).thenReturn(true);
 
         mHeadsetMediaButton.onCallAdded(externalCall);
+        mHeadsetMediaButton.onCallEndpointChanged(
+                new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
         waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
         verify(mMediaSessionAdapter, never()).setActive(eq(true));