diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java index 7ee61ee249d..f2bc6fcfde6 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java @@ -128,7 +128,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere if (device != null && mSelectedList.contains(device)) { setResult(RESULT_OK); finish(); - } else if (mDevicePreferenceMap.containsKey(cachedDevice)) { + } else { onDeviceDeleted(cachedDevice); } } @@ -175,8 +175,6 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere public void updateContent(int bluetoothState) { switch (bluetoothState) { case BluetoothAdapter.STATE_ON: - mDevicePreferenceMap.clear(); - clearPreferenceGroupCache(); mBluetoothAdapter.enable(); enableScanning(); break; @@ -187,14 +185,6 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere } } - /** - * Clears all cached preferences in {@code preferenceGroup}. - */ - private void clearPreferenceGroupCache() { - cacheRemoveAllPrefs(mAvailableDevicesCategory); - removeCachedPrefs(mAvailableDevicesCategory); - } - @VisibleForTesting void showBluetoothTurnedOnToast() { Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast, diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java index 5256f3d6596..039080b26ba 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2023 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. @@ -35,6 +35,8 @@ import android.view.View; import android.widget.ImageView; import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; @@ -52,6 +54,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.HashSet; import java.util.Set; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; /** * BluetoothDevicePreference is the preference type used to display each remote @@ -79,7 +82,9 @@ public final class BluetoothDevicePreference extends GearPreference { @VisibleForTesting BluetoothAdapter mBluetoothAdapter; private final boolean mShowDevicesWithoutNames; - private final long mCurrentTime; + @NonNull + private static final AtomicInteger sNextId = new AtomicInteger(); + private final int mId; private final int mType; private AlertDialog mDisconnectDialog; @@ -127,8 +132,9 @@ public final class BluetoothDevicePreference extends GearPreference { mCachedDevice = cachedDevice; mCallback = new BluetoothDevicePreferenceCallback(); - mCurrentTime = System.currentTimeMillis(); + mId = sNextId.getAndIncrement(); mType = type; + setVisible(false); onPreferenceAttributesChanged(); } @@ -229,35 +235,41 @@ public final class BluetoothDevicePreference extends GearPreference { @SuppressWarnings("FutureReturnValueIgnored") void onPreferenceAttributesChanged() { - Pair pair = mCachedDevice.getDrawableWithDescription(); - setIcon(pair.first); - contentDescription = pair.second; - - /* - * The preference framework takes care of making sure the value has - * changed before proceeding. It will also call notifyChanged() if - * any preference info has changed from the previous value. - */ - setTitle(mCachedDevice.getName()); try { ThreadUtils.postOnBackgroundThread(() -> { + @Nullable String name = mCachedDevice.getName(); // Null check is done at the framework - ThreadUtils.postOnMainThread(() -> setSummary(getConnectionSummary())); + @Nullable String connectionSummary = getConnectionSummary(); + @NonNull Pair pair = mCachedDevice.getDrawableWithDescription(); + boolean isBusy = mCachedDevice.isBusy(); + // Device is only visible in the UI if it has a valid name besides MAC address or + // when user allows showing devices without user-friendly name in developer settings + boolean isVisible = + mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName(); + + ThreadUtils.postOnMainThread(() -> { + /* + * The preference framework takes care of making sure the value has + * changed before proceeding. It will also call notifyChanged() if + * any preference info has changed from the previous value. + */ + setTitle(name); + setSummary(connectionSummary); + setIcon(pair.first); + contentDescription = pair.second; + // Used to gray out the item + setEnabled(!isBusy); + setVisible(isVisible); + + // This could affect ordering, so notify that + if (mNeedNotifyHierarchyChanged) { + notifyHierarchyChanged(); + } + }); }); } catch (RejectedExecutionException e) { Log.w(TAG, "Handler thread unavailable, skipping getConnectionSummary!"); } - // Used to gray out the item - setEnabled(!mCachedDevice.isBusy()); - - // Device is only visible in the UI if it has a valid name besides MAC address or when user - // allows showing devices without user-friendly name in developer settings - setVisible(mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName()); - - // This could affect ordering, so notify that - if (mNeedNotifyHierarchyChanged) { - notifyHierarchyChanged(); - } } @Override @@ -311,7 +323,7 @@ public final class BluetoothDevicePreference extends GearPreference { return mCachedDevice .compareTo(((BluetoothDevicePreference) another).mCachedDevice); case SortType.TYPE_FIFO: - return mCurrentTime > ((BluetoothDevicePreference) another).mCurrentTime ? 1 : -1; + return mId > ((BluetoothDevicePreference) another).mId ? 1 : -1; default: return super.compareTo(another); } diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java deleted file mode 100644 index a4a98917974..00000000000 --- a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright (C) 2011 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 android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.le.BluetoothLeScanner; -import android.bluetooth.le.ScanCallback; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanResult; -import android.bluetooth.le.ScanSettings; -import android.os.Bundle; -import android.os.SystemProperties; -import android.text.BidiFormatter; -import android.util.Log; - -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceGroup; - -import com.android.settings.R; -import com.android.settings.dashboard.RestrictedDashboardFragment; -import com.android.settingslib.bluetooth.BluetoothCallback; -import com.android.settingslib.bluetooth.BluetoothDeviceFilter; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.LocalBluetoothManager; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; - -/** - * Parent class for settings fragments that contain a list of Bluetooth - * devices. - * - * @see DevicePickerFragment - */ -// TODO: Refactor this fragment -public abstract class DeviceListPreferenceFragment extends - RestrictedDashboardFragment implements BluetoothCallback { - - private static final String TAG = "DeviceListPreferenceFragment"; - - private static final String KEY_BT_SCAN = "bt_scan"; - - // Copied from BluetoothDeviceNoNamePreferenceController.java - private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY = - "persist.bluetooth.showdeviceswithoutnames"; - - private BluetoothDeviceFilter.Filter mFilter; - private List mLeScanFilters; - private ScanCallback mScanCallback; - - @VisibleForTesting - protected boolean mScanEnabled; - - protected BluetoothDevice mSelectedDevice; - - protected BluetoothAdapter mBluetoothAdapter; - protected LocalBluetoothManager mLocalManager; - protected CachedBluetoothDeviceManager mCachedDeviceManager; - - @VisibleForTesting - protected PreferenceGroup mDeviceListGroup; - - protected final HashMap mDevicePreferenceMap = - new HashMap<>(); - protected final List mSelectedList = new ArrayList<>(); - - protected boolean mShowDevicesWithoutNames; - - public DeviceListPreferenceFragment(String restrictedKey) { - super(restrictedKey); - mFilter = BluetoothDeviceFilter.ALL_FILTER; - } - - protected final void setFilter(BluetoothDeviceFilter.Filter filter) { - mFilter = filter; - } - - protected final void setFilter(int filterType) { - mFilter = BluetoothDeviceFilter.getFilter(filterType); - } - - /** - * Sets the bluetooth device scanning filter with {@link ScanFilter}s. It will change to start - * {@link BluetoothLeScanner} which will scan BLE device only. - * - * @param leScanFilters list of settings to filter scan result - */ - protected void setFilter(List leScanFilters) { - mFilter = null; - mLeScanFilters = leScanFilters; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mLocalManager = Utils.getLocalBtManager(getActivity()); - if (mLocalManager == null) { - Log.e(TAG, "Bluetooth is not supported on this device"); - return; - } - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - mCachedDeviceManager = mLocalManager.getCachedDeviceManager(); - mShowDevicesWithoutNames = SystemProperties.getBoolean( - BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false); - - initPreferencesFromPreferenceScreen(); - - mDeviceListGroup = (PreferenceCategory) findPreference(getDeviceListKey()); - } - - /** find and update preference that already existed in preference screen */ - protected abstract void initPreferencesFromPreferenceScreen(); - - @Override - public void onStart() { - super.onStart(); - if (mLocalManager == null || isUiRestricted()) return; - - mLocalManager.setForegroundActivity(getActivity()); - mLocalManager.getEventManager().registerCallback(this); - } - - @Override - public void onStop() { - super.onStop(); - if (mLocalManager == null || isUiRestricted()) { - return; - } - - removeAllDevices(); - mLocalManager.setForegroundActivity(null); - mLocalManager.getEventManager().unregisterCallback(this); - } - - void removeAllDevices() { - mDevicePreferenceMap.clear(); - mDeviceListGroup.removeAll(); - } - - void addCachedDevices() { - Collection cachedDevices = - mCachedDeviceManager.getCachedDevicesCopy(); - for (CachedBluetoothDevice cachedDevice : cachedDevices) { - onDeviceAdded(cachedDevice); - } - } - - @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (KEY_BT_SCAN.equals(preference.getKey())) { - startScanning(); - return true; - } - - if (preference instanceof BluetoothDevicePreference) { - BluetoothDevicePreference btPreference = (BluetoothDevicePreference) preference; - CachedBluetoothDevice device = btPreference.getCachedDevice(); - mSelectedDevice = device.getDevice(); - mSelectedList.add(mSelectedDevice); - onDevicePreferenceClick(btPreference); - return true; - } - - return super.onPreferenceTreeClick(preference); - } - - protected void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { - btPreference.onClicked(); - } - - @Override - public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { - if (mDevicePreferenceMap.get(cachedDevice) != null) { - return; - } - - // Prevent updates while the list shows one of the state messages - if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) { - return; - } - - if (mFilter != null && mFilter.matches(cachedDevice.getDevice())) { - createDevicePreference(cachedDevice); - } - } - - void createDevicePreference(CachedBluetoothDevice cachedDevice) { - if (mDeviceListGroup == null) { - Log.w(TAG, "Trying to create a device preference before the list group/category " - + "exists!"); - return; - } - - String key = cachedDevice.getDevice().getAddress(); - BluetoothDevicePreference preference = (BluetoothDevicePreference) getCachedPreference(key); - - if (preference == null) { - preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice, - mShowDevicesWithoutNames, BluetoothDevicePreference.SortType.TYPE_FIFO); - preference.setKey(key); - //Set hideSecondTarget is true if it's bonded device. - preference.hideSecondTarget(true); - mDeviceListGroup.addPreference(preference); - } - - initDevicePreference(preference); - mDevicePreferenceMap.put(cachedDevice, preference); - } - - protected void initDevicePreference(BluetoothDevicePreference preference) { - // Does nothing by default - } - - @VisibleForTesting - void updateFooterPreference(Preference myDevicePreference) { - final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - - myDevicePreference.setTitle(getString( - R.string.bluetooth_footer_mac_message, - bidiFormatter.unicodeWrap(mBluetoothAdapter.getAddress()))); - } - - @Override - public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { - BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice); - if (preference != null) { - mDeviceListGroup.removePreference(preference); - } - } - - @VisibleForTesting - protected void enableScanning() { - // BluetoothAdapter already handles repeated scan requests - if (!mScanEnabled) { - startScanning(); - mScanEnabled = true; - } - } - - @VisibleForTesting - protected void disableScanning() { - if (mScanEnabled) { - stopScanning(); - mScanEnabled = false; - } - } - - @Override - public void onScanningStateChanged(boolean started) { - if (!started && mScanEnabled) { - startScanning(); - } - } - - /** - * Return the key of the {@link PreferenceGroup} that contains the bluetooth devices - */ - public abstract String getDeviceListKey(); - - public boolean shouldShowDevicesWithoutNames() { - return mShowDevicesWithoutNames; - } - - @VisibleForTesting - void startScanning() { - if (mFilter != null) { - startClassicScanning(); - } else if (mLeScanFilters != null) { - startLeScanning(); - } - - } - - @VisibleForTesting - void stopScanning() { - if (mFilter != null) { - stopClassicScanning(); - } else if (mLeScanFilters != null) { - stopLeScanning(); - } - } - - private void startClassicScanning() { - if (!mBluetoothAdapter.isDiscovering()) { - mBluetoothAdapter.startDiscovery(); - } - } - - private void stopClassicScanning() { - if (mBluetoothAdapter.isDiscovering()) { - mBluetoothAdapter.cancelDiscovery(); - } - } - - private void startLeScanning() { - final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); - final ScanSettings settings = new ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .build(); - mScanCallback = new ScanCallback() { - @Override - public void onScanResult(int callbackType, ScanResult result) { - final BluetoothDevice device = result.getDevice(); - CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device); - if (cachedDevice == null) { - cachedDevice = mCachedDeviceManager.addDevice(device); - } - // Only add device preference when it's not found in the map and there's no other - // state message showing in the list - if (mDevicePreferenceMap.get(cachedDevice) == null - && mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { - createDevicePreference(cachedDevice); - } - } - - @Override - public void onScanFailed(int errorCode) { - Log.w(TAG, "BLE Scan failed with error code " + errorCode); - } - }; - scanner.startScan(mLeScanFilters, settings, mScanCallback); - } - - private void stopLeScanning() { - final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); - if (scanner != null) { - scanner.stopScan(mScanCallback); - } - } -} diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt new file mode 100644 index 00000000000..9c86e4398f6 --- /dev/null +++ b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2023 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 android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.le.BluetoothLeScanner +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.os.Bundle +import android.os.SystemProperties +import android.text.BidiFormatter +import android.util.Log +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceGroup +import com.android.settings.R +import com.android.settings.dashboard.RestrictedDashboardFragment +import com.android.settingslib.bluetooth.BluetoothCallback +import com.android.settingslib.bluetooth.BluetoothDeviceFilter +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager +import com.android.settingslib.bluetooth.LocalBluetoothManager +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Parent class for settings fragments that contain a list of Bluetooth devices. + * + * @see DevicePickerFragment + * + * TODO: Refactor this fragment + */ +abstract class DeviceListPreferenceFragment(restrictedKey: String?) : + RestrictedDashboardFragment(restrictedKey), BluetoothCallback { + + private var filter: BluetoothDeviceFilter.Filter? = BluetoothDeviceFilter.ALL_FILTER + private var leScanFilters: List? = null + + @JvmField + @VisibleForTesting + var mScanEnabled = false + + @JvmField + var mSelectedDevice: BluetoothDevice? = null + + @JvmField + var mBluetoothAdapter: BluetoothAdapter? = null + + @JvmField + var mLocalManager: LocalBluetoothManager? = null + + @JvmField + var mCachedDeviceManager: CachedBluetoothDeviceManager? = null + + @JvmField + @VisibleForTesting + var mDeviceListGroup: PreferenceGroup? = null + + @VisibleForTesting + val devicePreferenceMap = + ConcurrentHashMap() + + @JvmField + val mSelectedList: MutableList = ArrayList() + + private var showDevicesWithoutNames = false + + protected fun setFilter(filter: BluetoothDeviceFilter.Filter?) { + this.filter = filter + } + + protected fun setFilter(filterType: Int) { + filter = BluetoothDeviceFilter.getFilter(filterType) + } + + /** + * Sets the bluetooth device scanning filter with [ScanFilter]s. It will change to start + * [BluetoothLeScanner] which will scan BLE device only. + * + * @param leScanFilters list of settings to filter scan result + */ + fun setFilter(leScanFilters: List?) { + filter = null + this.leScanFilters = leScanFilters + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mLocalManager = Utils.getLocalBtManager(activity) + if (mLocalManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device") + return + } + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + mCachedDeviceManager = mLocalManager!!.cachedDeviceManager + showDevicesWithoutNames = SystemProperties.getBoolean( + BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false + ) + initPreferencesFromPreferenceScreen() + mDeviceListGroup = findPreference(deviceListKey) as PreferenceCategory + } + + /** find and update preference that already existed in preference screen */ + protected abstract fun initPreferencesFromPreferenceScreen() + + private var lifecycleScope: LifecycleCoroutineScope? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + lifecycleScope = viewLifecycleOwner.lifecycleScope + } + + override fun onStart() { + super.onStart() + if (mLocalManager == null || isUiRestricted) return + mLocalManager!!.foregroundActivity = activity + mLocalManager!!.eventManager.registerCallback(this) + } + + override fun onStop() { + super.onStop() + if (mLocalManager == null || isUiRestricted) { + return + } + removeAllDevices() + mLocalManager!!.foregroundActivity = null + mLocalManager!!.eventManager.unregisterCallback(this) + } + + fun removeAllDevices() { + devicePreferenceMap.clear() + mDeviceListGroup!!.removeAll() + } + + fun addCachedDevices() { + lifecycleScope?.launch { + withContext(Dispatchers.Default) { + val cachedDevices = mCachedDeviceManager!!.cachedDevicesCopy + for (cachedDevice in cachedDevices) { + onDeviceAdded(cachedDevice) + } + } + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + if (KEY_BT_SCAN == preference.key) { + startScanning() + return true + } + if (preference is BluetoothDevicePreference) { + val device = preference.cachedDevice.device + mSelectedDevice = device + mSelectedList.add(device) + onDevicePreferenceClick(preference) + return true + } + return super.onPreferenceTreeClick(preference) + } + + protected open fun onDevicePreferenceClick(btPreference: BluetoothDevicePreference) { + btPreference.onClicked() + } + + override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) { + lifecycleScope?.launch { + addDevice(cachedDevice) + } + } + + private suspend fun addDevice(cachedDevice: CachedBluetoothDevice) = + withContext(Dispatchers.Default) { + // Prevent updates while the list shows one of the state messages + if (mBluetoothAdapter!!.state == BluetoothAdapter.STATE_ON && + filter?.matches(cachedDevice.device) == true + ) { + createDevicePreference(cachedDevice) + } + } + + private suspend fun createDevicePreference(cachedDevice: CachedBluetoothDevice) { + if (mDeviceListGroup == null) { + Log.w( + TAG, + "Trying to create a device preference before the list group/category exists!", + ) + return + } + // Only add device preference when it's not found in the map and there's no other state + // message showing in the list + val preference = devicePreferenceMap.computeIfAbsent(cachedDevice) { + BluetoothDevicePreference( + prefContext, + cachedDevice, + showDevicesWithoutNames, + BluetoothDevicePreference.SortType.TYPE_FIFO, + ).apply { + key = cachedDevice.device.address + //Set hideSecondTarget is true if it's bonded device. + hideSecondTarget(true) + } + } + withContext(Dispatchers.Main) { + mDeviceListGroup!!.addPreference(preference) + initDevicePreference(preference) + } + } + + protected open fun initDevicePreference(preference: BluetoothDevicePreference?) { + // Does nothing by default + } + + @VisibleForTesting + fun updateFooterPreference(myDevicePreference: Preference) { + val bidiFormatter = BidiFormatter.getInstance() + myDevicePreference.title = getString( + R.string.bluetooth_footer_mac_message, + bidiFormatter.unicodeWrap(mBluetoothAdapter!!.address) + ) + } + + override fun onDeviceDeleted(cachedDevice: CachedBluetoothDevice) { + devicePreferenceMap.remove(cachedDevice)?.let { + mDeviceListGroup!!.removePreference(it) + } + } + + @VisibleForTesting + open fun enableScanning() { + // BluetoothAdapter already handles repeated scan requests + if (!mScanEnabled) { + startScanning() + mScanEnabled = true + } + } + + @VisibleForTesting + fun disableScanning() { + if (mScanEnabled) { + stopScanning() + mScanEnabled = false + } + } + + override fun onScanningStateChanged(started: Boolean) { + if (!started && mScanEnabled) { + startScanning() + } + } + + /** + * Return the key of the [PreferenceGroup] that contains the bluetooth devices + */ + abstract val deviceListKey: String + + @VisibleForTesting + open fun startScanning() { + if (filter != null) { + startClassicScanning() + } else if (leScanFilters != null) { + startLeScanning() + } + } + + @VisibleForTesting + open fun stopScanning() { + if (filter != null) { + stopClassicScanning() + } else if (leScanFilters != null) { + stopLeScanning() + } + } + + private fun startClassicScanning() { + if (!mBluetoothAdapter!!.isDiscovering) { + mBluetoothAdapter!!.startDiscovery() + } + } + + private fun stopClassicScanning() { + if (mBluetoothAdapter!!.isDiscovering) { + mBluetoothAdapter!!.cancelDiscovery() + } + } + + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + lifecycleScope?.launch { + withContext(Dispatchers.Default) { + if (mBluetoothAdapter!!.state == BluetoothAdapter.STATE_ON) { + val device = result.device + val cachedDevice = mCachedDeviceManager!!.findDevice(device) + ?: mCachedDeviceManager!!.addDevice(device) + createDevicePreference(cachedDevice) + } + } + } + } + + override fun onScanFailed(errorCode: Int) { + Log.w(TAG, "BLE Scan failed with error code $errorCode") + } + } + + private fun startLeScanning() { + val scanner = mBluetoothAdapter!!.bluetoothLeScanner + val settings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build() + scanner.startScan(leScanFilters, settings, scanCallback) + } + + private fun stopLeScanning() { + val scanner = mBluetoothAdapter!!.bluetoothLeScanner + scanner?.stopScan(scanCallback) + } + + companion object { + private const val TAG = "DeviceListPreferenceFragment" + private const val KEY_BT_SCAN = "bt_scan" + + // Copied from BluetoothDeviceNoNamePreferenceController.java + private const val BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY = + "persist.bluetooth.showdeviceswithoutnames" + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java index 184f5212e77..7c598e00e42 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java @@ -202,7 +202,7 @@ public class BluetoothDevicePairingDetailBaseTest { new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, true, BluetoothDevicePreference.SortType.TYPE_FIFO); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS); - mFragment.mDevicePreferenceMap.put(mCachedBluetoothDevice, preference); + mFragment.getDevicePreferenceMap().put(mCachedBluetoothDevice, preference); when(mCachedBluetoothDevice.isConnected()).thenReturn(true); when(mCachedBluetoothDevice.getDevice()).thenReturn(device); @@ -210,7 +210,7 @@ public class BluetoothDevicePairingDetailBaseTest { mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); - assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0); + assertThat(mFragment.getDevicePreferenceMap().size()).isEqualTo(0); } @Test @@ -221,7 +221,7 @@ public class BluetoothDevicePairingDetailBaseTest { true, BluetoothDevicePreference.SortType.TYPE_FIFO); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS); final BluetoothDevice device2 = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); - mFragment.mDevicePreferenceMap.put(mCachedBluetoothDevice, preference); + mFragment.getDevicePreferenceMap().put(mCachedBluetoothDevice, preference); when(mCachedBluetoothDevice.isConnected()).thenReturn(true); when(mCachedBluetoothDevice.getDevice()).thenReturn(device); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java index 5fbfee8b50d..ce67051a7c7 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java @@ -27,7 +27,12 @@ import static org.mockito.Mockito.verify; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.test.core.app.ApplicationProvider; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; @@ -53,6 +58,20 @@ public class BluetoothPairingDetailTest { private final Context mContext = ApplicationProvider.getApplicationContext(); + private final Lifecycle mFakeLifecycle = new Lifecycle() { + @Override + public void addObserver(@NonNull LifecycleObserver observer) {} + + @Override + public void removeObserver(@NonNull LifecycleObserver observer) {} + + @NonNull + @Override + public State getCurrentState() { + return State.CREATED; + } + }; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private LocalBluetoothManager mLocalManager; @Mock(answer = Answers.RETURNS_DEEP_STUBS) @@ -74,6 +93,8 @@ public class BluetoothPairingDetailTest { .findPreference(BluetoothPairingDetail.KEY_AVAIL_DEVICES); doReturn(mFooterPreference).when(mFragment) .findPreference(BluetoothPairingDetail.KEY_FOOTER_PREF); + doReturn(new View(mContext)).when(mFragment).getView(); + doReturn((LifecycleOwner) () -> mFakeLifecycle).when(mFragment).getViewLifecycleOwner(); doReturn(Collections.emptyList()).when(mDeviceManager).getCachedDevicesCopy(); mFragment.mBluetoothAdapter = mBluetoothAdapter; @@ -82,7 +103,7 @@ public class BluetoothPairingDetailTest { mFragment.mDeviceListGroup = mAvailableDevicesCategory; mFragment.onViewCreated(mFragment.getView(), Bundle.EMPTY); } -// + @Test public void initPreferencesFromPreferenceScreen_findPreferences() { mFragment.initPreferencesFromPreferenceScreen();