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

Commit 52ba7e4d authored by chelseahao's avatar chelseahao
Browse files

Implement scan with filter and restart scan without filter.

Flag: com.android.settingslib.flags.enable_le_audio_sharing
Test: atest
Bug: 395978182
Change-Id: I73d27d19426c5c6620053a97696a73bd4612486b
parent e070b6b5
Loading
Loading
Loading
Loading
+82 −3
Original line number Diff line number Diff line
@@ -22,14 +22,23 @@ import static com.android.settings.connecteddevice.audiosharing.audiostreams.Aud
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamScanHelper.State.STATE_ON;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamScanHelper.State.STATE_TURNING_OFF;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamScanHelper.State.STATE_TURNING_ON;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamScanHelper.State.STATE_WAITING_TO_RESTART_WITH_NO_FILTER;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID;

import static java.util.Collections.emptyList;

import android.annotation.NonNull;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.le.ScanFilter;
import android.os.ParcelUuid;
import android.util.Log;

import androidx.annotation.NonNull;

import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

@@ -47,10 +56,14 @@ public class AudioStreamScanHelper implements
        STATE_OFF,
        STATE_TURNING_ON,
        STATE_ON,
        STATE_TURNING_OFF
        STATE_TURNING_OFF,
        STATE_WAITING_TO_RESTART_WITH_NO_FILTER,
    }

    private static final String TAG = "AudioStreamScanHelper";
    private static final ParcelUuid BAAS_UUID = ParcelUuid.fromString(
            "00001852-0000-1000-8000-00805F9B34FB");
    private static final String UNSET_DEVICE_ADDR = "FF:FF:FF:FF:FF:FF";
    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
    private final Consumer<Boolean> mScanStateChangedListener;
    private final @NonNull Executor mExecutor;
