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.car;
17 
18 import android.bluetooth.BluetoothA2dpSink;
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothHeadsetClient;
22 import android.bluetooth.BluetoothMapClient;
23 import android.bluetooth.BluetoothPan;
24 import android.bluetooth.BluetoothPbapClient;
25 import android.bluetooth.BluetoothProfile;
26 import android.car.ICarBluetoothUserService;
27 import android.util.Log;
28 import android.util.SparseBooleanArray;
29 
30 import java.util.Arrays;
31 import java.util.List;
32 import java.util.Objects;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.locks.Condition;
35 import java.util.concurrent.locks.ReentrantLock;
36 
37 public class CarBluetoothUserService extends ICarBluetoothUserService.Stub {
38     private static final String TAG = "CarBluetoothUserService";
39     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
40     private final PerUserCarService mService;
41     private final BluetoothAdapter mBluetoothAdapter;
42 
43     // Profiles we support
44     private static final List<Integer> sProfilesToConnect = Arrays.asList(
45             BluetoothProfile.HEADSET_CLIENT,
46             BluetoothProfile.PBAP_CLIENT,
47             BluetoothProfile.A2DP_SINK,
48             BluetoothProfile.MAP_CLIENT,
49             BluetoothProfile.PAN
50     );
51 
52     // Profile Proxies Objects to pair with above list. Access to these proxy objects will all be
53     // guarded by the below mBluetoothProxyLock
54     private BluetoothA2dpSink mBluetoothA2dpSink = null;
55     private BluetoothHeadsetClient mBluetoothHeadsetClient = null;
56     private BluetoothPbapClient mBluetoothPbapClient = null;
57     private BluetoothMapClient mBluetoothMapClient = null;
58     private BluetoothPan mBluetoothPan = null;
59 
60     // Concurrency variables for waitForProxies. Used so we can best effort block with a timeout
61     // while waiting for services to be bound to the proxy objects.
62     private final ReentrantLock mBluetoothProxyLock;
63     private final Condition mConditionAllProxiesConnected;
64     private SparseBooleanArray mBluetoothProfileStatus;
65     private int mConnectedProfiles;
66     private static final int PROXY_OPERATION_TIMEOUT_MS = 8000;
67 
68     /**
69      * Create a CarBluetoothUserService instance.
70      *
71      * @param service - A reference to a PerUserCarService, so we can use its context to receive
72      *                 updates as a particular user.
73      */
CarBluetoothUserService(PerUserCarService service)74     public CarBluetoothUserService(PerUserCarService service) {
75         mService = service;
76         mConnectedProfiles = 0;
77         mBluetoothProfileStatus = new SparseBooleanArray();
78         for (int profile : sProfilesToConnect) {
79             mBluetoothProfileStatus.put(profile, false);
80         }
81         mBluetoothProxyLock = new ReentrantLock();
82         mConditionAllProxiesConnected = mBluetoothProxyLock.newCondition();
83         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
84         Objects.requireNonNull(mBluetoothAdapter, "Bluetooth adapter cannot be null");
85     }
86 
87     /**
88      * Setup connections to the profile proxy objects that talk to the Bluetooth profile services.
89      *
90      * Proxy references are held by the Bluetooth Framework on our behalf. We will be notified each
91      * time the underlying service connects for each proxy we create. Notifications stop when we
92      * close the proxy. As such, each time this is called we clean up any existing proxies before
93      * creating new ones.
94      */
95     @Override
setupBluetoothConnectionProxies()96     public void setupBluetoothConnectionProxies() {
97         logd("Initiate connections to profile proxies");
98 
99         // Clear existing proxy objects
100         closeBluetoothConnectionProxies();
101 
102         // Create proxy for each supported profile. Objects arrive later in the profile listener.
103         // Operations on the proxies expect them to be connected. Functions below should call
104         // waitForProxies() to best effort wait for them to be up if Bluetooth is enabled.
105         for (int profile : sProfilesToConnect) {
106             logd("Creating proxy for " + Utils.getProfileName(profile));
107             mBluetoothAdapter.getProfileProxy(mService.getApplicationContext(),
108                     mProfileListener, profile);
109         }
110     }
111 
112     /**
113      * Close connections to the profile proxy objects
114      */
115     @Override
closeBluetoothConnectionProxies()116     public void closeBluetoothConnectionProxies() {
117         logd("Clean up profile proxy objects");
118         mBluetoothProxyLock.lock();
119         try {
120             mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP_SINK, mBluetoothA2dpSink);
121             mBluetoothA2dpSink = null;
122             mBluetoothProfileStatus.put(BluetoothProfile.A2DP_SINK, false);
123 
124             mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT,
125                     mBluetoothHeadsetClient);
126             mBluetoothHeadsetClient = null;
127             mBluetoothProfileStatus.put(BluetoothProfile.HEADSET_CLIENT, false);
128 
129             mBluetoothAdapter.closeProfileProxy(BluetoothProfile.PBAP_CLIENT, mBluetoothPbapClient);
130             mBluetoothPbapClient = null;
131             mBluetoothProfileStatus.put(BluetoothProfile.PBAP_CLIENT, false);
132 
133             mBluetoothAdapter.closeProfileProxy(BluetoothProfile.MAP_CLIENT, mBluetoothMapClient);
134             mBluetoothMapClient = null;
135             mBluetoothProfileStatus.put(BluetoothProfile.MAP_CLIENT, false);
136 
137             mBluetoothAdapter.closeProfileProxy(BluetoothProfile.PAN, mBluetoothPan);
138             mBluetoothPan = null;
139             mBluetoothProfileStatus.put(BluetoothProfile.PAN, false);
140 
141             mConnectedProfiles = 0;
142         } finally {
143             mBluetoothProxyLock.unlock();
144         }
145     }
146 
147     /**
148      * Listen for and collect Bluetooth profile proxy connections and disconnections.
149      */
150     private BluetoothProfile.ServiceListener mProfileListener =
151             new BluetoothProfile.ServiceListener() {
152         public void onServiceConnected(int profile, BluetoothProfile proxy) {
153             logd("onServiceConnected profile: " + Utils.getProfileName(profile));
154 
155             // Grab the profile proxy object and update the status book keeping in one step so the
156             // book keeping and proxy objects never disagree
157             mBluetoothProxyLock.lock();
158             try {
159                 switch (profile) {
160                     case BluetoothProfile.A2DP_SINK:
161                         mBluetoothA2dpSink = (BluetoothA2dpSink) proxy;
162                         break;
163                     case BluetoothProfile.HEADSET_CLIENT:
164                         mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
165                         break;
166                     case BluetoothProfile.PBAP_CLIENT:
167                         mBluetoothPbapClient = (BluetoothPbapClient) proxy;
168                         break;
169                     case BluetoothProfile.MAP_CLIENT:
170                         mBluetoothMapClient = (BluetoothMapClient) proxy;
171                         break;
172                     case BluetoothProfile.PAN:
173                         mBluetoothPan = (BluetoothPan) proxy;
174                         break;
175                     default:
176                         logd("Unhandled profile connected: " + Utils.getProfileName(profile));
177                         break;
178                 }
179 
180                 if (!mBluetoothProfileStatus.get(profile, false)) {
181                     mBluetoothProfileStatus.put(profile, true);
182                     mConnectedProfiles++;
183                     if (mConnectedProfiles == sProfilesToConnect.size()) {
184                         logd("All profiles have connected");
185                         mConditionAllProxiesConnected.signal();
186                     }
187                 } else {
188                     Log.w(TAG, "Received duplicate service connection event for: "
189                             + Utils.getProfileName(profile));
190                 }
191             } finally {
192                 mBluetoothProxyLock.unlock();
193             }
194         }
195 
196         public void onServiceDisconnected(int profile) {
197             logd("onServiceDisconnected profile: " + Utils.getProfileName(profile));
198             mBluetoothProxyLock.lock();
199             try {
200                 if (mBluetoothProfileStatus.get(profile, false)) {
201                     mBluetoothProfileStatus.put(profile, false);
202                     mConnectedProfiles--;
203                 } else {
204                     Log.w(TAG, "Received duplicate service disconnection event for: "
205                             + Utils.getProfileName(profile));
206                 }
207             } finally {
208                 mBluetoothProxyLock.unlock();
209             }
210         }
211     };
212 
213     /**
214      * Check if a proxy is available for the given profile to talk to the Profile's bluetooth
215      * service.
216      *
217      * @param profile - Bluetooth profile to check for
218      * @return - true if proxy available, false if not.
219      */
220     @Override
isBluetoothConnectionProxyAvailable(int profile)221     public boolean isBluetoothConnectionProxyAvailable(int profile) {
222         if (!mBluetoothAdapter.isEnabled()) return false;
223         boolean proxyConnected = false;
224         mBluetoothProxyLock.lock();
225         try {
226             proxyConnected = mBluetoothProfileStatus.get(profile, false);
227         } finally {
228             mBluetoothProxyLock.unlock();
229         }
230         return proxyConnected;
231     }
232 
233     /**
234      * Wait for the proxy objects to be up for all profiles, with a timeout.
235      *
236      * @param timeout Amount of time in milliseconds to wait for giving up on the wait operation
237      * @return True if the condition was satisfied within the timeout, False otherwise
238      */
waitForProxies(int timeout )239     private boolean waitForProxies(int timeout /* ms */) {
240         logd("waitForProxies()");
241         // If bluetooth isn't on then the operation waiting on proxies was never meant to actually
242         // work regardless if Bluetooth comes on within the timeout period or not. Return false.
243         if (!mBluetoothAdapter.isEnabled()) return false;
244         try {
245             while (mConnectedProfiles != sProfilesToConnect.size()) {
246                 if (!mConditionAllProxiesConnected.await(
247                         timeout, TimeUnit.MILLISECONDS)) {
248                     Log.e(TAG, "Timeout while waiting for proxies, Connected: " + mConnectedProfiles
249                             + "/" + sProfilesToConnect.size());
250                     return false;
251                 }
252             }
253         } catch (InterruptedException e) {
254             Log.w(TAG, "waitForProxies: interrupted", e);
255             return false;
256         }
257         return true;
258     }
259 
260     /**
261      * Connect a given remote device on a specific Bluetooth profile
262      *
263      * @param profile BluetoothProfile.* based profile ID
264      * @param device The device you wish to connect
265      */
266     @Override
bluetoothConnectToProfile(int profile, BluetoothDevice device)267     public boolean bluetoothConnectToProfile(int profile, BluetoothDevice device) {
268         if (device == null) {
269             Log.e(TAG, "Cannot connect to profile on null device");
270             return false;
271         }
272         logd("Trying to connect to " + device.getName() + " (" + device.getAddress() + ") Profile: "
273                 + Utils.getProfileName(profile));
274         mBluetoothProxyLock.lock();
275         try {
276             if (!isBluetoothConnectionProxyAvailable(profile)
277                     && !waitForProxies(PROXY_OPERATION_TIMEOUT_MS)) {
278                 Log.e(TAG, "Cannot connect to Profile. Proxy Unavailable");
279                 return false;
280             }
281             switch (profile) {
282                 case BluetoothProfile.A2DP_SINK:
283                     return mBluetoothA2dpSink.connect(device);
284                 case BluetoothProfile.HEADSET_CLIENT:
285                     return mBluetoothHeadsetClient.connect(device);
286                 case BluetoothProfile.MAP_CLIENT:
287                     return mBluetoothMapClient.connect(device);
288                 case BluetoothProfile.PBAP_CLIENT:
289                     return mBluetoothPbapClient.connect(device);
290                 case BluetoothProfile.PAN:
291                     return mBluetoothPan.connect(device);
292                 default:
293                     Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
294                     break;
295             }
296         } finally {
297             mBluetoothProxyLock.unlock();
298         }
299         return false;
300     }
301 
302     /**
303      * Disonnect a given remote device from a specific Bluetooth profile
304      *
305      * @param profile BluetoothProfile.* based profile ID
306      * @param device The device you wish to disconnect
307      */
308     @Override
bluetoothDisconnectFromProfile(int profile, BluetoothDevice device)309     public boolean bluetoothDisconnectFromProfile(int profile, BluetoothDevice device) {
310         if (device == null) {
311             Log.e(TAG, "Cannot disconnect from profile on null device");
312             return false;
313         }
314         logd("Trying to disconnect from " + device.getName() + " (" + device.getAddress()
315                 + ") Profile: " + Utils.getProfileName(profile));
316         mBluetoothProxyLock.lock();
317         try {
318             if (!isBluetoothConnectionProxyAvailable(profile)
319                     && !waitForProxies(PROXY_OPERATION_TIMEOUT_MS)) {
320                 Log.e(TAG, "Cannot disconnect from profile. Proxy Unavailable");
321                 return false;
322             }
323             switch (profile) {
324                 case BluetoothProfile.A2DP_SINK:
325                     return mBluetoothA2dpSink.disconnect(device);
326                 case BluetoothProfile.HEADSET_CLIENT:
327                     return mBluetoothHeadsetClient.disconnect(device);
328                 case BluetoothProfile.MAP_CLIENT:
329                     return mBluetoothMapClient.disconnect(device);
330                 case BluetoothProfile.PBAP_CLIENT:
331                     return mBluetoothPbapClient.disconnect(device);
332                 case BluetoothProfile.PAN:
333                     return mBluetoothPan.disconnect(device);
334                 default:
335                     Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
336                     break;
337             }
338         } finally {
339             mBluetoothProxyLock.unlock();
340         }
341         return false;
342     }
343 
344     /**
345      * Get the priority of the given Bluetooth profile for the given remote device
346      *
347      * @param profile - Bluetooth profile
348      * @param device - remote Bluetooth device
349      */
350     @Override
getProfilePriority(int profile, BluetoothDevice device)351     public int getProfilePriority(int profile, BluetoothDevice device) {
352         if (device == null) {
353             Log.e(TAG, "Cannot get " + Utils.getProfileName(profile)
354                     + " profile priority on null device");
355             return BluetoothProfile.PRIORITY_UNDEFINED;
356         }
357         int priority;
358         mBluetoothProxyLock.lock();
359         try {
360             if (!isBluetoothConnectionProxyAvailable(profile)
361                     && !waitForProxies(PROXY_OPERATION_TIMEOUT_MS)) {
362                 Log.e(TAG, "Cannot get " + Utils.getProfileName(profile)
363                         + " profile priority. Proxy Unavailable");
364                 return BluetoothProfile.PRIORITY_UNDEFINED;
365             }
366             switch (profile) {
367                 case BluetoothProfile.A2DP_SINK:
368                     priority = mBluetoothA2dpSink.getPriority(device);
369                     break;
370                 case BluetoothProfile.HEADSET_CLIENT:
371                     priority = mBluetoothHeadsetClient.getPriority(device);
372                     break;
373                 case BluetoothProfile.MAP_CLIENT:
374                     priority = mBluetoothMapClient.getPriority(device);
375                     break;
376                 case BluetoothProfile.PBAP_CLIENT:
377                     priority = mBluetoothPbapClient.getPriority(device);
378                     break;
379                 default:
380                     Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
381                     priority = BluetoothProfile.PRIORITY_UNDEFINED;
382                     break;
383             }
384         } finally {
385             mBluetoothProxyLock.unlock();
386         }
387         logd(Utils.getProfileName(profile) + " priority for " + device.getName() + " ("
388                 + device.getAddress() + ") = " + priority);
389         return priority;
390     }
391 
392     /**
393      * Set the priority of the given Bluetooth profile for the given remote device
394      *
395      * @param profile - Bluetooth profile
396      * @param device - remote Bluetooth device
397      * @param priority - priority to set
398      */
399     @Override
setProfilePriority(int profile, BluetoothDevice device, int priority)400     public void setProfilePriority(int profile, BluetoothDevice device, int priority) {
401         if (device == null) {
402             Log.e(TAG, "Cannot set " + Utils.getProfileName(profile)
403                     + " profile priority on null device");
404             return;
405         }
406         logd("Setting " + Utils.getProfileName(profile) + " priority for " + device.getName() + " ("
407                 + device.getAddress() + ") to " + priority);
408         mBluetoothProxyLock.lock();
409         try {
410             if (!isBluetoothConnectionProxyAvailable(profile)
411                     && !waitForProxies(PROXY_OPERATION_TIMEOUT_MS)) {
412                 Log.e(TAG, "Cannot set " + Utils.getProfileName(profile)
413                         + " profile priority. Proxy Unavailable");
414                 return;
415             }
416             switch (profile) {
417                 case BluetoothProfile.A2DP_SINK:
418                     mBluetoothA2dpSink.setPriority(device, priority);
419                     break;
420                 case BluetoothProfile.HEADSET_CLIENT:
421                     mBluetoothHeadsetClient.setPriority(device, priority);
422                     break;
423                 case BluetoothProfile.MAP_CLIENT:
424                     mBluetoothMapClient.setPriority(device, priority);
425                     break;
426                 case BluetoothProfile.PBAP_CLIENT:
427                     mBluetoothPbapClient.setPriority(device, priority);
428                     break;
429                 default:
430                     Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
431                     break;
432             }
433         } finally {
434             mBluetoothProxyLock.unlock();
435         }
436     }
437 
438     /**
439      * Log to debug if debug output is enabled
440      */
logd(String msg)441     private void logd(String msg) {
442         if (DBG) {
443             Log.d(TAG, msg);
444         }
445     }
446 }
447