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

Commit 130629b1 authored by Jason Chiu's avatar Jason Chiu
Browse files

Show paired devices on Bluetooth device slice card

- support tapping to activate for all available media devices including
  Hearing aid and Headset
- support tapping to connect for previously connected devices

Bug: 149667096
Test: robotest
Change-Id: I25f74b1b20fbb1876200a561775aa675ff60ac37
parent 7d6011be
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -123,7 +123,8 @@
                  android:label="@string/settings_label_launcher"
                  android:theme="@style/Theme.Settings.Home"
                  android:taskAffinity="com.android.settings.root"
                  android:launchMode="singleTask">
                  android:launchMode="singleTask"
                  android:configChanges="keyboard|keyboardHidden">
            <intent-filter android:priority="1">
                <action android:name="android.settings.SETTINGS" />
                <category android:name="android.intent.category.DEFAULT" />
+3 −4
Original line number Diff line number Diff line
@@ -69,18 +69,17 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback,

    public BluetoothDeviceUpdater(Context context, DashboardFragment fragment,
            DevicePreferenceCallback devicePreferenceCallback) {
        this(fragment, devicePreferenceCallback, Utils.getLocalBtManager(context));
        this(context, fragment, devicePreferenceCallback, Utils.getLocalBtManager(context));
    }

    @VisibleForTesting
    BluetoothDeviceUpdater(DashboardFragment fragment,
    BluetoothDeviceUpdater(Context context, DashboardFragment fragment,
            DevicePreferenceCallback devicePreferenceCallback, LocalBluetoothManager localManager) {
        mFragment = fragment;
        mDevicePreferenceCallback = devicePreferenceCallback;
        mPreferenceMap = new HashMap<>();
        mLocalManager = localManager;
        mMetricsFeatureProvider = FeatureFactory.getFactory(mFragment.getContext())
                .getMetricsFeatureProvider();
        mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
    }

    /**
+37 −28
Original line number Diff line number Diff line
@@ -39,8 +39,10 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.SubSettings;
import com.android.settings.Utils;
import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment;
import com.android.settings.bluetooth.BluetoothPairingDetail;
import com.android.settings.bluetooth.SavedBluetoothDeviceUpdater;
import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.slices.CustomSliceRegistry;
@@ -78,9 +80,15 @@ public class BluetoothDevicesSlice implements CustomSliceable {
    private static final String TAG = "BluetoothDevicesSlice";

    private final Context mContext;
    private final AvailableMediaBluetoothDeviceUpdater mAvailableMediaBtDeviceUpdater;
    private final SavedBluetoothDeviceUpdater mSavedBtDeviceUpdater;

    public BluetoothDevicesSlice(Context context) {
        mContext = context;
        mAvailableMediaBtDeviceUpdater = new AvailableMediaBluetoothDeviceUpdater(mContext,
                null /* fragment */, null /* devicePreferenceCallback */);
        mSavedBtDeviceUpdater = new SavedBluetoothDeviceUpdater(mContext,
                null /* fragment */, null /* devicePreferenceCallback */);
    }

    @Override
