Merge "Fix bluetooth settings pairing page stuck" into udc-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
a1fd330fb5
@@ -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,
|
||||
|
@@ -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<Drawable, String> pair = mCachedDevice.getDrawableWithDescription();
|
||||
setIcon(pair.first);
|
||||
contentDescription = pair.second;
|
||||
try {
|
||||
ThreadUtils.postOnBackgroundThread(() -> {
|
||||
@Nullable String name = mCachedDevice.getName();
|
||||
// Null check is done at the framework
|
||||
@Nullable String connectionSummary = getConnectionSummary();
|
||||
@NonNull Pair<Drawable, String> 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(mCachedDevice.getName());
|
||||
try {
|
||||
ThreadUtils.postOnBackgroundThread(() -> {
|
||||
// Null check is done at the framework
|
||||
ThreadUtils.postOnMainThread(() -> setSummary(getConnectionSummary()));
|
||||
});
|
||||
} catch (RejectedExecutionException e) {
|
||||
Log.w(TAG, "Handler thread unavailable, skipping getConnectionSummary!");
|
||||
}
|
||||
setTitle(name);
|
||||
setSummary(connectionSummary);
|
||||
setIcon(pair.first);
|
||||
contentDescription = pair.second;
|
||||
// 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());
|
||||
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!");
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
@@ -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<ScanFilter> 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<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap =
|
||||
new HashMap<>();
|
||||
protected final List<BluetoothDevice> 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<ScanFilter> 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<CachedBluetoothDevice> 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<ScanFilter>? = 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<CachedBluetoothDevice, BluetoothDevicePreference>()
|
||||
|
||||
@JvmField
|
||||
val mSelectedList: MutableList<BluetoothDevice> = 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<ScanFilter>?) {
|
||||
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<Preference>(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"
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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();
|
||||
|
Reference in New Issue
Block a user