1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.accessibility; 18 19 import static android.app.Activity.RESULT_OK; 20 import static android.bluetooth.BluetoothGatt.GATT_SUCCESS; 21 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; 22 23 import android.app.settings.SettingsEnums; 24 import android.bluetooth.BluetoothAdapter; 25 import android.bluetooth.BluetoothDevice; 26 import android.bluetooth.BluetoothGatt; 27 import android.bluetooth.BluetoothGattCallback; 28 import android.bluetooth.BluetoothManager; 29 import android.bluetooth.BluetoothProfile; 30 import android.bluetooth.BluetoothUuid; 31 import android.bluetooth.le.BluetoothLeScanner; 32 import android.bluetooth.le.ScanCallback; 33 import android.bluetooth.le.ScanFilter; 34 import android.bluetooth.le.ScanRecord; 35 import android.bluetooth.le.ScanResult; 36 import android.bluetooth.le.ScanSettings; 37 import android.content.Context; 38 import android.os.Bundle; 39 import android.os.ParcelUuid; 40 import android.os.SystemProperties; 41 import android.util.Log; 42 import android.widget.Toast; 43 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 import androidx.preference.Preference; 47 48 import com.android.settings.R; 49 import com.android.settings.bluetooth.BluetoothDevicePreference; 50 import com.android.settings.bluetooth.BluetoothProgressCategory; 51 import com.android.settings.bluetooth.Utils; 52 import com.android.settings.dashboard.RestrictedDashboardFragment; 53 import com.android.settings.overlay.FeatureFactory; 54 import com.android.settingslib.bluetooth.BluetoothCallback; 55 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 56 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 57 import com.android.settingslib.bluetooth.HearingAidInfo; 58 import com.android.settingslib.bluetooth.HearingAidStatsLogUtils; 59 import com.android.settingslib.bluetooth.LocalBluetoothManager; 60 61 import java.util.ArrayList; 62 import java.util.HashMap; 63 import java.util.List; 64 import java.util.Map; 65 66 /** 67 * This fragment shows all scanned hearing devices through BLE scanning. Users can 68 * pair them in this page. 69 */ 70 public class HearingDevicePairingFragment extends RestrictedDashboardFragment implements 71 BluetoothCallback { 72 73 private static final boolean DEBUG = true; 74 private static final String TAG = "HearingDevicePairingFragment"; 75 private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY = 76 "persist.bluetooth.showdeviceswithoutnames"; 77 private static final String KEY_AVAILABLE_HEARING_DEVICES = "available_hearing_devices"; 78 79 LocalBluetoothManager mLocalManager; 80 @Nullable 81 BluetoothAdapter mBluetoothAdapter; 82 @Nullable 83 CachedBluetoothDeviceManager mCachedDeviceManager; 84 85 private boolean mShowDevicesWithoutNames; 86 @Nullable 87 private BluetoothProgressCategory mAvailableHearingDeviceGroup; 88 89 @Nullable 90 BluetoothDevice mSelectedDevice; 91 final List<BluetoothDevice> mSelectedDeviceList = new ArrayList<>(); 92 final List<BluetoothGatt> mConnectingGattList = new ArrayList<>(); 93 final Map<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap = 94 new HashMap<>(); 95 96 private List<ScanFilter> mLeScanFilters; 97 HearingDevicePairingFragment()98 public HearingDevicePairingFragment() { 99 super(DISALLOW_CONFIG_BLUETOOTH); 100 } 101 102 @Override onCreate(Bundle savedInstanceState)103 public void onCreate(Bundle savedInstanceState) { 104 super.onCreate(savedInstanceState); 105 106 mLocalManager = Utils.getLocalBtManager(getActivity()); 107 if (mLocalManager == null) { 108 Log.e(TAG, "Bluetooth is not supported on this device"); 109 return; 110 } 111 mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter(); 112 mCachedDeviceManager = mLocalManager.getCachedDeviceManager(); 113 mShowDevicesWithoutNames = SystemProperties.getBoolean( 114 BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false); 115 116 initPreferencesFromPreferenceScreen(); 117 initHearingDeviceLeScanFilters(); 118 } 119 120 @Override onAttach(Context context)121 public void onAttach(Context context) { 122 super.onAttach(context); 123 use(ViewAllBluetoothDevicesPreferenceController.class).init(this); 124 } 125 126 @Override onStart()127 public void onStart() { 128 super.onStart(); 129 if (mLocalManager == null || mBluetoothAdapter == null || isUiRestricted()) { 130 return; 131 } 132 mLocalManager.setForegroundActivity(getActivity()); 133 mLocalManager.getEventManager().registerCallback(this); 134 if (mBluetoothAdapter.isEnabled()) { 135 startScanning(); 136 } else { 137 // Turn on bluetooth if it is disabled 138 mBluetoothAdapter.enable(); 139 } 140 } 141 142 @Override onStop()143 public void onStop() { 144 super.onStop(); 145 if (mLocalManager == null || isUiRestricted()) { 146 return; 147 } 148 stopScanning(); 149 removeAllDevices(); 150 for (BluetoothGatt gatt: mConnectingGattList) { 151 gatt.disconnect(); 152 } 153 mConnectingGattList.clear(); 154 mLocalManager.setForegroundActivity(null); 155 mLocalManager.getEventManager().unregisterCallback(this); 156 } 157 158 @Override onPreferenceTreeClick(Preference preference)159 public boolean onPreferenceTreeClick(Preference preference) { 160 if (preference instanceof BluetoothDevicePreference) { 161 stopScanning(); 162 BluetoothDevicePreference devicePreference = (BluetoothDevicePreference) preference; 163 mSelectedDevice = devicePreference.getCachedDevice().getDevice(); 164 if (mSelectedDevice != null) { 165 mSelectedDeviceList.add(mSelectedDevice); 166 } 167 devicePreference.onClicked(); 168 return true; 169 } 170 return super.onPreferenceTreeClick(preference); 171 } 172 173 @Override onDeviceDeleted(@onNull CachedBluetoothDevice cachedDevice)174 public void onDeviceDeleted(@NonNull CachedBluetoothDevice cachedDevice) { 175 removeDevice(cachedDevice); 176 } 177 178 @Override onBluetoothStateChanged(int bluetoothState)179 public void onBluetoothStateChanged(int bluetoothState) { 180 switch (bluetoothState) { 181 case BluetoothAdapter.STATE_ON: 182 startScanning(); 183 showBluetoothTurnedOnToast(); 184 break; 185 case BluetoothAdapter.STATE_OFF: 186 finish(); 187 break; 188 } 189 } 190 191 @Override onDeviceBondStateChanged(@onNull CachedBluetoothDevice cachedDevice, int bondState)192 public void onDeviceBondStateChanged(@NonNull CachedBluetoothDevice cachedDevice, 193 int bondState) { 194 if (DEBUG) { 195 Log.d(TAG, "onDeviceBondStateChanged: " + cachedDevice + ", state = " 196 + bondState); 197 } 198 if (bondState == BluetoothDevice.BOND_BONDED) { 199 // If one device is connected(bonded), then close this fragment. 200 setResult(RESULT_OK); 201 finish(); 202 return; 203 } else if (bondState == BluetoothDevice.BOND_BONDING) { 204 // Set the bond entry where binding process starts for logging hearing aid device info 205 final int pageId = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider() 206 .getAttribution(getActivity()); 207 final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry( 208 pageId); 209 HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice); 210 } 211 if (mSelectedDevice != null) { 212 BluetoothDevice device = cachedDevice.getDevice(); 213 if (mSelectedDevice.equals(device) && bondState == BluetoothDevice.BOND_NONE) { 214 // If current selected device failed to bond, restart scanning 215 startScanning(); 216 } 217 } 218 } 219 220 @Override onProfileConnectionStateChanged(@onNull CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile)221 public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice, 222 int state, int bluetoothProfile) { 223 // This callback is used to handle the case that bonded device is connected in pairing list. 224 // 1. If user selected multiple bonded devices in pairing list, after connected 225 // finish this page. 226 // 2. If the bonded devices auto connected in paring list, after connected it will be 227 // removed from paring list. 228 if (cachedDevice.isConnected()) { 229 final BluetoothDevice device = cachedDevice.getDevice(); 230 if (device != null && mSelectedDeviceList.contains(device)) { 231 setResult(RESULT_OK); 232 finish(); 233 } else { 234 removeDevice(cachedDevice); 235 } 236 } 237 } 238 239 @Override getMetricsCategory()240 public int getMetricsCategory() { 241 return SettingsEnums.HEARING_AID_PAIRING; 242 } 243 244 @Override getPreferenceScreenResId()245 protected int getPreferenceScreenResId() { 246 return R.xml.hearing_device_pairing_fragment; 247 } 248 249 250 @Override getLogTag()251 protected String getLogTag() { 252 return TAG; 253 } 254 addDevice(CachedBluetoothDevice cachedDevice)255 void addDevice(CachedBluetoothDevice cachedDevice) { 256 if (mBluetoothAdapter == null) { 257 return; 258 } 259 // Do not create new preference while the list shows one of the state messages 260 if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) { 261 return; 262 } 263 if (mDevicePreferenceMap.get(cachedDevice) != null) { 264 return; 265 } 266 String key = cachedDevice.getDevice().getAddress(); 267 BluetoothDevicePreference preference = (BluetoothDevicePreference) getCachedPreference(key); 268 if (preference == null) { 269 preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice, 270 mShowDevicesWithoutNames, BluetoothDevicePreference.SortType.TYPE_FIFO); 271 preference.setKey(key); 272 preference.hideSecondTarget(true); 273 } 274 if (mAvailableHearingDeviceGroup != null) { 275 mAvailableHearingDeviceGroup.addPreference(preference); 276 } 277 mDevicePreferenceMap.put(cachedDevice, preference); 278 if (DEBUG) { 279 Log.d(TAG, "Add device. device: " + cachedDevice); 280 } 281 } 282 removeDevice(CachedBluetoothDevice cachedDevice)283 void removeDevice(CachedBluetoothDevice cachedDevice) { 284 if (DEBUG) { 285 Log.d(TAG, "removeDevice: " + cachedDevice); 286 } 287 BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice); 288 if (mAvailableHearingDeviceGroup != null && preference != null) { 289 mAvailableHearingDeviceGroup.removePreference(preference); 290 } 291 } 292 startScanning()293 void startScanning() { 294 if (mCachedDeviceManager != null) { 295 mCachedDeviceManager.clearNonBondedDevices(); 296 } 297 removeAllDevices(); 298 startLeScanning(); 299 } 300 stopScanning()301 void stopScanning() { 302 stopLeScanning(); 303 } 304 305 private final ScanCallback mLeScanCallback = new ScanCallback() { 306 @Override 307 public void onScanResult(int callbackType, ScanResult result) { 308 handleLeScanResult(result); 309 } 310 311 @Override 312 public void onBatchScanResults(List<ScanResult> results) { 313 for (ScanResult result: results) { 314 handleLeScanResult(result); 315 } 316 } 317 318 @Override 319 public void onScanFailed(int errorCode) { 320 Log.w(TAG, "BLE Scan failed with error code " + errorCode); 321 } 322 }; 323 handleLeScanResult(ScanResult result)324 void handleLeScanResult(ScanResult result) { 325 if (mCachedDeviceManager == null) { 326 return; 327 } 328 final BluetoothDevice device = result.getDevice(); 329 CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device); 330 if (cachedDevice == null) { 331 cachedDevice = mCachedDeviceManager.addDevice(device); 332 } else if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { 333 if (DEBUG) { 334 Log.d(TAG, "Skip this device, already bonded: " + cachedDevice); 335 } 336 return; 337 } 338 if (cachedDevice.getHearingAidInfo() == null) { 339 if (DEBUG) { 340 Log.d(TAG, "Set hearing aid info on device: " + cachedDevice); 341 } 342 cachedDevice.setHearingAidInfo(new HearingAidInfo.Builder().build()); 343 } 344 // No need to handle the device if the device is already in the list or discovering services 345 if (mDevicePreferenceMap.get(cachedDevice) == null 346 && mConnectingGattList.stream().noneMatch( 347 gatt -> gatt.getDevice().equals(device))) { 348 if (isAndroidCompatibleHearingAid(result)) { 349 addDevice(cachedDevice); 350 } else { 351 discoverServices(cachedDevice); 352 } 353 } 354 } 355 startLeScanning()356 void startLeScanning() { 357 if (mBluetoothAdapter == null) { 358 return; 359 } 360 if (DEBUG) { 361 Log.v(TAG, "startLeScanning"); 362 } 363 final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner(); 364 if (leScanner == null) { 365 Log.w(TAG, "LE scanner not found, cannot start LE scanning"); 366 } else { 367 final ScanSettings settings = new ScanSettings.Builder() 368 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 369 .setLegacy(false) 370 .build(); 371 leScanner.startScan(mLeScanFilters, settings, mLeScanCallback); 372 if (mAvailableHearingDeviceGroup != null) { 373 mAvailableHearingDeviceGroup.setProgress(true); 374 } 375 } 376 } 377 stopLeScanning()378 void stopLeScanning() { 379 if (mBluetoothAdapter == null) { 380 return; 381 } 382 if (DEBUG) { 383 Log.v(TAG, "stopLeScanning"); 384 } 385 final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner(); 386 if (leScanner != null) { 387 leScanner.stopScan(mLeScanCallback); 388 if (mAvailableHearingDeviceGroup != null) { 389 mAvailableHearingDeviceGroup.setProgress(false); 390 } 391 } 392 } 393 removeAllDevices()394 private void removeAllDevices() { 395 mDevicePreferenceMap.clear(); 396 if (mAvailableHearingDeviceGroup != null) { 397 mAvailableHearingDeviceGroup.removeAll(); 398 } 399 } 400 initPreferencesFromPreferenceScreen()401 void initPreferencesFromPreferenceScreen() { 402 mAvailableHearingDeviceGroup = findPreference(KEY_AVAILABLE_HEARING_DEVICES); 403 } 404 initHearingDeviceLeScanFilters()405 private void initHearingDeviceLeScanFilters() { 406 mLeScanFilters = new ArrayList<>(); 407 // Filters for ASHA hearing aids 408 mLeScanFilters.add( 409 new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HEARING_AID).build()); 410 mLeScanFilters.add(new ScanFilter.Builder() 411 .setServiceData(BluetoothUuid.HEARING_AID, new byte[0]).build()); 412 // Filters for LE audio hearing aids 413 mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HAS).build()); 414 mLeScanFilters.add(new ScanFilter.Builder() 415 .setServiceData(BluetoothUuid.HAS, new byte[0]).build()); 416 // Filters for MFi hearing aids 417 mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.MFI_HAS).build()); 418 mLeScanFilters.add(new ScanFilter.Builder() 419 .setServiceData(BluetoothUuid.MFI_HAS, new byte[0]).build()); 420 } 421 isAndroidCompatibleHearingAid(ScanResult scanResult)422 boolean isAndroidCompatibleHearingAid(ScanResult scanResult) { 423 ScanRecord scanRecord = scanResult.getScanRecord(); 424 if (scanRecord == null) { 425 if (DEBUG) { 426 Log.d(TAG, "Scan record is null, not compatible with Android. device: " 427 + scanResult.getDevice()); 428 } 429 return false; 430 } 431 List<ParcelUuid> uuids = scanRecord.getServiceUuids(); 432 if (uuids != null) { 433 if (uuids.contains(BluetoothUuid.HEARING_AID) || uuids.contains(BluetoothUuid.HAS)) { 434 if (DEBUG) { 435 Log.d(TAG, "Scan record uuid matched, compatible with Android. device: " 436 + scanResult.getDevice()); 437 } 438 return true; 439 } 440 } 441 if (scanRecord.getServiceData(BluetoothUuid.HEARING_AID) != null 442 || scanRecord.getServiceData(BluetoothUuid.HAS) != null) { 443 if (DEBUG) { 444 Log.d(TAG, "Scan record service data matched, compatible with Android. device: " 445 + scanResult.getDevice()); 446 } 447 return true; 448 } 449 if (DEBUG) { 450 Log.d(TAG, "Scan record mismatched, not compatible with Android. device: " 451 + scanResult.getDevice()); 452 } 453 return false; 454 } 455 discoverServices(CachedBluetoothDevice cachedDevice)456 void discoverServices(CachedBluetoothDevice cachedDevice) { 457 if (DEBUG) { 458 Log.d(TAG, "connectGattToCheckCompatibility, device: " + cachedDevice); 459 } 460 BluetoothGatt gatt = cachedDevice.getDevice().connectGatt(getContext(), false, 461 new BluetoothGattCallback() { 462 @Override 463 public void onConnectionStateChange(BluetoothGatt gatt, int status, 464 int newState) { 465 super.onConnectionStateChange(gatt, status, newState); 466 if (DEBUG) { 467 Log.d(TAG, "onConnectionStateChange, status: " + status + ", newState: " 468 + newState + ", device: " + cachedDevice); 469 } 470 if (status == GATT_SUCCESS 471 && newState == BluetoothProfile.STATE_CONNECTED) { 472 gatt.discoverServices(); 473 } else { 474 gatt.disconnect(); 475 mConnectingGattList.remove(gatt); 476 } 477 } 478 479 @Override 480 public void onServicesDiscovered(BluetoothGatt gatt, int status) { 481 super.onServicesDiscovered(gatt, status); 482 if (DEBUG) { 483 Log.d(TAG, "onServicesDiscovered, status: " + status + ", device: " 484 + cachedDevice); 485 } 486 if (status == GATT_SUCCESS) { 487 if (gatt.getService(BluetoothUuid.HEARING_AID.getUuid()) != null 488 || gatt.getService(BluetoothUuid.HAS.getUuid()) != null) { 489 if (DEBUG) { 490 Log.d(TAG, "compatible with Android, device: " 491 + cachedDevice); 492 } 493 addDevice(cachedDevice); 494 } 495 } else { 496 gatt.disconnect(); 497 mConnectingGattList.remove(gatt); 498 } 499 } 500 }); 501 mConnectingGattList.add(gatt); 502 } 503 showBluetoothTurnedOnToast()504 void showBluetoothTurnedOnToast() { 505 Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast, 506 Toast.LENGTH_SHORT).show(); 507 } 508 } 509