diff --git a/res/values/strings.xml b/res/values/strings.xml
index 45305ece4f6..f82aa68075a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -132,6 +132,8 @@
Pair new device
bluetooth
+
+ Automatically turn on again tomorrow
@@ -1800,8 +1802,12 @@
Pair & connect
When Bluetooth is turned on, your device can communicate with other nearby Bluetooth devices
+
+ When Bluetooth is on, your device can communicate with other nearby Bluetooth devices. Features like Quick Share, Find My Device, and device location use Bluetooth.
When Bluetooth is turned on, your device can communicate with other nearby Bluetooth devices.\n\nTo improve device experience, apps and services can still scan for nearby devices at any time, even when Bluetooth is off. This can be used, for example, to improve location-based features and services. You can change this in Bluetooth scanning settings.
+
+ When Bluetooth is on, your device can communicate with other nearby Bluetooth devices. Features like Quick Share, Find My Device, and device location use Bluetooth.\n\nApps and services can still scan for nearby devices at any time, even when Bluetooth is off. This can be used, for example, to improve location-based features and services. You can change this in Bluetooth scanning settings.
Change
diff --git a/res/xml/bluetooth_screen.xml b/res/xml/bluetooth_screen.xml
index 86dc8a12c72..51507fb0ba1 100644
--- a/res/xml/bluetooth_screen.xml
+++ b/res/xml/bluetooth_screen.xml
@@ -18,6 +18,11 @@
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/bluetooth_settings_title">
+
+
{
+ updateValue();
+ mContext.getMainExecutor()
+ .execute(
+ () -> {
+ if (mPreference != null) {
+ updateState(mPreference);
+ }
+ });
+ });
+ }
+ };
+ private int mAutoOnValue = UNSET;
+ @Nullable private TwoStatePreference mPreference;
+
+ public BluetoothAutoOnPreferenceController(
+ @NonNull Context context, @NonNull String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ @Override
+ public void onStart() {
+ mContext.getContentResolver()
+ .registerContentObserver(
+ Settings.Secure.getUriFor(SETTING_NAME),
+ /* notifyForDescendants= */ false,
+ mContentObserver);
+ }
+
+ @Override
+ public void onStop() {
+ mContext.getContentResolver().unregisterContentObserver(mContentObserver);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ if (!Flags.bluetoothQsTileDialogAutoOnToggle()) {
+ return UNSUPPORTED_ON_DEVICE;
+ }
+ updateValue();
+ return mAutoOnValue != UNSET ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Override
+ public void displayPreference(@NonNull PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference = screen.findPreference(getPreferenceKey());
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return PREF_KEY;
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mAutoOnValue == ENABLED;
+ }
+
+ @Override
+ public boolean setChecked(boolean isChecked) {
+ if (getAvailabilityStatus() != AVAILABLE) {
+ Log.w(TAG, "Trying to set toggle value while feature not available.");
+ return false;
+ }
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ boolean updated =
+ Settings.Secure.putIntForUser(
+ mContext.getContentResolver(),
+ SETTING_NAME,
+ isChecked ? ENABLED : DISABLED,
+ UserHandle.myUserId());
+ if (updated) {
+ updateValue();
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public int getSliceHighlightMenuRes() {
+ return 0;
+ }
+
+ private void updateValue() {
+ mAutoOnValue =
+ Settings.Secure.getIntForUser(
+ mContext.getContentResolver(), SETTING_NAME, UNSET, UserHandle.myUserId());
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothSwitchPreferenceController.java b/src/com/android/settings/bluetooth/BluetoothSwitchPreferenceController.java
index 6fd5070303c..ac5575803c1 100644
--- a/src/com/android/settings/bluetooth/BluetoothSwitchPreferenceController.java
+++ b/src/com/android/settings/bluetooth/BluetoothSwitchPreferenceController.java
@@ -15,8 +15,13 @@
*/
package com.android.settings.bluetooth;
+import static com.android.settings.bluetooth.BluetoothAutoOnPreferenceController.SETTING_NAME;
+import static com.android.settings.bluetooth.BluetoothAutoOnPreferenceController.UNSET;
+
import android.app.settings.SettingsEnums;
import android.content.Context;
+import android.os.UserHandle;
+import android.provider.Settings;
import android.view.View;
import androidx.annotation.VisibleForTesting;
@@ -29,6 +34,7 @@ import com.android.settings.widget.SwitchWidgetController;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
+import com.android.settingslib.flags.Flags;
import com.android.settingslib.widget.FooterPreference;
/**
@@ -36,8 +42,11 @@ import com.android.settingslib.widget.FooterPreference;
* is delegated to the SwitchWidgetController it uses.
*/
public class BluetoothSwitchPreferenceController
- implements LifecycleObserver, OnStart, OnStop,
- SwitchWidgetController.OnSwitchChangeListener, View.OnClickListener {
+ implements LifecycleObserver,
+ OnStart,
+ OnStop,
+ SwitchWidgetController.OnSwitchChangeListener,
+ View.OnClickListener {
private BluetoothEnabler mBluetoothEnabler;
private RestrictionUtils mRestrictionUtils;
@@ -46,18 +55,21 @@ public class BluetoothSwitchPreferenceController
private FooterPreference mFooterPreference;
private boolean mIsAlwaysDiscoverable;
- @VisibleForTesting
- AlwaysDiscoverable mAlwaysDiscoverable;
+ @VisibleForTesting AlwaysDiscoverable mAlwaysDiscoverable;
- public BluetoothSwitchPreferenceController(Context context,
+ public BluetoothSwitchPreferenceController(
+ Context context,
SwitchWidgetController switchController,
FooterPreference footerPreference) {
this(context, new RestrictionUtils(), switchController, footerPreference);
}
@VisibleForTesting
- public BluetoothSwitchPreferenceController(Context context, RestrictionUtils restrictionUtils,
- SwitchWidgetController switchController, FooterPreference footerPreference) {
+ public BluetoothSwitchPreferenceController(
+ Context context,
+ RestrictionUtils restrictionUtils,
+ SwitchWidgetController switchController,
+ FooterPreference footerPreference) {
mRestrictionUtils = restrictionUtils;
mSwitch = switchController;
mContext = context;
@@ -66,11 +78,13 @@ public class BluetoothSwitchPreferenceController
mSwitch.setupView();
updateText(mSwitch.isChecked());
- mBluetoothEnabler = new BluetoothEnabler(context,
- switchController,
- FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(),
- SettingsEnums.ACTION_SETTINGS_MASTER_SWITCH_BLUETOOTH_TOGGLE,
- mRestrictionUtils);
+ mBluetoothEnabler =
+ new BluetoothEnabler(
+ context,
+ switchController,
+ FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(),
+ SettingsEnums.ACTION_SETTINGS_MASTER_SWITCH_BLUETOOTH_TOGGLE,
+ mRestrictionUtils);
mBluetoothEnabler.setToggleCallback(this);
mAlwaysDiscoverable = new AlwaysDiscoverable(context);
}
@@ -97,8 +111,8 @@ public class BluetoothSwitchPreferenceController
/**
* Set whether the device can be discovered. By default the value will be {@code false}.
*
- * @param isAlwaysDiscoverable {@code true} if the device can be discovered,
- * otherwise {@code false}
+ * @param isAlwaysDiscoverable {@code true} if the device can be discovered, otherwise {@code
+ * false}
*/
public void setAlwaysDiscoverable(boolean isAlwaysDiscoverable) {
mIsAlwaysDiscoverable = isAlwaysDiscoverable;
@@ -119,15 +133,35 @@ public class BluetoothSwitchPreferenceController
.launch();
}
- @VisibleForTesting void updateText(boolean isChecked) {
+ @VisibleForTesting
+ void updateText(boolean isChecked) {
if (!isChecked && Utils.isBluetoothScanningEnabled(mContext)) {
- mFooterPreference.setTitle(R.string.bluetooth_scanning_on_info_message);
+ if (isAutoOnFeatureAvailable()) {
+ mFooterPreference.setTitle(
+ R.string.bluetooth_scanning_on_info_message_auto_on_available);
+ } else {
+ mFooterPreference.setTitle(R.string.bluetooth_scanning_on_info_message);
+ }
mFooterPreference.setLearnMoreText(mContext.getString(R.string.bluetooth_scan_change));
mFooterPreference.setLearnMoreAction(v -> onClick(v));
} else {
- mFooterPreference.setTitle(R.string.bluetooth_empty_list_bluetooth_off);
+ if (isAutoOnFeatureAvailable()) {
+ mFooterPreference.setTitle(
+ R.string.bluetooth_empty_list_bluetooth_off_auto_on_available);
+ } else {
+ mFooterPreference.setTitle(R.string.bluetooth_empty_list_bluetooth_off);
+ }
mFooterPreference.setLearnMoreText("");
mFooterPreference.setLearnMoreAction(null);
}
}
+
+ private boolean isAutoOnFeatureAvailable() {
+ if (!Flags.bluetoothQsTileDialogAutoOnToggle()) {
+ return false;
+ }
+ return Settings.Secure.getIntForUser(
+ mContext.getContentResolver(), SETTING_NAME, UNSET, UserHandle.myUserId())
+ != UNSET;
+ }
}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothAutoOnPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothAutoOnPreferenceControllerTest.java
new file mode 100644
index 00000000000..d82dcc43ac3
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothAutoOnPreferenceControllerTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 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.android.settings.bluetooth.BluetoothAutoOnPreferenceController.DISABLED;
+import static com.android.settings.bluetooth.BluetoothAutoOnPreferenceController.ENABLED;
+import static com.android.settings.bluetooth.BluetoothAutoOnPreferenceController.PREF_KEY;
+import static com.android.settings.bluetooth.BluetoothAutoOnPreferenceController.SETTING_NAME;
+import static com.android.settings.bluetooth.BluetoothAutoOnPreferenceController.UNSET;
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+import static com.android.settingslib.flags.Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class BluetoothAutoOnPreferenceControllerTest {
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ private Context mContext;
+ private ContentResolver mContentResolver;
+ private BluetoothAutoOnPreferenceController mController;
+
+ @Before
+ public void setUp() {
+ mSetFlagsRule.enableFlags(FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE);
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mContentResolver = mContext.getContentResolver();
+ mController = new BluetoothAutoOnPreferenceController(mContext, PREF_KEY);
+ }
+
+ @Test
+ public void getAvailability_valueUnset_returnUnsupported() {
+ Settings.Secure.putInt(mContentResolver, SETTING_NAME, UNSET);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+ }
+
+ @Test
+ public void getAvailability_valueSet_returnAvailable() {
+ Settings.Secure.putInt(mContentResolver, SETTING_NAME, DISABLED);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ }
+
+ @Test
+ public void isChecked_valueEnabled_returnTrue() {
+ Settings.Secure.putInt(mContentResolver, SETTING_NAME, ENABLED);
+
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ assertThat(mController.isChecked()).isEqualTo(true);
+ }
+
+ @Test
+ public void setChecked_returnTrue() {
+ Settings.Secure.putInt(mContentResolver, SETTING_NAME, DISABLED);
+
+ mController.setChecked(true);
+ assertThat(mController.isChecked()).isEqualTo(true);
+ }
+
+ @Test
+ public void setChecked_returnFalse() {
+ Settings.Secure.putInt(mContentResolver, SETTING_NAME, ENABLED);
+
+ mController.setChecked(false);
+ assertThat(mController.isChecked()).isEqualTo(false);
+ }
+}