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

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

Merge "Add Bluetooth Slice"

parents b8a1df80 d2bb2ab2
Loading
Loading
Loading
Loading
+146 −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.bluetooth;

import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;

import android.annotation.ColorInt;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.provider.SettingsSlicesContract;

import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R;
import com.android.settings.SubSettings;
import com.android.settings.connecteddevice.BluetoothDashboardFragment;
import com.android.settings.search.DatabaseIndexingUtils;
import com.android.settings.slices.SliceBroadcastReceiver;
import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
import com.android.settingslib.bluetooth.LocalBluetoothManager;

import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.Slice;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;

/**
 * Utility class to build a Bluetooth Slice, and handle all associated actions.
 */
public class BluetoothSliceBuilder {

    private static final String TAG = "BluetoothSliceBuilder";

    /**
     * Backing Uri for the Bluetooth Slice.
     */
    public static final Uri BLUETOOTH_URI = new Uri.Builder()
            .scheme(ContentResolver.SCHEME_CONTENT)
            .authority(SettingsSlicesContract.AUTHORITY)
            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
            .appendPath(SettingsSlicesContract.KEY_BLUETOOTH)
            .build();

    /**
     * Action notifying a change on the BluetoothSlice.
     */
    public static final String ACTION_BLUETOOTH_SLICE_CHANGED =
            "com.android.settings.bluetooth.action.BLUETOOTH_MODE_CHANGED";

    public static final IntentFilter INTENT_FILTER = new IntentFilter();

    static {
        INTENT_FILTER.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
        INTENT_FILTER.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
    }

    private BluetoothSliceBuilder() {
    }

    /**
     * Return a Bluetooth Slice bound to {@link #BLUETOOTH_URI}.
     * <p>
     * Note that you should register a listener for {@link #INTENT_FILTER} to get changes for
     * Bluetooth.
     */
    public static Slice getSlice(Context context) {
        final boolean isBluetoothEnabled = isBluetoothEnabled(context);
        final CharSequence title = context.getText(R.string.bluetooth_settings);
        final IconCompat icon = IconCompat.createWithResource(context,
                R.drawable.ic_settings_bluetooth);
        @ColorInt final int color = com.android.settings.Utils.getColorAccent(
                context).getDefaultColor();
        final PendingIntent toggleAction = getBroadcastIntent(context);
        final PendingIntent primaryAction = getPrimaryAction(context);
        final SliceAction primarySliceAction = new SliceAction(primaryAction, icon, title);
        final SliceAction toggleSliceAction = new SliceAction(toggleAction, null /* actionTitle */,
                isBluetoothEnabled);

        return new ListBuilder(context, BLUETOOTH_URI, ListBuilder.INFINITY)
                .setAccentColor(color)
                .addRow(b -> b
                        .setTitle(title)
                        .addEndItem(toggleSliceAction)
                        .setPrimaryAction(primarySliceAction))
                .build();
    }

    /**
     * Update the current Bluetooth status to the boolean value keyed by
     * {@link android.app.slice.Slice#EXTRA_TOGGLE_STATE} on {@param intent}.
     */
    public static void handleUriChange(Context context, Intent intent) {
        final boolean newBluetoothState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, false);
        final LocalBluetoothAdapter adapter = LocalBluetoothManager.getInstance(context,
                null /* callback */).getBluetoothAdapter();

        adapter.setBluetoothEnabled(newBluetoothState);
        // Do not notifyChange on Uri. The service takes longer to update the current value than it
        // does for the Slice to check the current value again. Let {@link SliceBroadcastRelay}
        // handle it.
    }

    private static boolean isBluetoothEnabled(Context context) {
        final LocalBluetoothAdapter adapter = LocalBluetoothManager.getInstance(context,
                null /* callback */).getBluetoothAdapter();
        return adapter.isEnabled();
    }

    private static PendingIntent getPrimaryAction(Context context) {
        final String screenTitle = context.getText(R.string.bluetooth_settings_title).toString();
        final Uri contentUri = new Uri.Builder().appendPath(
                SettingsSlicesContract.KEY_BLUETOOTH).build();
        final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
                BluetoothDashboardFragment.class.getName(), null /* key */, screenTitle,
                MetricsProto.MetricsEvent.SETTINGS_CONNECTED_DEVICE_CATEGORY)
                .setClassName(context.getPackageName(), SubSettings.class.getName())
                .setData(contentUri);

        return PendingIntent.getActivity(context, 0 /* requestCode */,
                intent, 0 /* flags */);
    }

    private static PendingIntent getBroadcastIntent(Context context) {
        final Intent intent = new Intent(ACTION_BLUETOOTH_SLICE_CHANGED)
                .setClass(context, SliceBroadcastReceiver.class);
        return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent,
                PendingIntent.FLAG_CANCEL_CURRENT);
    }
}
+8 −2
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import com.android.settings.overlay.FeatureFactory;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.wifi.WifiSliceBuilder;
import com.android.settings.wifi.calling.WifiCallingSliceHelper;
import com.android.settings.bluetooth.BluetoothSliceBuilder;
import com.android.settings.notification.ZenModeSliceBuilder;
import com.android.settingslib.SliceBroadcastRelay;
import com.android.settingslib.utils.ThreadUtils;
@@ -147,7 +148,9 @@ public class SettingsSliceProvider extends SliceProvider {
            return;
        } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) {
            registerIntentToUri(ZenModeSliceBuilder.INTENT_FILTER, sliceUri);
            mRegisteredUris.add(sliceUri);
            return;
        } else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(sliceUri)) {
            registerIntentToUri(BluetoothSliceBuilder.INTENT_FILTER, sliceUri);
            return;
        }

