• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 android.bluetooth;
18 
19 import android.Manifest;
20 import android.annotation.NonNull;
21 import android.annotation.RequiresNoPermission;
22 import android.annotation.RequiresPermission;
23 import android.annotation.SdkConstant;
24 import android.annotation.SdkConstant.SdkConstantType;
25 import android.annotation.SuppressLint;
26 import android.annotation.SystemApi;
27 import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
28 import android.compat.annotation.UnsupportedAppUsage;
29 import android.content.AttributionSource;
30 import android.content.Context;
31 import android.os.Build;
32 import android.os.IBinder;
33 import android.os.IpcDataCache;
34 import android.os.RemoteException;
35 import android.util.CloseGuard;
36 import android.util.Log;
37 import android.util.Pair;
38 
39 import java.util.Collections;
40 import java.util.List;
41 
42 /**
43  * This class provides the APIs to control the Bluetooth MAP Profile.
44  *
45  * @hide
46  */
47 @SystemApi
48 public final class BluetoothMap implements BluetoothProfile, AutoCloseable {
49 
50     private static final String TAG = "BluetoothMap";
51     private static final boolean DBG = true;
52     private static final boolean VDBG = false;
53 
54     private CloseGuard mCloseGuard;
55 
56     /** @hide */
57     @SuppressLint("ActionValue")
58     @SystemApi
59     @RequiresBluetoothConnectPermission
60     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
61     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
62     public static final String ACTION_CONNECTION_STATE_CHANGED =
63             "android.bluetooth.map.profile.action.CONNECTION_STATE_CHANGED";
64 
65     /**
66      * There was an error trying to obtain the state
67      *
68      * @hide
69      */
70     public static final int STATE_ERROR = -1;
71 
72     /** @hide */
73     public static final int RESULT_FAILURE = 0;
74 
75     /** @hide */
76     public static final int RESULT_SUCCESS = 1;
77 
78     /**
79      * Connection canceled before completion.
80      *
81      * @hide
82      */
83     public static final int RESULT_CANCELED = 2;
84 
85     private final BluetoothAdapter mAdapter;
86     private final AttributionSource mAttributionSource;
87 
88     private IBluetoothMap mService;
89 
90     /** Create a BluetoothMap proxy object. */
BluetoothMap(Context context, BluetoothAdapter adapter)91     /* package */ BluetoothMap(Context context, BluetoothAdapter adapter) {
92         if (DBG) Log.d(TAG, "Create BluetoothMap proxy object");
93         mAdapter = adapter;
94         mAttributionSource = adapter.getAttributionSource();
95         mService = null;
96         mCloseGuard = new CloseGuard();
97         mCloseGuard.open("close");
98     }
99 
100     @Override
101     @SuppressWarnings("Finalize") // TODO(b/314811467)
finalize()102     protected void finalize() {
103         if (mCloseGuard != null) {
104             mCloseGuard.warnIfOpen();
105         }
106         close();
107     }
108 
109     /**
110      * Close the connection to the backing service. Other public functions of BluetoothMap will
111      * return default error results once close() has been called. Multiple invocations of close()
112      * are ok.
113      *
114      * @hide
115      */
116     @SystemApi
117     @Override
close()118     public void close() {
119         if (VDBG) log("close()");
120         mAdapter.closeProfileProxy(this);
121     }
122 
123     /** @hide */
124     @Override
onServiceConnected(IBinder service)125     public void onServiceConnected(IBinder service) {
126         mService = IBluetoothMap.Stub.asInterface(service);
127     }
128 
129     /** @hide */
130     @Override
onServiceDisconnected()131     public void onServiceDisconnected() {
132         mService = null;
133     }
134 
getService()135     private IBluetoothMap getService() {
136         return mService;
137     }
138 
139     /** @hide */
140     @Override
getAdapter()141     public BluetoothAdapter getAdapter() {
142         return mAdapter;
143     }
144 
145     /**
146      * Get the current state of the BluetoothMap service.
147      *
148      * @return One of the STATE_ return codes, or STATE_ERROR if this proxy object is currently not
149      *     connected to the Map service.
150      * @hide
151      */
152     @RequiresBluetoothConnectPermission
153     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getState()154     public int getState() {
155         if (VDBG) log("getState()");
156         final IBluetoothMap service = getService();
157         if (service == null) {
158             Log.w(TAG, "Proxy not attached to service");
159             if (DBG) log(Log.getStackTraceString(new Throwable()));
160         } else if (isEnabled()) {
161             try {
162                 return service.getState(mAttributionSource);
163             } catch (RemoteException e) {
164                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
165             }
166         }
167         return BluetoothMap.STATE_ERROR;
168     }
169 
170     /**
171      * Get the currently connected remote Bluetooth device (PCE).
172      *
173      * @return The remote Bluetooth device, or null if not in connected or connecting state, or if
174      *     this proxy object is not connected to the Map service.
175      * @hide
176      */
177     @RequiresBluetoothConnectPermission
178     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getClient()179     public BluetoothDevice getClient() {
180         if (VDBG) log("getClient()");
181         final IBluetoothMap service = getService();
182         if (service == null) {
183             Log.w(TAG, "Proxy not attached to service");
184             if (DBG) log(Log.getStackTraceString(new Throwable()));
185         } else if (isEnabled()) {
186             try {
187                 return Attributable.setAttributionSource(
188                         service.getClient(mAttributionSource), mAttributionSource);
189             } catch (RemoteException e) {
190                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
191             }
192         }
193         return null;
194     }
195 
196     /**
197      * Returns true if the specified Bluetooth device is connected. Returns false if not connected,
198      * or if this proxy object is not currently connected to the Map service.
199      *
200      * @hide
201      */
202     @RequiresBluetoothConnectPermission
203     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
isConnected(BluetoothDevice device)204     public boolean isConnected(BluetoothDevice device) {
205         if (VDBG) log("isConnected(" + device + ")");
206         final IBluetoothMap service = getService();
207         if (service == null) {
208             Log.w(TAG, "Proxy not attached to service");
209             if (DBG) log(Log.getStackTraceString(new Throwable()));
210         } else if (isEnabled() && isValidDevice(device)) {
211             try {
212                 return service.isConnected(device, mAttributionSource);
213             } catch (RemoteException e) {
214                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
215             }
216         }
217         return false;
218     }
219 
220     /**
221      * Initiate connection. Initiation of outgoing connections is not supported for MAP server.
222      *
223      * @hide
224      */
225     @RequiresNoPermission
connect(BluetoothDevice device)226     public boolean connect(BluetoothDevice device) {
227         if (DBG) log("connect(" + device + ")" + "not supported for MAPS");
228         return false;
229     }
230 
231     /**
232      * Initiate disconnect.
233      *
234      * @param device Remote Bluetooth Device
235      * @return false on error, true otherwise
236      * @hide
237      */
238     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
239     @RequiresBluetoothConnectPermission
240     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
disconnect(BluetoothDevice device)241     public boolean disconnect(BluetoothDevice device) {
242         if (DBG) log("disconnect(" + device + ")");
243         final IBluetoothMap service = getService();
244         if (service == null) {
245             Log.w(TAG, "Proxy not attached to service");
246             if (DBG) log(Log.getStackTraceString(new Throwable()));
247         } else if (isEnabled() && isValidDevice(device)) {
248             try {
249                 return service.disconnect(device, mAttributionSource);
250             } catch (RemoteException e) {
251                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
252             }
253         }
254         return false;
255     }
256 
257     /**
258      * Check class bits for possible Map support. This is a simple heuristic that tries to guess if
259      * a device with the given class bits might support Map. It is not accurate for all devices. It
260      * tries to err on the side of false positives.
261      *
262      * @return True if this device might support Map.
263      * @hide
264      */
doesClassMatchSink(BluetoothClass btClass)265     public static boolean doesClassMatchSink(BluetoothClass btClass) {
266         // TODO optimize the rule
267         switch (btClass.getDeviceClass()) {
268             case BluetoothClass.Device.COMPUTER_DESKTOP:
269             case BluetoothClass.Device.COMPUTER_LAPTOP:
270             case BluetoothClass.Device.COMPUTER_SERVER:
271             case BluetoothClass.Device.COMPUTER_UNCATEGORIZED:
272                 return true;
273             default:
274                 return false;
275         }
276     }
277 
278     /**
279      * Get the list of connected devices. Currently at most one.
280      *
281      * @return list of connected devices
282      * @hide
283      */
284     @SystemApi
285     @RequiresBluetoothConnectPermission
286     @RequiresPermission(
287             allOf = {
288                 android.Manifest.permission.BLUETOOTH_CONNECT,
289                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
290             })
getConnectedDevices()291     public @NonNull List<BluetoothDevice> getConnectedDevices() {
292         if (DBG) log("getConnectedDevices()");
293         final IBluetoothMap service = getService();
294         if (service == null) {
295             Log.w(TAG, "Proxy not attached to service");
296             if (DBG) log(Log.getStackTraceString(new Throwable()));
297         } else if (isEnabled()) {
298             try {
299                 return Attributable.setAttributionSource(
300                         service.getConnectedDevices(mAttributionSource), mAttributionSource);
301             } catch (RemoteException e) {
302                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
303             }
304         }
305         return Collections.emptyList();
306     }
307 
308     /**
309      * Get the list of devices matching specified states. Currently at most one.
310      *
311      * @return list of matching devices
312      * @hide
313      */
314     @RequiresBluetoothConnectPermission
315     @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
getDevicesMatchingConnectionStates(int[] states)316     public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
317         if (DBG) log("getDevicesMatchingStates()");
318         final IBluetoothMap service = getService();
319         if (service == null) {
320             Log.w(TAG, "Proxy not attached to service");
321             if (DBG) log(Log.getStackTraceString(new Throwable()));
322         } else if (isEnabled()) {
323             try {
324                 return Attributable.setAttributionSource(
325                         service.getDevicesMatchingConnectionStates(states, mAttributionSource),
326                         mAttributionSource);
327             } catch (RemoteException e) {
328                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
329             }
330         }
331         return Collections.emptyList();
332     }
333 
334     /**
335      * There are several instances of IpcDataCache used in this class. BluetoothCache wraps up the
336      * common code. All caches are created with a maximum of eight entries, and the key is in the
337      * bluetooth module. The name is set to the api.
338      */
339     private static class BluetoothCache<Q, R> extends IpcDataCache<Q, R> {
BluetoothCache(String api, IpcDataCache.QueryHandler query)340         BluetoothCache(String api, IpcDataCache.QueryHandler query) {
341             super(8, IpcDataCache.MODULE_BLUETOOTH, api, api, query);
342         }
343     }
344     ;
345 
346     /** @hide */
disableBluetoothGetConnectionStateCache()347     public void disableBluetoothGetConnectionStateCache() {
348         sBluetoothConnectionCache.disableForCurrentProcess();
349     }
350 
351     /** @hide */
invalidateBluetoothGetConnectionStateCache()352     public static void invalidateBluetoothGetConnectionStateCache() {
353         invalidateCache(GET_CONNECTION_STATE_API);
354     }
355 
356     /**
357      * Invalidate a bluetooth cache. This method is just a short-hand wrapper that enforces the
358      * bluetooth module.
359      */
invalidateCache(@onNull String api)360     private static void invalidateCache(@NonNull String api) {
361         IpcDataCache.invalidateCache(IpcDataCache.MODULE_BLUETOOTH, api);
362     }
363 
364     private static final IpcDataCache.QueryHandler<
365                     Pair<IBinder, Pair<AttributionSource, BluetoothDevice>>, Integer>
366             sBluetoothConnectionQuery =
367                     new IpcDataCache.QueryHandler<>() {
368                         @RequiresBluetoothConnectPermission
369                         @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
370                         @Override
371                         public Integer apply(
372                                 Pair<IBinder, Pair<AttributionSource, BluetoothDevice>> pairQuery) {
373                             IBluetoothMap service = IBluetoothMap.Stub.asInterface(pairQuery.first);
374                             AttributionSource source = pairQuery.second.first;
375                             BluetoothDevice device = pairQuery.second.second;
376                             if (DBG) {
377                                 log(
378                                         "getConnectionState("
379                                                 + device.getAnonymizedAddress()
380                                                 + ") uncached");
381                             }
382                             try {
383                                 return service.getConnectionState(device, source);
384                             } catch (RemoteException e) {
385                                 throw new RuntimeException(e);
386                             }
387                         }
388                     };
389 
390     private static final String GET_CONNECTION_STATE_API = "BluetoothMap_getConnectionState";
391 
392     private static final BluetoothCache<
393                     Pair<IBinder, Pair<AttributionSource, BluetoothDevice>>, Integer>
394             sBluetoothConnectionCache =
395                     new BluetoothCache<>(GET_CONNECTION_STATE_API, sBluetoothConnectionQuery);
396 
397     /**
398      * Get connection state of device
399      *
400      * @return device connection state
401      * @hide
402      */
403     @RequiresBluetoothConnectPermission
404     @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
getConnectionState(BluetoothDevice device)405     public int getConnectionState(BluetoothDevice device) {
406         if (DBG) log("getConnectionState(" + device + ")");
407         final IBluetoothMap service = getService();
408         if (service == null) {
409             Log.w(TAG, "BT not enabled. Cannot get connection state");
410             if (DBG) log(Log.getStackTraceString(new Throwable()));
411         } else if (isEnabled() && isValidDevice(device)) {
412             try {
413                 return sBluetoothConnectionCache.query(
414                         new Pair<>(service.asBinder(), new Pair<>(mAttributionSource, device)));
415             } catch (RuntimeException e) {
416                 if (!(e.getCause() instanceof RemoteException)) {
417                     throw e;
418                 }
419                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
420             }
421         }
422         return BluetoothProfile.STATE_DISCONNECTED;
423     }
424 
425     /**
426      * Set priority of the profile
427      *
428      * <p>The device should already be paired. Priority can be one of {@link #PRIORITY_ON} or {@link
429      * #PRIORITY_OFF},
430      *
431      * @param device Paired bluetooth device
432      * @return true if priority is set, false on error
433      * @hide
434      */
435     @RequiresBluetoothConnectPermission
436     @RequiresPermission(
437             allOf = {
438                 android.Manifest.permission.BLUETOOTH_CONNECT,
439                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
440             })
setPriority(BluetoothDevice device, int priority)441     public boolean setPriority(BluetoothDevice device, int priority) {
442         if (DBG) log("setPriority(" + device + ", " + priority + ")");
443         return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority));
444     }
445 
446     /**
447      * Set connection policy of the profile
448      *
449      * <p>The device should already be paired. Connection policy can be one of {@link
450      * #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, {@link
451      * #CONNECTION_POLICY_UNKNOWN}
452      *
453      * @param device Paired bluetooth device
454      * @param connectionPolicy is the connection policy to set to for this profile
455      * @return true if connectionPolicy is set, false on error
456      * @hide
457      */
458     @SystemApi
459     @RequiresBluetoothConnectPermission
460     @RequiresPermission(
461             allOf = {
462                 android.Manifest.permission.BLUETOOTH_CONNECT,
463                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
464             })
setConnectionPolicy( @onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)465     public boolean setConnectionPolicy(
466             @NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) {
467         if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
468         final IBluetoothMap service = getService();
469         if (service == null) {
470             Log.w(TAG, "Proxy not attached to service");
471             if (DBG) log(Log.getStackTraceString(new Throwable()));
472         } else if (isEnabled()
473                 && isValidDevice(device)
474                 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
475                         || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
476             try {
477                 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource);
478             } catch (RemoteException e) {
479                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
480             }
481         }
482         return false;
483     }
484 
485     /**
486      * Get the priority of the profile.
487      *
488      * <p>The priority can be any of: {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link
489      * #PRIORITY_UNDEFINED}
490      *
491      * @param device Bluetooth device
492      * @return priority of the device
493      * @hide
494      */
495     @RequiresBluetoothConnectPermission
496     @RequiresPermission(
497             allOf = {
498                 android.Manifest.permission.BLUETOOTH_CONNECT,
499                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
500             })
getPriority(BluetoothDevice device)501     public int getPriority(BluetoothDevice device) {
502         if (VDBG) log("getPriority(" + device + ")");
503         return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device));
504     }
505 
506     /**
507      * Get the connection policy of the profile.
508      *
509      * <p>The connection policy can be any of: {@link #CONNECTION_POLICY_ALLOWED}, {@link
510      * #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
511      *
512      * @param device Bluetooth device
513      * @return connection policy of the device
514      * @hide
515      */
516     @SystemApi
517     @RequiresBluetoothConnectPermission
518     @RequiresPermission(
519             allOf = {
520                 android.Manifest.permission.BLUETOOTH_CONNECT,
521                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
522             })
getConnectionPolicy(@onNull BluetoothDevice device)523     public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
524         if (VDBG) log("getConnectionPolicy(" + device + ")");
525         final IBluetoothMap service = getService();
526         if (service == null) {
527             Log.w(TAG, "Proxy not attached to service");
528             if (DBG) log(Log.getStackTraceString(new Throwable()));
529         } else if (isEnabled() && isValidDevice(device)) {
530             try {
531                 return service.getConnectionPolicy(device, mAttributionSource);
532             } catch (RemoteException e) {
533                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
534             }
535         }
536         return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
537     }
538 
log(String msg)539     private static void log(String msg) {
540         Log.d(TAG, msg);
541     }
542 
isEnabled()543     private boolean isEnabled() {
544         return mAdapter.isEnabled();
545     }
546 
isValidDevice(BluetoothDevice device)547     private static boolean isValidDevice(BluetoothDevice device) {
548         return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
549     }
550 }
551