From d2bb2ab2592004520298ecd68da94f89169320b8 Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Thu, 3 May 2018 15:12:18 -0700 Subject: [PATCH] Add Bluetooth Slice Bluetooth slice is added a special case, due to the migration of bluetooth to a Switch Bar instead of a preference with a controller. Bug: 67997327 Test: robotests Change-Id: Icfdcd77601ad1e64e0f6c352a8d691f0181515c8 --- .../bluetooth/BluetoothSliceBuilder.java | 146 ++++++++++++++++++ .../slices/SettingsSliceProvider.java | 10 +- .../slices/SliceBroadcastReceiver.java | 5 + .../bluetooth/BluetoothSliceBuilderTest.java | 105 +++++++++++++ .../slices/SettingsSliceProviderTest.java | 4 +- .../shadow/ShadowLocalBluetoothAdapter.java | 23 ++- .../ShadowLocalBluetoothProfileManager.java | 38 +++++ .../settings/wifi/WifiSliceBuilderTest.java | 2 +- 8 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 src/com/android/settings/bluetooth/BluetoothSliceBuilder.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothSliceBuilderTest.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowLocalBluetoothProfileManager.java diff --git a/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java b/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java new file mode 100644 index 00000000000..1de04f93204 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java @@ -0,0 +1,146 @@ +/* + * 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}. + *

+ * 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); + } +} diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 4b28e288408..907ede2d3ae 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -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 getSpecialCasePlatformUris() { return Arrays.asList( - WifiSliceBuilder.WIFI_URI + WifiSliceBuilder.WIFI_URI, + BluetoothSliceBuilder.BLUETOOTH_URI ); } diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java index b9f3b0051ea..80e3e3c1ef1 100644 --- a/src/com/android/settings/slices/SliceBroadcastReceiver.java +++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java @@ -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; diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothSliceBuilderTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothSliceBuilderTest.java new file mode 100644 index 00000000000..20bab8713f7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothSliceBuilderTest.java @@ -0,0 +1,105 @@ +/* + * 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 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 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 diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java index fe6d512ea88..722f481d10f 100644 --- a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java +++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java @@ -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 SPECIAL_CASE_PLATFORM_URIS = Arrays.asList( - WifiSliceBuilder.WIFI_URI + WifiSliceBuilder.WIFI_URI, + BluetoothSliceBuilder.BLUETOOTH_URI ); private static final List SPECIAL_CASE_OEM_URIS = Arrays.asList( diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLocalBluetoothAdapter.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLocalBluetoothAdapter.java index d215e2abfeb..ddac6c71a68 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLocalBluetoothAdapter.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLocalBluetoothAdapter.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.settings.testutils.shadow; import com.android.settingslib.bluetooth.LocalBluetoothAdapter; @@ -24,13 +25,31 @@ import org.robolectric.annotation.Implements; public class ShadowLocalBluetoothAdapter { private static String sName; + private boolean isBluetoothEnabled = true; + + public static void setName(String name) { + sName = name; + } @Implementation public String getName() { return sName; } - public static void setName(String name) { - sName = name; + @Implementation + public boolean isEnabled() { + return isBluetoothEnabled; + } + + @Implementation + public boolean enable() { + isBluetoothEnabled = true; + return true; + } + + @Implementation + public boolean disable() { + isBluetoothEnabled = false; + return true; } } diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLocalBluetoothProfileManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLocalBluetoothProfileManager.java new file mode 100644 index 00000000000..9e64e34f3b2 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLocalBluetoothProfileManager.java @@ -0,0 +1,38 @@ +/* + * 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.testutils.shadow; + +import android.content.Context; + +import com.android.settingslib.bluetooth.BluetoothEventManager; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothAdapter; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import org.robolectric.annotation.Implements; + +@Implements(LocalBluetoothProfileManager.class) +public class ShadowLocalBluetoothProfileManager { + + public void __constructor__(Context context, + LocalBluetoothAdapter adapter, + CachedBluetoothDeviceManager deviceManager, + BluetoothEventManager eventManager) { + + } +} diff --git a/tests/robotests/src/com/android/settings/wifi/WifiSliceBuilderTest.java b/tests/robotests/src/com/android/settings/wifi/WifiSliceBuilderTest.java index f709e00ed6e..a196bb8c23d 100644 --- a/tests/robotests/src/com/android/settings/wifi/WifiSliceBuilderTest.java +++ b/tests/robotests/src/com/android/settings/wifi/WifiSliceBuilderTest.java @@ -68,7 +68,7 @@ public class WifiSliceBuilderTest { } @Test - public void getWifiSlice_correctData() { + public void getWifiSlice_correctSliceContent() { final Slice wifiSlice = WifiSliceBuilder.getSlice(mContext); final SliceMetadata metadata = SliceMetadata.from(mContext, wifiSlice);