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