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.getFactory(context).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 if (DBG) { 187 Log.d(getLogTag(), "onAclConnectionStateChanged() device: " + cachedDevice.getName() 188 + ", state: " + state); 189 } 190 update(cachedDevice); 191 } 192 193 @Override onServiceConnected()194 public void onServiceConnected() { 195 // When bluetooth service connected update the UI 196 forceUpdate(); 197 } 198 199 @Override onServiceDisconnected()200 public void onServiceDisconnected() { 201 202 } 203 204 /** 205 * Set the context to generate the {@link Preference}, so it could get the correct theme. 206 */ setPrefContext(Context context)207 public void setPrefContext(Context context) { 208 mPrefContext = context; 209 } 210 211 /** 212 * Return {@code true} if {@code cachedBluetoothDevice} matches this 213 * {@link BluetoothDeviceUpdater} and should stay in the list, otherwise return {@code false} 214 */ isFilterMatched(CachedBluetoothDevice cachedBluetoothDevice)215 public abstract boolean isFilterMatched(CachedBluetoothDevice cachedBluetoothDevice); 216 217 /** 218 * Return a preference key for logging 219 */ getPreferenceKey()220 protected abstract String getPreferenceKey(); 221 222 /** 223 * Update whether to show {@link CachedBluetoothDevice} in the list. 224 */ update(CachedBluetoothDevice cachedBluetoothDevice)225 protected void update(CachedBluetoothDevice cachedBluetoothDevice) { 226 if (isFilterMatched(cachedBluetoothDevice)) { 227 // Add the preference if it is new one 228 addPreference(cachedBluetoothDevice); 229 } else { 230 removePreference(cachedBluetoothDevice); 231 } 232 } 233 234 /** 235 * Add the {@link Preference} that represents the {@code cachedDevice} 236 */ addPreference(CachedBluetoothDevice cachedDevice)237 protected void addPreference(CachedBluetoothDevice cachedDevice) { 238 addPreference(cachedDevice, BluetoothDevicePreference.SortType.TYPE_DEFAULT); 239 } 240 241 /** 242 * Add the {@link Preference} with {@link BluetoothDevicePreference.SortType} that 243 * represents the {@code cachedDevice} 244 */ addPreference(CachedBluetoothDevice cachedDevice, @BluetoothDevicePreference.SortType int type)245 protected void addPreference(CachedBluetoothDevice cachedDevice, 246 @BluetoothDevicePreference.SortType int type) { 247 final BluetoothDevice device = cachedDevice.getDevice(); 248 if (!mPreferenceMap.containsKey(device)) { 249 BluetoothDevicePreference btPreference = 250 new BluetoothDevicePreference(mPrefContext, cachedDevice, 251 true /* showDeviceWithoutNames */, 252 type); 253 btPreference.setKey(getPreferenceKey()); 254 btPreference.setOnGearClickListener(mDeviceProfilesListener); 255 if (this instanceof Preference.OnPreferenceClickListener) { 256 btPreference.setOnPreferenceClickListener( 257 (Preference.OnPreferenceClickListener) this); 258 } 259 mPreferenceMap.put(device, btPreference); 260 mDevicePreferenceCallback.onDeviceAdded(btPreference); 261 } 262 } 263 264 /** 265 * Remove the {@link Preference} that represents the {@code cachedDevice} 266 */ removePreference(CachedBluetoothDevice cachedDevice)267 protected void removePreference(CachedBluetoothDevice cachedDevice) { 268 final BluetoothDevice device = cachedDevice.getDevice(); 269 final CachedBluetoothDevice subCachedDevice = cachedDevice.getSubDevice(); 270 if (mPreferenceMap.containsKey(device)) { 271 removePreference(device); 272 } else if (subCachedDevice != null) { 273 // When doing remove, to check if preference maps to sub device. 274 // This would happen when connection state is changed in detail page that there is no 275 // callback from SettingsLib. 276 final BluetoothDevice subDevice = subCachedDevice.getDevice(); 277 removePreference(subDevice); 278 } 279 } 280 removePreference(BluetoothDevice device)281 private void removePreference(BluetoothDevice device) { 282 if (mPreferenceMap.containsKey(device)) { 283 mDevicePreferenceCallback.onDeviceRemoved(mPreferenceMap.get(device)); 284 mPreferenceMap.remove(device); 285 } 286 } 287 288 /** 289 * Get {@link CachedBluetoothDevice} from {@link Preference} and it is used to init 290 * {@link SubSettingLauncher} to launch {@link BluetoothDeviceDetailsFragment} 291 */ launchDeviceDetails(Preference preference)292 protected void launchDeviceDetails(Preference preference) { 293 mMetricsFeatureProvider.logClickedPreference(preference, mMetricsCategory); 294 final CachedBluetoothDevice device = 295 ((BluetoothDevicePreference) preference).getBluetoothDevice(); 296 if (device == null) { 297 return; 298 } 299 final Bundle args = new Bundle(); 300 args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, 301 device.getDevice().getAddress()); 302 303 new SubSettingLauncher(mContext) 304 .setDestination(BluetoothDeviceDetailsFragment.class.getName()) 305 .setArguments(args) 306 .setTitleRes(R.string.device_details_title) 307 .setSourceMetricsCategory(mMetricsCategory) 308 .launch(); 309 } 310 311 /** 312 * @return {@code true} if {@code cachedBluetoothDevice} is connected 313 * and the bond state is bonded. 314 */ isDeviceConnected(CachedBluetoothDevice cachedDevice)315 public boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) { 316 if (cachedDevice == null) { 317 return false; 318 } 319 final BluetoothDevice device = cachedDevice.getDevice(); 320 if (DBG) { 321 Log.d(getLogTag(), "isDeviceConnected() device name : " + cachedDevice.getName() 322 + ", is connected : " + device.isConnected() + " , is profile connected : " 323 + cachedDevice.isConnected()); 324 } 325 return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected(); 326 } 327 328 /** 329 * Update the attributes of {@link Preference}. 330 */ refreshPreference()331 public void refreshPreference() { 332 List<BluetoothDevice> removeList = new ArrayList<>(); 333 mPreferenceMap.forEach((key, preference) -> { 334 if (isDeviceOfMapInCachedDevicesList(key)) { 335 ((BluetoothDevicePreference) preference).onPreferenceAttributesChanged(); 336 } else { 337 // If the BluetoothDevice of preference is not in the CachedDevices List, then 338 // remove this preference. 339 removeList.add(key); 340 } 341 }); 342 343 for (BluetoothDevice bluetoothDevice : removeList) { 344 Log.d(getLogTag(), "removePreference key: " + bluetoothDevice.getAnonymizedAddress()); 345 removePreference(bluetoothDevice); 346 } 347 } 348 isDeviceInCachedDevicesList(CachedBluetoothDevice cachedDevice)349 protected boolean isDeviceInCachedDevicesList(CachedBluetoothDevice cachedDevice) { 350 return mLocalManager.getCachedDeviceManager().getCachedDevicesCopy().contains(cachedDevice); 351 } 352 isDeviceOfMapInCachedDevicesList(BluetoothDevice inputBluetoothDevice)353 private boolean isDeviceOfMapInCachedDevicesList(BluetoothDevice inputBluetoothDevice) { 354 Collection<CachedBluetoothDevice> cachedDevices = 355 mLocalManager.getCachedDeviceManager().getCachedDevicesCopy(); 356 if (cachedDevices == null || cachedDevices.isEmpty()) { 357 return false; 358 } 359 return cachedDevices.stream() 360 .anyMatch(cachedBluetoothDevice -> cachedBluetoothDevice.getDevice() != null 361 && cachedBluetoothDevice.getDevice().equals(inputBluetoothDevice)); 362 } 363 getLogTag()364 protected String getLogTag() { 365 return TAG; 366 } 367 } 368