@@ -178,6 +181,8 @@ public class SettingsSliceProvider extends SliceProvider {
            return WifiSliceBuilder.getSlice(getContext());
        } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) {
            return ZenModeSliceBuilder.getSlice(getContext());
        } else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(sliceUri)) {
            return BluetoothSliceBuilder.getSlice(getContext());
        }

        SliceData cachedSliceData = mSliceWeakDataCache.get(sliceUri);
@@ -325,7 +330,8 @@ public class SettingsSliceProvider extends SliceProvider {

    private List<Uri> getSpecialCasePlatformUris() {
        return Arrays.asList(
                WifiSliceBuilder.WIFI_URI
                WifiSliceBuilder.WIFI_URI,
                BluetoothSliceBuilder.BLUETOOTH_URI
        );
    }

+5 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.settings.slices;

import static com.android.settings.bluetooth.BluetoothSliceBuilder.ACTION_BLUETOOTH_SLICE_CHANGED;
import static com.android.settings.notification.ZenModeSliceBuilder.ACTION_ZEN_MODE_SLICE_CHANGED;
import static com.android.settings.slices.SettingsSliceProvider.ACTION_SLIDER_CHANGED;
import static com.android.settings.slices.SettingsSliceProvider.ACTION_TOGGLE_CHANGED;
@@ -35,6 +36,7 @@ import android.util.Log;
import android.util.Pair;

import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.bluetooth.BluetoothSliceBuilder;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SliderPreferenceController;
import com.android.settings.core.TogglePreferenceController;
@@ -66,6 +68,9 @@ public class SliceBroadcastReceiver extends BroadcastReceiver {
                final int newPosition = intent.getIntExtra(Slice.EXTRA_RANGE_VALUE, -1);
                handleSliderAction(context, key, newPosition, isPlatformSlice);
                break;
            case ACTION_BLUETOOTH_SLICE_CHANGED:
                BluetoothSliceBuilder.handleUriChange(context, intent);
                break;
            case ACTION_WIFI_SLICE_CHANGED:
                WifiSliceBuilder.handleUriChange(context, intent);
                break;
+105 −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.bluetooth;

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

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;

import android.content.Context;

import com.android.settings.R;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.android.settings.testutils.SliceTester;
import com.android.settings.testutils.shadow.ShadowLocalBluetoothAdapter;
import com.android.settings.testutils.shadow.ShadowLocalBluetoothProfileManager;
import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
import com.android.settingslib.bluetooth.LocalBluetoothManager;

import android.content.Intent;
import android.content.res.Resources;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

import java.util.List;

import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.SliceMetadata;
import androidx.slice.SliceProvider;
import androidx.slice.core.SliceAction;
import androidx.slice.widget.SliceLiveData;

@RunWith(SettingsRobolectricTestRunner.class)
@Config(shadows = {ShadowLocalBluetoothAdapter.class, ShadowLocalBluetoothProfileManager.class})
public class BluetoothSliceBuilderTest {

    private Context mContext;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mContext = spy(RuntimeEnvironment.application);

        // Prevent crash in SliceMetadata.
        Resources resources = spy(mContext.getResources());
        doReturn(60).when(resources).getDimensionPixelSize(anyInt());
        doReturn(resources).when(mContext).getResources();

        // Set-up specs for SliceMetadata.
        SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
    }

    @Test
    public void getBluetoothSlice_correctSliceContent() {
        final Slice BluetoothSlice = BluetoothSliceBuilder.getSlice(mContext);
        final SliceMetadata metadata = SliceMetadata.from(mContext, BluetoothSlice);

        final List<SliceAction> toggles = metadata.getToggles();
        assertThat(toggles).hasSize(1);

        final SliceAction primaryAction = metadata.getPrimaryAction();
        final IconCompat expectedToggleIcon = IconCompat.createWithResource(mContext,
                R.drawable.ic_settings_bluetooth);
        assertThat(primaryAction.getIcon().toString()).isEqualTo(expectedToggleIcon.toString());

        final List<SliceItem> sliceItems = BluetoothSlice.getItems();
        SliceTester.assertTitle(sliceItems, mContext.getString(R.string.bluetooth_settings_title));
    }

    @Test
    public void handleUriChange_updatesBluetooth() {
        final LocalBluetoothAdapter adapter = LocalBluetoothManager.getInstance(mContext,
                null /* callback */).getBluetoothAdapter();
        final Intent intent = new Intent();
        intent.putExtra(android.app.slice.Slice.EXTRA_TOGGLE_STATE, true);
        adapter.setBluetoothEnabled(false /* enabled */);

        BluetoothSliceBuilder.handleUriChange(mContext, intent);

        assertThat(adapter.isEnabled()).isTrue();
    }
}
 No newline at end of file
+3 −1
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import android.os.StrictMode;
import android.provider.SettingsSlicesContract;

import com.android.settings.wifi.WifiSliceBuilder;
import com.android.settings.bluetooth.BluetoothSliceBuilder;
import com.android.settings.notification.ZenModeSliceBuilder;
import com.android.settings.testutils.DatabaseTestUtils;
import com.android.settings.testutils.FakeToggleController;
@@ -79,7 +80,8 @@ public class SettingsSliceProviderTest {
    private SliceManager mManager;

    private static final List<Uri> SPECIAL_CASE_PLATFORM_URIS = Arrays.asList(
            WifiSliceBuilder.WIFI_URI
            WifiSliceBuilder.WIFI_URI,
            BluetoothSliceBuilder.BLUETOOTH_URI
    );

    private static final List<Uri> SPECIAL_CASE_OEM_URIS = Arrays.asList(
Loading