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