• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 HIMSA II K/S - www.himsa.com.
3  * Represented by EHIMA - www.ehima.com
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package android.bluetooth;
19 
20 import static android.bluetooth.BluetoothUtils.getSyncTimeout;
21 
22 import android.Manifest;
23 import android.annotation.CallbackExecutor;
24 import android.annotation.IntDef;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.RequiresPermission;
28 import android.annotation.SdkConstant;
29 import android.annotation.SdkConstant.SdkConstantType;
30 import android.annotation.SystemApi;
31 import android.content.AttributionSource;
32 import android.content.Context;
33 import android.os.IBinder;
34 import android.os.ParcelUuid;
35 import android.os.RemoteException;
36 import android.util.CloseGuard;
37 import android.util.Log;
38 
39 import com.android.modules.utils.SynchronousResultReceiver;
40 
41 import java.lang.annotation.Retention;
42 import java.lang.annotation.RetentionPolicy;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.UUID;
50 import java.util.concurrent.Executor;
51 import java.util.concurrent.TimeoutException;
52 
53 /**
54  * This class provides the public APIs to control the Bluetooth CSIP set coordinator.
55  *
56  * <p>BluetoothCsipSetCoordinator is a proxy object for controlling the Bluetooth CSIP set
57  * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
58  * the BluetoothCsipSetCoordinator proxy object.
59  *
60  */
61 public final class BluetoothCsipSetCoordinator implements BluetoothProfile, AutoCloseable {
62     private static final String TAG = "BluetoothCsipSetCoordinator";
63     private static final boolean DBG = false;
64     private static final boolean VDBG = false;
65 
66     private CloseGuard mCloseGuard;
67 
68     /**
69      * @hide
70      */
71     @SystemApi
72     public interface ClientLockCallback {
73         /** @hide */
74         @IntDef(value = {
75                 BluetoothStatusCodes.SUCCESS,
76                 BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED,
77                 BluetoothStatusCodes.ERROR_CSIP_INVALID_GROUP_ID,
78                 BluetoothStatusCodes.ERROR_CSIP_GROUP_LOCKED_BY_OTHER,
79                 BluetoothStatusCodes.ERROR_CSIP_LOCKED_GROUP_MEMBER_LOST,
80                 BluetoothStatusCodes.ERROR_UNKNOWN,
81         })
82         @Retention(RetentionPolicy.SOURCE)
83         @interface Status {}
84 
85         /**
86          * Callback is invoken as a result on {@link #groupLock()}.
87          *
88          * @param groupId group identifier
89          * @param opStatus status of lock operation
90          * @param isLocked inidcates if group is locked
91          *
92          * @hide
93          */
94         @SystemApi
onGroupLockSet(int groupId, @Status int opStatus, boolean isLocked)95         void onGroupLockSet(int groupId, @Status int opStatus, boolean isLocked);
96     }
97 
98     private static class BluetoothCsipSetCoordinatorLockCallbackDelegate
99             extends IBluetoothCsipSetCoordinatorLockCallback.Stub {
100         private final ClientLockCallback mCallback;
101         private final Executor mExecutor;
102 
BluetoothCsipSetCoordinatorLockCallbackDelegate( Executor executor, ClientLockCallback callback)103         BluetoothCsipSetCoordinatorLockCallbackDelegate(
104                 Executor executor, ClientLockCallback callback) {
105             mExecutor = executor;
106             mCallback = callback;
107         }
108 
109         @Override
onGroupLockSet(int groupId, int opStatus, boolean isLocked)110         public void onGroupLockSet(int groupId, int opStatus, boolean isLocked) {
111             mExecutor.execute(() -> mCallback.onGroupLockSet(groupId, opStatus, isLocked));
112         }
113     };
114 
115     /**
116      * Intent used to broadcast the change in connection state of the CSIS
117      * Client.
118      *
119      * <p>This intent will have 3 extras:
120      * <ul>
121      * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
122      * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
123      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
124      * </ul>
125      *
126      * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
127      * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
128      * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
129      */
130     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
131     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
132     public static final String ACTION_CSIS_CONNECTION_STATE_CHANGED =
133             "android.bluetooth.action.CSIS_CONNECTION_STATE_CHANGED";
134 
135     /**
136      * Intent used to expose broadcast receiving device.
137      *
138      * <p>This intent will have 2 extras:
139      * <ul>
140      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote Broadcast receiver device. </li>
141      * <li> {@link #EXTRA_CSIS_GROUP_ID} - Group identifier. </li>
142      * <li> {@link #EXTRA_CSIS_GROUP_SIZE} - Group size. </li>
143      * <li> {@link #EXTRA_CSIS_GROUP_TYPE_UUID} - Group type UUID. </li>
144      * </ul>
145      *
146      * @hide
147      */
148     @SystemApi
149     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
150     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
151     public static final String ACTION_CSIS_DEVICE_AVAILABLE =
152             "android.bluetooth.action.CSIS_DEVICE_AVAILABLE";
153 
154     /**
155      * Used as an extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent.
156      * Contains the group id.
157      *
158      * <p>Possible Values:
159      * {@link GROUP_ID_INVALID} Invalid group identifier
160      * 0x01 - 0xEF Valid group identifier
161      * @hide
162      */
163     @SystemApi
164     public static final String EXTRA_CSIS_GROUP_ID = "android.bluetooth.extra.CSIS_GROUP_ID";
165 
166     /**
167      * Group size as int extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent.
168      *
169      * @hide
170      */
171     public static final String EXTRA_CSIS_GROUP_SIZE = "android.bluetooth.extra.CSIS_GROUP_SIZE";
172 
173     /**
174      * Group type uuid extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent.
175      *
176      * @hide
177      */
178     public static final String EXTRA_CSIS_GROUP_TYPE_UUID =
179             "android.bluetooth.extra.CSIS_GROUP_TYPE_UUID";
180 
181     /**
182      * Intent used to broadcast information about identified set member
183      * ready to connect.
184      *
185      * <p>This intent will have one extra:
186      * <ul>
187      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
188      * be null if no device is active. </li>
189      * <li>  {@link #EXTRA_CSIS_GROUP_ID} - Group identifier. </li>
190      * </ul>
191      *
192      * @hide
193      */
194     @SystemApi
195     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
196     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
197     public static final String ACTION_CSIS_SET_MEMBER_AVAILABLE =
198             "android.bluetooth.action.CSIS_SET_MEMBER_AVAILABLE";
199 
200     /**
201      * This represents an invalid group ID.
202      *
203      * @hide
204      */
205     @SystemApi
206     public static final int GROUP_ID_INVALID = IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID;
207 
208     private final BluetoothAdapter mAdapter;
209     private final AttributionSource mAttributionSource;
210     private final BluetoothProfileConnector<IBluetoothCsipSetCoordinator> mProfileConnector =
211             new BluetoothProfileConnector(this, BluetoothProfile.CSIP_SET_COORDINATOR, TAG,
212                     IBluetoothCsipSetCoordinator.class.getName()) {
213                 @Override
214                 public IBluetoothCsipSetCoordinator getServiceInterface(IBinder service) {
215                     return IBluetoothCsipSetCoordinator.Stub.asInterface(service);
216                 }
217             };
218 
219     /**
220      * Create a BluetoothCsipSetCoordinator proxy object for interacting with the local
221      * Bluetooth CSIS service.
222      */
BluetoothCsipSetCoordinator(Context context, ServiceListener listener, BluetoothAdapter adapter)223     /*package*/ BluetoothCsipSetCoordinator(Context context, ServiceListener listener, BluetoothAdapter adapter) {
224         mAdapter = adapter;
225         mAttributionSource = adapter.getAttributionSource();
226         mProfileConnector.connect(context, listener);
227         mCloseGuard = new CloseGuard();
228         mCloseGuard.open("close");
229     }
230 
231     /**
232      * @hide
233      */
finalize()234     protected void finalize() {
235         if (mCloseGuard != null) {
236             mCloseGuard.warnIfOpen();
237         }
238         close();
239     }
240 
241     /**
242      * @hide
243      */
close()244     public void close() {
245         mProfileConnector.disconnect();
246     }
247 
getService()248     private IBluetoothCsipSetCoordinator getService() {
249         return mProfileConnector.getService();
250     }
251 
252     /**
253      * Lock the set.
254      * @param groupId group ID to lock,
255      * @param executor callback executor,
256      * @param callback callback to report lock and unlock events - stays valid until the app unlocks
257      *           using the returned lock identifier or the lock timeouts on the remote side,
258      *           as per CSIS specification,
259      * @return unique lock identifier used for unlocking or null if lock has failed.
260      * @throws {@link IllegalArgumentException} when executor or callback is null
261      *
262      * @hide
263      */
264     @SystemApi
265     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
266     public
lockGroup(int groupId, @NonNull @CallbackExecutor Executor executor, @NonNull ClientLockCallback callback)267     @Nullable UUID lockGroup(int groupId, @NonNull @CallbackExecutor Executor executor,
268             @NonNull ClientLockCallback callback) {
269         if (VDBG) log("lockGroup()");
270         Objects.requireNonNull(executor, "executor cannot be null");
271         Objects.requireNonNull(callback, "callback cannot be null");
272         final IBluetoothCsipSetCoordinator service = getService();
273         final UUID defaultValue = null;
274         if (service == null) {
275             Log.w(TAG, "Proxy not attached to service");
276             if (DBG) log(Log.getStackTraceString(new Throwable()));
277         } else if (isEnabled()) {
278             IBluetoothCsipSetCoordinatorLockCallback delegate =
279                     new BluetoothCsipSetCoordinatorLockCallbackDelegate(executor, callback);
280             try {
281                 final SynchronousResultReceiver<ParcelUuid> recv = SynchronousResultReceiver.get();
282                 service.lockGroup(groupId, delegate, mAttributionSource, recv);
283                 final ParcelUuid ret = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
284                 return ret == null ? defaultValue : ret.getUuid();
285             } catch (TimeoutException e) {
286                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
287             } catch (RemoteException e) {
288                 e.rethrowFromSystemServer();
289             }
290         }
291         return defaultValue;
292     }
293 
294     /**
295      * Unlock the set.
296      * @param lockUuid unique lock identifier
297      * @return true if unlocked, false on error
298      * @throws {@link IllegalArgumentException} when lockUuid is null
299      *
300      * @hide
301      */
302     @SystemApi
303     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
unlockGroup(@onNull UUID lockUuid)304     public boolean unlockGroup(@NonNull UUID lockUuid) {
305         if (VDBG) log("unlockGroup()");
306         Objects.requireNonNull(lockUuid, "lockUuid cannot be null");
307         final IBluetoothCsipSetCoordinator service = getService();
308         final boolean defaultValue = false;
309         if (service == null) {
310             Log.w(TAG, "Proxy not attached to service");
311             if (DBG) log(Log.getStackTraceString(new Throwable()));
312         } else if (isEnabled()) {
313             try {
314                 final SynchronousResultReceiver recv = SynchronousResultReceiver.get();
315                 service.unlockGroup(new ParcelUuid(lockUuid), mAttributionSource, recv);
316                 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
317                 return true;
318             } catch (TimeoutException e) {
319                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
320             } catch (RemoteException e) {
321                 e.rethrowFromSystemServer();
322             }
323         }
324         return defaultValue;
325     }
326 
327     /**
328      * Get device's groups.
329      * @param device the active device
330      * @return Map of groups ids and related UUIDs
331      *
332      * @hide
333      */
334     @SystemApi
335     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
getGroupUuidMapByDevice( @ullable BluetoothDevice device)336     public @NonNull Map<Integer, ParcelUuid> getGroupUuidMapByDevice(
337             @Nullable BluetoothDevice device) {
338         if (VDBG) log("getGroupUuidMapByDevice()");
339         final IBluetoothCsipSetCoordinator service = getService();
340         final Map defaultValue = new HashMap<>();
341         if (service == null) {
342             Log.w(TAG, "Proxy not attached to service");
343             if (DBG) log(Log.getStackTraceString(new Throwable()));
344         } else if (isEnabled()) {
345             try {
346                 final SynchronousResultReceiver<Map> recv = SynchronousResultReceiver.get();
347                 service.getGroupUuidMapByDevice(device, mAttributionSource, recv);
348                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
349             } catch (TimeoutException e) {
350                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
351             } catch (RemoteException e) {
352                 e.rethrowFromSystemServer();
353             }
354         }
355         return defaultValue;
356     }
357 
358     /**
359      * Get group id for the given UUID
360      * @param uuid
361      * @return list of group IDs
362      *
363      * @hide
364      */
365     @SystemApi
366     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
getAllGroupIds(@ullable ParcelUuid uuid)367     public @NonNull List<Integer> getAllGroupIds(@Nullable ParcelUuid uuid) {
368         if (VDBG) log("getAllGroupIds()");
369         final IBluetoothCsipSetCoordinator service = getService();
370         final List<Integer> defaultValue = new ArrayList<>();
371         if (service == null) {
372             Log.w(TAG, "Proxy not attached to service");
373             if (DBG) log(Log.getStackTraceString(new Throwable()));
374         } else if (isEnabled()) {
375             try {
376                 final SynchronousResultReceiver<List<Integer>> recv =
377                         SynchronousResultReceiver.get();
378                 service.getAllGroupIds(uuid, mAttributionSource, recv);
379                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
380             } catch (TimeoutException e) {
381                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
382             } catch (RemoteException e) {
383                 e.rethrowFromSystemServer();
384             }
385         }
386         return defaultValue;
387     }
388 
389     /**
390      * {@inheritDoc}
391      */
392     @Override
getConnectedDevices()393     public @NonNull List<BluetoothDevice> getConnectedDevices() {
394         if (VDBG) log("getConnectedDevices()");
395         final IBluetoothCsipSetCoordinator service = getService();
396         final List<BluetoothDevice> defaultValue = new ArrayList<>();
397         if (service == null) {
398             Log.w(TAG, "Proxy not attached to service");
399             if (DBG) log(Log.getStackTraceString(new Throwable()));
400         } else if (isEnabled()) {
401             try {
402                 final SynchronousResultReceiver<List<BluetoothDevice>> recv =
403                         SynchronousResultReceiver.get();
404                 service.getConnectedDevices(mAttributionSource, recv);
405                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
406             } catch (TimeoutException e) {
407                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
408             } catch (RemoteException e) {
409                 e.rethrowFromSystemServer();
410             }
411         }
412         return defaultValue;
413     }
414 
415     /**
416      * {@inheritDoc}
417      */
418     @Override
419     public
getDevicesMatchingConnectionStates(@onNull int[] states)420     @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) {
421         if (VDBG) log("getDevicesMatchingStates(states=" + Arrays.toString(states) + ")");
422         final IBluetoothCsipSetCoordinator service = getService();
423         final List<BluetoothDevice> defaultValue = new ArrayList<>();
424         if (service == null) {
425             Log.w(TAG, "Proxy not attached to service");
426             if (DBG) log(Log.getStackTraceString(new Throwable()));
427         } else if (isEnabled()) {
428             try {
429                 final SynchronousResultReceiver<List<BluetoothDevice>> recv =
430                         SynchronousResultReceiver.get();
431                 service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv);
432                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
433             } catch (TimeoutException e) {
434                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
435             } catch (RemoteException e) {
436                 e.rethrowFromSystemServer();
437             }
438         }
439         return defaultValue;
440     }
441 
442     /**
443      * {@inheritDoc}
444      */
445     @Override
446     public
getConnectionState(@ullable BluetoothDevice device)447     @BluetoothProfile.BtProfileState int getConnectionState(@Nullable BluetoothDevice device) {
448         if (VDBG) log("getState(" + device + ")");
449         final IBluetoothCsipSetCoordinator service = getService();
450         final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
451         if (service == null) {
452             Log.w(TAG, "Proxy not attached to service");
453             if (DBG) log(Log.getStackTraceString(new Throwable()));
454         } else if (isEnabled()) {
455             try {
456                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
457                 service.getConnectionState(device, mAttributionSource, recv);
458                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
459             } catch (TimeoutException e) {
460                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
461             } catch (RemoteException e) {
462                 e.rethrowFromSystemServer();
463             }
464         }
465         return defaultValue;
466     }
467 
468     /**
469      * Set connection policy of the profile
470      *
471      * <p> The device should already be paired.
472      * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
473      * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
474      *
475      * @param device Paired bluetooth device
476      * @param connectionPolicy is the connection policy to set to for this profile
477      * @return true if connectionPolicy is set, false on error
478      *
479      * @hide
480      */
481     @SystemApi
482     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
setConnectionPolicy( @ullable BluetoothDevice device, @ConnectionPolicy int connectionPolicy)483     public boolean setConnectionPolicy(
484             @Nullable BluetoothDevice device, @ConnectionPolicy int connectionPolicy) {
485         if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
486         final IBluetoothCsipSetCoordinator service = getService();
487         final boolean defaultValue = false;
488         if (service == null) {
489             Log.w(TAG, "Proxy not attached to service");
490             if (DBG) log(Log.getStackTraceString(new Throwable()));
491         } else if (isEnabled() && isValidDevice(device)
492                 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
493                     || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
494             try {
495                 final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
496                 service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv);
497                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
498             } catch (TimeoutException e) {
499                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
500             } catch (RemoteException e) {
501                 e.rethrowFromSystemServer();
502             }
503         }
504         return defaultValue;
505     }
506 
507     /**
508      * Get the connection policy of the profile.
509      *
510      * <p> The connection policy can be any of:
511      * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
512      * {@link #CONNECTION_POLICY_UNKNOWN}
513      *
514      * @param device Bluetooth device
515      * @return connection policy of the device
516      *
517      * @hide
518      */
519     @SystemApi
520     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
getConnectionPolicy(@ullable BluetoothDevice device)521     public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) {
522         if (VDBG) log("getConnectionPolicy(" + device + ")");
523         final IBluetoothCsipSetCoordinator service = getService();
524         final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
525         if (service == null) {
526             Log.w(TAG, "Proxy not attached to service");
527             if (DBG) log(Log.getStackTraceString(new Throwable()));
528         } else if (isEnabled() && isValidDevice(device)) {
529             try {
530                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
531                 service.getConnectionPolicy(device, mAttributionSource, recv);
532                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
533             } catch (TimeoutException e) {
534                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
535             } catch (RemoteException e) {
536                 e.rethrowFromSystemServer();
537             }
538         }
539         return defaultValue;
540     }
541 
isEnabled()542     private boolean isEnabled() {
543         return mAdapter.getState() == BluetoothAdapter.STATE_ON;
544     }
545 
isValidDevice(@ullable BluetoothDevice device)546     private static boolean isValidDevice(@Nullable BluetoothDevice device) {
547         return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
548     }
549 
log(String msg)550     private static void log(String msg) {
551         Log.d(TAG, msg);
552     }
553 }
554