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

Commit 85d7ba1c authored by Jean-Michel Trivi's avatar Jean-Michel Trivi
Browse files

AudioService: better A2DP (dis)connection testing

  A sequence of A2DP connection / disconnection / connection was
supposed to be covered by a unit test in AudioDeviceBrokerTest
with different timings. But if the test was run in the absence
of background media playback, the disconnection event was not
delayed by 1s after sending the BECOMING_NOISY event, and the
connection / disconn / connection sequence was executed in the
same order as the sender used, causing the test to succeed.
  This CL adds functionality in the adapter for AudioSystem to
configure whether it is to simulate playback or not, so all
tests can be run with and without.

Also:
- update system server adapter to support BECOMING_NOISY intent
  and make AudioDeviceBroker use the adapter code instead.
- move the "no op" code of the adapters from the server folder
 (where it doesn't belong) to the test folder (because it should
 only ever be used in the context of a test).

Bug: 142293357
Test: atest AudioDeviceBrokerTest
Change-Id: Ic9877b5cbff97930bfc4bc1e48fee07d96799a86
parent f58030e8
Loading
Loading
Loading
Loading
+9 −18
Original line number Diff line number Diff line
@@ -25,7 +25,6 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.media.AudioDeviceAttributes;
import android.media.AudioManager;
import android.media.AudioRoutesInfo;
import android.media.AudioSystem;
import android.media.IAudioRoutesObserver;
@@ -38,7 +37,6 @@ import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
import android.util.PrintWriterPrinter;

@@ -71,6 +69,8 @@ import java.io.PrintWriter;
    private final AudioDeviceInventory mDeviceInventory;
    // Manages notifications to BT service
    private final BtHelper mBtHelper;
    // Adapter for system_server-reserved operations
    private final SystemServerAdapter mSystemServer;


    //-------------------------------------------------------------------
@@ -97,17 +97,21 @@ import java.io.PrintWriter;
        mAudioService = service;
        mBtHelper = new BtHelper(this);
        mDeviceInventory = new AudioDeviceInventory(this);
        mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext);

        init();
    }

    /** for test purposes only, inject AudioDeviceInventory */
    /** for test purposes only, inject AudioDeviceInventory and adapter for operations running
     *  in system_server */
    AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
                      @NonNull AudioDeviceInventory mockDeviceInventory) {
                      @NonNull AudioDeviceInventory mockDeviceInventory,
                      @NonNull SystemServerAdapter mockSystemServer) {
        mContext = context;
        mAudioService = service;
        mBtHelper = new BtHelper(this);
        mDeviceInventory = mockDeviceInventory;
        mSystemServer = mockSystemServer;

        init();
    }
