1 /* 2 * Copyright (C) 2016 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 17 package com.android.server.telecom.bluetooth; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothHeadset; 22 import android.bluetooth.BluetoothHearingAid; 23 import android.bluetooth.BluetoothProfile; 24 import android.content.Context; 25 import android.telecom.Log; 26 import android.util.LocalLog; 27 28 import com.android.internal.util.IndentingPrintWriter; 29 import com.android.server.telecom.BluetoothAdapterProxy; 30 import com.android.server.telecom.BluetoothHeadsetProxy; 31 32 import java.util.ArrayList; 33 import java.util.Collection; 34 import java.util.Collections; 35 import java.util.LinkedHashMap; 36 import java.util.LinkedHashSet; 37 import java.util.LinkedList; 38 import java.util.List; 39 import java.util.Set; 40 41 public class BluetoothDeviceManager { 42 private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = 43 new BluetoothProfile.ServiceListener() { 44 @Override 45 public void onServiceConnected(int profile, BluetoothProfile proxy) { 46 Log.startSession("BMSL.oSC"); 47 try { 48 synchronized (mLock) { 49 String logString; 50 if (profile == BluetoothProfile.HEADSET) { 51 mBluetoothHeadsetService = 52 new BluetoothHeadsetProxy((BluetoothHeadset) proxy); 53 logString = "Got BluetoothHeadset: " + mBluetoothHeadsetService; 54 } else if (profile == BluetoothProfile.HEARING_AID) { 55 mBluetoothHearingAidService = (BluetoothHearingAid) proxy; 56 logString = "Got BluetoothHearingAid: " 57 + mBluetoothHearingAidService; 58 } else { 59 logString = "Connected to non-requested bluetooth service." + 60 " Not changing bluetooth headset."; 61 } 62 Log.i(BluetoothDeviceManager.this, logString); 63 mLocalLog.log(logString); 64 } 65 } finally { 66 Log.endSession(); 67 } 68 } 69 70 @Override 71 public void onServiceDisconnected(int profile) { 72 Log.startSession("BMSL.oSD"); 73 try { 74 synchronized (mLock) { 75 LinkedHashMap<String, BluetoothDevice> lostServiceDevices; 76 String logString; 77 if (profile == BluetoothProfile.HEADSET) { 78 mBluetoothHeadsetService = null; 79 lostServiceDevices = mHfpDevicesByAddress; 80 mBluetoothRouteManager.onActiveDeviceChanged(null, false); 81 logString = "Lost BluetoothHeadset service. " + 82 "Removing all tracked devices"; 83 } else if (profile == BluetoothProfile.HEARING_AID) { 84 mBluetoothHearingAidService = null; 85 logString = "Lost BluetoothHearingAid service. " + 86 "Removing all tracked devices."; 87 lostServiceDevices = mHearingAidDevicesByAddress; 88 mBluetoothRouteManager.onActiveDeviceChanged(null, true); 89 } else { 90 return; 91 } 92 Log.i(BluetoothDeviceManager.this, logString); 93 mLocalLog.log(logString); 94 95 List<BluetoothDevice> devicesToRemove = new LinkedList<>( 96 lostServiceDevices.values()); 97 lostServiceDevices.clear(); 98 for (BluetoothDevice device : devicesToRemove) { 99 mBluetoothRouteManager.onDeviceLost(device.getAddress()); 100 } 101 } 102 } finally { 103 Log.endSession(); 104 } 105 } 106 }; 107 108 private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress = 109 new LinkedHashMap<>(); 110 private final LinkedHashMap<String, BluetoothDevice> mHearingAidDevicesByAddress = 111 new LinkedHashMap<>(); 112 private final LinkedHashMap<BluetoothDevice, Long> mHearingAidDeviceSyncIds = 113 new LinkedHashMap<>(); 114 private final LocalLog mLocalLog = new LocalLog(20); 115 116 // This lock only protects internal state -- it doesn't lock on anything going into Telecom. 117 private final Object mLock = new Object(); 118 119 private BluetoothRouteManager mBluetoothRouteManager; 120 private BluetoothHeadsetProxy mBluetoothHeadsetService; 121 private BluetoothHearingAid mBluetoothHearingAidService; 122 private BluetoothDevice mBluetoothHearingAidActiveDeviceCache; 123 private BluetoothAdapterProxy mBluetoothAdapterProxy; 124 BluetoothDeviceManager(Context context, BluetoothAdapterProxy bluetoothAdapter)125 public BluetoothDeviceManager(Context context, BluetoothAdapterProxy bluetoothAdapter) { 126 if (bluetoothAdapter != null) { 127 mBluetoothAdapterProxy = bluetoothAdapter; 128 bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener, 129 BluetoothProfile.HEADSET); 130 bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener, 131 BluetoothProfile.HEARING_AID); 132 } 133 } 134 setBluetoothRouteManager(BluetoothRouteManager brm)135 public void setBluetoothRouteManager(BluetoothRouteManager brm) { 136 mBluetoothRouteManager = brm; 137 } 138 getNumConnectedDevices()139 public int getNumConnectedDevices() { 140 synchronized (mLock) { 141 return mHfpDevicesByAddress.size() + mHearingAidDevicesByAddress.size(); 142 } 143 } 144 getConnectedDevices()145 public Collection<BluetoothDevice> getConnectedDevices() { 146 synchronized (mLock) { 147 ArrayList<BluetoothDevice> result = new ArrayList<>(mHfpDevicesByAddress.values()); 148 result.addAll(mHearingAidDevicesByAddress.values()); 149 return Collections.unmodifiableCollection(result); 150 } 151 } 152 153 // Same as getConnectedDevices except it filters out the hearing aid devices that are linked 154 // together by their hiSyncId. getUniqueConnectedDevices()155 public Collection<BluetoothDevice> getUniqueConnectedDevices() { 156 ArrayList<BluetoothDevice> result; 157 synchronized (mLock) { 158 result = new ArrayList<>(mHfpDevicesByAddress.values()); 159 } 160 Set<Long> seenHiSyncIds = new LinkedHashSet<>(); 161 // Add the left-most active device to the seen list so that we match up with the list 162 // generated in BluetoothRouteManager. 163 if (mBluetoothHearingAidService != null) { 164 for (BluetoothDevice device : mBluetoothHearingAidService.getActiveDevices()) { 165 if (device != null) { 166 result.add(device); 167 seenHiSyncIds.add(mHearingAidDeviceSyncIds.getOrDefault(device, -1L)); 168 break; 169 } 170 } 171 } 172 synchronized (mLock) { 173 for (BluetoothDevice d : mHearingAidDevicesByAddress.values()) { 174 long hiSyncId = mHearingAidDeviceSyncIds.getOrDefault(d, -1L); 175 if (seenHiSyncIds.contains(hiSyncId)) { 176 continue; 177 } 178 result.add(d); 179 seenHiSyncIds.add(hiSyncId); 180 } 181 } 182 return Collections.unmodifiableCollection(result); 183 } 184 getHeadsetService()185 public BluetoothHeadsetProxy getHeadsetService() { 186 return mBluetoothHeadsetService; 187 } 188 getHearingAidService()189 public BluetoothHearingAid getHearingAidService() { 190 return mBluetoothHearingAidService; 191 } 192 setHeadsetServiceForTesting(BluetoothHeadsetProxy bluetoothHeadset)193 public void setHeadsetServiceForTesting(BluetoothHeadsetProxy bluetoothHeadset) { 194 mBluetoothHeadsetService = bluetoothHeadset; 195 } 196 setHearingAidServiceForTesting(BluetoothHearingAid bluetoothHearingAid)197 public void setHearingAidServiceForTesting(BluetoothHearingAid bluetoothHearingAid) { 198 mBluetoothHearingAidService = bluetoothHearingAid; 199 } 200 onDeviceConnected(BluetoothDevice device, boolean isHearingAid)201 void onDeviceConnected(BluetoothDevice device, boolean isHearingAid) { 202 mLocalLog.log("Device connected -- address: " + device.getAddress() + " isHeadingAid: " 203 + isHearingAid); 204 synchronized (mLock) { 205 LinkedHashMap<String, BluetoothDevice> targetDeviceMap; 206 if (isHearingAid) { 207 if (mBluetoothHearingAidService == null) { 208 Log.w(this, "Hearing aid service null when receiving device added broadcast"); 209 return; 210 } 211 long hiSyncId = mBluetoothHearingAidService.getHiSyncId(device); 212 mHearingAidDeviceSyncIds.put(device, hiSyncId); 213 targetDeviceMap = mHearingAidDevicesByAddress; 214 } else { 215 if (mBluetoothHeadsetService == null) { 216 Log.w(this, "Headset service null when receiving device added broadcast"); 217 return; 218 } 219 targetDeviceMap = mHfpDevicesByAddress; 220 } 221 if (!targetDeviceMap.containsKey(device.getAddress())) { 222 targetDeviceMap.put(device.getAddress(), device); 223 mBluetoothRouteManager.onDeviceAdded(device.getAddress()); 224 } 225 } 226 } 227 onDeviceDisconnected(BluetoothDevice device, boolean isHearingAid)228 void onDeviceDisconnected(BluetoothDevice device, boolean isHearingAid) { 229 mLocalLog.log("Device disconnected -- address: " + device.getAddress() + " isHeadingAid: " 230 + isHearingAid); 231 synchronized (mLock) { 232 LinkedHashMap<String, BluetoothDevice> targetDeviceMap; 233 if (isHearingAid) { 234 mHearingAidDeviceSyncIds.remove(device); 235 targetDeviceMap = mHearingAidDevicesByAddress; 236 } else { 237 targetDeviceMap = mHfpDevicesByAddress; 238 } 239 if (targetDeviceMap.containsKey(device.getAddress())) { 240 targetDeviceMap.remove(device.getAddress()); 241 mBluetoothRouteManager.onDeviceLost(device.getAddress()); 242 } 243 } 244 } 245 disconnectAudio()246 public void disconnectAudio() { 247 if (mBluetoothHearingAidService == null) { 248 Log.w(this, "Trying to disconnect audio but no hearing aid service exists"); 249 } else { 250 for (BluetoothDevice device : mBluetoothHearingAidService.getActiveDevices()) { 251 if (device != null) { 252 mBluetoothAdapterProxy.setActiveDevice(null, 253 BluetoothAdapter.ACTIVE_DEVICE_ALL); 254 } 255 } 256 } 257 disconnectSco(); 258 } 259 disconnectSco()260 public void disconnectSco() { 261 if (mBluetoothHeadsetService == null) { 262 Log.w(this, "Trying to disconnect audio but no headset service exists."); 263 } else { 264 mBluetoothHeadsetService.disconnectAudio(); 265 } 266 } 267 268 // Connect audio to the bluetooth device at address, checking to see whether it's a hearing aid 269 // or a HFP device, and using the proper BT API. connectAudio(String address)270 public boolean connectAudio(String address) { 271 if (mHearingAidDevicesByAddress.containsKey(address)) { 272 if (mBluetoothHearingAidService == null) { 273 Log.w(this, "Attempting to turn on audio when the hearing aid service is null"); 274 return false; 275 } 276 return mBluetoothAdapterProxy.setActiveDevice( 277 mHearingAidDevicesByAddress.get(address), 278 BluetoothAdapter.ACTIVE_DEVICE_ALL); 279 } else if (mHfpDevicesByAddress.containsKey(address)) { 280 BluetoothDevice device = mHfpDevicesByAddress.get(address); 281 if (mBluetoothHeadsetService == null) { 282 Log.w(this, "Attempting to turn on audio when the headset service is null"); 283 return false; 284 } 285 boolean success = mBluetoothAdapterProxy.setActiveDevice(device, 286 BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL); 287 if (!success) { 288 Log.w(this, "Couldn't set active device to %s", address); 289 return false; 290 } 291 if (!mBluetoothHeadsetService.isAudioOn()) { 292 return mBluetoothHeadsetService.connectAudio(); 293 } 294 return true; 295 } else { 296 Log.w(this, "Attempting to turn on audio for a disconnected device"); 297 return false; 298 } 299 } 300 cacheHearingAidDevice()301 public void cacheHearingAidDevice() { 302 if (mBluetoothHearingAidService != null) { 303 for (BluetoothDevice device : mBluetoothHearingAidService.getActiveDevices()) { 304 if (device != null) { 305 mBluetoothHearingAidActiveDeviceCache = device; 306 } 307 } 308 } 309 } 310 restoreHearingAidDevice()311 public void restoreHearingAidDevice() { 312 if (mBluetoothHearingAidActiveDeviceCache != null && mBluetoothHearingAidService != null) { 313 mBluetoothAdapterProxy.setActiveDevice( 314 mBluetoothHearingAidActiveDeviceCache, 315 BluetoothAdapter.ACTIVE_DEVICE_ALL); 316 mBluetoothHearingAidActiveDeviceCache = null; 317 } 318 } 319 dump(IndentingPrintWriter pw)320 public void dump(IndentingPrintWriter pw) { 321 mLocalLog.dump(pw); 322 } 323 } 324