@@ -123,10 +131,10 @@ public class BluetoothDevicesSlice implements CustomSliceable {
        final List<ListBuilder.RowBuilder> rows = getBluetoothRowBuilder();

        // Get displayable device count.
        final int deviceCount = Math.min(rows.size(), DEFAULT_EXPANDED_ROW_COUNT);
        final int displayableCount = Math.min(rows.size(), DEFAULT_EXPANDED_ROW_COUNT);

        // According to the displayable device count to add bluetooth device rows.
        for (int i = 0; i < deviceCount; i++) {
        for (int i = 0; i < displayableCount; i++) {
            listBuilder.addRow(rows.get(i));
        }

@@ -148,11 +156,14 @@ public class BluetoothDevicesSlice implements CustomSliceable {

    @Override
    public void onNotifyChange(Intent intent) {
        // Activate available media device.
        final int bluetoothDeviceHashCode = intent.getIntExtra(BLUETOOTH_DEVICE_HASH_CODE, -1);
        for (CachedBluetoothDevice cachedBluetoothDevice : getConnectedBluetoothDevices()) {
            if (cachedBluetoothDevice.hashCode() == bluetoothDeviceHashCode) {
                cachedBluetoothDevice.setActive();
        for (CachedBluetoothDevice device : getPairedBluetoothDevices()) {
            if (device.hashCode() == bluetoothDeviceHashCode) {
                if (device.isConnected()) {
                    device.setActive();
                } else if (!device.isBusy()) {
                    device.connect();
                }
                return;
            }
        }
@@ -164,7 +175,7 @@ public class BluetoothDevicesSlice implements CustomSliceable {
    }

    @VisibleForTesting
    List<CachedBluetoothDevice> getConnectedBluetoothDevices() {
    List<CachedBluetoothDevice> getPairedBluetoothDevices() {
        final List<CachedBluetoothDevice> bluetoothDeviceList = new ArrayList<>();

        // If Bluetooth is disable, skip to get the Bluetooth devices.
@@ -174,19 +185,18 @@ public class BluetoothDevicesSlice implements CustomSliceable {
        }

        // Get the Bluetooth devices from LocalBluetoothManager.
        final LocalBluetoothManager bluetoothManager =
        final LocalBluetoothManager localBtManager =
                com.android.settings.bluetooth.Utils.getLocalBtManager(mContext);
        if (bluetoothManager == null) {
        if (localBtManager == null) {
            Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is unsupported.");
            return bluetoothDeviceList;
        }
        final Collection<CachedBluetoothDevice> cachedDevices =
                bluetoothManager.getCachedDeviceManager().getCachedDevicesCopy();
                localBtManager.getCachedDeviceManager().getCachedDevicesCopy();

        // Get all connected devices and sort them.
        // Get all paired devices and sort them.
        return cachedDevices.stream()
                .filter(device -> device.getDevice().getBondState() == BluetoothDevice.BOND_BONDED
                        && device.getDevice().isConnected())
                .filter(device -> device.getDevice().getBondState() == BluetoothDevice.BOND_BONDED)
                .sorted(COMPARATOR).collect(Collectors.toList());
    }

@@ -242,21 +252,21 @@ public class BluetoothDevicesSlice implements CustomSliceable {
    private List<ListBuilder.RowBuilder> getBluetoothRowBuilder() {
        // According to Bluetooth devices to create row builders.
        final List<ListBuilder.RowBuilder> bluetoothRows = new ArrayList<>();
        final List<CachedBluetoothDevice> bluetoothDevices = getConnectedBluetoothDevices();
        for (CachedBluetoothDevice bluetoothDevice : bluetoothDevices) {
        for (CachedBluetoothDevice device : getPairedBluetoothDevices()) {
            final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
                    .setTitleItem(getBluetoothDeviceIcon(bluetoothDevice), ListBuilder.ICON_IMAGE)
                    .setTitle(bluetoothDevice.getName())
                    .setSubtitle(bluetoothDevice.getConnectionSummary());

            if (bluetoothDevice.isConnectedA2dpDevice()) {
                // For available media devices, the primary action is to activate audio stream and
                // add setting icon to the end to link detail page.
                rowBuilder.setPrimaryAction(buildMediaBluetoothAction(bluetoothDevice));
                rowBuilder.addEndItem(buildBluetoothDetailDeepLinkAction(bluetoothDevice));
                    .setTitleItem(getBluetoothDeviceIcon(device), ListBuilder.ICON_IMAGE)
                    .setTitle(device.getName())
                    .setSubtitle(device.getConnectionSummary());

            if (mAvailableMediaBtDeviceUpdater.isFilterMatched(device)
                    || mSavedBtDeviceUpdater.isFilterMatched(device)) {
                // For all available media devices and previously connected devices, the primary
                // action is to activate or connect, and the end gear icon links to detail page.
                rowBuilder.setPrimaryAction(buildPrimaryBluetoothAction(device));
                rowBuilder.addEndItem(buildBluetoothDetailDeepLinkAction(device));
            } else {
                // For other devices, the primary action is to link detail page.
                rowBuilder.setPrimaryAction(buildBluetoothDetailDeepLinkAction(bluetoothDevice));
                // For other devices, the primary action is to link to detail page.
                rowBuilder.setPrimaryAction(buildBluetoothDetailDeepLinkAction(device));
            }

            bluetoothRows.add(rowBuilder);
@@ -266,8 +276,7 @@ public class BluetoothDevicesSlice implements CustomSliceable {
    }

    @VisibleForTesting
    SliceAction buildMediaBluetoothAction(CachedBluetoothDevice bluetoothDevice) {
        // Send broadcast to activate available media device.
    SliceAction buildPrimaryBluetoothAction(CachedBluetoothDevice bluetoothDevice) {
        final Intent intent = new Intent(getUri().toString())
                .setClass(mContext, SliceBroadcastReceiver.class)
                .putExtra(BLUETOOTH_DEVICE_HASH_CODE, bluetoothDevice.hashCode());
+1 −1
Original line number Diff line number Diff line
@@ -102,7 +102,7 @@ public class BluetoothDeviceUpdaterTest {
        mPreference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice,
                false, BluetoothDevicePreference.SortType.TYPE_DEFAULT);
        mBluetoothDeviceUpdater =
            new BluetoothDeviceUpdater(mDashboardFragment, mDevicePreferenceCallback,
            new BluetoothDeviceUpdater(mContext, mDashboardFragment, mDevicePreferenceCallback,
                    mLocalManager) {
                @Override
                public boolean isFilterMatched(CachedBluetoothDevice cachedBluetoothDevice) {
+72 −26
Original line number Diff line number Diff line
@@ -23,12 +23,15 @@ import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;

@@ -62,6 +65,7 @@ import java.util.ArrayList;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
@Config(shadows = ShadowBluetoothAdapter.class)
public class BluetoothDevicesSliceTest {

    private static final String BLUETOOTH_MOCK_ADDRESS = "00:11:00:11:00:11";
@@ -96,6 +100,13 @@ public class BluetoothDevicesSliceTest {

        // Initial Bluetooth device list.
        mBluetoothDeviceList = new ArrayList<>();

        final BluetoothAdapter defaultAdapter = BluetoothAdapter.getDefaultAdapter();
        if (defaultAdapter != null) {
            final ShadowBluetoothAdapter shadowBluetoothAdapter = Shadow.extract(defaultAdapter);
            shadowBluetoothAdapter.setEnabled(true);
            shadowBluetoothAdapter.setState(BluetoothAdapter.STATE_ON);
        }
    }

    @After
@@ -114,7 +125,6 @@ public class BluetoothDevicesSliceTest {
    }

    @Test
    @Config(shadows = ShadowBluetoothAdapter.class)
    public void getSlice_hasBluetoothHardware_shouldHaveBluetoothDevicesTitleAndPairNewDevice() {
        final Slice slice = mBluetoothDevicesSlice.getSlice();

@@ -127,12 +137,9 @@ public class BluetoothDevicesSliceTest {
    }

    @Test
    @Config(shadows = ShadowBluetoothAdapter.class)
    public void getSlice_hasBluetoothDevices_shouldMatchBluetoothMockTitle() {
        final ShadowBluetoothAdapter adapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        adapter.setState(BluetoothAdapter.STATE_ON);
        mockBluetoothDeviceList(1);
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getConnectedBluetoothDevices();
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();

        final Slice slice = mBluetoothDevicesSlice.getSlice();

@@ -141,39 +148,43 @@ public class BluetoothDevicesSliceTest {
    }

    @Test
    @Config(shadows = ShadowBluetoothAdapter.class)
    public void getSlice_hasMediaBluetoothDevice_shouldBuildMediaBluetoothAction() {
        final ShadowBluetoothAdapter adapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        adapter.setState(BluetoothAdapter.STATE_ON);
        mockBluetoothDeviceList(1 /* deviceCount */);
        doReturn(true).when(mBluetoothDeviceList.get(0)).isConnectedA2dpDevice();
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getConnectedBluetoothDevices();
    public void getSlice_hasAvailableMediaDevice_shouldBuildPrimaryBluetoothAction() {
        mockBluetoothDeviceList(1);
        when(mBluetoothDeviceList.get(0).getDevice().isConnected()).thenReturn(true);
        doReturn(true).when(mBluetoothDeviceList.get(0)).isConnectedHearingAidDevice();
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();

        mBluetoothDevicesSlice.getSlice();

        verify(mBluetoothDevicesSlice).buildMediaBluetoothAction(any());
        verify(mBluetoothDevicesSlice).buildPrimaryBluetoothAction(any());
    }

    @Test
    @Config(shadows = ShadowBluetoothAdapter.class)
    public void getSlice_noMediaBluetoothDevice_shouldNotBuildMediaBluetoothAction() {
        final ShadowBluetoothAdapter adapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        adapter.setState(BluetoothAdapter.STATE_ON);
        mockBluetoothDeviceList(1 /* deviceCount */);
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getConnectedBluetoothDevices();
    public void getSlice_hasPreviouslyConnectedDevice_shouldBuildPrimaryBluetoothAction() {
        mockBluetoothDeviceList(1);
        when(mBluetoothDeviceList.get(0).getDevice().isConnected()).thenReturn(false);
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();

        mBluetoothDevicesSlice.getSlice();

        verify(mBluetoothDevicesSlice, never()).buildMediaBluetoothAction(any());
        verify(mBluetoothDevicesSlice).buildPrimaryBluetoothAction(any());
    }

    @Test
    public void getSlice_hasNonMediaDeviceConnected_shouldNotBuildPrimaryBluetoothAction() {
        mockBluetoothDeviceList(1);
        when(mBluetoothDeviceList.get(0).getDevice().isConnected()).thenReturn(true);
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();

        mBluetoothDevicesSlice.getSlice();

        verify(mBluetoothDevicesSlice, never()).buildPrimaryBluetoothAction(any());
    }

    @Test
    @Config(shadows = ShadowBluetoothAdapter.class)
    public void getSlice_exceedDefaultRowCount_shouldOnlyShowDefaultRows() {
        final ShadowBluetoothAdapter adapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        adapter.setState(BluetoothAdapter.STATE_ON);
        mockBluetoothDeviceList(BluetoothDevicesSlice.DEFAULT_EXPANDED_ROW_COUNT + 1);
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getConnectedBluetoothDevices();
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();

        final Slice slice = mBluetoothDevicesSlice.getSlice();

@@ -183,9 +194,10 @@ public class BluetoothDevicesSliceTest {
    }

    @Test
    public void onNotifyChange_mediaDevice_shouldActivateDevice() {
    public void onNotifyChange_connectedDevice_shouldActivateDevice() {
        mockBluetoothDeviceList(1);
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getConnectedBluetoothDevices();
        doReturn(true).when(mBluetoothDeviceList.get(0)).isConnected();
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();
        final Intent intent = new Intent().putExtra(
                BluetoothDevicesSlice.BLUETOOTH_DEVICE_HASH_CODE,
                mCachedBluetoothDevice.hashCode());
@@ -195,7 +207,41 @@ public class BluetoothDevicesSliceTest {
        verify(mCachedBluetoothDevice).setActive();
    }

    @Test
    public void onNotifyChange_availableDisconnectedDevice_shouldConnectToDevice() {
        mockBluetoothDeviceList(1);
        doReturn(false).when(mBluetoothDeviceList.get(0)).isConnected();
        doReturn(false).when(mBluetoothDeviceList.get(0)).isBusy();
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();
        final Intent intent = new Intent().putExtra(
                BluetoothDevicesSlice.BLUETOOTH_DEVICE_HASH_CODE,
                mCachedBluetoothDevice.hashCode());

        mBluetoothDevicesSlice.onNotifyChange(intent);

        verify(mCachedBluetoothDevice).connect();
    }

    @Test
    public void onNotifyChange_busyDisconnectedDevice_shouldDoNothing() {
        mockBluetoothDeviceList(1);
        doReturn(false).when(mBluetoothDeviceList.get(0)).isConnected();
        doReturn(true).when(mBluetoothDeviceList.get(0)).isBusy();
        doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();
        final Intent intent = new Intent().putExtra(
                BluetoothDevicesSlice.BLUETOOTH_DEVICE_HASH_CODE,
                mCachedBluetoothDevice.hashCode());

        mBluetoothDevicesSlice.onNotifyChange(intent);

        verify(mCachedBluetoothDevice, never()).setActive();
        verify(mCachedBluetoothDevice, never()).connect();
    }

    private void mockBluetoothDeviceList(int deviceCount) {
        final BluetoothDevice device = mock(BluetoothDevice.class);
        doReturn(BluetoothDevice.BOND_BONDED).when(device).getBondState();
        doReturn(device).when(mCachedBluetoothDevice).getDevice();
        doReturn(BLUETOOTH_MOCK_TITLE).when(mCachedBluetoothDevice).getName();
        doReturn(BLUETOOTH_MOCK_SUMMARY).when(mCachedBluetoothDevice).getConnectionSummary();
        doReturn(BLUETOOTH_MOCK_ADDRESS).when(mCachedBluetoothDevice).getAddress();