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

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

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

This CL re-lands the functionality. 

In the past we always made a global MediaSession active in Telecom when
there was a call present on the device.  This was to ensure that when the
device used a wired headset that we'd be able to handle the button presses.

Due to our more recent change to not inform HFP devices of DND calls, we
saw a regression in car head unit operation during calls in DND.  Because
the HFP device is no longer made aware of DND calls and we were still
opening a media session, HFP car kits were not properly routing control
to an ongoing music playback session.

Modified Telecom's HeadsetMediaButton class to ONLY open a media session if
there is a call present and the audio is routed to a wired headset.
Control of calls via bluetooth headsets are always routed via the
BluetoothInCallService and never rely on keycode events.  Further, when
routed to speaker or earpiece, there are never going to be "pause/resume"
media keycode events that Telecom needs to handle.

Test: Manual test with wired headset to ensure basic operation of media
buttons on wired headset operate as expected.
Test: Added new unit test cases to ensure media session is not started until
we are routed to a wired headset.
Test: Fix existing unit tests to account for the new behavior.
Fixes: 266171157

Change-Id: I7355f6b0f1d5f07b5c3779831dea0adf29932a58
parent 8f25e34f
Loading
Loading
Loading
Loading
+48 −21
Original line number Diff line number Diff line
@@ -23,11 +23,16 @@ import android.media.session.MediaSession;
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 @@ public class HeadsetMediaButton extends CallsManagerListenerBase {
    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 @@ public class HeadsetMediaButton extends CallsManagerListenerBase {
            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,50 +233,70 @@ public class HeadsetMediaButton extends CallsManagerListenerBase {
        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() {
    private void handleCallAddition(Call call) {
        mCalls.add(call);
        maybeChangeSessionState();
    }

    /**
     * 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 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 onCallRemoved(Call call) {
        if (call.isExternalCall()) {
            return;
        }
        handleCallRemoval();
        handleCallRemoval(call);
    }

    /**
     * Triggers session deactivation due to call removal.
     */
    private void handleCallRemoval() {
        if (!mCallsManager.hasAnyCalls()) {
            mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget();
    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) {
        // 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();
        }
        maybeChangeSessionState();
    }

    @VisibleForTesting
+90 −3
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.telecom.tests;

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 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
    }

    /**
     * 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 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
        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 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
        // 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 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
        // 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 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
        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 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
        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));