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