Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a1367aa0 authored by Tyler Gunn's avatar Tyler Gunn Committed by Android (Google) Code Review
Browse files

Merge "Correct HeadsetMediaButton behavior for external calls." into tm-dev

parents 699de70a 9b3500e2
Loading
Loading
Loading
Loading
+1 −1
Original line number Original line Diff line number Diff line
@@ -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;
        }
        }
+97 −4
Original line number Original line Diff line number Diff line
@@ -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) {
@@ -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: {
@@ -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,
@@ -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();
    }
    }


@@ -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();
        }
        }
@@ -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;
    }
}
}
+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;
    }
}