@@ -682,7 +686,7 @@ import java.io.PrintWriter;
    private void onSendBecomingNoisyIntent() {
        AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
                "broadcast ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG));
        sendBroadcastToAll(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
        mSystemServer.sendDeviceBecomingNoisyIntent();
    }

    //---------------------------------------------------------------------
@@ -1100,17 +1104,4 @@ import java.io.PrintWriter;
                    time);
        }
    }

    //-------------------------------------------------------------
    // internal utilities
    private void sendBroadcastToAll(Intent intent) {
        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        final long ident = Binder.clearCallingIdentity();
        try {
            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -1118,7 +1118,7 @@ public class AudioDeviceInventory {
                && AudioSystem.isSingleAudioDeviceType(devices, device)
                && !mDeviceBroker.hasMediaDynamicPolicy()
                && (musicDevice != AudioSystem.DEVICE_OUT_REMOTE_SUBMIX)) {
            if (!AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0 /*not looking in past*/)
            if (!mAudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0 /*not looking in past*/)
                    && !mDeviceBroker.hasAudioFocusUsers()) {
                // no media playback, not a "becoming noisy" situation, otherwise it could cause
                // the pausing of some apps that are playing remotely
+5 −80
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.server.audio;
import android.annotation.NonNull;
import android.media.AudioDeviceAttributes;
import android.media.AudioSystem;
import android.util.Log;

/**
 * Provides an adapter to access functionality of the android.media.AudioSystem class for device
@@ -38,15 +37,6 @@ public class AudioSystemAdapter {
        return new AudioSystemAdapter();
    }

    /**
     * Create an adapter for AudioSystem that always succeeds, and does nothing.
     * Overridden methods can be configured
     * @return a no-op AudioSystem adapter with configurable adapter
     */
    static final @NonNull AudioSystemAdapter getConfigurableAdapter() {
        return new AudioSystemConfigurableAdapter();
    }

    /**
     * Same as {@link AudioSystem#setDeviceConnectionState(int, int, String, String, int)}
     * @param device
@@ -143,75 +133,10 @@ public class AudioSystemAdapter {
        return AudioSystem.setCurrentImeUid(uid);
    }

    //--------------------------------------------------------------------
    protected static class AudioSystemConfigurableAdapter extends AudioSystemAdapter {
        private static final String TAG = "ASA";
        private boolean mIsMicMuted = false;
        private boolean mMuteMicrophoneFails = false;

        public void configureIsMicrophoneMuted(boolean muted) {
            mIsMicMuted = muted;
        }

        public void configureMuteMicrophoneToFail(boolean fail) {
            mMuteMicrophoneFails = fail;
        }

        //-----------------------------------------------------------------
        // Overrides of AudioSystemAdapter
        @Override
        public int setDeviceConnectionState(int device, int state, String deviceAddress,
                                            String deviceName, int codecFormat) {
            Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %s, %s, 0x%s",
                    Integer.toHexString(device), state, deviceAddress, deviceName,
                    Integer.toHexString(codecFormat)));
            return AudioSystem.AUDIO_STATUS_OK;
        }

        @Override
        public int getDeviceConnectionState(int device, String deviceAddress) {
            return AudioSystem.AUDIO_STATUS_OK;
        }

        @Override
        public int handleDeviceConfigChange(int device, String deviceAddress,
                                                   String deviceName, int codecFormat) {
            return AudioSystem.AUDIO_STATUS_OK;
        }

        @Override
        public int setPreferredDeviceForStrategy(int strategy,
                                                 @NonNull AudioDeviceAttributes device) {
            return AudioSystem.AUDIO_STATUS_OK;
        }

        @Override
        public int removePreferredDeviceForStrategy(int strategy) {
            return AudioSystem.AUDIO_STATUS_OK;
        }

        @Override
        public int setParameters(String keyValuePairs) {
            return AudioSystem.AUDIO_STATUS_OK;
        }

        @Override
        public boolean isMicrophoneMuted() {
            return mIsMicMuted;
        }

        @Override
        public int muteMicrophone(boolean on) {
            if (mMuteMicrophoneFails) {
                return AudioSystem.AUDIO_STATUS_ERROR;
            }
            mIsMicMuted = on;
            return AudioSystem.AUDIO_STATUS_OK;
        }

        @Override
        public int setCurrentImeUid(int uid) {
            return AudioSystem.AUDIO_STATUS_OK;
        }
    /**
     * Same as {@link AudioSystem#isStreamActive(int, int)}
     */
    public boolean isStreamActive(int stream, int inPastMs) {
        return AudioSystem.isStreamActive(stream, inPastMs);
    }
}
+19 −24
Original line number Diff line number Diff line
@@ -21,8 +21,11 @@ import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Binder;
import android.os.UserHandle;

import java.util.Objects;

/**
 * Provides an adapter to access functionality reserved to components running in system_server
 * Functionality such as sending privileged broadcasts is to be accessed through the default
@@ -32,7 +35,7 @@ public class SystemServerAdapter {

    protected final Context mContext;

    private SystemServerAdapter(@Nullable Context context) {
    protected SystemServerAdapter(@Nullable Context context) {
        mContext = context;
    }
    /**
@@ -40,18 +43,10 @@ public class SystemServerAdapter {
     * @return the adapter
     */
    static final @NonNull SystemServerAdapter getDefaultAdapter(Context context) {
        Objects.requireNonNull(context);
        return new SystemServerAdapter(context);
    }

    /**
     * Create an adapter that does nothing.
     * Use for running non-privileged tests, such as unit tests
     * @return a no-op adapter
     */
    static final @NonNull SystemServerAdapter getNoOpAdapter() {
        return new NoOpSystemServerAdapter();
    }

    /**
     * @return true if this is supposed to be run in system_server, false otherwise (e.g. for a
     *     unit test)
@@ -70,21 +65,21 @@ public class SystemServerAdapter {
                UserHandle.ALL);
    }

    //--------------------------------------------------------------------
    protected static class NoOpSystemServerAdapter extends SystemServerAdapter {

        NoOpSystemServerAdapter() {
            super(null);
        }

        @Override
        public boolean isPrivileged() {
            return false;
    /**
     * Broadcast ACTION_AUDIO_BECOMING_NOISY
     */
    public void sendDeviceBecomingNoisyIntent() {
        if (mContext == null) {
            return;
        }

        @Override
        public void sendMicrophoneMuteChangedIntent() {
            // no-op
        final Intent intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        final long ident = Binder.clearCallingIdentity();
        try {
            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }
}
+77 −19
Original line number Diff line number Diff line
@@ -58,6 +58,7 @@ public class AudioDeviceBrokerTest {
    @Mock private AudioService mMockAudioService;
    @Spy private AudioDeviceInventory mSpyDevInventory;
    @Spy private AudioSystemAdapter mSpyAudioSystem;
    private SystemServerAdapter mSystemServer;

    private BluetoothDevice mFakeBtDevice;

@@ -66,9 +67,11 @@ public class AudioDeviceBrokerTest {
        mContext = InstrumentationRegistry.getTargetContext();

        mMockAudioService = mock(AudioService.class);
        mSpyAudioSystem = spy(AudioSystemAdapter.getConfigurableAdapter());
        mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
        mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem));
        mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory);
        mSystemServer = new NoOpSystemServerAdapter();
        mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory,
                mSystemServer);
        mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);

        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
