Loading res/values/strings.xml +8 −0 Original line number Diff line number Diff line Loading @@ -10264,4 +10264,12 @@ <string name="see_more">See more</string> <!-- See less items in contextual homepage [CHAR LIMIT=30]--> <string name="see_less">See less</string> <!-- Summary for connected devices count in connected device slice. [CHAR LIMIT=NONE] --> <plurals name="show_connected_devices"> <item quantity="one"><xliff:g id="number_device_count">%1$d</xliff:g> device connected</item> <item quantity="other"><xliff:g id="number_device_count">%1$d</xliff:g> devices connected</item> </plurals> <!-- Title for no connected devices in connected device slice. [CHAR LIMIT=NONE] --> <string name="no_connected_devices">No connected devices</string> </resources> src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java +7 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import com.android.settings.homepage.contextualcards.deviceinfo.DataUsageSlice; import com.android.settings.homepage.contextualcards.deviceinfo.DeviceInfoSlice; import com.android.settings.homepage.contextualcards.deviceinfo.EmergencyInfoSlice; import com.android.settings.homepage.contextualcards.deviceinfo.StorageSlice; import com.android.settings.homepage.contextualcards.slices.ConnectedDeviceSlice; import com.android.settings.intelligence.ContextualCardProto.ContextualCard; import com.android.settings.intelligence.ContextualCardProto.ContextualCardList; import com.android.settings.wifi.WifiSlice; Loading Loading @@ -69,6 +70,11 @@ public class SettingsContextualCardProvider extends ContextualCardProvider { .setSliceUri(BatterySlice.BATTERY_CARD_URI.toSafeString()) .setCardName(BatterySlice.PATH_BATTERY_INFO) .build(); final ContextualCard connectedDeviceCard = ContextualCard.newBuilder() .setSliceUri(ConnectedDeviceSlice.CONNECTED_DEVICE_URI.toString()) .setCardName(ConnectedDeviceSlice.PATH_CONNECTED_DEVICE) .build(); final ContextualCardList cards = ContextualCardList.newBuilder() .addCard(wifiCard) .addCard(dataUsageCard) Loading @@ -76,6 +82,7 @@ public class SettingsContextualCardProvider extends ContextualCardProvider { .addCard(storageInfoCard) .addCard(emergencyInfoCard) .addCard(batteryInfoCard) .addCard(connectedDeviceCard) .build(); return cards; Loading src/com/android/settings/homepage/contextualcards/slices/ConnectedDeviceSlice.java 0 → 100644 +286 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.homepage.contextualcards.slices; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; import androidx.core.graphics.drawable.IconCompat; import androidx.slice.Slice; import androidx.slice.builders.ListBuilder; import androidx.slice.builders.SliceAction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SubSettings; import com.android.settings.Utils; import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment; import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.slices.CustomSliceable; import com.android.settings.slices.SettingsSliceProvider; import com.android.settings.slices.SliceBuilderUtils; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.instrumentation.Instrumentable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; /** * TODO(b/114807655): Contextual Home Page - Connected Device * * Show connected device info if one is currently connected. UI for connected device should * match Connected Devices > Currently Connected Devices * * This Slice will show multiple currently connected devices, which includes: * 1) Bluetooth. * 2) Docks. * ... * TODO Other device types are under checking to support, will update later. */ public class ConnectedDeviceSlice implements CustomSliceable { /** * The path denotes the unique name of Connected device Slice. */ public static final String PATH_CONNECTED_DEVICE = "connected_device"; /** * Backing Uri for Connected device Slice. */ public static final Uri CONNECTED_DEVICE_URI = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(PATH_CONNECTED_DEVICE) .build(); /** * To sort the Bluetooth devices by {@link CachedBluetoothDevice}. * Refer compareTo method from {@link com.android.settings.bluetooth.BluetoothDevicePreference}. */ private static final Comparator<CachedBluetoothDevice> COMPARATOR = Comparator.naturalOrder(); private static final int DEFAULT_EXPANDED_ROW_COUNT = 4; private static final String TAG = "ConnectedDeviceSlice"; private final Context mContext; public ConnectedDeviceSlice(Context context) { mContext = context; } private static Bitmap getBitmapFromVectorDrawable(Drawable VectorDrawable) { final Bitmap bitmap = Bitmap.createBitmap(VectorDrawable.getIntrinsicWidth(), VectorDrawable.getIntrinsicHeight(), Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); VectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); VectorDrawable.draw(canvas); return bitmap; } @Override public Uri getUri() { return CONNECTED_DEVICE_URI; } /** * Return a Connected Device Slice bound to {@link #CONNECTED_DEVICE_URI}. */ @Override public Slice getSlice() { final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_homepage_connected_device); final CharSequence title = mContext.getText(R.string.connected_devices_dashboard_title); final CharSequence titleNoConnectedDevices = mContext.getText( R.string.no_connected_devices); final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext, 0, getIntent(), 0); final SliceAction primarySliceAction = new SliceAction(primaryActionIntent, icon, title); final ListBuilder listBuilder = new ListBuilder(mContext, CONNECTED_DEVICE_URI, ListBuilder.INFINITY) .setAccentColor(Utils.getColorAccentDefaultColor(mContext)); // Get row builders by connected devices, e.g. Bluetooth. // TODO Add other type connected devices, e.g. Docks. final List<ListBuilder.RowBuilder> rows = getBluetoothRowBuilder(primarySliceAction); // Return a header with IsError flag, if no connected devices. if (rows.isEmpty()) { return listBuilder.setHeader(new ListBuilder.HeaderBuilder() .setTitle(titleNoConnectedDevices) .setPrimaryAction(primarySliceAction)) .setIsError(true) .build(); } // According the number of connected devices to set sub title of header. listBuilder.setHeader(new ListBuilder.HeaderBuilder() .setTitle(title) .setSubtitle(getSubTitle(rows.size())) .setPrimaryAction(primarySliceAction)); // Add rows. for (ListBuilder.RowBuilder rowBuilder : rows) { listBuilder.addRow(rowBuilder); } // Only show "see more" button when the number of data row is more than or equal to 4. // TODO(b/118465996): SHOW MORE button won't work properly when having two data rows if (rows.size() >= DEFAULT_EXPANDED_ROW_COUNT) { listBuilder.setSeeMoreAction(primaryActionIntent); } return listBuilder.build(); } @Override public Intent getIntent() { final String screenTitle = mContext.getText(R.string.connected_devices_dashboard_title) .toString(); final Uri contentUri = new Uri.Builder().appendPath(PATH_CONNECTED_DEVICE).build(); return SliceBuilderUtils.buildSearchResultPageIntent(mContext, ConnectedDeviceDashboardFragment.class.getName(), PATH_CONNECTED_DEVICE, screenTitle, MetricsProto.MetricsEvent.SLICE) .setClassName(mContext.getPackageName(), SubSettings.class.getName()) .setData(contentUri); } @Override public void onNotifyChange(Intent intent) { } @VisibleForTesting List<CachedBluetoothDevice> getBluetoothConnectedDevices() { final List<CachedBluetoothDevice> connectedBluetoothList = new ArrayList<>(); // If Bluetooth is disable, skip to get the bluetooth devices. if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { Log.d(TAG, "Cannot get Bluetooth connected devices, Bluetooth is disabled."); return connectedBluetoothList; } // Get the Bluetooth devices from LocalBluetoothManager. final LocalBluetoothManager bluetoothManager = com.android.settings.bluetooth.Utils.getLocalBtManager(mContext); if (bluetoothManager == null) { Log.d(TAG, "Cannot get Bluetooth connected devices, Bluetooth is not supported."); return connectedBluetoothList; } final Collection<CachedBluetoothDevice> cachedDevices = bluetoothManager.getCachedDeviceManager().getCachedDevicesCopy(); // Get all connected Bluetooth devices and use Map to filter duplicated Bluetooth. final Map<BluetoothDevice, CachedBluetoothDevice> connectedBluetoothMap = new ArrayMap<>(); for (CachedBluetoothDevice device : cachedDevices) { if (device.isConnected() && !connectedBluetoothMap.containsKey(device.getDevice())) { connectedBluetoothMap.put(device.getDevice(), device); } } // Sort connected Bluetooth devices. connectedBluetoothList.addAll(connectedBluetoothMap.values()); Collections.sort(connectedBluetoothList, COMPARATOR); return connectedBluetoothList; } @VisibleForTesting PendingIntent getBluetoothDetailIntent(CachedBluetoothDevice device) { final Bundle args = new Bundle(); args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, device.getDevice().getAddress()); final SubSettingLauncher subSettingLauncher = new SubSettingLauncher(mContext); subSettingLauncher.setDestination(BluetoothDeviceDetailsFragment.class.getName()) .setArguments(args) .setTitleRes(R.string.device_details_title) .setSourceMetricsCategory(Instrumentable.METRICS_CATEGORY_UNKNOWN); // The requestCode should be unique, use the hashcode of device as request code. return PendingIntent .getActivity(mContext, device.hashCode() /* requestCode */, subSettingLauncher.toIntent(), 0 /* flags */); } @VisibleForTesting IconCompat getConnectedDeviceIcon(CachedBluetoothDevice device) { final Pair<Drawable, String> pair = BluetoothUtils .getBtClassDrawableWithDescription(mContext, device); if (pair.first != null) { return IconCompat.createWithBitmap(getBitmapFromVectorDrawable(pair.first)); } else { return IconCompat.createWithResource(mContext, R.drawable.ic_homepage_connected_device); } } private List<ListBuilder.RowBuilder> getBluetoothRowBuilder(SliceAction primarySliceAction) { final List<ListBuilder.RowBuilder> bluetoothRows = new ArrayList<>(); // According Bluetooth connected device to create row builders. final List<CachedBluetoothDevice> bluetoothDevices = getBluetoothConnectedDevices(); for (CachedBluetoothDevice bluetoothDevice : bluetoothDevices) { bluetoothRows.add(new ListBuilder.RowBuilder() .setTitleItem(getConnectedDeviceIcon(bluetoothDevice), ListBuilder.ICON_IMAGE) .setTitle(bluetoothDevice.getName()) .setSubtitle(bluetoothDevice.getConnectionSummary()) .setPrimaryAction(primarySliceAction) .addEndItem(buildBluetoothDetailDeepLinkAction(bluetoothDevice))); } return bluetoothRows; } private SliceAction buildBluetoothDetailDeepLinkAction(CachedBluetoothDevice bluetoothDevice) { return new SliceAction( getBluetoothDetailIntent(bluetoothDevice), IconCompat.createWithResource(mContext, R.drawable.ic_settings), bluetoothDevice.getName()); } private CharSequence getSubTitle(int deviceCount) { return mContext.getResources().getQuantityString(R.plurals.show_connected_devices, deviceCount, deviceCount); } } No newline at end of file src/com/android/settings/slices/CustomSliceManager.java +2 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import com.android.settings.homepage.contextualcards.deviceinfo.BatterySlice; import com.android.settings.homepage.contextualcards.deviceinfo.DataUsageSlice; import com.android.settings.homepage.contextualcards.deviceinfo.DeviceInfoSlice; import com.android.settings.homepage.contextualcards.deviceinfo.StorageSlice; import com.android.settings.homepage.contextualcards.slices.ConnectedDeviceSlice; import com.android.settings.wifi.WifiSlice; import java.util.Map; Loading Loading @@ -103,5 +104,6 @@ public class CustomSliceManager { mUriMap.put(DeviceInfoSlice.DEVICE_INFO_CARD_URI, DeviceInfoSlice.class); mUriMap.put(StorageSlice.STORAGE_CARD_URI, StorageSlice.class); mUriMap.put(BatterySlice.BATTERY_CARD_URI, BatterySlice.class); mUriMap.put(ConnectedDeviceSlice.CONNECTED_DEVICE_URI, ConnectedDeviceSlice.class); } } tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ConnectedDeviceSliceTest.java 0 → 100644 +99 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.homepage.contextualcards.slices; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import androidx.core.graphics.drawable.IconCompat; import androidx.slice.Slice; import androidx.slice.SliceItem; import androidx.slice.SliceProvider; import androidx.slice.widget.SliceLiveData; import com.android.settings.R; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.SliceTester; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import java.util.ArrayList; import java.util.List; @RunWith(SettingsRobolectricTestRunner.class) public class ConnectedDeviceSliceTest { @Mock private CachedBluetoothDevice mCachedBluetoothDevice; private List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>(); private Context mContext; private ConnectedDeviceSlice mConnectedDeviceSlice; @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; // Set-up specs for SliceMetadata. SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); mConnectedDeviceSlice = spy(new ConnectedDeviceSlice(mContext)); } @Test public void getSlice_hasConnectedDevices_shouldBeCorrectSliceContent() { final String title = "BluetoothTitle"; final String summary = "BluetoothSummary"; final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_homepage_connected_device); final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent("test action"), 0); doReturn(title).when(mCachedBluetoothDevice).getName(); doReturn(summary).when(mCachedBluetoothDevice).getConnectionSummary(); mCachedDevices.add(mCachedBluetoothDevice); doReturn(mCachedDevices).when(mConnectedDeviceSlice).getBluetoothConnectedDevices(); doReturn(icon).when(mConnectedDeviceSlice).getConnectedDeviceIcon(any()); doReturn(pendingIntent).when(mConnectedDeviceSlice).getBluetoothDetailIntent(any()); final Slice slice = mConnectedDeviceSlice.getSlice(); final List<SliceItem> sliceItems = slice.getItems(); SliceTester.assertTitle(sliceItems, title); } @Test public void getSlice_hasNoConnectedDevices_shouldReturnCorrectHeader() { final List<CachedBluetoothDevice> connectedBluetoothList = new ArrayList<>(); doReturn(connectedBluetoothList).when(mConnectedDeviceSlice).getBluetoothConnectedDevices(); final Slice slice = mConnectedDeviceSlice.getSlice(); final List<SliceItem> sliceItems = slice.getItems(); SliceTester.assertTitle(sliceItems, mContext.getString(R.string.no_connected_devices)); } } No newline at end of file Loading
res/values/strings.xml +8 −0 Original line number Diff line number Diff line Loading @@ -10264,4 +10264,12 @@ <string name="see_more">See more</string> <!-- See less items in contextual homepage [CHAR LIMIT=30]--> <string name="see_less">See less</string> <!-- Summary for connected devices count in connected device slice. [CHAR LIMIT=NONE] --> <plurals name="show_connected_devices"> <item quantity="one"><xliff:g id="number_device_count">%1$d</xliff:g> device connected</item> <item quantity="other"><xliff:g id="number_device_count">%1$d</xliff:g> devices connected</item> </plurals> <!-- Title for no connected devices in connected device slice. [CHAR LIMIT=NONE] --> <string name="no_connected_devices">No connected devices</string> </resources>
src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java +7 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import com.android.settings.homepage.contextualcards.deviceinfo.DataUsageSlice; import com.android.settings.homepage.contextualcards.deviceinfo.DeviceInfoSlice; import com.android.settings.homepage.contextualcards.deviceinfo.EmergencyInfoSlice; import com.android.settings.homepage.contextualcards.deviceinfo.StorageSlice; import com.android.settings.homepage.contextualcards.slices.ConnectedDeviceSlice; import com.android.settings.intelligence.ContextualCardProto.ContextualCard; import com.android.settings.intelligence.ContextualCardProto.ContextualCardList; import com.android.settings.wifi.WifiSlice; Loading Loading @@ -69,6 +70,11 @@ public class SettingsContextualCardProvider extends ContextualCardProvider { .setSliceUri(BatterySlice.BATTERY_CARD_URI.toSafeString()) .setCardName(BatterySlice.PATH_BATTERY_INFO) .build(); final ContextualCard connectedDeviceCard = ContextualCard.newBuilder() .setSliceUri(ConnectedDeviceSlice.CONNECTED_DEVICE_URI.toString()) .setCardName(ConnectedDeviceSlice.PATH_CONNECTED_DEVICE) .build(); final ContextualCardList cards = ContextualCardList.newBuilder() .addCard(wifiCard) .addCard(dataUsageCard) Loading @@ -76,6 +82,7 @@ public class SettingsContextualCardProvider extends ContextualCardProvider { .addCard(storageInfoCard) .addCard(emergencyInfoCard) .addCard(batteryInfoCard) .addCard(connectedDeviceCard) .build(); return cards; Loading
src/com/android/settings/homepage/contextualcards/slices/ConnectedDeviceSlice.java 0 → 100644 +286 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.homepage.contextualcards.slices; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; import androidx.core.graphics.drawable.IconCompat; import androidx.slice.Slice; import androidx.slice.builders.ListBuilder; import androidx.slice.builders.SliceAction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SubSettings; import com.android.settings.Utils; import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment; import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.slices.CustomSliceable; import com.android.settings.slices.SettingsSliceProvider; import com.android.settings.slices.SliceBuilderUtils; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.instrumentation.Instrumentable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; /** * TODO(b/114807655): Contextual Home Page - Connected Device * * Show connected device info if one is currently connected. UI for connected device should * match Connected Devices > Currently Connected Devices * * This Slice will show multiple currently connected devices, which includes: * 1) Bluetooth. * 2) Docks. * ... * TODO Other device types are under checking to support, will update later. */ public class ConnectedDeviceSlice implements CustomSliceable { /** * The path denotes the unique name of Connected device Slice. */ public static final String PATH_CONNECTED_DEVICE = "connected_device"; /** * Backing Uri for Connected device Slice. */ public static final Uri CONNECTED_DEVICE_URI = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(PATH_CONNECTED_DEVICE) .build(); /** * To sort the Bluetooth devices by {@link CachedBluetoothDevice}. * Refer compareTo method from {@link com.android.settings.bluetooth.BluetoothDevicePreference}. */ private static final Comparator<CachedBluetoothDevice> COMPARATOR = Comparator.naturalOrder(); private static final int DEFAULT_EXPANDED_ROW_COUNT = 4; private static final String TAG = "ConnectedDeviceSlice"; private final Context mContext; public ConnectedDeviceSlice(Context context) { mContext = context; } private static Bitmap getBitmapFromVectorDrawable(Drawable VectorDrawable) { final Bitmap bitmap = Bitmap.createBitmap(VectorDrawable.getIntrinsicWidth(), VectorDrawable.getIntrinsicHeight(), Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); VectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); VectorDrawable.draw(canvas); return bitmap; } @Override public Uri getUri() { return CONNECTED_DEVICE_URI; } /** * Return a Connected Device Slice bound to {@link #CONNECTED_DEVICE_URI}. */ @Override public Slice getSlice() { final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_homepage_connected_device); final CharSequence title = mContext.getText(R.string.connected_devices_dashboard_title); final CharSequence titleNoConnectedDevices = mContext.getText( R.string.no_connected_devices); final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext, 0, getIntent(), 0); final SliceAction primarySliceAction = new SliceAction(primaryActionIntent, icon, title); final ListBuilder listBuilder = new ListBuilder(mContext, CONNECTED_DEVICE_URI, ListBuilder.INFINITY) .setAccentColor(Utils.getColorAccentDefaultColor(mContext)); // Get row builders by connected devices, e.g. Bluetooth. // TODO Add other type connected devices, e.g. Docks. final List<ListBuilder.RowBuilder> rows = getBluetoothRowBuilder(primarySliceAction); // Return a header with IsError flag, if no connected devices. if (rows.isEmpty()) { return listBuilder.setHeader(new ListBuilder.HeaderBuilder() .setTitle(titleNoConnectedDevices) .setPrimaryAction(primarySliceAction)) .setIsError(true) .build(); } // According the number of connected devices to set sub title of header. listBuilder.setHeader(new ListBuilder.HeaderBuilder() .setTitle(title) .setSubtitle(getSubTitle(rows.size())) .setPrimaryAction(primarySliceAction)); // Add rows. for (ListBuilder.RowBuilder rowBuilder : rows) { listBuilder.addRow(rowBuilder); } // Only show "see more" button when the number of data row is more than or equal to 4. // TODO(b/118465996): SHOW MORE button won't work properly when having two data rows if (rows.size() >= DEFAULT_EXPANDED_ROW_COUNT) { listBuilder.setSeeMoreAction(primaryActionIntent); } return listBuilder.build(); } @Override public Intent getIntent() { final String screenTitle = mContext.getText(R.string.connected_devices_dashboard_title) .toString(); final Uri contentUri = new Uri.Builder().appendPath(PATH_CONNECTED_DEVICE).build(); return SliceBuilderUtils.buildSearchResultPageIntent(mContext, ConnectedDeviceDashboardFragment.class.getName(), PATH_CONNECTED_DEVICE, screenTitle, MetricsProto.MetricsEvent.SLICE) .setClassName(mContext.getPackageName(), SubSettings.class.getName()) .setData(contentUri); } @Override public void onNotifyChange(Intent intent) { } @VisibleForTesting List<CachedBluetoothDevice> getBluetoothConnectedDevices() { final List<CachedBluetoothDevice> connectedBluetoothList = new ArrayList<>(); // If Bluetooth is disable, skip to get the bluetooth devices. if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { Log.d(TAG, "Cannot get Bluetooth connected devices, Bluetooth is disabled."); return connectedBluetoothList; } // Get the Bluetooth devices from LocalBluetoothManager. final LocalBluetoothManager bluetoothManager = com.android.settings.bluetooth.Utils.getLocalBtManager(mContext); if (bluetoothManager == null) { Log.d(TAG, "Cannot get Bluetooth connected devices, Bluetooth is not supported."); return connectedBluetoothList; } final Collection<CachedBluetoothDevice> cachedDevices = bluetoothManager.getCachedDeviceManager().getCachedDevicesCopy(); // Get all connected Bluetooth devices and use Map to filter duplicated Bluetooth. final Map<BluetoothDevice, CachedBluetoothDevice> connectedBluetoothMap = new ArrayMap<>(); for (CachedBluetoothDevice device : cachedDevices) { if (device.isConnected() && !connectedBluetoothMap.containsKey(device.getDevice())) { connectedBluetoothMap.put(device.getDevice(), device); } } // Sort connected Bluetooth devices. connectedBluetoothList.addAll(connectedBluetoothMap.values()); Collections.sort(connectedBluetoothList, COMPARATOR); return connectedBluetoothList; } @VisibleForTesting PendingIntent getBluetoothDetailIntent(CachedBluetoothDevice device) { final Bundle args = new Bundle(); args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, device.getDevice().getAddress()); final SubSettingLauncher subSettingLauncher = new SubSettingLauncher(mContext); subSettingLauncher.setDestination(BluetoothDeviceDetailsFragment.class.getName()) .setArguments(args) .setTitleRes(R.string.device_details_title) .setSourceMetricsCategory(Instrumentable.METRICS_CATEGORY_UNKNOWN); // The requestCode should be unique, use the hashcode of device as request code. return PendingIntent .getActivity(mContext, device.hashCode() /* requestCode */, subSettingLauncher.toIntent(), 0 /* flags */); } @VisibleForTesting IconCompat getConnectedDeviceIcon(CachedBluetoothDevice device) { final Pair<Drawable, String> pair = BluetoothUtils .getBtClassDrawableWithDescription(mContext, device); if (pair.first != null) { return IconCompat.createWithBitmap(getBitmapFromVectorDrawable(pair.first)); } else { return IconCompat.createWithResource(mContext, R.drawable.ic_homepage_connected_device); } } private List<ListBuilder.RowBuilder> getBluetoothRowBuilder(SliceAction primarySliceAction) { final List<ListBuilder.RowBuilder> bluetoothRows = new ArrayList<>(); // According Bluetooth connected device to create row builders. final List<CachedBluetoothDevice> bluetoothDevices = getBluetoothConnectedDevices(); for (CachedBluetoothDevice bluetoothDevice : bluetoothDevices) { bluetoothRows.add(new ListBuilder.RowBuilder() .setTitleItem(getConnectedDeviceIcon(bluetoothDevice), ListBuilder.ICON_IMAGE) .setTitle(bluetoothDevice.getName()) .setSubtitle(bluetoothDevice.getConnectionSummary()) .setPrimaryAction(primarySliceAction) .addEndItem(buildBluetoothDetailDeepLinkAction(bluetoothDevice))); } return bluetoothRows; } private SliceAction buildBluetoothDetailDeepLinkAction(CachedBluetoothDevice bluetoothDevice) { return new SliceAction( getBluetoothDetailIntent(bluetoothDevice), IconCompat.createWithResource(mContext, R.drawable.ic_settings), bluetoothDevice.getName()); } private CharSequence getSubTitle(int deviceCount) { return mContext.getResources().getQuantityString(R.plurals.show_connected_devices, deviceCount, deviceCount); } } No newline at end of file
src/com/android/settings/slices/CustomSliceManager.java +2 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import com.android.settings.homepage.contextualcards.deviceinfo.BatterySlice; import com.android.settings.homepage.contextualcards.deviceinfo.DataUsageSlice; import com.android.settings.homepage.contextualcards.deviceinfo.DeviceInfoSlice; import com.android.settings.homepage.contextualcards.deviceinfo.StorageSlice; import com.android.settings.homepage.contextualcards.slices.ConnectedDeviceSlice; import com.android.settings.wifi.WifiSlice; import java.util.Map; Loading Loading @@ -103,5 +104,6 @@ public class CustomSliceManager { mUriMap.put(DeviceInfoSlice.DEVICE_INFO_CARD_URI, DeviceInfoSlice.class); mUriMap.put(StorageSlice.STORAGE_CARD_URI, StorageSlice.class); mUriMap.put(BatterySlice.BATTERY_CARD_URI, BatterySlice.class); mUriMap.put(ConnectedDeviceSlice.CONNECTED_DEVICE_URI, ConnectedDeviceSlice.class); } }
tests/robotests/src/com/android/settings/homepage/contextualcards/slices/ConnectedDeviceSliceTest.java 0 → 100644 +99 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.homepage.contextualcards.slices; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import androidx.core.graphics.drawable.IconCompat; import androidx.slice.Slice; import androidx.slice.SliceItem; import androidx.slice.SliceProvider; import androidx.slice.widget.SliceLiveData; import com.android.settings.R; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.SliceTester; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import java.util.ArrayList; import java.util.List; @RunWith(SettingsRobolectricTestRunner.class) public class ConnectedDeviceSliceTest { @Mock private CachedBluetoothDevice mCachedBluetoothDevice; private List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>(); private Context mContext; private ConnectedDeviceSlice mConnectedDeviceSlice; @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; // Set-up specs for SliceMetadata. SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); mConnectedDeviceSlice = spy(new ConnectedDeviceSlice(mContext)); } @Test public void getSlice_hasConnectedDevices_shouldBeCorrectSliceContent() { final String title = "BluetoothTitle"; final String summary = "BluetoothSummary"; final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_homepage_connected_device); final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent("test action"), 0); doReturn(title).when(mCachedBluetoothDevice).getName(); doReturn(summary).when(mCachedBluetoothDevice).getConnectionSummary(); mCachedDevices.add(mCachedBluetoothDevice); doReturn(mCachedDevices).when(mConnectedDeviceSlice).getBluetoothConnectedDevices(); doReturn(icon).when(mConnectedDeviceSlice).getConnectedDeviceIcon(any()); doReturn(pendingIntent).when(mConnectedDeviceSlice).getBluetoothDetailIntent(any()); final Slice slice = mConnectedDeviceSlice.getSlice(); final List<SliceItem> sliceItems = slice.getItems(); SliceTester.assertTitle(sliceItems, title); } @Test public void getSlice_hasNoConnectedDevices_shouldReturnCorrectHeader() { final List<CachedBluetoothDevice> connectedBluetoothList = new ArrayList<>(); doReturn(connectedBluetoothList).when(mConnectedDeviceSlice).getBluetoothConnectedDevices(); final Slice slice = mConnectedDeviceSlice.getSlice(); final List<SliceItem> sliceItems = slice.getItems(); SliceTester.assertTitle(sliceItems, mContext.getString(R.string.no_connected_devices)); } } No newline at end of file