• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 package com.android.settings.bluetooth;
17 
18 import android.bluetooth.BluetoothAdapter;
19 import android.bluetooth.BluetoothDevice;
20 import android.content.Context;
21 import android.os.Bundle;
22 import android.util.Log;
23 
24 import androidx.annotation.VisibleForTesting;
25 import androidx.preference.Preference;
26 
27 import com.android.settings.R;
28 import com.android.settings.connecteddevice.DevicePreferenceCallback;
29 import com.android.settings.core.SubSettingLauncher;
30 import com.android.settings.overlay.FeatureFactory;
31 import com.android.settings.widget.GearPreference;
32 import com.android.settingslib.bluetooth.BluetoothCallback;
33 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
34 import com.android.settingslib.bluetooth.LocalBluetoothManager;
35 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
36 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
37 
38 import java.util.ArrayList;
39 import java.util.Collection;
40 import java.util.List;
41 import java.util.concurrent.ConcurrentHashMap;
42 
43 /**
44  * Update the bluetooth devices. It gets bluetooth event from {@link LocalBluetoothManager} using
45  * {@link BluetoothCallback}. It notifies the upper level whether to add/remove the preference
46  * through {@link DevicePreferenceCallback}
47  *
48  * In {@link BluetoothDeviceUpdater}, it uses {@link #isFilterMatched(CachedBluetoothDevice)} to
49  * detect whether the {@link CachedBluetoothDevice} is relevant.
50  */
51 public abstract class BluetoothDeviceUpdater implements BluetoothCallback,
52         LocalBluetoothProfileManager.ServiceListener {
53     protected final MetricsFeatureProvider mMetricsFeatureProvider;
54     protected final DevicePreferenceCallback mDevicePreferenceCallback;
55     protected final ConcurrentHashMap<BluetoothDevice, Preference> mPreferenceMap;
56     protected Context mContext;
57     protected Context mPrefContext;
58     @VisibleForTesting
59     protected LocalBluetoothManager mLocalManager;
60     protected int mMetricsCategory;
61 
62     protected static final String TAG = "BluetoothDeviceUpdater";
63     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
64 
65     @VisibleForTesting
66     final GearPreference.OnGearClickListener mDeviceProfilesListener = pref -> {
67         launchDeviceDetails(pref);
68     };
69 
BluetoothDeviceUpdater(Context context, DevicePreferenceCallback devicePreferenceCallback, int metricsCategory)70     public BluetoothDeviceUpdater(Context context,
71             DevicePreferenceCallback devicePreferenceCallback, int metricsCategory) {
72         this(context, devicePreferenceCallback, Utils.getLocalBtManager(context), metricsCategory);
73     }
74 
75     @VisibleForTesting
BluetoothDeviceUpdater(Context context, DevicePreferenceCallback devicePreferenceCallback, LocalBluetoothManager localManager, int metricsCategory)76     BluetoothDeviceUpdater(Context context,
77             DevicePreferenceCallback devicePreferenceCallback, LocalBluetoothManager localManager,
78             int metricsCategory) {
79         mContext = context;
80         mDevicePreferenceCallback = devicePreferenceCallback;
81         mPreferenceMap = new ConcurrentHashMap<>();
82         mLocalManager = localManager;
83         mMetricsCategory = metricsCategory;
84         mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
85     }
86 
87     /**
88      * Register the bluetooth event callback and update the list
89      */
registerCallback()90     public void registerCallback() {
91         if (mLocalManager == null) {
92             Log.e(getLogTag(), "registerCallback() Bluetooth is not supported on this device");
93             return;
94         }
95         mLocalManager.setForegroundActivity(mContext);
96         mLocalManager.getEventManager().registerCallback(this);
97         mLocalManager.getProfileManager().addServiceListener(this);
98         forceUpdate();
99     }
100 
101     /**
102      * Unregister the bluetooth event callback
103      */
unregisterCallback()104     public void unregisterCallback() {
105         if (mLocalManager == null) {
106             Log.e(getLogTag(), "unregisterCallback() Bluetooth is not supported on this device");
107             return;
108         }
109         mLocalManager.setForegroundActivity(null);
110         mLocalManager.getEventManager().unregisterCallback(this);
111         mLocalManager.getProfileManager().removeServiceListener(this);
112     }
113 
114     /**
115      * Force to update the list of bluetooth devices
116      */
forceUpdate()117     public void forceUpdate() {
118         if (mLocalManager == null) {
119             Log.e(getLogTag(), "forceUpdate() Bluetooth is not supported on this device");
120             return;
121         }
122         if (BluetoothAdapter.getDefaultAdapter().isEnabled()) {
123             final Collection<CachedBluetoothDevice> cachedDevices =
124                     mLocalManager.getCachedDeviceManager().getCachedDevicesCopy();
125             for (CachedBluetoothDevice cachedBluetoothDevice : cachedDevices) {
126                 update(cachedBluetoothDevice);
127             }
128         } else {
129             removeAllDevicesFromPreference();
130         }
131     }
132 
removeAllDevicesFromPreference()133     public void removeAllDevicesFromPreference() {
134         if (mLocalManager == null) {
135             Log.e(getLogTag(),
136                     "removeAllDevicesFromPreference() BT is not supported on this device");
137             return;
138         }
139         final Collection<CachedBluetoothDevice> cachedDevices =
140                 mLocalManager.getCachedDeviceManager().getCachedDevicesCopy();
141         for (CachedBluetoothDevice cachedBluetoothDevice : cachedDevices) {
142             removePreference(cachedBluetoothDevice);
143         }
144     }
145 
146     @Override
onBluetoothStateChanged(int bluetoothState)147     public void onBluetoothStateChanged(int bluetoothState) {
148         if (BluetoothAdapter.STATE_ON == bluetoothState) {
149             forceUpdate();
150         } else if (BluetoothAdapter.STATE_OFF == bluetoothState) {
151             removeAllDevicesFromPreference();
152         }
153     }
154 
155     @Override
onDeviceAdded(CachedBluetoothDevice cachedDevice)156     public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
157         Log.d(getLogTag(), "onDeviceAdded() device: " + cachedDevice.getName());
158         update(cachedDevice);
159     }
160 
161     @Override
onDeviceDeleted(CachedBluetoothDevice cachedDevice)162     public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
163         Log.d(getLogTag(), "onDeviceDeleted() device: " + cachedDevice.getName());
164         // Used to combine the hearing aid entries just after pairing. Once both the hearing aids
165         // get connected and their hiSyncId gets populated, this gets called for one of the
166         // 2 hearing aids so that only one entry in the connected devices list will be seen.
167         removePreference(cachedDevice);
168     }
169 
170     @Override
onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)171     public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
172         update(cachedDevice);
173     }
174 
175     @Override
onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile)176     public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state,
177             int bluetoothProfile) {
178         if (DBG) {
179             Log.d(getLogTag(), "onProfileConnectionStateChanged() device: " + cachedDevice.getName()
180                     + ", state: " + state + ", bluetoothProfile: " + bluetoothProfile);
181         }
182         update(cachedDevice);
183     }
184 
185     @Override
onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state)186     public void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
187         Log.d(getLogTag(), "onAclConnectionStateChanged() device: " + cachedDevice.getName()
188                 + ", state: " + state);
189         update(cachedDevice);
190     }
191 
192     @Override
onServiceConnected()193     public void onServiceConnected() {
194         // When bluetooth service connected update the UI
195         forceUpdate();
196     }
197 
198     @Override
onServiceDisconnected()199     public void onServiceDisconnected() {
200 
201     }
202 
203     /**
204      * Set the context to generate the {@link Preference}, so it could get the correct theme.
205      */
setPrefContext(Context context)206     public void setPrefContext(Context context) {
207         mPrefContext = context;
208     }
209 
210     /**
211      * Return {@code true} if {@code cachedBluetoothDevice} matches this
212      * {@link BluetoothDeviceUpdater} and should stay in the list, otherwise return {@code false}
213      */
isFilterMatched(CachedBluetoothDevice cachedBluetoothDevice)214     public abstract boolean isFilterMatched(CachedBluetoothDevice cachedBluetoothDevice);
215 
216     /**
217      * Return a preference key prefix for logging
218      */
getPreferenceKeyPrefix()219     protected abstract String getPreferenceKeyPrefix();
220 
221     /**
222      * Update whether to show {@link CachedBluetoothDevice} in the list.
223      */
update(CachedBluetoothDevice cachedBluetoothDevice)224     protected void update(CachedBluetoothDevice cachedBluetoothDevice) {
225         if (isFilterMatched(cachedBluetoothDevice)) {
226             // Add the preference if it is new one
227             addPreference(cachedBluetoothDevice);
228         } else {
229             removePreference(cachedBluetoothDevice);
230         }
231     }
232 
233     /**
234      * Add the {@link Preference} that represents the {@code cachedDevice}
235      */
addPreference(CachedBluetoothDevice cachedDevice)236     protected void addPreference(CachedBluetoothDevice cachedDevice) {
237         addPreference(cachedDevice, BluetoothDevicePreference.SortType.TYPE_DEFAULT);
238     }
239 
240     /**
241      * Add the {@link Preference} with {@link BluetoothDevicePreference.SortType} that
242      * represents the {@code cachedDevice}
243      */
addPreference(CachedBluetoothDevice cachedDevice, @BluetoothDevicePreference.SortType int type)244     protected void addPreference(CachedBluetoothDevice cachedDevice,
245             @BluetoothDevicePreference.SortType int type) {
246         final BluetoothDevice device = cachedDevice.getDevice();
247         if (!mPreferenceMap.containsKey(device)) {
248             BluetoothDevicePreference btPreference =
249                     new BluetoothDevicePreference(mPrefContext, cachedDevice,
250                             true /* showDeviceWithoutNames */,
251                             type);
252             btPreference.setKey(getPreferenceKeyPrefix() + cachedDevice.hashCode());
253             btPreference.setOnGearClickListener(mDeviceProfilesListener);
254             if (this instanceof Preference.OnPreferenceClickListener) {
255                 btPreference.setOnPreferenceClickListener(
256                         (Preference.OnPreferenceClickListener) this);
257             }
258             mPreferenceMap.put(device, btPreference);
259             mDevicePreferenceCallback.onDeviceAdded(btPreference);
260         }
261     }
262 
263     /**
264      * Remove the {@link Preference} that represents the {@code cachedDevice}
265      */
removePreference(CachedBluetoothDevice cachedDevice)266     protected void removePreference(CachedBluetoothDevice cachedDevice) {
267         final BluetoothDevice device = cachedDevice.getDevice();
268         final CachedBluetoothDevice subCachedDevice = cachedDevice.getSubDevice();
269         if (mPreferenceMap.containsKey(device)) {
270             removePreference(device);
271         } else if (subCachedDevice != null) {
272             // When doing remove, to check if preference maps to sub device.
273             // This would happen when connection state is changed in detail page that there is no
274             // callback from SettingsLib.
275             final BluetoothDevice subDevice = subCachedDevice.getDevice();
276             removePreference(subDevice);
277         }
278     }
279 
280     /**
281      * Remove the {@link Preference} that represents the {@code device}
282      */
removePreference(BluetoothDevice device)283     protected void removePreference(BluetoothDevice device) {
284         if (mPreferenceMap.containsKey(device)) {
285             if (mPreferenceMap.get(device) instanceof BluetoothDevicePreference preference) {
286                 BluetoothDevice prefDevice = preference.getBluetoothDevice().getDevice();
287                 // For CSIP device, when it {@link CachedBluetoothDevice}#switchMemberDeviceContent,
288                 // it will change its mDevice and lead to the hashcode change for this preference.
289                 // This will cause unintended remove preference, see b/394765052
290                 if (device.equals(prefDevice) || !mPreferenceMap.containsKey(prefDevice)) {
291                     mDevicePreferenceCallback.onDeviceRemoved(preference);
292                 } else {
293                     Log.w(getLogTag(), "Inconsistent key and preference when removePreference");
294                 }
295                 mPreferenceMap.remove(device);
296             } else {
297                 mDevicePreferenceCallback.onDeviceRemoved(mPreferenceMap.get(device));
298                 mPreferenceMap.remove(device);
299             }
300         }
301     }
302 
303     /**
304      * Get {@link CachedBluetoothDevice} from {@link Preference} and it is used to init
305      * {@link SubSettingLauncher} to launch {@link BluetoothDeviceDetailsFragment}
306      */
launchDeviceDetails(Preference preference)307     protected void launchDeviceDetails(Preference preference) {
308         mMetricsFeatureProvider.logClickedPreference(preference, mMetricsCategory);
309         final CachedBluetoothDevice device =
310                 ((BluetoothDevicePreference) preference).getBluetoothDevice();
311         if (device == null) {
312             return;
313         }
314         final Bundle args = new Bundle();
315         args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS,
316                 device.getDevice().getAddress());
317 
318         new SubSettingLauncher(mContext)
319                 .setDestination(BluetoothDeviceDetailsFragment.class.getName())
320                 .setArguments(args)
321                 .setTitleRes(R.string.device_details_title)
322                 .setSourceMetricsCategory(mMetricsCategory)
323                 .launch();
324     }
325 
326     /**
327      * @return {@code true} if {@code cachedBluetoothDevice} is connected
328      * and the bond state is bonded.
329      */
isDeviceConnected(CachedBluetoothDevice cachedDevice)330     public boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
331         if (cachedDevice == null) {
332             return false;
333         }
334         final BluetoothDevice device = cachedDevice.getDevice();
335         if (DBG) {
336             Log.d(getLogTag(), "isDeviceConnected() device name : " + cachedDevice.getName()
337                     + ", is connected : " + device.isConnected() + " , is profile connected : "
338                     + cachedDevice.isConnected());
339         }
340         return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected();
341     }
342 
343     /**
344      * Update the attributes of {@link Preference}.
345      */
refreshPreference()346     public void refreshPreference() {
347         List<BluetoothDevice> removeList = new ArrayList<>();
348         mPreferenceMap.forEach((key, preference) -> {
349             if (isDeviceOfMapInCachedDevicesList(key)) {
350                 ((BluetoothDevicePreference) preference).onPreferenceAttributesChanged();
351             } else {
352                 // If the BluetoothDevice of preference is not in the CachedDevices List, then
353                 // remove this preference.
354                 removeList.add(key);
355             }
356         });
357 
358         for (BluetoothDevice bluetoothDevice : removeList) {
359             Log.d(getLogTag(), "removePreference key: " + bluetoothDevice.getAnonymizedAddress());
360             removePreference(bluetoothDevice);
361         }
362     }
363 
isDeviceInCachedDevicesList(CachedBluetoothDevice cachedDevice)364     protected boolean isDeviceInCachedDevicesList(CachedBluetoothDevice cachedDevice) {
365         return mLocalManager.getCachedDeviceManager().getCachedDevicesCopy().contains(cachedDevice);
366     }
367 
isDeviceOfMapInCachedDevicesList(BluetoothDevice inputBluetoothDevice)368     protected boolean isDeviceOfMapInCachedDevicesList(BluetoothDevice inputBluetoothDevice) {
369         Collection<CachedBluetoothDevice> cachedDevices =
370                 mLocalManager.getCachedDeviceManager().getCachedDevicesCopy();
371         if (cachedDevices == null || cachedDevices.isEmpty()) {
372             return false;
373         }
374         return cachedDevices.stream()
375                 .anyMatch(cachedBluetoothDevice -> cachedBluetoothDevice.getDevice() != null
376                         && cachedBluetoothDevice.getDevice().equals(inputBluetoothDevice));
377     }
378 
getLogTag()379     protected String getLogTag() {
380         return TAG;
381     }
382 }
383