@@ -79,8 +82,8 @@ public class AudioDeviceBrokerTest {
    @After
    public void tearDown() throws Exception { }

    @Test
    public void testSetUpAndTearDown() { }
//    @Test
//    public void testSetUpAndTearDown() { }

    /**
     * postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for connection:
@@ -90,7 +93,7 @@ public class AudioDeviceBrokerTest {
     */
    @Test
    public void testPostA2dpDeviceConnectionChange() throws Exception {
        Log.i(TAG, "testPostA2dpDeviceConnectionChange");
        Log.i(TAG, "starting testPostA2dpDeviceConnectionChange");
        Assert.assertNotNull("invalid null BT device", mFakeBtDevice);

        mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
@@ -104,13 +107,8 @@ public class AudioDeviceBrokerTest {
                ArgumentMatchers.eq(1) /*a2dpVolume*/
        );

        final String expectedName = mFakeBtDevice.getName() == null ? "" : mFakeBtDevice.getName();
        verify(mSpyAudioSystem, times(1)).setDeviceConnectionState(
                ArgumentMatchers.eq(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP),
                ArgumentMatchers.eq(AudioSystem.DEVICE_STATE_AVAILABLE),
                ArgumentMatchers.eq(mFakeBtDevice.getAddress()),
                ArgumentMatchers.eq(expectedName),
                anyInt() /*codec*/);
        // verify the connection was reported to AudioSystem
        checkSingleSystemConnection(mFakeBtDevice);
    }

    /**
@@ -121,31 +119,70 @@ public class AudioDeviceBrokerTest {
     */
    @Test
    public void testA2dpDeviceConnectionDisconnectionConnectionChange() throws Exception {
        Log.i(TAG, "testA2dpDeviceConnectionDisconnectionConnectionChange");
        Log.i(TAG, "starting testA2dpDeviceConnectionDisconnectionConnectionChange");

        doTestConnectionDisconnectionReconnection(0);
        doTestConnectionDisconnectionReconnection(0, false,
                // cannot guarantee single connection since commands are posted in separate thread
                // than they are processed
                false);
    }

    /**
     * Verify device disconnection and reconnection within the BECOMING_NOISY window
     * in the absence of media playback
     * @throws Exception
     */
    @Test
    public void testA2dpDeviceReconnectionWithinBecomingNoisyDelay() throws Exception {
        Log.i(TAG, "testA2dpDeviceReconnectionWithinBecomingNoisyDelay");
        Log.i(TAG, "starting testA2dpDeviceReconnectionWithinBecomingNoisyDelay");

        doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2,
                false,
                // do not check single connection since the connection command will come much
                // after the disconnection command
                false);
    }

        doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2);
    /**
     * Same as testA2dpDeviceConnectionDisconnectionConnectionChange() but with mock media playback
     * @throws Exception
     */
    @Test
    public void testA2dpConnectionDisconnectionConnectionChange_MediaPlayback() throws Exception {
        Log.i(TAG, "starting testA2dpConnectionDisconnectionConnectionChange_MediaPlayback");

        doTestConnectionDisconnectionReconnection(0, true,
                // guarantee single connection since because of media playback the disconnection
                // is supposed to be delayed, and thus cancelled because of the connection
                true);
    }

    /**
     * Same as testA2dpDeviceReconnectionWithinBecomingNoisyDelay() but with mock media playback
     * @throws Exception
     */
    @Test
    public void testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback() throws Exception {
        Log.i(TAG, "starting testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback");

        doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2,
                true,
                // guarantee single connection since because of media playback the disconnection
                // is supposed to be delayed, and thus cancelled because of the connection
                true);
    }

    private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection)
            throws Exception {
    private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection,
            boolean mockMediaPlayback, boolean guaranteeSingleConnection) throws Exception {
        when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC))
                .thenReturn(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
        when(mMockAudioService.isInCommunication()).thenReturn(false);
        when(mMockAudioService.hasMediaDynamicPolicy()).thenReturn(false);
        when(mMockAudioService.hasAudioFocusUsers()).thenReturn(false);

        // first connection
        ((NoOpAudioSystemAdapter) mSpyAudioSystem).configureIsStreamActive(mockMediaPlayback);

        // first connection: ensure the device is connected as a starting condition for the test
        mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
                BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1);
        Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
@@ -169,5 +206,26 @@ public class AudioDeviceBrokerTest {
                ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED));
        Assert.assertTrue("Mock device not connected",
                mSpyDevInventory.isA2dpDeviceConnected(mFakeBtDevice));

        if (guaranteeSingleConnection) {
            // when the disconnection was expected to be cancelled, there should have been a single
            //  call to AudioSystem to declare the device connected (available)
            checkSingleSystemConnection(mFakeBtDevice);
        }
    }

    /**
     * Verifies the given device was reported to AudioSystem exactly once as available
     * @param btDevice
     * @throws Exception
     */
    private void checkSingleSystemConnection(BluetoothDevice btDevice) throws Exception {
        final String expectedName = btDevice.getName() == null ? "" : btDevice.getName();
        verify(mSpyAudioSystem, times(1)).setDeviceConnectionState(
                ArgumentMatchers.eq(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP),
                ArgumentMatchers.eq(AudioSystem.DEVICE_STATE_AVAILABLE),
                ArgumentMatchers.eq(btDevice.getAddress()),
                ArgumentMatchers.eq(expectedName),
                anyInt() /*codec*/);
    }
}
Loading