/******************************************************************************
 *  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.
 *
 * <p> This class provides functionalities to enable communication with remote
 * devices which are grouped together to achieve common use cases in
 * synchronized manner.
 * <p> 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.
 * <p> 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<IBluetoothDeviceGroup> 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<BluetoothDevice> getConnectedDevices() {
        if (VDBG) log("getConnectedDevices()");

        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<BluetoothDevice> 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<BluetoothDevice> 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.
     *
     * <p> 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.
     *
     * <p> 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.
     *
     * <p> {@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<DeviceGroup> 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<DeviceGroup> 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<DeviceGroup> 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<BluetoothDevice> 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<BluetoothDevice> 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.
     *
     * <p> 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);
        }
    }
}
