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