Loading src/com/android/server/telecom/CallsManager.java +1 −1 Original line number Original line Diff line number Diff line Loading @@ -3300,7 +3300,7 @@ public class CallsManager extends Call.ListenerBase * * * @return {@code True} if there are any non-external calls, {@code false} otherwise. * @return {@code True} if there are any non-external calls, {@code false} otherwise. */ */ boolean hasAnyCalls() { public boolean hasAnyCalls() { if (mCalls.isEmpty()) { if (mCalls.isEmpty()) { return false; return false; } } Loading src/com/android/server/telecom/HeadsetMediaButton.java +97 −4 Original line number Original line Diff line number Diff line Loading @@ -46,6 +46,47 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { private static final int MSG_MEDIA_SESSION_INITIALIZE = 0; private static final int MSG_MEDIA_SESSION_INITIALIZE = 0; private static final int MSG_MEDIA_SESSION_SET_ACTIVE = 1; 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() { private final MediaSession.Callback mSessionCallback = new MediaSession.Callback() { @Override @Override public boolean onMediaButtonEvent(Intent intent) { public boolean onMediaButtonEvent(Intent intent) { Loading Loading @@ -81,7 +122,7 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { session.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY session.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS); | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS); session.setPlaybackToLocal(AUDIO_ATTRIBUTES); session.setPlaybackToLocal(AUDIO_ATTRIBUTES); mSession = session; mSession = new MediaSessionWrapper(session); break; break; } } case MSG_MEDIA_SESSION_SET_ACTIVE: { case MSG_MEDIA_SESSION_SET_ACTIVE: { Loading @@ -102,9 +143,37 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { private final Context mContext; private final Context mContext; private final CallsManager mCallsManager; private final CallsManager mCallsManager; private final TelecomSystem.SyncRoot mLock; private final TelecomSystem.SyncRoot mLock; private MediaSession mSession; private MediaSessionAdapter mSession; private KeyEvent mLastHookEvent; 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( public HeadsetMediaButton( Context context, Context context, CallsManager callsManager, CallsManager callsManager, Loading Loading @@ -155,6 +224,13 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { if (call.isExternalCall()) { if (call.isExternalCall()) { return; return; } } handleCallAddition(); } /** * Triggers session activation due to call addition. */ private void handleCallAddition() { mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget(); mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget(); } } Loading @@ -164,6 +240,13 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { if (call.isExternalCall()) { if (call.isExternalCall()) { return; return; } } handleCallRemoval(); } /** * Triggers session deactivation due to call removal. */ private void handleCallRemoval() { if (!mCallsManager.hasAnyCalls()) { if (!mCallsManager.hasAnyCalls()) { mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget(); mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget(); } } Loading @@ -172,10 +255,20 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { /** ${inheritDoc} */ /** ${inheritDoc} */ @Override @Override public void onExternalCallChanged(Call call, boolean isExternalCall) { 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) { if (isExternalCall) { onCallRemoved(call); handleCallRemoval(); } else { } else { onCallAdded(call); handleCallAddition(); } } } } @VisibleForTesting /** * @return the handler this class instance uses for operation; used for unit testing. */ public Handler getHandler() { return mMediaSessionHandler; } } } tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java 0 → 100644 +120 −0 Original line number Original line Diff line number Diff line /* * 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; } } Loading
src/com/android/server/telecom/CallsManager.java +1 −1 Original line number Original line Diff line number Diff line Loading @@ -3300,7 +3300,7 @@ public class CallsManager extends Call.ListenerBase * * * @return {@code True} if there are any non-external calls, {@code false} otherwise. * @return {@code True} if there are any non-external calls, {@code false} otherwise. */ */ boolean hasAnyCalls() { public boolean hasAnyCalls() { if (mCalls.isEmpty()) { if (mCalls.isEmpty()) { return false; return false; } } Loading
src/com/android/server/telecom/HeadsetMediaButton.java +97 −4 Original line number Original line Diff line number Diff line Loading @@ -46,6 +46,47 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { private static final int MSG_MEDIA_SESSION_INITIALIZE = 0; private static final int MSG_MEDIA_SESSION_INITIALIZE = 0; private static final int MSG_MEDIA_SESSION_SET_ACTIVE = 1; 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() { private final MediaSession.Callback mSessionCallback = new MediaSession.Callback() { @Override @Override public boolean onMediaButtonEvent(Intent intent) { public boolean onMediaButtonEvent(Intent intent) { Loading Loading @@ -81,7 +122,7 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { session.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY session.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS); | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS); session.setPlaybackToLocal(AUDIO_ATTRIBUTES); session.setPlaybackToLocal(AUDIO_ATTRIBUTES); mSession = session; mSession = new MediaSessionWrapper(session); break; break; } } case MSG_MEDIA_SESSION_SET_ACTIVE: { case MSG_MEDIA_SESSION_SET_ACTIVE: { Loading @@ -102,9 +143,37 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { private final Context mContext; private final Context mContext; private final CallsManager mCallsManager; private final CallsManager mCallsManager; private final TelecomSystem.SyncRoot mLock; private final TelecomSystem.SyncRoot mLock; private MediaSession mSession; private MediaSessionAdapter mSession; private KeyEvent mLastHookEvent; 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( public HeadsetMediaButton( Context context, Context context, CallsManager callsManager, CallsManager callsManager, Loading Loading @@ -155,6 +224,13 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { if (call.isExternalCall()) { if (call.isExternalCall()) { return; return; } } handleCallAddition(); } /** * Triggers session activation due to call addition. */ private void handleCallAddition() { mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget(); mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget(); } } Loading @@ -164,6 +240,13 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { if (call.isExternalCall()) { if (call.isExternalCall()) { return; return; } } handleCallRemoval(); } /** * Triggers session deactivation due to call removal. */ private void handleCallRemoval() { if (!mCallsManager.hasAnyCalls()) { if (!mCallsManager.hasAnyCalls()) { mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget(); mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget(); } } Loading @@ -172,10 +255,20 @@ public class HeadsetMediaButton extends CallsManagerListenerBase { /** ${inheritDoc} */ /** ${inheritDoc} */ @Override @Override public void onExternalCallChanged(Call call, boolean isExternalCall) { 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) { if (isExternalCall) { onCallRemoved(call); handleCallRemoval(); } else { } else { onCallAdded(call); handleCallAddition(); } } } } @VisibleForTesting /** * @return the handler this class instance uses for operation; used for unit testing. */ public Handler getHandler() { return mMediaSessionHandler; } } }
tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java 0 → 100644 +120 −0 Original line number Original line Diff line number Diff line /* * 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; } }