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