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