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