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
This commit is contained in:
Jason Chiu
2020-03-06 10:52:19 +08:00
parent 7d6011be10
commit 130629b1db
5 changed files with 114 additions and 59 deletions

View File

@@ -123,7 +123,8 @@
android:label="@string/settings_label_launcher" android:label="@string/settings_label_launcher"
android:theme="@style/Theme.Settings.Home" android:theme="@style/Theme.Settings.Home"
android:taskAffinity="com.android.settings.root" android:taskAffinity="com.android.settings.root"
android:launchMode="singleTask"> android:launchMode="singleTask"
android:configChanges="keyboard|keyboardHidden">
<intent-filter android:priority="1"> <intent-filter android:priority="1">
<action android:name="android.settings.SETTINGS" /> <action android:name="android.settings.SETTINGS" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />

View File

@@ -69,18 +69,17 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback,
public BluetoothDeviceUpdater(Context context, DashboardFragment fragment, public BluetoothDeviceUpdater(Context context, DashboardFragment fragment,
DevicePreferenceCallback devicePreferenceCallback) { DevicePreferenceCallback devicePreferenceCallback) {
this(fragment, devicePreferenceCallback, Utils.getLocalBtManager(context)); this(context, fragment, devicePreferenceCallback, Utils.getLocalBtManager(context));
} }
@VisibleForTesting @VisibleForTesting
BluetoothDeviceUpdater(DashboardFragment fragment, BluetoothDeviceUpdater(Context context, DashboardFragment fragment,
DevicePreferenceCallback devicePreferenceCallback, LocalBluetoothManager localManager) { DevicePreferenceCallback devicePreferenceCallback, LocalBluetoothManager localManager) {
mFragment = fragment; mFragment = fragment;
mDevicePreferenceCallback = devicePreferenceCallback; mDevicePreferenceCallback = devicePreferenceCallback;
mPreferenceMap = new HashMap<>(); mPreferenceMap = new HashMap<>();
mLocalManager = localManager; mLocalManager = localManager;
mMetricsFeatureProvider = FeatureFactory.getFactory(mFragment.getContext()) mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
.getMetricsFeatureProvider();
} }
/** /**

View File

@@ -39,8 +39,10 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.SubSettings; import com.android.settings.SubSettings;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment; import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment;
import com.android.settings.bluetooth.BluetoothPairingDetail; import com.android.settings.bluetooth.BluetoothPairingDetail;
import com.android.settings.bluetooth.SavedBluetoothDeviceUpdater;
import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment; import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment;
import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.SubSettingLauncher;
import com.android.settings.slices.CustomSliceRegistry; import com.android.settings.slices.CustomSliceRegistry;
@@ -78,9 +80,15 @@ public class BluetoothDevicesSlice implements CustomSliceable {
private static final String TAG = "BluetoothDevicesSlice"; private static final String TAG = "BluetoothDevicesSlice";
private final Context mContext; private final Context mContext;
private final AvailableMediaBluetoothDeviceUpdater mAvailableMediaBtDeviceUpdater;
private final SavedBluetoothDeviceUpdater mSavedBtDeviceUpdater;
public BluetoothDevicesSlice(Context context) { public BluetoothDevicesSlice(Context context) {
mContext = context; mContext = context;
mAvailableMediaBtDeviceUpdater = new AvailableMediaBluetoothDeviceUpdater(mContext,
null /* fragment */, null /* devicePreferenceCallback */);
mSavedBtDeviceUpdater = new SavedBluetoothDeviceUpdater(mContext,
null /* fragment */, null /* devicePreferenceCallback */);
} }
@Override @Override
@@ -123,10 +131,10 @@ public class BluetoothDevicesSlice implements CustomSliceable {
final List<ListBuilder.RowBuilder> rows = getBluetoothRowBuilder(); final List<ListBuilder.RowBuilder> rows = getBluetoothRowBuilder();
// Get displayable device count. // 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. // 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)); listBuilder.addRow(rows.get(i));
} }
@@ -148,11 +156,14 @@ public class BluetoothDevicesSlice implements CustomSliceable {
@Override @Override
public void onNotifyChange(Intent intent) { public void onNotifyChange(Intent intent) {
// Activate available media device.
final int bluetoothDeviceHashCode = intent.getIntExtra(BLUETOOTH_DEVICE_HASH_CODE, -1); final int bluetoothDeviceHashCode = intent.getIntExtra(BLUETOOTH_DEVICE_HASH_CODE, -1);
for (CachedBluetoothDevice cachedBluetoothDevice : getConnectedBluetoothDevices()) { for (CachedBluetoothDevice device : getPairedBluetoothDevices()) {
if (cachedBluetoothDevice.hashCode() == bluetoothDeviceHashCode) { if (device.hashCode() == bluetoothDeviceHashCode) {
cachedBluetoothDevice.setActive(); if (device.isConnected()) {
device.setActive();
} else if (!device.isBusy()) {
device.connect();
}
return; return;
} }
} }
@@ -164,7 +175,7 @@ public class BluetoothDevicesSlice implements CustomSliceable {
} }
@VisibleForTesting @VisibleForTesting
List<CachedBluetoothDevice> getConnectedBluetoothDevices() { List<CachedBluetoothDevice> getPairedBluetoothDevices() {
final List<CachedBluetoothDevice> bluetoothDeviceList = new ArrayList<>(); final List<CachedBluetoothDevice> bluetoothDeviceList = new ArrayList<>();
// If Bluetooth is disable, skip to get the Bluetooth devices. // 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. // Get the Bluetooth devices from LocalBluetoothManager.
final LocalBluetoothManager bluetoothManager = final LocalBluetoothManager localBtManager =
com.android.settings.bluetooth.Utils.getLocalBtManager(mContext); com.android.settings.bluetooth.Utils.getLocalBtManager(mContext);
if (bluetoothManager == null) { if (localBtManager == null) {
Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is unsupported."); Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is unsupported.");
return bluetoothDeviceList; return bluetoothDeviceList;
} }
final Collection<CachedBluetoothDevice> cachedDevices = 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() return cachedDevices.stream()
.filter(device -> device.getDevice().getBondState() == BluetoothDevice.BOND_BONDED .filter(device -> device.getDevice().getBondState() == BluetoothDevice.BOND_BONDED)
&& device.getDevice().isConnected())
.sorted(COMPARATOR).collect(Collectors.toList()); .sorted(COMPARATOR).collect(Collectors.toList());
} }
@@ -242,21 +252,21 @@ public class BluetoothDevicesSlice implements CustomSliceable {
private List<ListBuilder.RowBuilder> getBluetoothRowBuilder() { private List<ListBuilder.RowBuilder> getBluetoothRowBuilder() {
// According to Bluetooth devices to create row builders. // According to Bluetooth devices to create row builders.
final List<ListBuilder.RowBuilder> bluetoothRows = new ArrayList<>(); final List<ListBuilder.RowBuilder> bluetoothRows = new ArrayList<>();
final List<CachedBluetoothDevice> bluetoothDevices = getConnectedBluetoothDevices(); for (CachedBluetoothDevice device : getPairedBluetoothDevices()) {
for (CachedBluetoothDevice bluetoothDevice : bluetoothDevices) {
final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
.setTitleItem(getBluetoothDeviceIcon(bluetoothDevice), ListBuilder.ICON_IMAGE) .setTitleItem(getBluetoothDeviceIcon(device), ListBuilder.ICON_IMAGE)
.setTitle(bluetoothDevice.getName()) .setTitle(device.getName())
.setSubtitle(bluetoothDevice.getConnectionSummary()); .setSubtitle(device.getConnectionSummary());
if (bluetoothDevice.isConnectedA2dpDevice()) { if (mAvailableMediaBtDeviceUpdater.isFilterMatched(device)
// For available media devices, the primary action is to activate audio stream and || mSavedBtDeviceUpdater.isFilterMatched(device)) {
// add setting icon to the end to link detail page. // For all available media devices and previously connected devices, the primary
rowBuilder.setPrimaryAction(buildMediaBluetoothAction(bluetoothDevice)); // action is to activate or connect, and the end gear icon links to detail page.
rowBuilder.addEndItem(buildBluetoothDetailDeepLinkAction(bluetoothDevice)); rowBuilder.setPrimaryAction(buildPrimaryBluetoothAction(device));
rowBuilder.addEndItem(buildBluetoothDetailDeepLinkAction(device));
} else { } else {
// For other devices, the primary action is to link detail page. // For other devices, the primary action is to link to detail page.
rowBuilder.setPrimaryAction(buildBluetoothDetailDeepLinkAction(bluetoothDevice)); rowBuilder.setPrimaryAction(buildBluetoothDetailDeepLinkAction(device));
} }
bluetoothRows.add(rowBuilder); bluetoothRows.add(rowBuilder);
@@ -266,8 +276,7 @@ public class BluetoothDevicesSlice implements CustomSliceable {
} }
@VisibleForTesting @VisibleForTesting
SliceAction buildMediaBluetoothAction(CachedBluetoothDevice bluetoothDevice) { SliceAction buildPrimaryBluetoothAction(CachedBluetoothDevice bluetoothDevice) {
// Send broadcast to activate available media device.
final Intent intent = new Intent(getUri().toString()) final Intent intent = new Intent(getUri().toString())
.setClass(mContext, SliceBroadcastReceiver.class) .setClass(mContext, SliceBroadcastReceiver.class)
.putExtra(BLUETOOTH_DEVICE_HASH_CODE, bluetoothDevice.hashCode()); .putExtra(BLUETOOTH_DEVICE_HASH_CODE, bluetoothDevice.hashCode());

View File

@@ -102,7 +102,7 @@ public class BluetoothDeviceUpdaterTest {
mPreference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, mPreference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice,
false, BluetoothDevicePreference.SortType.TYPE_DEFAULT); false, BluetoothDevicePreference.SortType.TYPE_DEFAULT);
mBluetoothDeviceUpdater = mBluetoothDeviceUpdater =
new BluetoothDeviceUpdater(mDashboardFragment, mDevicePreferenceCallback, new BluetoothDeviceUpdater(mContext, mDashboardFragment, mDevicePreferenceCallback,
mLocalManager) { mLocalManager) {
@Override @Override
public boolean isFilterMatched(CachedBluetoothDevice cachedBluetoothDevice) { public boolean isFilterMatched(CachedBluetoothDevice cachedBluetoothDevice) {

View File

@@ -23,12 +23,15 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -62,6 +65,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config(shadows = ShadowBluetoothAdapter.class)
public class BluetoothDevicesSliceTest { public class BluetoothDevicesSliceTest {
private static final String BLUETOOTH_MOCK_ADDRESS = "00:11:00:11:00:11"; private static final String BLUETOOTH_MOCK_ADDRESS = "00:11:00:11:00:11";
@@ -96,6 +100,13 @@ public class BluetoothDevicesSliceTest {
// Initial Bluetooth device list. // Initial Bluetooth device list.
mBluetoothDeviceList = new ArrayList<>(); 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 @After
@@ -114,7 +125,6 @@ public class BluetoothDevicesSliceTest {
} }
@Test @Test
@Config(shadows = ShadowBluetoothAdapter.class)
public void getSlice_hasBluetoothHardware_shouldHaveBluetoothDevicesTitleAndPairNewDevice() { public void getSlice_hasBluetoothHardware_shouldHaveBluetoothDevicesTitleAndPairNewDevice() {
final Slice slice = mBluetoothDevicesSlice.getSlice(); final Slice slice = mBluetoothDevicesSlice.getSlice();
@@ -127,12 +137,9 @@ public class BluetoothDevicesSliceTest {
} }
@Test @Test
@Config(shadows = ShadowBluetoothAdapter.class)
public void getSlice_hasBluetoothDevices_shouldMatchBluetoothMockTitle() { public void getSlice_hasBluetoothDevices_shouldMatchBluetoothMockTitle() {
final ShadowBluetoothAdapter adapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
adapter.setState(BluetoothAdapter.STATE_ON);
mockBluetoothDeviceList(1); mockBluetoothDeviceList(1);
doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getConnectedBluetoothDevices(); doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();
final Slice slice = mBluetoothDevicesSlice.getSlice(); final Slice slice = mBluetoothDevicesSlice.getSlice();
@@ -141,39 +148,43 @@ public class BluetoothDevicesSliceTest {
} }
@Test @Test
@Config(shadows = ShadowBluetoothAdapter.class) public void getSlice_hasAvailableMediaDevice_shouldBuildPrimaryBluetoothAction() {
public void getSlice_hasMediaBluetoothDevice_shouldBuildMediaBluetoothAction() { mockBluetoothDeviceList(1);
final ShadowBluetoothAdapter adapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); when(mBluetoothDeviceList.get(0).getDevice().isConnected()).thenReturn(true);
adapter.setState(BluetoothAdapter.STATE_ON); doReturn(true).when(mBluetoothDeviceList.get(0)).isConnectedHearingAidDevice();
mockBluetoothDeviceList(1 /* deviceCount */); doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();
doReturn(true).when(mBluetoothDeviceList.get(0)).isConnectedA2dpDevice();
doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getConnectedBluetoothDevices();
mBluetoothDevicesSlice.getSlice(); mBluetoothDevicesSlice.getSlice();
verify(mBluetoothDevicesSlice).buildMediaBluetoothAction(any()); verify(mBluetoothDevicesSlice).buildPrimaryBluetoothAction(any());
} }
@Test @Test
@Config(shadows = ShadowBluetoothAdapter.class) public void getSlice_hasPreviouslyConnectedDevice_shouldBuildPrimaryBluetoothAction() {
public void getSlice_noMediaBluetoothDevice_shouldNotBuildMediaBluetoothAction() { mockBluetoothDeviceList(1);
final ShadowBluetoothAdapter adapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); when(mBluetoothDeviceList.get(0).getDevice().isConnected()).thenReturn(false);
adapter.setState(BluetoothAdapter.STATE_ON); doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();
mockBluetoothDeviceList(1 /* deviceCount */);
doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getConnectedBluetoothDevices();
mBluetoothDevicesSlice.getSlice(); 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 @Test
@Config(shadows = ShadowBluetoothAdapter.class)
public void getSlice_exceedDefaultRowCount_shouldOnlyShowDefaultRows() { public void getSlice_exceedDefaultRowCount_shouldOnlyShowDefaultRows() {
final ShadowBluetoothAdapter adapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
adapter.setState(BluetoothAdapter.STATE_ON);
mockBluetoothDeviceList(BluetoothDevicesSlice.DEFAULT_EXPANDED_ROW_COUNT + 1); mockBluetoothDeviceList(BluetoothDevicesSlice.DEFAULT_EXPANDED_ROW_COUNT + 1);
doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getConnectedBluetoothDevices(); doReturn(mBluetoothDeviceList).when(mBluetoothDevicesSlice).getPairedBluetoothDevices();
final Slice slice = mBluetoothDevicesSlice.getSlice(); final Slice slice = mBluetoothDevicesSlice.getSlice();
@@ -183,9 +194,10 @@ public class BluetoothDevicesSliceTest {
} }
@Test @Test
public void onNotifyChange_mediaDevice_shouldActivateDevice() { public void onNotifyChange_connectedDevice_shouldActivateDevice() {
mockBluetoothDeviceList(1); 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( final Intent intent = new Intent().putExtra(
BluetoothDevicesSlice.BLUETOOTH_DEVICE_HASH_CODE, BluetoothDevicesSlice.BLUETOOTH_DEVICE_HASH_CODE,
mCachedBluetoothDevice.hashCode()); mCachedBluetoothDevice.hashCode());
@@ -195,7 +207,41 @@ public class BluetoothDevicesSliceTest {
verify(mCachedBluetoothDevice).setActive(); 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) { 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_TITLE).when(mCachedBluetoothDevice).getName();
doReturn(BLUETOOTH_MOCK_SUMMARY).when(mCachedBluetoothDevice).getConnectionSummary(); doReturn(BLUETOOTH_MOCK_SUMMARY).when(mCachedBluetoothDevice).getConnectionSummary();
doReturn(BLUETOOTH_MOCK_ADDRESS).when(mCachedBluetoothDevice).getAddress(); doReturn(BLUETOOTH_MOCK_ADDRESS).when(mCachedBluetoothDevice).getAddress();