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