diff --git a/res/xml/bluetooth_screen.xml b/res/xml/bluetooth_screen.xml index e815d44410b..51cff333e38 100644 --- a/res/xml/bluetooth_screen.xml +++ b/res/xml/bluetooth_screen.xml @@ -16,6 +16,7 @@ R.string.bluetooth_scanning_on_info_message_auto_on_available + else -> R.string.bluetooth_scanning_on_info_message + } + } else { + when (isAutoOnFeatureAvailable()) { + true -> R.string.bluetooth_empty_list_bluetooth_off_auto_on_available + else -> R.string.bluetooth_empty_list_bluetooth_off + } + } + return context.getString(resId) + } + + private fun isAutoOnFeatureAvailable() = + try { + bluetoothDataStore.bluetoothAdapter?.isAutoOnSupported == true + } catch (e: Exception) { + Log.e(TAG, "isAutoOnSupported failed", e) + false + } + + companion object { + const val KEY = "bluetooth_screen_footer" + const val TAG = "BluetoothFooterPreference" + } +} diff --git a/src/com/android/settings/connecteddevice/BluetoothMainSwitchPreference.kt b/src/com/android/settings/connecteddevice/BluetoothMainSwitchPreference.kt deleted file mode 100644 index bf806531135..00000000000 --- a/src/com/android/settings/connecteddevice/BluetoothMainSwitchPreference.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.connecteddevice - -import android.bluetooth.BluetoothAdapter -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import com.android.settings.R -import com.android.settings.widget.MainSwitchBarMetadata -import com.android.settingslib.datastore.KeyValueStore -import com.android.settingslib.datastore.NoOpKeyedObservable -import com.android.settingslib.metadata.PreferenceLifecycleContext -import com.android.settingslib.metadata.PreferenceLifecycleProvider -import com.android.settingslib.metadata.ReadWritePermit - -class BluetoothMainSwitchPreference(private val bluetoothAdapter: BluetoothAdapter?) : - MainSwitchBarMetadata, PreferenceLifecycleProvider { - - private lateinit var broadcastReceiver: BroadcastReceiver - - override val key - get() = "use_bluetooth" - - override val title - get() = R.string.bluetooth_main_switch_title - - override fun getReadPermit(context: Context, myUid: Int, callingUid: Int) = - ReadWritePermit.ALLOW - - override fun getWritePermit(context: Context, value: Boolean?, myUid: Int, callingUid: Int) = - ReadWritePermit.ALLOW - - override fun storage(context: Context) = BluetoothStateStore(bluetoothAdapter) - - override fun onStart(context: PreferenceLifecycleContext) { - broadcastReceiver = - object : BroadcastReceiver() { - override fun onReceive(receiverContext: Context, intent: Intent) { - context.notifyPreferenceChange(key) - } - } - context.registerReceiver( - broadcastReceiver, - IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED), - Context.RECEIVER_EXPORTED_UNAUDITED - ) - } - - override fun onStop(context: PreferenceLifecycleContext) { - if (::broadcastReceiver.isInitialized) { - context.unregisterReceiver(broadcastReceiver) - } - } - - override fun isEnabled(context: Context): Boolean { - return bluetoothAdapter?.state.let { - it == BluetoothAdapter.STATE_ON || it == BluetoothAdapter.STATE_OFF - } - } - - @Suppress("UNCHECKED_CAST") - class BluetoothStateStore(private val bluetoothAdapter: BluetoothAdapter?) : - NoOpKeyedObservable(), KeyValueStore { - - override fun contains(key: String) = true - - override fun getValue(key: String, valueType: Class): T? { - return (bluetoothAdapter?.state.let { - it == BluetoothAdapter.STATE_ON || it == BluetoothAdapter.STATE_TURNING_ON - }) as T - } - - override fun setValue(key: String, valueType: Class, value: T?) { - if (value is Boolean) { - if (value) { - bluetoothAdapter?.enable() - } else { - bluetoothAdapter?.disable() - } - } - } - } -} diff --git a/src/com/android/settings/connecteddevice/BluetoothPreference.kt b/src/com/android/settings/connecteddevice/BluetoothPreference.kt new file mode 100644 index 00000000000..c9b39537331 --- /dev/null +++ b/src/com/android/settings/connecteddevice/BluetoothPreference.kt @@ -0,0 +1,175 @@ +/* + * 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.connecteddevice + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.UserManager +import android.provider.Settings +import android.widget.Toast +import androidx.preference.Preference +import com.android.settings.PreferenceRestrictionMixin +import com.android.settings.R +import com.android.settings.network.SatelliteRepository.Companion.isSatelliteOn +import com.android.settings.network.SatelliteWarningDialogActivity +import com.android.settings.widget.MainSwitchBarMetadata +import com.android.settingslib.WirelessUtils +import com.android.settingslib.datastore.AbstractKeyedDataObservable +import com.android.settingslib.datastore.DataChangeReason +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.metadata.PreferenceMetadata +import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel + +@SuppressLint("MissingPermission") +class BluetoothPreference(private val bluetoothDataStore: BluetoothDataStore) : + MainSwitchBarMetadata, PreferenceRestrictionMixin, Preference.OnPreferenceChangeListener { + + override val key + get() = KEY + + override val title + get() = R.string.bluetooth_main_switch_title + + override val restrictionKeys: Array + get() = arrayOf(UserManager.DISALLOW_BLUETOOTH, UserManager.DISALLOW_CONFIG_BLUETOOTH) + + override fun getReadPermit(context: Context, myUid: Int, callingUid: Int) = + ReadWritePermit.ALLOW + + override fun getWritePermit(context: Context, value: Boolean?, myUid: Int, callingUid: Int) = + when { + isSatelliteOn(context, 3000) || + (value == true && + !WirelessUtils.isRadioAllowed(context, Settings.Global.RADIO_BLUETOOTH)) -> + ReadWritePermit.DISALLOW + else -> ReadWritePermit.ALLOW + } + + override val sensitivityLevel + get() = SensitivityLevel.LOW_SENSITIVITY + + override fun storage(context: Context) = bluetoothDataStore + + override fun isEnabled(context: Context): Boolean { + return super.isEnabled(context) && + bluetoothDataStore.bluetoothAdapter?.state.let { + it == BluetoothAdapter.STATE_ON || it == BluetoothAdapter.STATE_OFF + } + } + + override fun bind(preference: Preference, metadata: PreferenceMetadata) { + super.bind(preference, metadata) + preference.onPreferenceChangeListener = this + } + + override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { + val context = preference.context + + if (isSatelliteOn(context, 3000)) { + context.startActivity( + Intent(context, SatelliteWarningDialogActivity::class.java) + .putExtra( + SatelliteWarningDialogActivity.EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG, + SatelliteWarningDialogActivity.TYPE_IS_BLUETOOTH, + ) + ) + return false + } + + // Show toast message if Bluetooth is not allowed in airplane mode + if ( + newValue == true && + !WirelessUtils.isRadioAllowed(context, Settings.Global.RADIO_BLUETOOTH) + ) { + Toast.makeText(context, R.string.wifi_in_airplane_mode, Toast.LENGTH_SHORT).show() + return false + } + + return true + } + + @Suppress("UNCHECKED_CAST") + private class BluetoothStorage( + private val context: Context, + override val bluetoothAdapter: BluetoothAdapter?, + ) : AbstractKeyedDataObservable(), BluetoothDataStore { + + private var broadcastReceiver: BroadcastReceiver? = null + + override fun contains(key: String) = key == KEY && bluetoothAdapter != null + + override fun getValue(key: String, valueType: Class): T { + return (bluetoothAdapter?.state.let { + it == BluetoothAdapter.STATE_ON || it == BluetoothAdapter.STATE_TURNING_ON + }) + as T + } + + @Suppress("DEPRECATION") + override fun setValue(key: String, valueType: Class, value: T?) { + if (value is Boolean) { + if (value) { + bluetoothAdapter?.enable() + } else { + bluetoothAdapter?.disable() + } + } + } + + @SuppressLint("WrongConstant") + override fun onFirstObserverAdded() { + broadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + notifyChange(KEY, DataChangeReason.UPDATE) + } + } + context.registerReceiver( + broadcastReceiver, + IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED), + Context.RECEIVER_EXPORTED_UNAUDITED, + ) + } + + override fun onLastObserverRemoved() { + context.unregisterReceiver(broadcastReceiver) + } + } + + companion object { + const val KEY = "use_bluetooth" + + @Suppress("DEPRECATION") + fun createDataStore(context: Context) = + createDataStore(context, BluetoothAdapter.getDefaultAdapter()) + + fun createDataStore( + context: Context, + bluetoothAdapter: BluetoothAdapter?, + ): BluetoothDataStore = BluetoothStorage(context, bluetoothAdapter) + } +} + +/** Datastore of the bluetooth preference. */ +interface BluetoothDataStore : KeyValueStore { + val bluetoothAdapter: BluetoothAdapter? +} diff --git a/src/com/android/settings/network/AirplaneModePreference.kt b/src/com/android/settings/network/AirplaneModePreference.kt index 758bcfbe487..e3b7f5503d6 100644 --- a/src/com/android/settings/network/AirplaneModePreference.kt +++ b/src/com/android/settings/network/AirplaneModePreference.kt @@ -26,13 +26,13 @@ import android.os.UserManager import android.provider.Settings import android.telephony.PhoneStateListener import android.telephony.TelephonyManager -import android.util.Log import androidx.annotation.DrawableRes import androidx.preference.Preference import com.android.settings.AirplaneModeEnabler import com.android.settings.PreferenceRestrictionMixin import com.android.settings.R import com.android.settings.Utils +import com.android.settings.network.SatelliteRepository.Companion.isSatelliteOn import com.android.settingslib.RestrictedSwitchPreference import com.android.settingslib.datastore.AbstractKeyedDataObservable import com.android.settingslib.datastore.DataChangeReason @@ -45,8 +45,6 @@ import com.android.settingslib.metadata.PreferenceLifecycleProvider import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.SensitivityLevel import com.android.settingslib.metadata.SwitchPreference -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit // LINT.IfChange class AirplaneModePreference : @@ -113,9 +111,7 @@ class AirplaneModePreference : context.getSystemService(TelephonyManager::class.java)?.let { phoneStateListener = object : PhoneStateListener(Looper.getMainLooper()) { - @Deprecated("Deprecated in Java") override fun onRadioPowerStateChanged(state: Int) { - Log.d(TAG, "onRadioPowerStateChanged(), state=$state") notifyChange(KEY, DataChangeReason.UPDATE) } } @@ -163,17 +159,6 @@ class AirplaneModePreference : context.getSystemService(TelephonyManager::class.java), ) - private fun isSatelliteOn(context: Context): Boolean { - try { - return SatelliteRepository(context) - .requestIsSessionStarted(Executors.newSingleThreadExecutor()) - .get(2000, TimeUnit.MILLISECONDS) - } catch (e: Exception) { - Log.e(TAG, "Error to get satellite status : $e") - } - return false - } - private fun showEcmDialog(context: PreferenceLifecycleContext) { val intent = Intent(TelephonyManager.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null) @@ -192,7 +177,6 @@ class AirplaneModePreference : } companion object { - const val TAG = "AirplaneModePreference" const val KEY = Settings.Global.AIRPLANE_MODE_ON const val DEFAULT_VALUE = false const val REQUEST_CODE_EXIT_ECM = 1 diff --git a/src/com/android/settings/network/NetworkDashboardScreen.kt b/src/com/android/settings/network/NetworkDashboardScreen.kt index 15bf590f462..1ed88c080d1 100644 --- a/src/com/android/settings/network/NetworkDashboardScreen.kt +++ b/src/com/android/settings/network/NetworkDashboardScreen.kt @@ -17,9 +17,12 @@ package com.android.settings.network import android.content.Context import com.android.settings.R +import com.android.settings.Settings.NetworkDashboardActivity import com.android.settings.datausage.DataSaverScreen import com.android.settings.flags.Flags +import com.android.settings.utils.makeLaunchIntent import com.android.settingslib.metadata.PreferenceIconProvider +import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.ProvidePreferenceScreen import com.android.settingslib.metadata.preferenceHierarchy import com.android.settingslib.preference.PreferenceScreenCreator @@ -44,6 +47,9 @@ class NetworkDashboardScreen : PreferenceScreenCreator, PreferenceIconProvider { override fun fragmentClass() = NetworkDashboardFragment::class.java + override fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?) = + makeLaunchIntent(context, NetworkDashboardActivity::class.java, metadata?.key) + override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) { +MobileNetworkListScreen.KEY order -15 diff --git a/src/com/android/settings/network/SatelliteRepository.kt b/src/com/android/settings/network/SatelliteRepository.kt index b7c25f4f658..c70484a7e7a 100644 --- a/src/com/android/settings/network/SatelliteRepository.kt +++ b/src/com/android/settings/network/SatelliteRepository.kt @@ -26,6 +26,8 @@ import androidx.concurrent.futures.CallbackToFutureAdapter import com.google.common.util.concurrent.Futures.immediateFuture import com.google.common.util.concurrent.ListenableFuture import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor @@ -203,5 +205,15 @@ class SatelliteRepository( fun setIsSessionStartedForTesting(isEnabled: Boolean) { this.isSessionStarted = isEnabled } + + fun isSatelliteOn(context: Context, timeoutMs: Long = 2000): Boolean = + try { + SatelliteRepository(context) + .requestIsSessionStarted(Executors.newSingleThreadExecutor()) + .get(timeoutMs, TimeUnit.MILLISECONDS) + } catch (e: Exception) { + Log.e(TAG, "Error to get satellite status : $e") + false + } } } diff --git a/src/com/android/settings/widget/MainSwitchBarPreference.kt b/src/com/android/settings/widget/MainSwitchBarPreference.kt index 6ed887790c8..5cf96397184 100644 --- a/src/com/android/settings/widget/MainSwitchBarPreference.kt +++ b/src/com/android/settings/widget/MainSwitchBarPreference.kt @@ -26,7 +26,7 @@ import com.android.settingslib.widget.MainSwitchBar /** Preference abstraction of the [MainSwitchBar] in settings activity. */ class MainSwitchBarPreference(context: Context, private val metadata: MainSwitchBarMetadata) : - TwoStatePreference(context), OnCheckedChangeListener { + TwoStatePreference(context), OnCheckedChangeListener, MainSwitchBar.PreChangeListener { private val mainSwitchBar: MainSwitchBar = (context as SettingsActivity).switchBar @@ -62,9 +62,12 @@ class MainSwitchBarPreference(context: Context, private val metadata: MainSwitch override fun onAttached() { super.onAttached() + mainSwitchBar.setPreChangeListener(this) mainSwitchBar.addOnSwitchChangeListener(this) } + override fun preChange(isCheck: Boolean) = callChangeListener(isCheck) + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { // prevent user from toggling the switch before data store operation is done isEnabled = false @@ -74,6 +77,7 @@ class MainSwitchBarPreference(context: Context, private val metadata: MainSwitch override fun onDetached() { mainSwitchBar.removeOnSwitchChangeListener(this) + mainSwitchBar.setPreChangeListener(null) super.onDetached() } } diff --git a/src/com/android/settings/widget/SettingsMainSwitchBar.java b/src/com/android/settings/widget/SettingsMainSwitchBar.java index e8c6fc9b0b9..6bccbd7205d 100644 --- a/src/com/android/settings/widget/SettingsMainSwitchBar.java +++ b/src/com/android/settings/widget/SettingsMainSwitchBar.java @@ -109,7 +109,7 @@ public class SettingsMainSwitchBar extends MainSwitchBar { return true; } - return mSwitch.performClick(); + return callPreChangeListener() && mSwitch.performClick(); } @Override diff --git a/src/com/android/settings/wifi/WifiSwitchPreference.kt b/src/com/android/settings/wifi/WifiSwitchPreference.kt index 2a18d3f5616..ba6fb02b769 100644 --- a/src/com/android/settings/wifi/WifiSwitchPreference.kt +++ b/src/com/android/settings/wifi/WifiSwitchPreference.kt @@ -23,13 +23,12 @@ import android.content.IntentFilter import android.net.wifi.WifiManager import android.os.UserManager import android.provider.Settings -import android.util.Log import android.widget.Toast import androidx.preference.Preference import androidx.preference.Preference.OnPreferenceChangeListener import com.android.settings.PreferenceRestrictionMixin import com.android.settings.R -import com.android.settings.network.SatelliteRepository +import com.android.settings.network.SatelliteRepository.Companion.isSatelliteOn import com.android.settings.network.SatelliteWarningDialogActivity import com.android.settingslib.RestrictedSwitchPreference import com.android.settingslib.WirelessUtils @@ -42,8 +41,6 @@ import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.SensitivityLevel import com.android.settingslib.metadata.SwitchPreference import com.android.settingslib.preference.SwitchPreferenceBinding -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit // LINT.IfChange class WifiSwitchPreference : @@ -75,7 +72,7 @@ class WifiSwitchPreference : val context = preference.context // Show dialog and do nothing under satellite mode. - if (context.isSatelliteOn()) { + if (isSatelliteOn(context)) { context.startActivity( Intent(context, SatelliteWarningDialogActivity::class.java) .putExtra( @@ -100,7 +97,7 @@ class WifiSwitchPreference : override fun getWritePermit(context: Context, value: Boolean?, myUid: Int, callingUid: Int) = when { - (value == true && !context.isRadioAllowed()) || context.isSatelliteOn() -> + (value == true && !context.isRadioAllowed()) || isSatelliteOn(context) -> ReadWritePermit.DISALLOW else -> ReadWritePermit.ALLOW } @@ -155,22 +152,11 @@ class WifiSwitchPreference : } companion object { - const val TAG = "WifiSwitchPreference" const val KEY = "main_toggle_wifi" private fun Context.isRadioAllowed() = WirelessUtils.isRadioAllowed(this, Settings.Global.RADIO_WIFI) - private fun Context.isSatelliteOn() = - try { - SatelliteRepository(this) - .requestIsSessionStarted(Executors.newSingleThreadExecutor()) - .get(2000, TimeUnit.MILLISECONDS) - } catch (e: Exception) { - Log.e(TAG, "Error to get satellite status : $e") - false - } - private val Intent.wifiState get() = getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN) } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/BluetoothMainSwitchPreferenceTest.kt b/tests/robotests/src/com/android/settings/connecteddevice/BluetoothPreferenceTest.kt similarity index 75% rename from tests/robotests/src/com/android/settings/connecteddevice/BluetoothMainSwitchPreferenceTest.kt rename to tests/robotests/src/com/android/settings/connecteddevice/BluetoothPreferenceTest.kt index 15db130795d..8b739dbc776 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/BluetoothMainSwitchPreferenceTest.kt +++ b/tests/robotests/src/com/android/settings/connecteddevice/BluetoothPreferenceTest.kt @@ -31,45 +31,42 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) -class BluetoothMainSwitchPreferenceTest { +class BluetoothPreferenceTest { @get:Rule val setFlagsRule = SetFlagsRule() private val context: Context = ApplicationProvider.getApplicationContext() private lateinit var bluetoothAdapter: BluetoothAdapter - private lateinit var bluetoothMainSwitchPreference: BluetoothMainSwitchPreference + private lateinit var bluetoothPreference: BluetoothPreference @Before fun setUp() { bluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter()) whenever(bluetoothAdapter.state).thenReturn(BluetoothAdapter.STATE_ON) - bluetoothMainSwitchPreference = BluetoothMainSwitchPreference(bluetoothAdapter) + bluetoothPreference = + BluetoothPreference(BluetoothPreference.createDataStore(context, bluetoothAdapter)) } @Test fun isEnabled_bluetoothOn_returnTrue() { - assertThat(bluetoothMainSwitchPreference.isEnabled(context)).isTrue() + assertThat(bluetoothPreference.isEnabled(context)).isTrue() } @Test fun isEnabled_bluetoothTurningOn_returnFalse() { whenever(bluetoothAdapter.state).thenReturn(BluetoothAdapter.STATE_TURNING_ON) - assertThat(bluetoothMainSwitchPreference.isEnabled(context)).isFalse() + assertThat(bluetoothPreference.isEnabled(context)).isFalse() } @Test fun storageSetOff_turnOff() { - bluetoothMainSwitchPreference - .storage(context) - .setBoolean(bluetoothMainSwitchPreference.key, false) + bluetoothPreference.storage(context).setBoolean(bluetoothPreference.key, false) verify(bluetoothAdapter).disable() } @Test fun storageSetOn_turnOn() { - bluetoothMainSwitchPreference - .storage(context) - .setBoolean(bluetoothMainSwitchPreference.key, true) + bluetoothPreference.storage(context).setBoolean(bluetoothPreference.key, true) verify(bluetoothAdapter).enable() }