Merge "Correct HeadsetMediaButton behavior for external calls." into tm-dev
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index f98513e..9d6dd07 100755
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -3300,7 +3300,7 @@
      *
      * @return {@code True} if there are any non-external calls, {@code false} otherwise.
      */
-    boolean hasAnyCalls() {
+    public boolean hasAnyCalls() {
         if (mCalls.isEmpty()) {
             return false;
         }
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
index ad95c34..b1471c2 100644
--- a/src/com/android/server/telecom/HeadsetMediaButton.java
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -46,6 +46,47 @@
     private static final int MSG_MEDIA_SESSION_INITIALIZE = 0;
     private static final int MSG_MEDIA_SESSION_SET_ACTIVE = 1;
 
+    /**
+     * Wrapper class that abstracts an instance of {@link MediaSession} to the
+     * {@link MediaSessionAdapter} interface this class uses.  This is done because
+     * {@link MediaSession} is a final class and cannot be mocked for testing purposes.
+     */
+    public class MediaSessionWrapper implements MediaSessionAdapter {
+        private final MediaSession mMediaSession;
+
+        public MediaSessionWrapper(MediaSession mediaSession) {
+            mMediaSession = mediaSession;
+        }
+
+        /**
+         * Sets the underlying {@link MediaSession} active status.
+         * @param active
+         */
+        @Override
+        public void setActive(boolean active) {
+            mMediaSession.setActive(active);
+        }
+
+        /**
+         * Gets the underlying {@link MediaSession} active status.
+         * @return {@code true} if active, {@code false} otherwise.
+         */
+        @Override
+        public boolean isActive() {
+            return mMediaSession.isActive();
+        }
+    }
+
+    /**
+     * Interface which defines the basic functionality of a {@link MediaSession} which is important
+     * for the {@link HeadsetMediaButton} to operator; this is for testing purposes so we can mock
+     * out that functionality.
+     */
+    public interface MediaSessionAdapter {
+        void setActive(boolean active);
+        boolean isActive();
+    }
+
     private final MediaSession.Callback mSessionCallback = new MediaSession.Callback() {
         @Override
         public boolean onMediaButtonEvent(Intent intent) {
@@ -81,7 +122,7 @@
                     session.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY
                             | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
                     session.setPlaybackToLocal(AUDIO_ATTRIBUTES);
-                    mSession = session;
+                    mSession = new MediaSessionWrapper(session);
                     break;
                 }
                 case MSG_MEDIA_SESSION_SET_ACTIVE: {
@@ -102,9 +143,37 @@
     private final Context mContext;
     private final CallsManager mCallsManager;
     private final TelecomSystem.SyncRoot mLock;
-    private MediaSession mSession;
+    private MediaSessionAdapter mSession;
     private KeyEvent mLastHookEvent;
 
+    /**
+     * Constructor used for testing purposes to initialize a {@link HeadsetMediaButton} with a
+     * specified {@link MediaSessionAdapter}.  Will not trigger MSG_MEDIA_SESSION_INITIALIZE and
+     * cause an actual {@link MediaSession} instance to be created.
+     * @param context the context
+     * @param callsManager the mock calls manager
+     * @param lock the lock
+     * @param adapter the adapter
+     */
+    @VisibleForTesting
+    public HeadsetMediaButton(
+            Context context,
+            CallsManager callsManager,
+            TelecomSystem.SyncRoot lock,
+            MediaSessionAdapter adapter) {
+        mContext = context;
+        mCallsManager = callsManager;
+        mLock = lock;
+        mSession = adapter;
+    }
+
+    /**
+     * Production code constructor; this version triggers MSG_MEDIA_SESSION_INITIALIZE which will
+     * create an actual instance of {@link MediaSession}.
+     * @param context the context
+     * @param callsManager the calls manager
+     * @param lock the telecom lock
+     */
     public HeadsetMediaButton(
             Context context,
             CallsManager callsManager,
@@ -155,6 +224,13 @@
         if (call.isExternalCall()) {
             return;
         }
+        handleCallAddition();
+    }
+
+    /**
+     * Triggers session activation due to call addition.
+     */
+    private void handleCallAddition() {
         mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget();
     }
 
@@ -164,6 +240,13 @@
         if (call.isExternalCall()) {
             return;
         }
+        handleCallRemoval();
+    }
+
+    /**
+     * Triggers session deactivation due to call removal.
+     */
+    private void handleCallRemoval() {
         if (!mCallsManager.hasAnyCalls()) {
             mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget();
         }
@@ -172,10 +255,20 @@
     /** ${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) {
-            onCallRemoved(call);
+            handleCallRemoval();
         } else {
-            onCallAdded(call);
+            handleCallAddition();
         }
     }
+
+    @VisibleForTesting
+    /**
+     * @return the handler this class instance uses for operation; used for unit testing.
+     */
+    public Handler getHandler() {
+        return mMediaSessionHandler;
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
new file mode 100644
index 0000000..6d15e60
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.HeadsetMediaButton;
+import com.android.server.telecom.TelecomSystem;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(JUnit4.class)
+public class HeadsetMediaButtonTest extends TelecomTestCase {
+    private static final int TEST_TIMEOUT_MILLIS = 1000;
+
+    private HeadsetMediaButton mHeadsetMediaButton;
+
+    @Mock private CallsManager mMockCallsManager;
+    @Mock private HeadsetMediaButton.MediaSessionAdapter mMediaSessionAdapter;
+    private TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {};
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mHeadsetMediaButton = new HeadsetMediaButton(mContext, mMockCallsManager, mLock,
+                mMediaSessionAdapter);
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        mHeadsetMediaButton = null;
+        super.tearDown();
+    }
+
+    /**
+     * Nominal case; just add a call and remove it.
+     */
+    @Test
+    public void testAddCall() {
+        Call regularCall = getRegularCall();
+
+        when(mMockCallsManager.hasAnyCalls()).thenReturn(true);
+        mHeadsetMediaButton.onCallAdded(regularCall);
+        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);
+        mHeadsetMediaButton.onCallRemoved(regularCall);
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter).setActive(eq(false));
+    }
+
+    /**
+     * Test a case where a regular call becomes an external call, and back again.
+     */
+    @Test
+    public void testRegularCallThatBecomesExternal() {
+        Call regularCall = getRegularCall();
+
+        // Start with a regular old call.
+        when(mMockCallsManager.hasAnyCalls()).thenReturn(true);
+        mHeadsetMediaButton.onCallAdded(regularCall);
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter).setActive(eq(true));
+        when(mMediaSessionAdapter.isActive()).thenReturn(true);
+
+        // Change so it is external.
+        when(regularCall.isExternalCall()).thenReturn(true);
+        when(mMockCallsManager.hasAnyCalls()).thenReturn(false);
+        mHeadsetMediaButton.onExternalCallChanged(regularCall, true);
+        // Expect to set session inactive.
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter).setActive(eq(false));
+
+        // For good measure lets make it non-external again.
+        when(regularCall.isExternalCall()).thenReturn(false);
+        when(mMockCallsManager.hasAnyCalls()).thenReturn(true);
+        mHeadsetMediaButton.onExternalCallChanged(regularCall, false);
+        // Expect to set session active.
+        waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+        verify(mMediaSessionAdapter).setActive(eq(true));
+    }
+
+    /**
+     * @return a mock call instance of a regular non-external call.
+     */
+    private Call getRegularCall() {
+        Call regularCall = Mockito.mock(Call.class);
+        when(regularCall.isExternalCall()).thenReturn(false);
+        return regularCall;
+    }
+}