/****************************************************************************** * Copyright (c) 2020, The Linux Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of The Linux Foundation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ package android.bluetooth; import android.annotation.RequiresPermission; import android.bluetooth.annotations.RequiresBluetoothConnectPermission; import android.bluetooth.annotations.RequiresBluetoothScanPermission; import android.compat.annotation.UnsupportedAppUsage; import android.content.AttributionSource; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.bluetooth.IBluetoothGroupCallback; import android.content.Context; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; import android.util.Log; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * This class provides the public APIs to perform operations of * the Group Identification Profile. * *

This class provides functionalities to enable communication with remote * devices which are grouped together to achieve common use cases in * synchronized manner. *

BluetoothDeviceGroup is a proxy object for controlling the Bluetooth Group * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothDeviceGroup * proxy object. Use {@link BluetoothAdapter#closeProfileProxy} to close connection * of the BluetoothDeviceGroup proxy object with the profile service. *

BluetoothDeviceGroup proxy object can be used to identify and fetch Device Group. * Also, API’s are exposed to get exclusive access of group devices for critical * operations. Implement BluetoothGroupCallback to get results invoked API's. * * @hide */ public final class BluetoothDeviceGroup implements BluetoothProfile { private static final String TAG = "BluetoothDeviceGroup"; private static final boolean DBG = true; private static final boolean VDBG = false; /** Group Client App is registerd for callbacks successfully */ public static final int APP_REGISTRATION_SUCCESSFUL = 0; /** Group Client App registration failed for callbacks */ public static final int APP_REGISTRATION_FAILED = 1; /** Group Discovery Status when discovery is started */ public static final int GROUP_DISCOVERY_STARTED = 0x00; /** Group Discovery Status when discovery is stopped */ public static final int GROUP_DISCOVERY_STOPPED = 0x01; /** When Application starts Group discovery */ public static final int DISCOVERY_STARTED_BY_APPL = 0x00; /** When Application stops Group discovery */ public static final int DISCOVERY_STOPPED_BY_APPL = 0x01; /** When Group discovery is started as a result of * change in Group property. */ public static final int DISCOVERY_STARTED_GROUP_PROP_CHANGED = 0x02; /** When all devices of Group are discovered */ public static final int DISCOVERY_COMPLETED = 0x03; /** Group discovery by timeeut. Group device not found in 10 sec. */ public static final int DISCOVERY_STOPPED_BY_TIMEOUT = 0x04; /** Invalid params are provided for Group discovery */ public static final int DISCOVERY_NOT_STARTED_INVALID_PARAMS = 0x05; /** Value to release Exclusive Access */ public static final int ACCESS_RELEASED = 0x01; /** Value to acquire Exclusive Access */ public static final int ACCESS_GRANTED = 0x02; /** When exclusive access is changed to #ACCESS_RELEASED for all reqested Group devices */ public static final int EXCLUSIVE_ACCESS_RELEASED = 0x00; /** When exclusive access of the Group device is changed to #ACCESS_RELEASED by timeout */ public static final int EXCLUSIVE_ACCESS_RELEASED_BY_TIMEOUT = 0x01; /** When exclusive access of all requested Group devices is changed to #ACCESS_GRANTED */ public static final int ALL_DEVICES_GRANTED_ACCESS = 0x02; /** When exclusive access of some of the requested Group devices is changed to #ACCESS_GRANTED * because of timeout in #setExclusiveAccess operation */ public static final int SOME_GRANTED_ACCESS_REASON_TIMEOUT = 0x03; /** When access value of some of the requested Group devices is changed to #ACCESS_GRANTED * because some of the Group devices were disconnected */ public static final int SOME_GRANTED_ACCESS_REASON_DISCONNECTION = 0x04; /** When Exclusive Access couldnt be fetched as one of the Group devices denied * to set value to #ACCESS_DENIED*/ public static final int ACCESS_DENIED = 0x05; /** Suggests that invalid parameters are passed in #setExclusiveAccess request*/ public static final int INVALID_ACCESS_REQ_PARAMS = 0x06; /** Invalid Group ID */ public static final int INVALID_GROUP_ID = 0x10; /** MIN GROUP_ID Value*/ public static final int GROUP_ID_MIN = 0x00; /** MAX GROUP_ID Value*/ public static final int GROUP_ID_MAX = 0x0F; /** Invalid APP ID */ public static final int INVALID_APP_ID = 0x10; /** MIN APP_ID Value*/ public static final int APP_ID_MIN = 0x00; /** MAX APP_ID Value*/ public static final int APP_ID_MAX = 0x0F; public static final String ACTION_CONNECTION_STATE_CHANGED = "android.bluetooth.group.profile.action.CONNECTION_STATE_CHANGED"; private int mAppId; private boolean mAppRegistered = false; private Handler mHandler; private BluetoothGroupCallback mCallback; private BluetoothAdapter mAdapter; private final AttributionSource mAttributionSource; private final BluetoothProfileConnector mProfileConnector = new BluetoothProfileConnector(this, BluetoothProfile.GROUP_CLIENT, "BluetoothDeviceGroup", IBluetoothDeviceGroup.class.getName()) { @Override public IBluetoothDeviceGroup getServiceInterface(IBinder service) { return IBluetoothDeviceGroup.Stub.asInterface(Binder.allowBlocking(service)); } }; /** * Creates a BluetoothDeviceGroup proxy object for interacting with the local * Bluetooth Service which handles Group operations. * @hide */ /*package*/ BluetoothDeviceGroup(Context context, ServiceListener listener) { mProfileConnector.connect(context, listener); mAdapter = BluetoothAdapter.getDefaultAdapter(); mAttributionSource = mAdapter.getAttributionSource(); IBluetoothManager mgr = mAdapter.getBluetoothManager(); if (mgr != null) { try { mgr.registerStateChangeCallback(mBluetoothStateChangeCallback); } catch (RemoteException re) { Log.e(TAG, "", re); } } } private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback = new IBluetoothStateChangeCallback.Stub() { public void onBluetoothStateChange(boolean up) { if (!up) { mAppRegistered = false; } } }; /** * Close this BluetoothGroupDevice client object. * * Application should call this method as soon as it is done with * Group operations. */ /*package*/ void close() { if (VDBG) log("close()"); mAppRegistered = false; final IBluetoothDeviceGroup service = getService(); if (service != null) { try { service.unregisterGroupClientApp(mAppId, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } } mProfileConnector.disconnect(); } /** * @hide */ private IBluetoothDeviceGroup getService() { return mProfileConnector.getService(); } /** * {@inheritDoc} */ @Override public void finalize() { close(); } /** * {@inheritDoc} */ @Override public List getConnectedDevices() { if (VDBG) log("getConnectedDevices()"); return null; } /** * {@inheritDoc} */ @Override public List getDevicesMatchingConnectionStates(int[] states) { if (VDBG) log("getDevicesMatchingStates()"); return null; } private boolean isEnabled() { return mAdapter.getState() == BluetoothAdapter.STATE_ON; } private static boolean isValidDevice(BluetoothDevice device) { return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); } /** * {@inheritDoc} */ @Override public int getConnectionState(BluetoothDevice device) { if (VDBG) log("getState(" + device + ")"); return BluetoothProfile.STATE_DISCONNECTED; } private final IBluetoothGroupCallback.Stub mBluetoothGroupCallback = new IBluetoothGroupCallback.Stub() { @Override public void onGroupClientAppRegistered(int status, int appId) { if (DBG) { Log.d(TAG, "onGroupClientAppRegistered() - status=" + status + " appId = " + appId); } if (status != APP_REGISTRATION_SUCCESSFUL) { mAppRegistered = false; } mAppId = appId; runOrQueueCallback(new Runnable() { @Override public void run() { final BluetoothGroupCallback callback = mCallback; if (callback != null) { callback.onGroupClientAppRegistered(status, appId); } } }); } @Override public void onGroupClientAppUnregistered(int status) { if (DBG) { Log.d(TAG, "onGroupClientAppUnregistered() - status=" + status + " mAppId=" + mAppId); } } @Override public void onConnectionStateChanged (int state, BluetoothDevice device) { if (DBG) { Log.d(TAG, "onConnectionStateChanged() - state = " + state + " device = " + device); } runOrQueueCallback(new Runnable() { @Override public void run() { final BluetoothGroupCallback callback = mCallback; if (callback != null) { callback.onConnectionStateChanged(state, device); } } }); } @Override public void onNewGroupFound(int groupId, BluetoothDevice device, ParcelUuid uuid) { if (DBG) { Log.d(TAG, "onNewGroupFound() - appId = " + mAppId + ", groupId = " + groupId + ", device: " + device + ", Including service UUID: " + uuid.toString()); } runOrQueueCallback(new Runnable() { @Override public void run() { final BluetoothGroupCallback callback = mCallback; if (callback != null) { callback.onNewGroupFound(groupId, device, uuid.getUuid()); } } }); } @Override public void onGroupDiscoveryStatusChanged(int groupId, int status, int reason) { if (DBG) { Log.d(TAG, "onGroupDiscoveryStatusChanged() - appId = " + mAppId + ", groupId = " + groupId + ", status: " + status + ", reason: " + reason); } runOrQueueCallback(new Runnable() { @Override public void run() { final BluetoothGroupCallback callback = mCallback; if (callback != null) { callback.onGroupDiscoveryStatusChanged(groupId, status, reason); } } }); } @Override public void onGroupDeviceFound(int groupId, BluetoothDevice device) { if (DBG) { Log.d(TAG, "onGroupDeviceFound() - appId = " + mAppId + ", device = " + device); } runOrQueueCallback(new Runnable() { @Override public void run() { final BluetoothGroupCallback callback = mCallback; if (callback != null) { callback.onGroupDeviceFound(groupId, device); } } }); } @Override public void onExclusiveAccessChanged(int groupId, int value, int status, List devices) { if (DBG) { Log.d(TAG, "onExclusiveAccessChanged() - appId = " + mAppId + ", groupId = " + groupId + ", value = " + value + " accessStatus = " + status + ", devices: " + devices); } runOrQueueCallback(new Runnable() { @Override public void run() { final BluetoothGroupCallback callback = mCallback; if (callback != null) { callback.onExclusiveAccessChanged(groupId, value, status, devices); } } }); } @Override public void onExclusiveAccessStatusFetched(int groupId, int accessValue) { } @Override public void onExclusiveAccessAvailable (int groupId, BluetoothDevice device) { if (DBG) { Log.d(TAG, "onExclusiveAccessAvailable() - appId = " + mAppId + ", groupId = " + groupId + ", device: " + device); } runOrQueueCallback(new Runnable() { @Override public void run() { final BluetoothGroupCallback callback = mCallback; if (callback != null) { callback.onExclusiveAccessAvailable(groupId, device); } } }); } }; /** * Registers callbacks to be received by application on completion of * required operations. * * @param callbacks Reference of BluetoothGroupCallback implemented in * application. * @param handler handler that will receive asynchronous callbacks. * @return true, if operation was initiated successfully. */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public boolean registerGroupClientApp(BluetoothGroupCallback callbacks, Handler handler) { if (DBG) log("registerGroupClientApp() mAppRegistered = " + mAppRegistered); /* Check if app is trying multiple registrations */ if (mAppRegistered) { Log.e(TAG, "App already registered."); return false; } mHandler = handler; mCallback = callbacks; final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy not attached to Profile Service. Can't register App."); return false; } mAppRegistered = true; try { UUID uuid = UUID.randomUUID(); service.registerGroupClientApp(new ParcelUuid(uuid), mBluetoothGroupCallback, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return true; } /** * Starts discovery of the remaining Group devices which are part of the group. * *

This API should be called when onNewGroupFound() is received in the * application and when given group is the required device group. This * API can also be used to rediscover the undiscovered Group devices. * *

To the application that started group discovery, * {@link BluetoothGroupCallback#onGroupDeviceFound} callback will be given when * a new Group device is found and {@link BluetoothGroupCallback#onGroupDiscoveryStatusChanged} * callback will be given when discovery is started. * * @param groupId Identifier of the Group for which group * discovery has to be started. * @return true, if operation was initiated successfully. */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public boolean startGroupDiscovery(int groupId) { if (DBG) log("startGroupDiscovery() : groupId = " + groupId); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return false; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service. Can't start group discovery"); return false; } try { UUID uuid = UUID.randomUUID(); service.startGroupDiscovery(mAppId ,groupId, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return true; } /** * Stops ongoing group discovery for Group identified by groupId. * *

{@link BluetoothGroupCallback#onGroupDiscoveryStatusChanged} is given * when group discovery is stopped. * * @param groupId Identifier of the Group for which group * discovery has to be stopped. * @return true, if operation was initiated successfully. */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public boolean stopGroupDiscovery(int groupId) { if (DBG) log("stopGroupDiscovery() : groupId = " + groupId); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return false; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service. Can't Stop group discovery"); return false; } try { service.stopGroupDiscovery(mAppId ,groupId, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return true; } /** * Fetches already discovered Groups. * * @return List of DeviceGroup that are already discovered. */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public List getDiscoveredGroups() { return getDiscoveredGroups(false); } /** * Fetches already discovered device groups. * * @param mPublicAddr All discovered device groups with public address of devices. * @return List of Device Groups that are already discovered. * @hide */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public List getDiscoveredGroups(boolean mPublicAddr) { if (DBG) log("getDiscoveredGroups()"); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return null; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service. Can't fetch Groups."); return null; } try { List groups = service.getDiscoveredGroups(mPublicAddr, mAttributionSource); return groups; } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return null; } /** * Fetch details of a already discovered Group identified by groupId. * * @param groupId Identifier of the Group for which Group details are required. * @return Required DeviceGroup. */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public DeviceGroup getGroup(int groupId) { return getGroup(groupId, false); } /** * Fetch details of a already discovered Group identified by groupId. * * @param groupId Identifier of the device group for which group * details are required. * @param mPublicAddr DeviceGroup with Public Address of the group devices. * @return Required DeviceGroup. * @hide */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public DeviceGroup getGroup(int groupId, boolean mPublicAddr) { if (DBG) log("getGroup() : groupId = " + groupId); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return null; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service. Can't fetch Group."); return null; } try { DeviceGroup group = service.getDeviceGroup(groupId, mPublicAddr, mAttributionSource); return group; } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return null; } /** * Get Group Identifier of the remote device to which it belongs. * * @param device BluetoothDevice instance of the remote device. * @param uuid ParcelUuid of the primary service in which this * Group Service is included. * @return Group identifier of the required device. */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public int getRemoteDeviceGroupId (BluetoothDevice device, ParcelUuid uuid) { return getRemoteDeviceGroupId(device, uuid, false); } /** * Get Group Identifier of the remote device to which it belongs. * * @param device BluetoothDevice instance of the remote device. * @param uuid ParcelUuid of the primary service in which this * Group Service is included. * @param mPublicAddr Suggests that group identifier is required for passed * public address of the remote device. * @return Group identifier of the required group for the device * @hide */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public int getRemoteDeviceGroupId (BluetoothDevice device, ParcelUuid uuid, boolean mPublicAddr) { if (DBG) log("getRemoteDeviceGroupId() : device = " + device); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return INVALID_GROUP_ID; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service." + "Can't get group id for device."); return INVALID_GROUP_ID; } try { return service.getRemoteDeviceGroupId(device, uuid, mPublicAddr, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return INVALID_GROUP_ID; } /** * Suggests whether discovery for a given Group is ongoing. * * @param groupId Identifier of the Group for which discovery * status is to be known. * @return true, if group discovery is ongoing for mentioned group. * Otherwise, false. */ @RequiresBluetoothScanPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN) public boolean isGroupDiscoveryInProgress (int groupId) { if (DBG) log("isGroupDiscoveryInProgress() : groupId = " + groupId); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return false; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service.Can't get discovery status."); return false; } try { return service.isGroupDiscoveryInProgress(groupId, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return false; } /** * Acquires/Releases exclusive access of a given Group or subgroup. * The result of this operation is returned in * {@link BluetoothGroupCallback#onExclusiveAccessChanged} callback. * * @param groupId Identifier of the Group. * @param devices List of BluetoothDevice for which access has to be changed. * If this parameter is passed as null, all Group devices in the * mentioned group will be considered for request. * @param value Access which required to be changed. * 0x01 – Access released ({@link #ACCESS_RELEASED}). * 0x02 - Access granted ({@link #ACCESS_GRANTED}). * @return true, if operation was initiated successfully. */ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public boolean setExclusiveAccess(int groupId, List devices, int value) { if (DBG) log("setExclusiveAccess() : groupId = " + groupId + ", access value: " + value); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return false; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service. Can't proceed."); return false; } try { service.setExclusiveAccess(mAppId, groupId, devices, value, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return true; } /** * Returns Status of the exclusive access for mentioned Group. * * @param groupId Identifier of the Group. * @param devices List of BluetoothDevice for which access value has to be known. * If this parameter is passed as null, all Group devices in the * mentioned group will be queried for access status. * @return true, if operation was initiated successfully. * @hide */ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public boolean getExclusiveAccessStatus (int groupId, List devices) { if (DBG) log("getExclusiveAccessStatus() : groupId = " + groupId); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return false; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service." + " Can't get exclusive access status."); return false; } try { service.getExclusiveAccessStatus(mAppId, groupId, devices, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return true; } /** * Creates GATT Connection with remote device for Group Operations. * *

This API acts as trigger to start service discovery to identify * new device group on remote device once connection has been established * successfully. Application calling connect will get * {@link BluetoothGroupCallback#onNewGroupFoundcallback} after * {@link #onConnectionStateChanged} (once connection has been established * and group discovery is completed.) * * @param device BluetoothDevice instance od remote device with which * Connection is required to be established. * @return true, if operation was initiated successfully. */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public boolean connect (BluetoothDevice device) { if (DBG) log("connect : device = " + device); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return false; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service. Can't connect."); return false; } try { service.connect(mAppId, device, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return true; } /** * Initiates GATT disconnection for Group Operations. * * @param device BluetoothDevice instance of remote device. * This API must be called if application is not * interested in any Group operations. * @return true, if operation was initiated successfully. */ @RequiresBluetoothConnectPermission @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public boolean disconnect (BluetoothDevice device) { if (DBG) log("disconnect : device = " + device); if (!mAppRegistered) { Log.e(TAG, "App not registered for Group operations." + " Register App using registerGroupClientApp"); return false; } final IBluetoothDeviceGroup service = getService(); if (service == null) { Log.e(TAG, "Proxy is not attached to Profile Service. Can't disconnect"); return false; } try { service.disconnect(mAppId, device, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); } return true; } private static void log(String msg) { Log.d(TAG, msg); } /** * Queue the runnable on a {@link Handler} provided by the user, or execute the runnable * immediately if no Handler was provided. */ private void runOrQueueCallback(final Runnable cb) { if (mHandler == null) { try { cb.run(); } catch (Exception ex) { Log.w(TAG, "Unhandled exception in callback", ex); } } else { mHandler.post(cb); } } }