• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     // Flags data type from CSS 1.3 Flags
79     private static final int BT_DISCOVERABLE_MASK = 0x03;
80 
81     LocalBluetoothManager mLocalManager;
82     @Nullable
83     BluetoothAdapter mBluetoothAdapter;
84     @Nullable
85     CachedBluetoothDeviceManager mCachedDeviceManager;
86 
87     private boolean mShowDevicesWithoutNames;
88     @Nullable
89     private BluetoothProgressCategory mAvailableHearingDeviceGroup;
90 
91     @Nullable
92     BluetoothDevice mSelectedDevice;
93     final List<BluetoothDevice> mSelectedDeviceList = new ArrayList<>();
94     final List<BluetoothGatt> mConnectingGattList = new ArrayList<>();
95     final Map<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap =
96             new HashMap<>();
97 
98     private List<ScanFilter> mLeScanFilters;
99 
HearingDevicePairingFragment()100     public HearingDevicePairingFragment() {
101         super(DISALLOW_CONFIG_BLUETOOTH);
102     }
103 
104     @Override
onCreate(Bundle savedInstanceState)105     public void onCreate(Bundle savedInstanceState) {
106         super.onCreate(savedInstanceState);
107 
108         mLocalManager = Utils.getLocalBtManager(getActivity());
109         if (mLocalManager == null) {
110             Log.e(TAG, "Bluetooth is not supported on this device");
111             return;
112         }
113         mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter();
114         mCachedDeviceManager = mLocalManager.getCachedDeviceManager();
115         mShowDevicesWithoutNames = SystemProperties.getBoolean(
116                 BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false);
117 
118         initPreferencesFromPreferenceScreen();
119         initHearingDeviceLeScanFilters();
120     }
121 
122     @Override
onAttach(Context context)123     public void onAttach(Context context) {
124         super.onAttach(context);
125         use(ViewAllBluetoothDevicesPreferenceController.class).init(this);
126     }
127 
128     @Override
onStart()129     public void onStart() {
130         super.onStart();
131         if (mLocalManager == null || mBluetoothAdapter == null || isUiRestricted()) {
132             return;
133         }
134         mLocalManager.setForegroundActivity(getActivity());
135         mLocalManager.getEventManager().registerCallback(this);
136         if (mBluetoothAdapter.isEnabled()) {
137             startScanning();
138         } else {
139             // Turn on bluetooth if it is disabled
140             mBluetoothAdapter.enable();
141         }
142     }
143 
144     @Override
onStop()145     public void onStop() {
146         super.onStop();
147         if (mLocalManager == null || isUiRestricted()) {
148             return;
149         }
150         stopScanning();
151         removeAllDevices();
152         for (BluetoothGatt gatt: mConnectingGattList) {
153             gatt.disconnect();
154         }
155         mConnectingGattList.clear();
156         mLocalManager.setForegroundActivity(null);
157         mLocalManager.getEventManager().unregisterCallback(this);
158     }
159 
160     @Override
onPreferenceTreeClick(Preference preference)161     public boolean onPreferenceTreeClick(Preference preference) {
162         if (preference instanceof BluetoothDevicePreference) {
163             stopScanning();
164             BluetoothDevicePreference devicePreference = (BluetoothDevicePreference) preference;
165             mSelectedDevice = devicePreference.getCachedDevice().getDevice();
166             if (mSelectedDevice != null) {
167                 mSelectedDeviceList.add(mSelectedDevice);
168             }
169             devicePreference.onClicked();
170             return true;
171         }
172         return super.onPreferenceTreeClick(preference);
173     }
174 
175     @Override
onDeviceDeleted(@onNull CachedBluetoothDevice cachedDevice)176     public void onDeviceDeleted(@NonNull CachedBluetoothDevice cachedDevice) {
177         removeDevice(cachedDevice);
178     }
179 
180     @Override
onBluetoothStateChanged(int bluetoothState)181     public void onBluetoothStateChanged(int bluetoothState) {
182         switch (bluetoothState) {
183             case BluetoothAdapter.STATE_ON:
184                 startScanning();
185                 showBluetoothTurnedOnToast();
186                 break;
187             case BluetoothAdapter.STATE_OFF:
188                 finish();
189                 break;
190         }
191     }
192 
193     @Override
onDeviceBondStateChanged(@onNull CachedBluetoothDevice cachedDevice, int bondState)194     public void onDeviceBondStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
195             int bondState) {
196         if (DEBUG) {
197             Log.d(TAG, "onDeviceBondStateChanged: " + cachedDevice + ", state = "
198                     + bondState);
199         }
200         if (bondState == BluetoothDevice.BOND_BONDED) {
201             // If one device is connected(bonded), then close this fragment.
202             setResult(RESULT_OK);
203             finish();
204             return;
205         } else if (bondState == BluetoothDevice.BOND_BONDING) {
206             // Set the bond entry where binding process starts for logging hearing aid device info
207             final int pageId = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
208                     .getAttribution(getActivity());
209             final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry(
210                     pageId);
211             HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice);
212         }
213         if (mSelectedDevice != null) {
214             BluetoothDevice device = cachedDevice.getDevice();
215             if (mSelectedDevice.equals(device) && bondState == BluetoothDevice.BOND_NONE) {
216                 // If current selected device failed to bond, restart scanning
217                 startScanning();
218             }
219         }
220     }
221 
222     @Override
onProfileConnectionStateChanged(@onNull CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile)223     public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
224             int state, int bluetoothProfile) {
225         // This callback is used to handle the case that bonded device is connected in pairing list.
226         // 1. If user selected multiple bonded devices in pairing list, after connected
227         // finish this page.
228         // 2. If the bonded devices auto connected in paring list, after connected it will be
229         // removed from paring list.
230         if (cachedDevice.isConnected()) {
231             final BluetoothDevice device = cachedDevice.getDevice();
232             if (device != null && mSelectedDeviceList.contains(device)) {
233                 setResult(RESULT_OK);
234                 finish();
235             } else {
236                 removeDevice(cachedDevice);
237             }
238         }
239     }
240 
241     @Override
getMetricsCategory()242     public int getMetricsCategory() {
243         return SettingsEnums.HEARING_AID_PAIRING;
244     }
245 
246     @Override
getPreferenceScreenResId()247     protected int getPreferenceScreenResId() {
248         return R.xml.hearing_device_pairing_fragment;
249     }
250 
251 
252     @Override
getLogTag()253     protected String getLogTag() {
254         return TAG;
255     }
256 
addDevice(CachedBluetoothDevice cachedDevice)257     void addDevice(CachedBluetoothDevice cachedDevice) {
258         if (mBluetoothAdapter == null) {
259             return;
260         }
261         // Do not create new preference while the list shows one of the state messages
262         if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) {
263             return;
264         }
265         if (mDevicePreferenceMap.get(cachedDevice) != null) {
266             return;
267         }
268         String key = cachedDevice.getDevice().getAddress();
269         BluetoothDevicePreference preference = (BluetoothDevicePreference) getCachedPreference(key);
270         if (preference == null) {
271             preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice,
272                     mShowDevicesWithoutNames, BluetoothDevicePreference.SortType.TYPE_FIFO);
273             preference.setKey(key);
274             preference.hideSecondTarget(true);
275         }
276         if (mAvailableHearingDeviceGroup != null) {
277             mAvailableHearingDeviceGroup.addPreference(preference);
278         }
279         mDevicePreferenceMap.put(cachedDevice, preference);
280         if (DEBUG) {
281             Log.d(TAG, "Add device. device: " + cachedDevice);
282         }
283     }
284 
removeDevice(CachedBluetoothDevice cachedDevice)285     void removeDevice(CachedBluetoothDevice cachedDevice) {
286         if (DEBUG) {
287             Log.d(TAG, "removeDevice: " + cachedDevice);
288         }
289         BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice);
290         if (mAvailableHearingDeviceGroup != null && preference != null) {
291             mAvailableHearingDeviceGroup.removePreference(preference);
292         }
293     }
294 
startScanning()295     void startScanning() {
296         if (mCachedDeviceManager != null) {
297             mCachedDeviceManager.clearNonBondedDevices();
298         }
299         removeAllDevices();
300         startLeScanning();
301     }
302 
stopScanning()303     void stopScanning() {
304         stopLeScanning();
305     }
306 
307     private final ScanCallback mLeScanCallback = new ScanCallback() {
308         @Override
309         public void onScanResult(int callbackType, ScanResult result) {
310             handleLeScanResult(result);
311         }
312 
313         @Override
314         public void onBatchScanResults(List<ScanResult> results) {
315             for (ScanResult result: results) {
316                 handleLeScanResult(result);
317             }
318         }
319 
320         @Override
321         public void onScanFailed(int errorCode) {
322             Log.w(TAG, "BLE Scan failed with error code " + errorCode);
323         }
324     };
325 
handleLeScanResult(ScanResult result)326     void handleLeScanResult(ScanResult result) {
327         if (mCachedDeviceManager == null || !isDeviceDiscoverable(result)) {
328             return;
329         }
330         final BluetoothDevice device = result.getDevice();
331         CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device);
332         if (cachedDevice == null) {
333             cachedDevice = mCachedDeviceManager.addDevice(device);
334         } else if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
335             if (DEBUG) {
336                 Log.d(TAG, "Skip this device, already bonded: " + cachedDevice);
337             }
338             return;
339         }
340         if (cachedDevice.getHearingAidInfo() == null) {
341             if (DEBUG) {
342                 Log.d(TAG, "Set hearing aid info on device: " + cachedDevice);
343             }
344             cachedDevice.setHearingAidInfo(new HearingAidInfo.Builder().build());
345         }
346         // No need to handle the device if the device is already in the list or discovering services
347         if (mDevicePreferenceMap.get(cachedDevice) == null
348                 && mConnectingGattList.stream().noneMatch(
349                         gatt -> gatt.getDevice().equals(device))) {
350             if (isAndroidCompatibleHearingAid(result)) {
351                 addDevice(cachedDevice);
352             } else {
353                 discoverServices(cachedDevice);
354             }
355         }
356     }
357 
startLeScanning()358     void startLeScanning() {
359         if (mBluetoothAdapter == null) {
360             return;
361         }
362         if (DEBUG) {
363             Log.v(TAG, "startLeScanning");
364         }
365         final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner();
366         if (leScanner == null) {
367             Log.w(TAG, "LE scanner not found, cannot start LE scanning");
368         } else {
369             final ScanSettings settings = new ScanSettings.Builder()
370                     .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
371                     .setLegacy(false)
372                     .build();
373             leScanner.startScan(mLeScanFilters, settings, mLeScanCallback);
374             if (mAvailableHearingDeviceGroup != null) {
375                 mAvailableHearingDeviceGroup.setProgress(true);
376             }
377         }
378     }
379 
stopLeScanning()380     void stopLeScanning() {
381         if (mBluetoothAdapter == null) {
382             return;
383         }
384         if (DEBUG) {
385             Log.v(TAG, "stopLeScanning");
386         }
387         final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner();
388         if (leScanner != null) {
389             leScanner.stopScan(mLeScanCallback);
390             if (mAvailableHearingDeviceGroup != null) {
391                 mAvailableHearingDeviceGroup.setProgress(false);
392             }
393         }
394     }
395 
removeAllDevices()396     private void removeAllDevices() {
397         mDevicePreferenceMap.clear();
398         if (mAvailableHearingDeviceGroup != null) {
399             mAvailableHearingDeviceGroup.removeAll();
400         }
401     }
402 
initPreferencesFromPreferenceScreen()403     void initPreferencesFromPreferenceScreen() {
404         mAvailableHearingDeviceGroup = findPreference(KEY_AVAILABLE_HEARING_DEVICES);
405     }
406 
initHearingDeviceLeScanFilters()407     private void initHearingDeviceLeScanFilters() {
408         mLeScanFilters = new ArrayList<>();
409         // Filters for ASHA hearing aids
410         mLeScanFilters.add(
411                 new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HEARING_AID).build());
412         mLeScanFilters.add(new ScanFilter.Builder()
413                 .setServiceData(BluetoothUuid.HEARING_AID, new byte[0]).build());
414         // Filters for LE audio hearing aids
415         mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HAS).build());
416         mLeScanFilters.add(new ScanFilter.Builder()
417                 .setServiceData(BluetoothUuid.HAS, new byte[0]).build());
418         // Filters for MFi hearing aids
419         mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.MFI_HAS).build());
420         mLeScanFilters.add(new ScanFilter.Builder()
421                 .setServiceData(BluetoothUuid.MFI_HAS, new byte[0]).build());
422     }
423 
isAndroidCompatibleHearingAid(ScanResult scanResult)424     boolean isAndroidCompatibleHearingAid(ScanResult scanResult) {
425         ScanRecord scanRecord = scanResult.getScanRecord();
426         if (scanRecord == null) {
427             if (DEBUG) {
428                 Log.d(TAG, "Scan record is null, not compatible with Android. device: "
429                         + scanResult.getDevice());
430             }
431             return false;
432         }
433         List<ParcelUuid> uuids = scanRecord.getServiceUuids();
434         if (uuids != null) {
435             if (uuids.contains(BluetoothUuid.HEARING_AID) || uuids.contains(BluetoothUuid.HAS)) {
436                 if (DEBUG) {
437                     Log.d(TAG, "Scan record uuid matched, compatible with Android. device: "
438                             + scanResult.getDevice());
439                 }
440                 return true;
441             }
442         }
443         if (scanRecord.getServiceData(BluetoothUuid.HEARING_AID) != null
444                 || scanRecord.getServiceData(BluetoothUuid.HAS) != null) {
445             if (DEBUG) {
446                 Log.d(TAG, "Scan record service data matched, compatible with Android. device: "
447                         + scanResult.getDevice());
448             }
449             return true;
450         }
451         if (DEBUG) {
452             Log.d(TAG, "Scan record mismatched, not compatible with Android. device: "
453                     + scanResult.getDevice());
454         }
455         return false;
456     }
457 
discoverServices(CachedBluetoothDevice cachedDevice)458     void discoverServices(CachedBluetoothDevice cachedDevice) {
459         if (DEBUG) {
460             Log.d(TAG, "connectGattToCheckCompatibility, device: " + cachedDevice);
461         }
462         BluetoothGatt gatt = cachedDevice.getDevice().connectGatt(getContext(), false,
463                 new BluetoothGattCallback() {
464                     @Override
465                     public void onConnectionStateChange(BluetoothGatt gatt, int status,
466                             int newState) {
467                         super.onConnectionStateChange(gatt, status, newState);
468                         if (DEBUG) {
469                             Log.d(TAG, "onConnectionStateChange, status: " + status + ", newState: "
470                                     + newState + ", device: " + cachedDevice);
471                         }
472                         if (status == GATT_SUCCESS
473                                 && newState == BluetoothProfile.STATE_CONNECTED) {
474                             gatt.discoverServices();
475                         } else {
476                             gatt.disconnect();
477                             mConnectingGattList.remove(gatt);
478                         }
479                     }
480 
481                     @Override
482                     public void onServicesDiscovered(BluetoothGatt gatt, int status) {
483                         super.onServicesDiscovered(gatt, status);
484                         if (DEBUG) {
485                             Log.d(TAG, "onServicesDiscovered, status: " + status + ", device: "
486                                     + cachedDevice);
487                         }
488                         if (status == GATT_SUCCESS) {
489                             if (gatt.getService(BluetoothUuid.HEARING_AID.getUuid()) != null
490                                     || gatt.getService(BluetoothUuid.HAS.getUuid()) != null) {
491                                 if (DEBUG) {
492                                     Log.d(TAG, "compatible with Android, device: "
493                                             + cachedDevice);
494                                 }
495                                 addDevice(cachedDevice);
496                             }
497                         } else {
498                             gatt.disconnect();
499                             mConnectingGattList.remove(gatt);
500                         }
501                     }
502                 });
503         mConnectingGattList.add(gatt);
504     }
505 
showBluetoothTurnedOnToast()506     void showBluetoothTurnedOnToast() {
507         Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast,
508                 Toast.LENGTH_SHORT).show();
509     }
510 
isDeviceDiscoverable(ScanResult result)511     boolean isDeviceDiscoverable(ScanResult result) {
512         final ScanRecord scanRecord = result.getScanRecord();
513         if (scanRecord == null) {
514             return false;
515         }
516         final int flags = scanRecord.getAdvertiseFlags();
517         return (flags & BT_DISCOVERABLE_MASK) != 0;
518     }
519 }
520