diff --git a/src/com/android/settings/network/tether/BluetoothTetherSwitchPreference.kt b/src/com/android/settings/network/tether/BluetoothTetherSwitchPreference.kt new file mode 100644 index 00000000000..a22df124262 --- /dev/null +++ b/src/com/android/settings/network/tether/BluetoothTetherSwitchPreference.kt @@ -0,0 +1,226 @@ +/* + * 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.network.tether + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothPan +import android.bluetooth.BluetoothProfile +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.TetheringManager +import android.os.Handler +import android.os.Looper +import com.android.settings.R +import com.android.settings.datausage.DataSaverBackend +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.KeyedDataObservable +import com.android.settingslib.metadata.PreferenceAvailabilityProvider +import com.android.settingslib.metadata.PreferenceLifecycleContext +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.atomic.AtomicReference + +// LINT.IfChange +@Suppress("DEPRECATION") +class BluetoothTetherSwitchPreference : + SwitchPreference(KEY, R.string.bluetooth_tether_checkbox_text), + PreferenceAvailabilityProvider, + PreferenceLifecycleProvider { + + private var tetherChangeReceiver: BroadcastReceiver? = null + + override val summary: Int + get() = R.string.bluetooth_tethering_subtext + + override val keywords: Int + get() = R.string.keywords_hotspot_tethering + + override fun storage(context: Context): KeyValueStore = BluetoothTetherStore(context) + + override fun isAvailable(context: Context): Boolean { + BluetoothAdapter.getDefaultAdapter() ?: return false + val tetheringManager = context.getSystemService(TetheringManager::class.java) + val bluetoothRegexs = tetheringManager?.tetherableBluetoothRegexs + return bluetoothRegexs?.isNotEmpty() == true + } + + override fun isEnabled(context: Context): Boolean { + val adapter = BluetoothAdapter.getDefaultAdapter() ?: return false + val btState = adapter.state + /* TODO: when bluetooth is off, btstate will be `state_turning_on` -> `state_off` -> + `state_turning_on` -> `state_on`, causing preference enable status incorrect. */ + when (btState) { + BluetoothAdapter.STATE_TURNING_OFF, + BluetoothAdapter.STATE_TURNING_ON -> return false + else -> {} + } + val dataSaverBackend = DataSaverBackend(context) + return !dataSaverBackend.isDataSaverEnabled + } + + 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 val sensitivityLevel: Int + get() = SensitivityLevel.LOW_SENSITIVITY + + override fun onCreate(context: PreferenceLifecycleContext) { + val receiver = + object : BroadcastReceiver() { + override fun onReceive(content: Context, intent: Intent) { + when (intent.action) { + TetheringManager.ACTION_TETHER_STATE_CHANGED, + Intent.ACTION_MEDIA_SHARED, + Intent.ACTION_MEDIA_UNSHARED, + BluetoothAdapter.ACTION_STATE_CHANGED, + BluetoothPan.ACTION_TETHERING_STATE_CHANGED -> + context.notifyPreferenceChange(KEY) + } + } + } + tetherChangeReceiver = receiver + var filter = IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED) + val intent = context.registerReceiver(receiver, filter) + + filter = IntentFilter() + filter.addAction(Intent.ACTION_MEDIA_SHARED) + filter.addAction(Intent.ACTION_MEDIA_UNSHARED) + filter.addDataScheme("file") + context.registerReceiver(receiver, filter) + + filter = IntentFilter() + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED) + filter.addAction(BluetoothPan.ACTION_TETHERING_STATE_CHANGED) + context.registerReceiver(receiver, filter) + } + + override fun onDestroy(context: PreferenceLifecycleContext) { + tetherChangeReceiver?.let { + context.unregisterReceiver(it) + tetherChangeReceiver = null + } + } + + @Suppress("UNCHECKED_CAST") + private class BluetoothTetherStore(private val context: Context) : + KeyedDataObservable(), KeyValueStore { + + val bluetoothPan = AtomicReference() + + override fun contains(key: String) = key == KEY + + override fun getValue(key: String, valueType: Class): T? { + // TODO: support async operation in background thread + val adapter = BluetoothAdapter.getDefaultAdapter() ?: return false as T + if (bluetoothPan.get() == null) { + val profileServiceListener: BluetoothProfile.ServiceListener = + object : BluetoothProfile.ServiceListener { + override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { + if (bluetoothPan.get() == null) { + bluetoothPan.set(proxy as BluetoothPan) + notifyChange(KEY, 0) + } + } + + override fun onServiceDisconnected(profile: Int) { + /* Do nothing */ + } + } + // TODO: adapter.closeProfileProxy(bluetoothPan.get()) + adapter.getProfileProxy( + context.applicationContext, + profileServiceListener, + BluetoothProfile.PAN, + ) + } + + val btState = adapter.state + val pan = bluetoothPan.get() + return ((btState == BluetoothAdapter.STATE_ON || + btState == BluetoothAdapter.STATE_TURNING_OFF) && pan != null && pan.isTetheringOn) + as T? + } + + override fun setValue(key: String, valueType: Class, value: T?) { + if (value == null) return + val connectivityManager = + context.getSystemService(ConnectivityManager::class.java) ?: return + if (value as Boolean) { + val handler by lazy { Handler(Looper.getMainLooper()) } + val startTetheringCallback = OnStartTetheringCallback() + fun startTethering() { + connectivityManager.startTethering( + ConnectivityManager.TETHERING_BLUETOOTH, + true, + startTetheringCallback, + handler, + ) + } + + val adapter = BluetoothAdapter.getDefaultAdapter() + if (adapter.state == BluetoothAdapter.STATE_OFF) { + adapter.enable() + val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) + val tetherChangeReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if ( + intent.getIntExtra( + BluetoothAdapter.EXTRA_STATE, + BluetoothAdapter.ERROR, + ) == BluetoothAdapter.STATE_ON + ) { + startTethering() + context.unregisterReceiver(this) + } + } + } + val intent = context.registerReceiver(tetherChangeReceiver, filter) + if (intent != null) tetherChangeReceiver.onReceive(context, intent) + } else { + startTethering() + } + } else { + connectivityManager.stopTethering(ConnectivityManager.TETHERING_BLUETOOTH) + } + } + + private inner class OnStartTetheringCallback : + ConnectivityManager.OnStartTetheringCallback() { + override fun onTetheringStarted() { + notifyChange(KEY, 0) + } + + override fun onTetheringFailed() { + notifyChange(KEY, 0) + } + } + } + + companion object { + const val KEY = "enable_bluetooth_tethering" + } +} +// LINT.ThenChange(TetherSettings.java) diff --git a/src/com/android/settings/network/tether/TetherSettings.java b/src/com/android/settings/network/tether/TetherSettings.java index 1db1802d2c0..af5d5df8222 100644 --- a/src/com/android/settings/network/tether/TetherSettings.java +++ b/src/com/android/settings/network/tether/TetherSettings.java @@ -72,6 +72,7 @@ import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +// LINT.IfChange /** * Displays preferences for Tethering. */ @@ -154,6 +155,7 @@ public class TetherSettings extends RestrictedDashboardFragment return R.xml.tether_prefs; } + @SuppressWarnings("NullAway") @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -205,16 +207,19 @@ public class TetherSettings extends RestrictedDashboardFragment mWifiTetherPreferenceController.displayPreference(getPreferenceScreen()); - if (!bluetoothAvailable) { - getPreferenceScreen().removePreference(mBluetoothTether); - } else { - BluetoothPan pan = mBluetoothPan.get(); - if (pan != null && pan.isTetheringOn()) { - mBluetoothTether.setChecked(true); + if (!isCatalystEnabled()) { + if (!bluetoothAvailable) { + mBluetoothTether.setVisible(false); } else { - mBluetoothTether.setChecked(false); + BluetoothPan pan = mBluetoothPan.get(); + if (pan != null && pan.isTetheringOn()) { + mBluetoothTether.setChecked(true); + } else { + mBluetoothTether.setChecked(false); + } } } + if (!ethernetAvailable) getPreferenceScreen().removePreference(mEthernetTether); // Set initial state based on Data Saver mode. onDataSaverChanged(mDataSaverBackend.isDataSaverEnabled()); @@ -263,7 +268,9 @@ public class TetherSettings extends RestrictedDashboardFragment mDataSaverEnabled = isDataSaving; mWifiTetherPreferenceController.setDataSaverEnabled(mDataSaverEnabled); mUsbTether.setEnabled(!mDataSaverEnabled); - mBluetoothTether.setEnabled(!mDataSaverEnabled); + if (!isCatalystEnabled()) { + mBluetoothTether.setEnabled(!mDataSaverEnabled); + } mEthernetTether.setEnabled(!mDataSaverEnabled); mDataSaverFooter.setVisible(mDataSaverEnabled); } @@ -513,6 +520,8 @@ public class TetherSettings extends RestrictedDashboardFragment } private void updateBluetoothState() { + if (isCatalystEnabled()) return; + final int btState = getBluetoothState(); if (DEBUG) { Log.d(TAG, "updateBluetoothState() btState : " + btState); @@ -568,7 +577,7 @@ public class TetherSettings extends RestrictedDashboardFragment } private void startTethering(int choice) { - if (choice == TETHERING_BLUETOOTH) { + if (choice == TETHERING_BLUETOOTH && !isCatalystEnabled()) { // Turn on Bluetooth first. BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter.getState() == BluetoothAdapter.STATE_OFF) { @@ -590,7 +599,7 @@ public class TetherSettings extends RestrictedDashboardFragment } else { mCm.stopTethering(TETHERING_USB); } - } else if (preference == mBluetoothTether) { + } else if (preference == mBluetoothTether && !isCatalystEnabled()) { if (mBluetoothTether.isChecked()) { startTethering(TETHERING_BLUETOOTH); } else { @@ -738,3 +747,4 @@ public class TetherSettings extends RestrictedDashboardFragment return TetherScreen.KEY; } } +// LINT.ThenChange(BluetoothTetherSwitchPreference.kt)