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