@@ -82,6 +95,68 @@ public class AudioStreamScanHelper implements
        });
    }

    /**
     * Starts the scanning process for one specific audio stream when info in metadata is available.
     * This method will do nothing if scanning is already started or in the process of starting.
     */
    public void startScanningWithFilter(@NonNull BluetoothLeBroadcastMetadata metadata) {
        mExecutor.execute(() -> {
            if (mState == STATE_ON || mState == STATE_TURNING_ON) {
                Log.d(TAG, "startScanningWithFilter() : do nothing, state = " + mState);
                return;
            }
            boolean useFilter = false;
            ScanFilter.Builder builder = new ScanFilter.Builder();
            if (metadata.getBroadcastId() != UNSET_BROADCAST_ID) {
                byte[] broadcastIdBytes = createBroadcastIdBytes(metadata.getBroadcastId());
                byte[] serviceDataMask = new byte[broadcastIdBytes.length];
                Arrays.fill(serviceDataMask, (byte) 0xFF);
                builder.setServiceData(BAAS_UUID, broadcastIdBytes, serviceDataMask);
                useFilter = true;
            }
            if (metadata.getSourceDevice() != null && !Objects.equals(
                    metadata.getSourceDevice().getAddress(), UNSET_DEVICE_ADDR)) {
                builder.setDeviceAddress(metadata.getSourceDevice().getAddress());
                useFilter = true;
            }
            var filter = builder.build();
            if (mLeBroadcastAssistant != null) {
                Log.d(TAG, "startScanningWithFilter() : scanFilter applied : " + filter);
                mLeBroadcastAssistant.startSearchingForSources(
                        useFilter ? List.of(filter) : emptyList());
                setState(STATE_TURNING_ON);
            }
        });
    }

    /**
     * Restarts the scanning process without scan filter.
     * This method will do nothing if scanning is not started or in the process of starting.
     */
    public void restartScanningWithoutFilter() {
        mExecutor.execute(() -> {
            if (mState != STATE_ON && mState != STATE_TURNING_ON) {
                Log.d(TAG, "restartScanningWithoutFilter() : do nothing, state = " + mState);
                return;
            }
            if (mLeBroadcastAssistant != null) {
                Log.d(TAG, "restartScanningWithoutFilter() : stop scanning");
                mLeBroadcastAssistant.stopSearchingForSources();
                setState(STATE_WAITING_TO_RESTART_WITH_NO_FILTER);
            }
        });
    }

    private static byte[] createBroadcastIdBytes(int broadcastId) {
        byte[] byteArray = new byte[3];

        byteArray[0] = (byte) (broadcastId & 0xFF);
        byteArray[1] = (byte) ((broadcastId >> 8) & 0xFF);
        byteArray[2] = (byte) ((broadcastId >> 16) & 0xFF);

        return byteArray;
    }

    /**
     * Stops the ongoing scanning process for audio stream sources.
     * This method will do nothing if scanning is already off or in the process of stopping.
@@ -120,7 +195,11 @@ public class AudioStreamScanHelper implements
    public void scanningStopped() {
        mExecutor.execute(() -> {
            Log.d(TAG, "scanningStopped()");
            if (mState == STATE_WAITING_TO_RESTART_WITH_NO_FILTER) {
                startScanning();
            } else {
                setState(STATE_OFF);
            }
        });
    }

+148 −0
Original line number Diff line number Diff line
@@ -18,17 +18,26 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;

import static android.bluetooth.BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE;

import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.os.Looper;
import android.os.ParcelUuid;

import androidx.test.core.app.ApplicationProvider;

@@ -44,6 +53,7 @@ import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;

import java.util.List;
import java.util.function.Consumer;

@RunWith(RobolectricTestRunner.class)
@@ -195,4 +205,142 @@ public class AudioStreamScanHelperTest {
        verify(mScanStateChangedListener, times(2)).accept(captor.capture());
        assertThat(captor.getValue()).isTrue();
    }

    @Test
    public void startScanningWithFilter_assistantNotNullWithBroadcastId_startWithFilter() {
        BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class);
        when(metadata.getBroadcastId()).thenReturn(123);
        when(metadata.getSourceDevice()).thenReturn(null);

        mScanHelper.startScanningWithFilter(metadata);
        shadowOf(Looper.getMainLooper()).idle();

        ArgumentCaptor<List<ScanFilter>> filterCaptor = ArgumentCaptor.forClass(List.class);
        verify(mLeBroadcastAssistant).startSearchingForSources(filterCaptor.capture());
        assertThat(filterCaptor.getValue()).hasSize(1);
        ScanFilter filter = filterCaptor.getValue().getFirst();
        assertThat(filter.getServiceDataUuid()).isEqualTo(
                ParcelUuid.fromString("00001852-0000-1000-8000-00805F9B34FB"));
        assertThat(filter.getServiceData()).isEqualTo(new byte[]{0x7B, 0x00, 0x00});
        assertThat(filter.getServiceDataMask()).isEqualTo(
                new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF});
        assertThat(filter.getDeviceAddress()).isNull();

        ArgumentCaptor<Boolean> stateCaptor = ArgumentCaptor.forClass(Boolean.class);
        verify(mScanStateChangedListener).accept(stateCaptor.capture());
        assertThat(stateCaptor.getValue()).isTrue();
    }

    @Test
    public void startScanningWithFilter_assistantNotNullWithDeviceAddress_startWithFilter() {
        BluetoothDevice device = mock(BluetoothDevice.class);
        when(device.getAddress()).thenReturn("AA:BB:CC:DD:EE:FF");
        BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class);
        when(metadata.getBroadcastId()).thenReturn(UNSET_BROADCAST_ID);
        when(metadata.getSourceDevice()).thenReturn(device);

        mScanHelper.startScanningWithFilter(metadata);
        shadowOf(Looper.getMainLooper()).idle();

        ArgumentCaptor<List<ScanFilter>> filterCaptor = ArgumentCaptor.forClass(List.class);
        verify(mLeBroadcastAssistant).startSearchingForSources(filterCaptor.capture());
        assertThat(filterCaptor.getValue()).hasSize(1);
        ScanFilter filter = filterCaptor.getValue().getFirst();
        assertThat(filter.getDeviceAddress()).isEqualTo("AA:BB:CC:DD:EE:FF");
        assertThat(filter.getServiceDataUuid()).isNull();
        assertThat(filter.getServiceData()).isNull();
        assertThat(filter.getServiceDataMask()).isNull();

        ArgumentCaptor<Boolean> stateCaptor = ArgumentCaptor.forClass(Boolean.class);
        verify(mScanStateChangedListener).accept(stateCaptor.capture());
        assertThat(stateCaptor.getValue()).isTrue();
    }

    @Test
    public void startScanningWithFilter_assistantNotNullWithBothFilters_startWithFilter() {
        BluetoothDevice device = mock(BluetoothDevice.class);
        when(device.getAddress()).thenReturn("AA:BB:CC:DD:EE:FF");
        BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class);
        when(metadata.getBroadcastId()).thenReturn(456);
        when(metadata.getSourceDevice()).thenReturn(device);

        mScanHelper.startScanningWithFilter(metadata);
        shadowOf(Looper.getMainLooper()).idle();

        ArgumentCaptor<List<ScanFilter>> filterCaptor = ArgumentCaptor.forClass(List.class);
        verify(mLeBroadcastAssistant).startSearchingForSources(filterCaptor.capture());
        assertThat(filterCaptor.getValue()).hasSize(1);
        ScanFilter filter = filterCaptor.getValue().getFirst();
        assertThat(filter.getServiceDataUuid()).isEqualTo(
                ParcelUuid.fromString("00001852-0000-1000-8000-00805F9B34FB"));
        assertThat(filter.getServiceData()).isEqualTo(new byte[]{(byte) 0xC8, 0x01, 0x00});
        assertThat(filter.getServiceDataMask()).isEqualTo(
                new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF});
        assertThat(filter.getDeviceAddress()).isEqualTo("AA:BB:CC:DD:EE:FF");

        ArgumentCaptor<Boolean> stateCaptor = ArgumentCaptor.forClass(Boolean.class);
        verify(mScanStateChangedListener).accept(stateCaptor.capture());
        assertThat(stateCaptor.getValue()).isTrue();
    }

    @Test
    public void startScanningWithFilter_scanningAlreadyOn_doesNothing() {
        mScanHelper.startScanning();
        shadowOf(Looper.getMainLooper()).idle();

        BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class);
        mScanHelper.startScanningWithFilter(metadata);
        shadowOf(Looper.getMainLooper()).idle();

        // Trigger only once
        verify(mLeBroadcastAssistant).startSearchingForSources(any());
        verify(mScanStateChangedListener).accept(anyBoolean());
    }

    @Test
    public void restartScanningWithoutFilter_scanningOn_stopsAndRestartsScanningWithoutFilter() {
        mScanHelper.scanningStarted();
        shadowOf(Looper.getMainLooper()).idle();

        mScanHelper.restartScanningWithoutFilter();
        shadowOf(Looper.getMainLooper()).idle();

        verify(mLeBroadcastAssistant).stopSearchingForSources();
        shadowOf(Looper.getMainLooper()).idle();

        mScanHelper.scanningStopped();
        shadowOf(Looper.getMainLooper()).idle();

        verify(mLeBroadcastAssistant).startSearchingForSources(any());
        ArgumentCaptor<Boolean> stateCaptor = ArgumentCaptor.forClass(Boolean.class);
        verify(mScanStateChangedListener, times(3)).accept(stateCaptor.capture());
        var values = stateCaptor.getAllValues();
        assertThat(values.get(0)).isTrue();
        assertThat(values.get(1)).isFalse();
        assertThat(values.get(2)).isTrue();
    }

    @Test
    public void restartScanningWithoutFilter_scanningOff_doesNothing() {
        mScanHelper.stopScanning();

        mScanHelper.restartScanningWithoutFilter();
        shadowOf(Looper.getMainLooper()).idle();

        verify(mLeBroadcastAssistant, never()).stopSearchingForSources();
        verify(mLeBroadcastAssistant, never()).startSearchingForSources(any());
        verify(mScanStateChangedListener, never()).accept(anyBoolean());
    }

    @Test
    public void restartScanningWithoutFilter_scanningTurningOff_doesNothing() {
        mScanHelper.stopScanning();

        mScanHelper.restartScanningWithoutFilter();
        shadowOf(Looper.getMainLooper()).idle();

        verify(mLeBroadcastAssistant, never()).stopSearchingForSources();
        verify(mLeBroadcastAssistant, never()).startSearchingForSources(any());
        verify(mScanStateChangedListener, never()).accept(anyBoolean());
    }
}