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