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