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

Commit 5422d7ea authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add Connected Device slice to Contextual Settings Homepage"

parents 42c57293 f61bc19f
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -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>
+7 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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)
@@ -76,6 +82,7 @@ public class SettingsContextualCardProvider extends ContextualCardProvider {
                .addCard(storageInfoCard)
                .addCard(emergencyInfoCard)
                .addCard(batteryInfoCard)
                .addCard(connectedDeviceCard)
                .build();

        return cards;
+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
+2 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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);
    }
}
+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