android.bluetooth.BluetoothGattServerCallback

Here are the examples of the java api android.bluetooth.BluetoothGattServerCallback taken from open source projects. By voting up you can indicate which examples are most useful and appropriate.

13 Examples 7

18 Source : BluetoothServerHelper.java
with MIT License
from covidsafe

public clreplaced BluetoothServerHelper {

    static Context cxt;

    public static void createServer(Context cxt) {
        BluetoothServerHelper.cxt = cxt;
        Constants.writtenUUIDs = new HashSet<>();
        Log.e("bleserver", "createserver");
        BluetoothManager bluetoothManager = (BluetoothManager) cxt.getSystemService(Context.BLUETOOTH_SERVICE);
        if (Constants.gattServer == null) {
            Constants.gattServer = bluetoothManager.openGattServer(cxt, gattServerCallback);
            Log.e("bleserver", "is server null " + (Constants.gattServer == null));
            if (Constants.gattServer != null) {
                BluetoothGattService service = new BluetoothGattService(Constants.GATT_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
                int permission = BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ;
                int property = BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_READ;
                BluetoothGattCharacteristic charac1 = new BluetoothGattCharacteristic(Constants.CHARACTERISTIC_UUID, property, permission);
                service.addCharacteristic(charac1);
                Constants.gattServer.addService(service);
            }
        }
    }

    public static void stopServer() {
        if (Constants.gattServer != null) {
            Constants.gattServer.close();
            Constants.gattServer = null;
        }
    }

    public static BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
            Log.e("bleserver", "read request " + characteristic.getUuid().toString());
            if (characteristic.getUuid().equals(Constants.CHARACTERISTIC_UUID)) {
                Log.e("bleserver", "going to send " + Constants.contactUUID);
                byte[] contactUuidBytes = ByteUtils.uuid2bytes(Constants.contactUUID);
                Log.e("bleserver", "converted to bytes " + contactUuidBytes.length);
                boolean status = Constants.gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, contactUuidBytes);
                Log.e("bleserver", "status " + status);
            }
            Log.e("bleserver", "finished read");
        }

        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
            Log.e("bleserver", "write request " + characteristic.getUuid().toString());
            Log.e("bleserver", "preparedwrite " + preparedWrite);
            Log.e("bleserver", "responseNeeded " + responseNeeded);
            if (responseNeeded) {
                Constants.gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
            }
            // 0-15: UUID
            // 16: RSSI
            // 17: phone model
            if (value != null) {
                Log.e("bleserver", "data len " + value.length);
                if (value.length == 16 || value.length == 17 || value.length == 18) {
                    // 
                    byte[] uuidByte = Arrays.copyOfRange(value, 0, 16);
                    String contactUuid = ByteUtils.byte2UUIDstring(uuidByte);
                    Log.e("bleserver", "contactuuid " + contactUuid);
                    int rssi = 0;
                    // byte[-128,127] => int[0,255] => rssi[-255,0]
                    if (value.length >= 17) {
                        rssi = -ByteUtils.byteConvert(value[16]);
                        Log.e("bleserver", "received an rssi value of " + rssi);
                    } else {
                        Log.e("bleserver", "rssi value not received");
                    }
                    Log.e("bleserver", "rssi " + rssi + "," + device.getAddress());
                    int deviceID = 0;
                    if (value.length == 18) {
                        deviceID = value[17];
                    }
                    if (!Constants.writtenUUIDs.contains(contactUuid) && BluetoothUtils.rssiThresholdCheck(rssi, deviceID)) {
                        Constants.writtenUUIDs.add(contactUuid);
                        Utils.bleLogToDatabase(cxt, contactUuid, rssi, TimeUtils.getTime(), deviceID);
                    }
                } else {
                    for (Byte b : value) {
                        Log.e("bleserver", b + "");
                    }
                }
            } else {
                Log.e("bleserver", "write data is null");
            }
            Log.e("bleserver", "finished write");
        }

        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            super.onConnectionStateChange(device, status, newState);
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.e("bleserver", "connected " + device.getAddress());
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.e("bleserver", "disconnected");
            }
        }

        @Override
        public void onServiceAdded(int status, BluetoothGattService service) {
            super.onServiceAdded(status, service);
            if (service != null) {
                Log.e("bleserver", "service added " + service.getUuid());
                List<BluetoothGattCharacteristic> characs = service.getCharacteristics();
                if (characs != null) {
                    for (BluetoothGattCharacteristic charac : characs) {
                        Log.e("bleserver", "charac " + charac.getUuid());
                        if (charac != null) {
                            byte[] bb = charac.getValue();
                            if (bb != null) {
                                for (Byte b : bb) {
                                    Log.e("bleserver", b + "");
                                }
                            }
                        }
                    }
                }
            }
        }
    };
}

17 Source : ShadowBluetoothManager.java
with Apache License 2.0
from google

@Implementation
public BluetoothGattServer openGattServer(Context context, BluetoothGattServerCallback callback) {
    return ReflectionHelpers.newInstance(BluetoothGattServer.clreplaced);
}

15 Source : GattServerMainActivity.java
with The Unlicense
from kshtj24

/**
 * Created by kreplacedij.saxena on 20-11-2017.
 */
public clreplaced GattServerMainActivity extends AppCompatActivity {

    private ArrayAdapter<?> genericListAdapter;

    private ArrayList<BluetoothDevice> deviceArrayList;

    private ListView deviceListView;

    private BluetoothGattServer bluetoothGattServer;

    private BluetoothManager bluetoothManager;

    private BluetoothGattServerCallback bluetoothGattServerCallback;

    private BluetoothDevice miBand;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initialiseViews();
        getPermissions();
        enableBTAndDiscover();
    }

    private void initialiseViews() {
        if (bluetoothManager == null)
            bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
        bluetoothGattServerCallback = new BluetoothGattServerCallback() {

            @Override
            public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
                switch(newState) {
                    case BluetoothGattServer.STATE_CONNECTED:
                        Log.i("Device", "Connected");
                        addServices();
                        break;
                    case BluetoothGattServer.STATE_DISCONNECTED:
                        Log.e("Device", "Disconnected");
                        break;
                }
            }

            @Override
            public void onServiceAdded(int status, BluetoothGattService service) {
                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                    bluetoothGattServer.notifyCharacteristicChanged(miBand, characteristic, false);
                }
            }

            @Override
            public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            // super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
            }

            @Override
            public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
                Log.e("Characteristic", "Write request received");
            }

            @Override
            public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
                super.onDescriptorReadRequest(device, requestId, offset, descriptor);
            }

            @Override
            public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
                super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
            }

            @Override
            public void onNotificationSent(BluetoothDevice device, int status) {
                super.onNotificationSent(device, status);
            }
        };
        bluetoothGattServer = bluetoothManager.openGattServer(GattServerMainActivity.this, bluetoothGattServerCallback);
        deviceListView = (ListView) findViewById(R.id.deviceListView);
        deviceListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                miBand = (BluetoothDevice) parent.gereplacedemAtPosition(position);
                connectDevice();
            }
        });
    }

    private void addServices() {
    // bluetoothGattServer.addService(new BluetoothGattService(UUIDs.HEART_RATE_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY));
    // bluetoothGattServer.addService(new BluetoothGattService(UUIDs.CUSTOM_BATTERY_SERVICE,BluetoothGattService.SERVICE_TYPE_PRIMARY));
    }

    private void getPermissions() {
        if (getPackageManager().hreplacedystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            if (ContextCompat.checkSelfPermission(getApplicationContext(), android.Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED) {
                shouldShowRequestPermissionRationale("Please grant this app the following permissions to make it work properly");
                requestPermissions(new String[] { android.Manifest.permission.BLUETOOTH_ADMIN, android.Manifest.permission.BLUETOOTH }, 1);
            }
            if (ContextCompat.checkSelfPermission(getApplicationContext(), android.Manifest.permission_group.LOCATION) == PackageManager.PERMISSION_DENIED) {
                shouldShowRequestPermissionRationale("Please grand Location permission for scanning devices nearby");
                requestPermissions(new String[] { android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION }, 1);
            }
        } else {
            Toast.makeText(GattServerMainActivity.this, "This device doesn't support Bluetooth LE", Toast.LENGTH_LONG).show();
        }
    }

    private void connectDevice() {
        if (miBand.getBondState() == BluetoothDevice.BOND_NONE) {
            miBand.createBond();
            Log.e("Bond", "Created with Device");
        }
        bluetoothGattServer.connect(miBand, true);
    }

    private void enableBTAndDiscover() {
        final BluetoothAdapter bluetoothAdapter = ((BluetoothManager) getSystemService(BLUETOOTH_SERVICE)).getAdapter();
        final ProgressDialog searchProgress = new ProgressDialog(GattServerMainActivity.this);
        searchProgress.setIndeterminate(true);
        searchProgress.setreplacedle("BlueTooth LE Device");
        searchProgress.setMessage("Searching...");
        searchProgress.setCancelable(false);
        searchProgress.show();
        deviceArrayList = new ArrayList<BluetoothDevice>();
        genericListAdapter = new ArrayAdapter<>(GattServerMainActivity.this, android.R.layout.simple_list_item_1, deviceArrayList);
        deviceListView.setAdapter(genericListAdapter);
        if (bluetoothAdapter == null) {
            Toast.makeText(GattServerMainActivity.this, "Bluetooth not supported on this device", Toast.LENGTH_LONG).show();
            return;
        }
        if (!bluetoothAdapter.isEnabled()) {
            startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), 1);
        }
        final ScanCallback leDeviceScanCallback = new ScanCallback() {

            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                Log.e("TAG", "Device found" + " " + result.getDevice().getAddress() + " " + result.getDevice().getName());
                if (!deviceArrayList.contains(result.getDevice())) {
                    deviceArrayList.add(result.getDevice());
                    genericListAdapter.notifyDataSetChanged();
                }
            }

            @Override
            public void onScanFailed(int errorCode) {
                super.onScanFailed(errorCode);
            }
        };
        bluetoothAdapter.getBluetoothLeScanner().startScan(leDeviceScanCallback);
        new Handler().postDelayed(new Runnable() {

            @Override
            public void run() {
                bluetoothAdapter.getBluetoothLeScanner().stopScan(leDeviceScanCallback);
                searchProgress.dismiss();
            }
        }, 10000);
    }
}

15 Source : BluetoothGattServerHelper.java
with Apache License 2.0
from google

/**
 * Helper for simplifying operations on {@link BluetoothGattServer}.
 */
@TargetApi(18)
public clreplaced BluetoothGattServerHelper {

    private static final String TAG = "BluetoothGattServerHelper";

    @VisibleForTesting
    static final long OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1);

    private static final int MAX_PARALLEL_OPERATIONS = 5;

    /**
     * BT operation types that can be in flight.
     */
    public enum OperationType {

        ADD_SERVICE, CLOSE_CONNECTION, START_ADVERTISING
    }

    /**
     * Server handler to notify of server status changes.
     */
    public interface ConnectionStatusListener {

        /**
         * Listener called when the number of connections to the server changes.
         *
         * @param numConnections the number of current connections.
         * @param device The remote device which causes the change
         * @param changeState
         * BluetoothGattServer.STATE_CONNECTED or BluetoothGattServer.STATE_DISCONNECTED indicating
         * whether the callback is triggered by a device connected or disconnected
         */
        void onConnectionsChanged(int numConnections, BluetoothDevice device, int changeState);
    }

    private final Object operationLock = new Object();

    private final BluetoothGattServerCallback gattServerCallback = new GattServerCallback();

    private BluetoothOperationExecutor bluetoothOperationScheduler = new BluetoothOperationExecutor(MAX_PARALLEL_OPERATIONS);

    private final Context context;

    private final ConnectionStatusListener statusListener;

    final ConcurrentMap<BluetoothDevice, BluetoothGattServerConnection> connections = new ConcurrentHashMap<BluetoothDevice, BluetoothGattServerConnection>();

    final ConcurrentMap<UUID, BluetoothSocketService> services = new ConcurrentHashMap<>();

    private BluetoothGattServer bluetoothGattServer;

    public BluetoothGattServerHelper(Context context, ConnectionStatusListener statusListener) {
        this.context = context;
        this.statusListener = statusListener;
    }

    public void open() {
        synchronized (operationLock) {
            BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
            if (bluetoothManager == null) {
                Log.e(TAG, "BluetoothManager is null!");
                return;
            }
            bluetoothGattServer = bluetoothManager.openGattServer(context, gattServerCallback);
            if (bluetoothGattServer == null) {
                Log.e(TAG, "BluetoothGattServer is null!");
            }
        }
    }

    /**
     * Notifies all clients of a new camera status.
     */
    public void notifyStatusChanged(BluetoothSocketService service) {
        for (BluetoothGattServerConnection connection : connections.values()) {
            connection.notifyStatusChanged(service.getServiceConfig());
        }
    }

    public void addService(BluetoothSocketService service) throws BluetoothException {
        services.put(service.getServiceConfig().getServiceUuid(), service);
        openService(service);
    }

    public void removeService(BluetoothSocketService service) {
        synchronized (operationLock) {
            services.remove(service.getServiceConfig().getServiceUuid());
            if (bluetoothGattServer != null) {
                bluetoothGattServer.removeService(service.getGattService());
            }
        }
    }

    public void close() {
        synchronized (operationLock) {
            if (bluetoothGattServer != null) {
                bluetoothGattServer.close();
                bluetoothGattServer = null;
            }
            services.clear();
            connections.clear();
        }
    }

    @VisibleForTesting
    void sendNotification(BluetoothDevice device, BluetoothGattCharacteristic characteristic, byte[] data, boolean confirm) throws BluetoothException {
        Log.d(TAG, String.format("Sending a %s of %d bytes on characteristics %s on device %s.", confirm ? "indication" : "notification", data.length, characteristic.getUuid(), device));
        if (getConnectionByDevice(device) == null) {
            Log.e(TAG, String.format("Ignoring request to send notification on unknown device: %s", device));
            return;
        }
        synchronized (operationLock) {
            BluetoothGattCharacteristic clonedCharacteristic = BluetoothGattUtils.clone(characteristic);
            clonedCharacteristic.setValue(data);
            try {
                getBluetoothGattServer().notifyCharacteristicChanged(device, clonedCharacteristic, confirm);
            } catch (Exception e) {
                throw new BluetoothException(e.getMessage(), e);
            }
        }
    }

    @VisibleForTesting
    void closeConnection(final BluetoothDevice bluetoothDevice) throws BluetoothException {
        BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
        if (bluetoothManager == null) {
            Log.e(TAG, "BluetoothManager is null!");
            return;
        }
        int connectionSate = bluetoothManager.getConnectionState(bluetoothDevice, BluetoothProfile.GATT);
        if (connectionSate != BluetoothGatt.STATE_CONNECTED) {
            Log.i(TAG, "Connection already closed.");
            return;
        }
        bluetoothOperationScheduler.execute(new Operation<Void>(OperationType.CLOSE_CONNECTION) {

            @Override
            public void run() throws BluetoothException {
                Log.i(TAG, String.format("Cancelling connection to BLE device:%s", bluetoothDevice));
                getBluetoothGattServer().cancelConnection(bluetoothDevice);
            }
        }, OPERATION_TIMEOUT_MILLIS);
    }

    private void openService(BluetoothSocketService service) throws BluetoothException {
        synchronized (operationLock) {
            BluetoothGattService gattService = service.getServiceConfig().getBluetoothGattService();
            service.setGattService(gattService);
            try {
                bluetoothOperationScheduler.execute(new Operation<Void>(OperationType.ADD_SERVICE, gattService) {

                    @Override
                    public void run() throws BluetoothException {
                        boolean success = bluetoothGattServer != null && bluetoothGattServer.addService(gattService);
                        if (!success) {
                            throw new BluetoothException(String.format("Fails on adding service:%s", gattService));
                        } else {
                            Log.i(TAG, String.format("Added service:%s", gattService));
                        }
                    }
                }, OPERATION_TIMEOUT_MILLIS);
            } catch (BluetoothException e) {
                close();
                throw e;
            }
        }
    }

    private BluetoothGattServerConnection getConnectionByDevice(BluetoothDevice device) throws BluetoothGattException {
        BluetoothGattServerConnection bluetoothLeConnection = connections.get(device);
        if (bluetoothLeConnection == null) {
            throw new BluetoothGattException(String.format("Received operation on an unknown device: %s", device), BluetoothGatt.GATT_FAILURE);
        }
        return bluetoothLeConnection;
    }

    private BluetoothGattServer getBluetoothGattServer() {
        synchronized (operationLock) {
            if (bluetoothGattServer == null) {
                throw new IllegalStateException("Bluetooth GATT server is not open.");
            } else {
                return bluetoothGattServer;
            }
        }
    }

    private clreplaced GattServerCallback extends BluetoothGattServerCallback {

        private static final String TAG = "GattServerCallback";

        @Override
        public void onServiceAdded(int status, BluetoothGattService service) {
            Log.e(TAG, "onServiceAdded");
            bluetoothOperationScheduler.notifyCompletion(new Operation<Void>(OperationType.ADD_SERVICE, service), status);
        }

        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            Log.i(TAG, String.format("onConnectionStateChange: device:%s status:%s state:%d", device, BluetoothGattUtils.getMessageForStatusCode(status), newState));
            switch(newState) {
                case BluetoothGattServer.STATE_CONNECTED:
                    if (status != BluetoothGatt.GATT_SUCCESS) {
                        Log.e(TAG, String.format("Connection to %s failed: %s", device, BluetoothGattUtils.getMessageForStatusCode(status)));
                        return;
                    }
                    Log.i(TAG, String.format("Connected to device %s.", device));
                    if (connections.containsKey(device)) {
                        Log.w(TAG, String.format("A connection is already open with device %s. Keeping existing one.", device));
                        return;
                    }
                    connections.put(device, new BluetoothGattServerConnection(BluetoothGattServerHelper.this, device));
                    if (statusListener != null) {
                        statusListener.onConnectionsChanged(connections.size(), device, BluetoothGattServer.STATE_CONNECTED);
                    }
                    break;
                case BluetoothGattServer.STATE_DISCONNECTED:
                    Log.d(TAG, String.format("Disconnection from %s for: %s", device, BluetoothGattUtils.getMessageForStatusCode(status)));
                    connections.remove(device);
                    bluetoothOperationScheduler.notifyCompletion(new Operation<Void>(OperationType.CLOSE_CONNECTION), status);
                    if (statusListener != null) {
                        statusListener.onConnectionsChanged(connections.size(), device, BluetoothGattServer.STATE_DISCONNECTED);
                    }
                    break;
                default:
                    Log.e(TAG, String.format("Unexpected connection state: %d", newState));
                    return;
            }
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            BluetoothSocketService service = services.get(characteristic.getService().getUuid());
            if (service == null) {
                Log.e(TAG, "Service not found " + characteristic.getService().getUuid());
                return;
            }
            if (!service.isEnabled()) {
                Log.e(TAG, "Service not enabled " + characteristic.getService().getUuid());
                return;
            }
            try {
                byte[] value = getConnectionByDevice(device).readCharacteristic(service.getServiceConfig(), offset, characteristic);
                getBluetoothGattServer().sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
            } catch (BluetoothGattException e) {
                Log.e(TAG, String.format("Could not read  %s on device %s at offset %d", BluetoothGattUtils.toString(characteristic), device, offset), e);
                getBluetoothGattServer().sendResponse(device, requestId, e.getGattErrorCode(), offset, null);
            }
        }

        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            BluetoothSocketService service = services.get(characteristic.getService().getUuid());
            if (service == null) {
                Log.e(TAG, "Service not found " + characteristic.getService().getUuid());
                return;
            }
            if (!service.isEnabled()) {
                Log.e(TAG, "Service not enabled " + characteristic.getService().getUuid());
                return;
            }
            try {
                getConnectionByDevice(device).writeCharacteristic(service.getServiceConfig(), characteristic, preparedWrite, offset, value);
                if (responseNeeded) {
                    getBluetoothGattServer().sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
                }
            } catch (BluetoothGattException e) {
                Log.e(TAG, String.format("Could not write %s on device %s at offset %d", BluetoothGattUtils.toString(characteristic), device, offset), e);
                getBluetoothGattServer().sendResponse(device, requestId, e.getGattErrorCode(), offset, null);
            }
        }

        @Override
        public void onNotificationSent(BluetoothDevice device, int status) {
            Log.d(TAG, String.format("Received onNotificationSent for device %s with status %s", device, status));
            try {
                getConnectionByDevice(device).onNotificationSent(status);
            } catch (BluetoothGattException e) {
                Log.e(TAG, String.format("An error occurred when receiving onNotificationSent"), e);
            }
        }

        @Override
        public void onMtuChanged(BluetoothDevice device, int mtu) {
            Log.d(TAG, String.format("Received onMtuChanged for device %s with mtu %s", device, mtu));
            try {
                getConnectionByDevice(device).setMtu(mtu);
            } catch (BluetoothGattException e) {
                Log.e(TAG, String.format("An error occurred when receiving onMtuChanged."), e);
            }
        }

        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            getBluetoothGattServer().sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
        }

        @Override
        public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
            Log.d(TAG, String.format("Received onExecuteWrite for device %s with execute: %s", device, execute));
            try {
                getConnectionByDevice(device).executeWrite(execute);
                getBluetoothGattServer().sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
            } catch (BluetoothGattException e) {
                Log.e(TAG, String.format("Could not execute pending writes on device %s", device), e);
                getBluetoothGattServer().sendResponse(device, requestId, e.getGattErrorCode(), 0, null);
            }
        }
    }
}

14 Source : BleAdvertiser.java
with MIT License
from ito-org

public clreplaced BleAdvertiser {

    private static final String LOG_TAG = "BleAdvertiser";

    private final Context context;

    private byte[] broadcastData;

    private BluetoothLeAdvertiser bluetoothLeAdvertiser;

    private AdvertiseCallback bluetoothAdvertiseCallback;

    private BluetoothGattServer mBluetoothGattServer;

    private BluetoothManager bluetoothManager;

    private final BluetoothAdapter bluetoothAdapter;

    public BleAdvertiser(BluetoothManager bluetoothManager, Context context) {
        bluetoothAdapter = bluetoothManager.getAdapter();
        this.bluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
        this.bluetoothManager = bluetoothManager;
        this.context = context;
    }

    public void setBroadcastData(byte[] broadcastData) {
        this.broadcastData = broadcastData;
        // Restart advertising so that mac address changes
        // Otherwise the device could be recognized that way even though the UUID has changed
        if (bluetoothAdvertiseCallback != null) {
            restartAdvertising();
        }
    }

    private void restartAdvertising() {
        stopAdvertising();
        startAdvertising();
    }

    public void startAdvertising() {
        Log.i(LOG_TAG, "Starting Advertising");
        AdvertiseSettings settings = new AdvertiseSettings.Builder().setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED).setConnectable(true).setTimeout(0).setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM).build();
        AdvertiseData data = new AdvertiseData.Builder().setIncludeTxPowerLevel(true).addServiceUuid(new ParcelUuid(BandemicProfile.BANDEMIC_SERVICE)).setIncludeDeviceName(false).build();
        bluetoothAdvertiseCallback = new AdvertiseCallback() {

            @Override
            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                super.onStartSuccess(settingsInEffect);
                Log.i(LOG_TAG, "Advertising onStartSuccess");
            }

            @Override
            public void onStartFailure(int errorCode) {
                super.onStartFailure(errorCode);
                Log.e(LOG_TAG, "Advertising onStartFailure: " + errorCode);
            // TODO
            }
        };
        // Set fixed device name to avoid being recognizable except though UUID
        // Name is not sent in advertisement anyway but can be requested though GATT Server
        // and I don't see any way to disable that
        bluetoothAdapter.setName("Phone");
        // TODO: check if null when launching with Bluetooth disabled
        bluetoothLeAdvertiser.startAdvertising(settings, data, bluetoothAdvertiseCallback);
        mBluetoothGattServer = bluetoothManager.openGattServer(context, mGattServerCallback);
        if (mBluetoothGattServer == null) {
            Log.w(LOG_TAG, "Unable to create GATT server");
            return;
        }
        mBluetoothGattServer.addService(BandemicProfile.createBandemicService());
    }

    public void stopAdvertising() {
        Log.d(LOG_TAG, "Stopping advertising");
        if (bluetoothAdvertiseCallback != null) {
            bluetoothLeAdvertiser.stopAdvertising(bluetoothAdvertiseCallback);
            bluetoothAdvertiseCallback = null;
        }
        if (mBluetoothGattServer != null) {
            mBluetoothGattServer.close();
            mBluetoothGattServer = null;
        }
    }

    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.i(LOG_TAG, "BluetoothDevice CONNECTED: " + device);
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.i(LOG_TAG, "BluetoothDevice DISCONNECTED: " + device);
            }
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            if (BandemicProfile.HASH_OF_UUID.equals(characteristic.getUuid())) {
                Log.i(LOG_TAG, "Read Hash of UUID");
                Log.i(LOG_TAG, "Offset: " + offset);
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, Arrays.copyOfRange(broadcastData, offset, broadcastData.length));
            } else {
                // Invalid characteristic
                Log.w(LOG_TAG, "Invalid Characteristic Read: " + characteristic.getUuid());
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
            }
        }

        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            Log.w(LOG_TAG, "Unknown descriptor read request");
            mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
        }

        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            Log.w(LOG_TAG, "Unknown descriptor write request");
            if (responseNeeded) {
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
            }
        }
    };
}

13 Source : GattServer.java
with Apache License 2.0
from Nilhcem

public clreplaced GattServer {

    private static final String TAG = GattServer.clreplaced.getSimpleName();

    public interface GattServerListener {

        void onInteractorWritten();

        byte[] onCounterRead();
    }

    private Context mContext;

    private GattServerListener mListener;

    private BluetoothManager mBluetoothManager;

    private BluetoothGattServer mBluetoothGattServer;

    private BluetoothLeAdvertiser mBluetoothLeAdvertiser;

    /* Collection of notification subscribers */
    private Set<BluetoothDevice> mRegisteredDevices = new HashSet<>();

    /**
     * Listens for Bluetooth adapter events to enable/disable
     * advertising and server functionality.
     */
    private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
            switch(state) {
                case BluetoothAdapter.STATE_ON:
                    startAdvertising();
                    startServer();
                    break;
                case BluetoothAdapter.STATE_OFF:
                    stopServer();
                    stopAdvertising();
                    break;
                default:
                    // Do nothing
                    break;
            }
        }
    };

    private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {

        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            Log.i(TAG, "LE Advertise Started.");
        }

        @Override
        public void onStartFailure(int errorCode) {
            Log.w(TAG, "LE Advertise Failed: " + errorCode);
        }
    };

    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.i(TAG, "BluetoothDevice CONNECTED: " + device);
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.i(TAG, "BluetoothDevice DISCONNECTED: " + device);
                // Remove device from any active subscriptions
                mRegisteredDevices.remove(device);
            }
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            if (CHARACTERISTIC_COUNTER_UUID.equals(characteristic.getUuid())) {
                Log.i(TAG, "Read counter");
                byte[] value = mListener.onCounterRead();
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, value);
            } else {
                // Invalid characteristic
                Log.w(TAG, "Invalid Characteristic Read: " + characteristic.getUuid());
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
            }
        }

        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            if (CHARACTERISTIC_INTERACTOR_UUID.equals(characteristic.getUuid())) {
                Log.i(TAG, "Write interactor");
                if (mListener != null) {
                    mListener.onInteractorWritten();
                }
                notifyRegisteredDevices();
            } else {
                // Invalid characteristic
                Log.w(TAG, "Invalid Characteristic Write: " + characteristic.getUuid());
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
            }
        }

        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            if (DESCRIPTOR_CONFIG.equals(descriptor.getUuid())) {
                Log.d(TAG, "Config descriptor read request");
                byte[] returnValue;
                if (mRegisteredDevices.contains(device)) {
                    returnValue = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
                } else {
                    returnValue = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
                }
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, returnValue);
            } else if (DESCRIPTOR_USER_DESC.equals(descriptor.getUuid())) {
                Log.d(TAG, "User description descriptor read request");
                byte[] returnValue = AwesomenessProfile.getUserDescription(descriptor.getCharacteristic().getUuid());
                returnValue = Arrays.copyOfRange(returnValue, offset, returnValue.length);
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, returnValue);
            } else {
                Log.w(TAG, "Unknown descriptor read request");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
            }
        }

        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            if (DESCRIPTOR_CONFIG.equals(descriptor.getUuid())) {
                if (Arrays.equals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, value)) {
                    Log.d(TAG, "Subscribe device to notifications: " + device);
                    mRegisteredDevices.add(device);
                } else if (Arrays.equals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, value)) {
                    Log.d(TAG, "Unsubscribe device from notifications: " + device);
                    mRegisteredDevices.remove(device);
                }
                if (responseNeeded) {
                    mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
                }
            } else {
                Log.w(TAG, "Unknown descriptor write request");
                if (responseNeeded) {
                    mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
                }
            }
        }
    };

    public void onCreate(Context context, GattServerListener listener) throws RuntimeException {
        mContext = context;
        mListener = listener;
        mBluetoothManager = (BluetoothManager) context.getSystemService(BLUETOOTH_SERVICE);
        BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
        if (!checkBluetoothSupport(bluetoothAdapter)) {
            throw new RuntimeException("GATT server requires Bluetooth support");
        }
        // Register for system Bluetooth events
        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        mContext.registerReceiver(mBluetoothReceiver, filter);
        if (!bluetoothAdapter.isEnabled()) {
            Log.d(TAG, "Bluetooth is currently disabled... enabling");
            bluetoothAdapter.enable();
        } else {
            Log.d(TAG, "Bluetooth enabled... starting services");
            startAdvertising();
            startServer();
        }
    }

    public void onDestroy() {
        BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
        if (bluetoothAdapter.isEnabled()) {
            stopServer();
            stopAdvertising();
        }
        mContext.unregisterReceiver(mBluetoothReceiver);
        mListener = null;
    }

    private boolean checkBluetoothSupport(BluetoothAdapter bluetoothAdapter) {
        if (bluetoothAdapter == null) {
            Log.w(TAG, "Bluetooth is not supported");
            return false;
        }
        if (!mContext.getPackageManager().hreplacedystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Log.w(TAG, "Bluetooth LE is not supported");
            return false;
        }
        return true;
    }

    private void startAdvertising() {
        BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
        mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
        if (mBluetoothLeAdvertiser == null) {
            Log.w(TAG, "Failed to create advertiser");
            return;
        }
        AdvertiseSettings settings = new AdvertiseSettings.Builder().setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED).setConnectable(true).setTimeout(0).setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM).build();
        AdvertiseData data = new AdvertiseData.Builder().setIncludeDeviceName(true).setIncludeTxPowerLevel(false).addServiceUuid(new ParcelUuid(SERVICE_UUID)).build();
        mBluetoothLeAdvertiser.startAdvertising(settings, data, mAdvertiseCallback);
    }

    private void stopAdvertising() {
        if (mBluetoothLeAdvertiser == null) {
            return;
        }
        mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
    }

    private void startServer() {
        mBluetoothGattServer = mBluetoothManager.openGattServer(mContext, mGattServerCallback);
        if (mBluetoothGattServer == null) {
            Log.w(TAG, "Unable to create GATT server");
            return;
        }
        mBluetoothGattServer.addService(createAwesomenessService());
    }

    private void stopServer() {
        if (mBluetoothGattServer == null) {
            return;
        }
        mBluetoothGattServer.close();
    }

    private BluetoothGattService createAwesomenessService() {
        BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
        // Counter characteristic (read-only, supports notifications)
        BluetoothGattCharacteristic counter = new BluetoothGattCharacteristic(CHARACTERISTIC_COUNTER_UUID, BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
        BluetoothGattDescriptor counterConfig = new BluetoothGattDescriptor(DESCRIPTOR_CONFIG, BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
        counter.addDescriptor(counterConfig);
        BluetoothGattDescriptor counterDescription = new BluetoothGattDescriptor(DESCRIPTOR_USER_DESC, BluetoothGattDescriptor.PERMISSION_READ);
        counter.addDescriptor(counterDescription);
        // Interactor characteristic
        BluetoothGattCharacteristic interactor = new BluetoothGattCharacteristic(CHARACTERISTIC_INTERACTOR_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PERMISSION_WRITE);
        BluetoothGattDescriptor interactorDescription = new BluetoothGattDescriptor(DESCRIPTOR_USER_DESC, BluetoothGattDescriptor.PERMISSION_READ);
        interactor.addDescriptor(interactorDescription);
        service.addCharacteristic(counter);
        service.addCharacteristic(interactor);
        return service;
    }

    private void notifyRegisteredDevices() {
        if (mRegisteredDevices.isEmpty()) {
            Log.i(TAG, "No subscribers registered");
            return;
        }
        Log.i(TAG, "Sending update to " + mRegisteredDevices.size() + " subscribers");
        for (BluetoothDevice device : mRegisteredDevices) {
            BluetoothGattCharacteristic counterCharacteristic = mBluetoothGattServer.getService(SERVICE_UUID).getCharacteristic(CHARACTERISTIC_COUNTER_UUID);
            byte[] value = mListener.onCounterRead();
            counterCharacteristic.setValue(value);
            mBluetoothGattServer.notifyCharacteristicChanged(device, counterCharacteristic, false);
        }
    }
}

13 Source : GattServerActivity.java
with Apache License 2.0
from androidthings

public clreplaced GattServerActivity extends Activity {

    private static final String TAG = GattServerActivity.clreplaced.getSimpleName();

    /* Local UI */
    private TextView mLocalTimeView;

    /* Bluetooth API */
    private BluetoothManager mBluetoothManager;

    private BluetoothGattServer mBluetoothGattServer;

    private BluetoothLeAdvertiser mBluetoothLeAdvertiser;

    /* Collection of notification subscribers */
    private Set<BluetoothDevice> mRegisteredDevices = new HashSet<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_server);
        mLocalTimeView = (TextView) findViewById(R.id.text_time);
        // Devices with a display should not go to sleep
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        mBluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
        BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
        // We can't continue without proper Bluetooth support
        if (!checkBluetoothSupport(bluetoothAdapter)) {
            finish();
        }
        // Register for system Bluetooth events
        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        registerReceiver(mBluetoothReceiver, filter);
        if (!bluetoothAdapter.isEnabled()) {
            Log.d(TAG, "Bluetooth is currently disabled...enabling");
            bluetoothAdapter.enable();
        } else {
            Log.d(TAG, "Bluetooth enabled...starting services");
            startAdvertising();
            startServer();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        // Register for system clock events
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_TIME_TICK);
        filter.addAction(Intent.ACTION_TIME_CHANGED);
        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
        registerReceiver(mTimeReceiver, filter);
    }

    @Override
    protected void onStop() {
        super.onStop();
        unregisterReceiver(mTimeReceiver);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
        if (bluetoothAdapter.isEnabled()) {
            stopServer();
            stopAdvertising();
        }
        unregisterReceiver(mBluetoothReceiver);
    }

    /**
     * Verify the level of Bluetooth support provided by the hardware.
     * @param bluetoothAdapter System {@link BluetoothAdapter}.
     * @return true if Bluetooth is properly supported, false otherwise.
     */
    private boolean checkBluetoothSupport(BluetoothAdapter bluetoothAdapter) {
        if (bluetoothAdapter == null) {
            Log.w(TAG, "Bluetooth is not supported");
            return false;
        }
        if (!getPackageManager().hreplacedystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Log.w(TAG, "Bluetooth LE is not supported");
            return false;
        }
        return true;
    }

    /**
     * Listens for system time changes and triggers a notification to
     * Bluetooth subscribers.
     */
    private BroadcastReceiver mTimeReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            byte adjustReason;
            switch(intent.getAction()) {
                case Intent.ACTION_TIME_CHANGED:
                    adjustReason = TimeProfile.ADJUST_MANUAL;
                    break;
                case Intent.ACTION_TIMEZONE_CHANGED:
                    adjustReason = TimeProfile.ADJUST_TIMEZONE;
                    break;
                default:
                case Intent.ACTION_TIME_TICK:
                    adjustReason = TimeProfile.ADJUST_NONE;
                    break;
            }
            long now = System.currentTimeMillis();
            notifyRegisteredDevices(now, adjustReason);
            updateLocalUi(now);
        }
    };

    /**
     * Listens for Bluetooth adapter events to enable/disable
     * advertising and server functionality.
     */
    private BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
            switch(state) {
                case BluetoothAdapter.STATE_ON:
                    startAdvertising();
                    startServer();
                    break;
                case BluetoothAdapter.STATE_OFF:
                    stopServer();
                    stopAdvertising();
                    break;
                default:
            }
        }
    };

    /**
     * Begin advertising over Bluetooth that this device is connectable
     * and supports the Current Time Service.
     */
    private void startAdvertising() {
        BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
        mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
        if (mBluetoothLeAdvertiser == null) {
            Log.w(TAG, "Failed to create advertiser");
            return;
        }
        AdvertiseSettings settings = new AdvertiseSettings.Builder().setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED).setConnectable(true).setTimeout(0).setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM).build();
        AdvertiseData data = new AdvertiseData.Builder().setIncludeDeviceName(true).setIncludeTxPowerLevel(false).addServiceUuid(new ParcelUuid(TimeProfile.TIME_SERVICE)).build();
        mBluetoothLeAdvertiser.startAdvertising(settings, data, mAdvertiseCallback);
    }

    /**
     * Stop Bluetooth advertisements.
     */
    private void stopAdvertising() {
        if (mBluetoothLeAdvertiser == null)
            return;
        mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
    }

    /**
     * Initialize the GATT server instance with the services/characteristics
     * from the Time Profile.
     */
    private void startServer() {
        mBluetoothGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
        if (mBluetoothGattServer == null) {
            Log.w(TAG, "Unable to create GATT server");
            return;
        }
        mBluetoothGattServer.addService(TimeProfile.createTimeService());
        // Initialize the local UI
        updateLocalUi(System.currentTimeMillis());
    }

    /**
     * Shut down the GATT server.
     */
    private void stopServer() {
        if (mBluetoothGattServer == null)
            return;
        mBluetoothGattServer.close();
    }

    /**
     * Callback to receive information about the advertisement process.
     */
    private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {

        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            Log.i(TAG, "LE Advertise Started.");
        }

        @Override
        public void onStartFailure(int errorCode) {
            Log.w(TAG, "LE Advertise Failed: " + errorCode);
        }
    };

    /**
     * Send a time service notification to any devices that are subscribed
     * to the characteristic.
     */
    private void notifyRegisteredDevices(long timestamp, byte adjustReason) {
        if (mRegisteredDevices.isEmpty()) {
            Log.i(TAG, "No subscribers registered");
            return;
        }
        byte[] exactTime = TimeProfile.getExactTime(timestamp, adjustReason);
        Log.i(TAG, "Sending update to " + mRegisteredDevices.size() + " subscribers");
        for (BluetoothDevice device : mRegisteredDevices) {
            BluetoothGattCharacteristic timeCharacteristic = mBluetoothGattServer.getService(TimeProfile.TIME_SERVICE).getCharacteristic(TimeProfile.CURRENT_TIME);
            timeCharacteristic.setValue(exactTime);
            mBluetoothGattServer.notifyCharacteristicChanged(device, timeCharacteristic, false);
        }
    }

    /**
     * Update graphical UI on devices that support it with the current time.
     */
    private void updateLocalUi(long timestamp) {
        Date date = new Date(timestamp);
        String displayDate = DateFormat.getMediumDateFormat(this).format(date) + "\n" + DateFormat.getTimeFormat(this).format(date);
        mLocalTimeView.setText(displayDate);
    }

    /**
     * Callback to handle incoming requests to the GATT server.
     * All read/write requests for characteristics and descriptors are handled here.
     */
    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.i(TAG, "BluetoothDevice CONNECTED: " + device);
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.i(TAG, "BluetoothDevice DISCONNECTED: " + device);
                // Remove device from any active subscriptions
                mRegisteredDevices.remove(device);
            }
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            long now = System.currentTimeMillis();
            if (TimeProfile.CURRENT_TIME.equals(characteristic.getUuid())) {
                Log.i(TAG, "Read CurrentTime");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, TimeProfile.getExactTime(now, TimeProfile.ADJUST_NONE));
            } else if (TimeProfile.LOCAL_TIME_INFO.equals(characteristic.getUuid())) {
                Log.i(TAG, "Read LocalTimeInfo");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, TimeProfile.getLocalTimeInfo(now));
            } else {
                // Invalid characteristic
                Log.w(TAG, "Invalid Characteristic Read: " + characteristic.getUuid());
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
            }
        }

        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            if (TimeProfile.CLIENT_CONFIG.equals(descriptor.getUuid())) {
                Log.d(TAG, "Config descriptor read");
                byte[] returnValue;
                if (mRegisteredDevices.contains(device)) {
                    returnValue = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
                } else {
                    returnValue = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
                }
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, returnValue);
            } else {
                Log.w(TAG, "Unknown descriptor read request");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
            }
        }

        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            if (TimeProfile.CLIENT_CONFIG.equals(descriptor.getUuid())) {
                if (Arrays.equals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, value)) {
                    Log.d(TAG, "Subscribe device to notifications: " + device);
                    mRegisteredDevices.add(device);
                } else if (Arrays.equals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, value)) {
                    Log.d(TAG, "Unsubscribe device from notifications: " + device);
                    mRegisteredDevices.remove(device);
                }
                if (responseNeeded) {
                    mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
                }
            } else {
                Log.w(TAG, "Unknown descriptor write request");
                if (responseNeeded) {
                    mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
                }
            }
        }
    };
}

12 Source : BleServerManager.java
with BSD 3-Clause "New" or "Revised" License
from NordicSemiconductor

/**
 * The manager for local GATT server. To be used with one or more instances of {@link BleManager}
 *
 * @since 2.2
 */
@SuppressWarnings({ "WeakerAccess", "unused" })
public abstract clreplaced BleServerManager implements ILogger {

    private final static UUID CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR_UUID = UUID.fromString("00002900-0000-1000-8000-00805f9b34fb");

    private final static UUID CLIENT_USER_DESCRIPTION_DESCRIPTOR_UUID = UUID.fromString("00002901-0000-1000-8000-00805f9b34fb");

    private final static UUID CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    /**
     * Bluetooth GATT server instance, or null if not opened.
     */
    private BluetoothGattServer server;

    private final List<BleManager> managers = new ArrayList<>();

    private final Context context;

    private ServerObserver serverObserver;

    /**
     * List of server services returned by {@link #initializeServer()}.
     * This list is empties when the services are being added one by one to the server.
     * To get the server services, use {@link BluetoothGattServer#getServices()} instead.
     */
    private Queue<BluetoothGattService> serverServices;

    private List<BluetoothGattCharacteristic> sharedCharacteristics;

    private List<BluetoothGattDescriptor> sharedDescriptors;

    public BleServerManager(@NonNull final Context context) {
        this.context = context;
    }

    /**
     * Opens the GATT server and starts initializing services. This method only starts initializing
     * services. The {@link ServerObserver#onServerReady()} will be called when all
     * services are done.
     *
     * @return true, if the server has been started successfully. If GATT server could not
     * be started, for example the Bluetooth is disabled, false is returned.
     * @see #close()
     */
    public final boolean open() {
        if (server != null)
            return true;
        serverServices = new LinkedList<>(initializeServer());
        final BluetoothManager bm = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
        if (bm != null) {
            server = bm.openGattServer(context, gattServerCallback);
        }
        if (server != null) {
            log(Log.INFO, "[Server] Server started successfully");
            try {
                final BluetoothGattService service = serverServices.remove();
                server.addService(service);
            } catch (final NoSuchElementException e) {
                if (serverObserver != null)
                    serverObserver.onServerReady();
            } catch (final Exception e) {
                close();
                return false;
            }
            return true;
        }
        log(Log.WARN, "GATT server initialization failed");
        serverServices = null;
        return false;
    }

    /**
     * Closes the GATT server.
     */
    public final void close() {
        if (server != null) {
            server.close();
            server = null;
        }
        serverServices = null;
        for (BleManager manager : managers) {
            // closeServer() must be called before close(). Otherwise close() would remove
            // the manager from managers list while iterating this loop.
            manager.closeServer();
            manager.close();
        }
        managers.clear();
    }

    /**
     * Sets the server observer.
     *
     * @param observer the observer.
     */
    public final void setServerObserver(@NonNull final ServerObserver observer) {
        this.serverObserver = observer;
    }

    /**
     * Returns the {@link BluetoothGattServer} instance.
     */
    @Nullable
    final BluetoothGattServer getServer() {
        return server;
    }

    /**
     * Adds the BLE Manager to be handled.
     * @param manager the Ble Manager.
     */
    final void addManager(@NonNull final BleManager manager) {
        if (!managers.contains(manager)) {
            managers.add(manager);
        }
    }

    /**
     * Removes the manager. Callbacks will no longer be preplaceded to it.
     * @param manager the manager to be removed.
     */
    final void removeManager(@NonNull final BleManager manager) {
        managers.remove(manager);
    }

    final boolean isShared(@NonNull final BluetoothGattCharacteristic characteristic) {
        return sharedCharacteristics != null && sharedCharacteristics.contains(characteristic);
    }

    final boolean isShared(@NonNull final BluetoothGattDescriptor descriptor) {
        return sharedDescriptors != null && sharedDescriptors.contains(descriptor);
    }

    @Nullable
    private BleManagerHandler getRequestHandler(@NonNull final BluetoothDevice device) {
        for (final BleManager manager : managers) {
            if (device.equals(manager.getBluetoothDevice())) {
                return manager.requestHandler;
            }
        }
        return null;
    }

    @Override
    public void log(final int priority, @NonNull final String message) {
    // Override to log events. Simple log can use Logcat:
    // 
    // Log.println(priority, TAG, message);
    // 
    // You may also use Timber:
    // 
    // Timber.log(priority, message);
    // 
    // or nRF Logger:
    // 
    // Logger.log(logSession, LogContract.Log.Level.fromPriority(priority), message);
    // 
    // Starting from nRF Logger 2.1.3, you may use log-timber and plant nRFLoggerTree.
    // https://github.com/NordicSemiconductor/nRF-Logger-API
    }

    @Override
    public void log(final int priority, @StringRes final int messageRes, @Nullable final Object... params) {
        final String message = context.getString(messageRes, params);
        log(priority, message);
    }

    /**
     * This method is called once, just after instantiating the {@link BleServerManager}.
     * It should return a list of server GATT services that will be available for the remote device
     * to use. You may use {@link #service(UUID, BluetoothGattCharacteristic...)} to easily
     * instantiate a service.
     * <p>
     * Server services will be added to the local GATT configuration on the Android device.
     * The library does not know what services are already set up by other apps or
     * {@link BleServerManager} instances, so a UUID collision is possible.
     * The remote device will discover all services set up by all apps.
     * <p>
     * In order to enable server callbacks (see {@link android.bluetooth.BluetoothGattServerCallback}),
     * but without defining own services, return an empty list.
     *
     * @since 2.2
     * @return The list of server GATT services, or null if no services should be created. An
     * empty array to start the GATT server without any services.
     */
    @NonNull
    protected abstract List<BluetoothGattService> initializeServer();

    /**
     * A helper method for creating a primary service with given UUID and list of characteristics.
     * This method can be called from {@link #initializeServer()}.
     *
     * @param uuid The service UUID.
     * @param characteristics The optional list of characteristics.
     * @return The new service.
     */
    @NonNull
    protected final BluetoothGattService service(@NonNull final UUID uuid, final BluetoothGattCharacteristic... characteristics) {
        final BluetoothGattService service = new BluetoothGattService(uuid, BluetoothGattService.SERVICE_TYPE_PRIMARY);
        for (BluetoothGattCharacteristic characteristic : characteristics) {
            service.addCharacteristic(characteristic);
        }
        return service;
    }

    /**
     * A helper method that creates a characteristic with given UUID, properties and permissions.
     * Optionally, an initial value and a list of descriptors may be set.
     * <p>
     * The Client Characteristic Configuration Descriptor (CCCD) will be added automatically if
     * {@link BluetoothGattCharacteristic#PROPERTY_NOTIFY} or {@link BluetoothGattCharacteristic#PROPERTY_INDICATE}
     * was set, if not added explicitly in the descriptors list.
     * <p>
     * If {@link #reliableWrite()} was added as one of the descriptors or the Characteristic User
     * Description descriptor was created with any of write permissions
     * (see {@link #description(String, boolean)}) the
     * {@link BluetoothGattCharacteristic#PROPERTY_EXTENDED_PROPS} property will be added automatically.
     * <p>
     * The value of the characteristic will NOT be shared between clients. Each client will write
     * and read its own copy. To create a shared characteristic, use
     * {@link #sharedCharacteristic(UUID, int, int, byte[], BluetoothGattDescriptor...)} instead.
     *
     * @param uuid The characteristic UUID.
     * @param properties The bit mask of characteristic properties. See {@link BluetoothGattCharacteristic}
     *                   for details.
     * @param permissions The bit mask or characteristic permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param initialValue The optional initial value of the characteristic.
     * @param descriptors The optional list of descriptors.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattCharacteristic characteristic(@NonNull final UUID uuid, @CharacteristicProperties int properties, @CharacteristicPermissions final int permissions, @Nullable final byte[] initialValue, final BluetoothGattDescriptor... descriptors) {
        // Look for Client Characteristic Configuration descriptor,
        // Characteristic User Description descriptor and Characteristic Extended Properties descriptor.
        boolean writableAuxiliaries = false;
        boolean cccdFound = false;
        boolean cepdFound = false;
        BluetoothGattDescriptor cepd = null;
        for (final BluetoothGattDescriptor descriptor : descriptors) {
            if (CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID.equals(descriptor.getUuid())) {
                cccdFound = true;
            } else if (CLIENT_USER_DESCRIPTION_DESCRIPTOR_UUID.equals(descriptor.getUuid()) && 0 != (descriptor.getPermissions() & (BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED | BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED_MITM))) {
                writableAuxiliaries = true;
            } else if (CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR_UUID.equals(descriptor.getUuid())) {
                cepd = descriptor;
                cepdFound = true;
            }
        }
        if (writableAuxiliaries) {
            if (cepd == null) {
                cepd = new BluetoothGattDescriptor(CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_READ);
                cepd.setValue(new byte[] { 0x02, 0x00 });
            } else {
                if (cepd.getValue() != null && cepd.getValue().length == 2) {
                    cepd.getValue()[0] |= 0x02;
                } else {
                    cepd.setValue(new byte[] { 0x02, 0x00 });
                }
            }
        }
        final boolean cccdRequired = (properties & (BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_INDICATE)) != 0;
        final boolean reliableWrite = cepd != null && cepd.getValue() != null && cepd.getValue().length == 2 && (cepd.getValue()[0] & 0x01) != 0;
        if (writableAuxiliaries || reliableWrite) {
            properties |= BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS;
        }
        if ((properties & BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS) != 0 && cepd == null) {
            cepd = new BluetoothGattDescriptor(CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_READ);
            cepd.setValue(new byte[] { 0, 0 });
        }
        final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(uuid, properties, permissions);
        if (cccdRequired && !cccdFound) {
            characteristic.addDescriptor(cccd());
        }
        for (BluetoothGattDescriptor descriptor : descriptors) {
            characteristic.addDescriptor(descriptor);
        }
        if (cepd != null && !cepdFound) {
            characteristic.addDescriptor(cepd);
        }
        characteristic.setValue(initialValue);
        return characteristic;
    }

    /**
     * A helper method that creates a characteristic with given UUID, properties and permissions.
     * Optionally, an initial value and a list of descriptors may be set.
     * <p>
     * The Client Characteristic Configuration Descriptor (CCCD) will be added automatically if
     * {@link BluetoothGattCharacteristic#PROPERTY_NOTIFY} or {@link BluetoothGattCharacteristic#PROPERTY_INDICATE}
     * was set, if not added explicitly in the descriptors list.
     * <p>
     * If {@link #reliableWrite()} was added as one of the descriptors or the Characteristic User
     * Description descriptor was created with any of write permissions
     * (see {@link #description(String, boolean)}) the
     * {@link BluetoothGattCharacteristic#PROPERTY_EXTENDED_PROPS} property will be added automatically.
     * <p>
     * The value of the characteristic will NOT be shared between clients. Each client will write
     * and read its own copy. To create a shared characteristic, use
     * {@link #sharedCharacteristic(UUID, int, int, byte[], BluetoothGattDescriptor...)} instead.
     *
     * @param uuid The characteristic UUID.
     * @param properties The bit mask of characteristic properties. See {@link BluetoothGattCharacteristic}
     *                   for details.
     * @param permissions The bit mask or characteristic permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param initialValue The optional initial value of the characteristic.
     * @param descriptors The optional list of descriptors.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattCharacteristic characteristic(@NonNull final UUID uuid, @CharacteristicProperties final int properties, @CharacteristicPermissions final int permissions, @Nullable final Data initialValue, final BluetoothGattDescriptor... descriptors) {
        return characteristic(uuid, properties, permissions, initialValue != null ? initialValue.getValue() : null, descriptors);
    }

    /**
     * A helper method that creates a characteristic with given UUID, properties and permissions.
     * Optionally, a list of descriptors may be set.
     * <p>
     * The Client Characteristic Configuration Descriptor (CCCD) will be added automatically if
     * {@link BluetoothGattCharacteristic#PROPERTY_NOTIFY} or {@link BluetoothGattCharacteristic#PROPERTY_INDICATE}
     * was set, if not added explicitly in the descriptors list.
     * <p>
     * If {@link #reliableWrite()} was added as one of the descriptors or the Characteristic User
     * Description descriptor was created with any of write permissions
     * (see {@link #description(String, boolean)}) the
     * {@link BluetoothGattCharacteristic#PROPERTY_EXTENDED_PROPS} property will be added automatically.
     * <p>
     * The value of the characteristic will NOT be shared between clients. Each client will write
     * and read its own copy. To create a shared characteristic, use
     * {@link #sharedCharacteristic(UUID, int, int, byte[], BluetoothGattDescriptor...)} instead.
     *
     * @param uuid The characteristic UUID.
     * @param properties The bit mask of characteristic properties. See {@link BluetoothGattCharacteristic}
     *                   for details.
     * @param permissions The bit mask or characteristic permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param descriptors The optional list of descriptors.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattCharacteristic characteristic(@NonNull final UUID uuid, @CharacteristicProperties final int properties, @CharacteristicPermissions final int permissions, final BluetoothGattDescriptor... descriptors) {
        return characteristic(uuid, properties, permissions, (byte[]) null, descriptors);
    }

    /**
     * A helper method that creates a characteristic with given UUID, properties and permissions.
     * Optionally, an initial value and a list of descriptors may be set.
     * <p>
     * The Client Characteristic Configuration Descriptor (CCCD) will be added automatically if
     * {@link BluetoothGattCharacteristic#PROPERTY_NOTIFY} or {@link BluetoothGattCharacteristic#PROPERTY_INDICATE}
     * was set, if not added explicitly in the descriptors list.
     * <p>
     * If {@link #reliableWrite()} was added as one of the descriptors or the Characteristic User
     * Description descriptor was created with any of write permissions
     * (see {@link #description(String, boolean)}) the
     * {@link BluetoothGattCharacteristic#PROPERTY_EXTENDED_PROPS} property will be added automatically.
     * <p>
     * The value of the characteristic is shared between clients. A value written by one of the
     * connected clients will be available for all other clients. To create a sandboxed characteristic,
     * use {@link #characteristic(UUID, int, int, byte[], BluetoothGattDescriptor...)} instead.
     *
     * @param uuid The characteristic UUID.
     * @param properties The bit mask of characteristic properties. See {@link BluetoothGattCharacteristic}
     *                   for details.
     * @param permissions The bit mask or characteristic permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param initialValue The optional initial value of the characteristic.
     * @param descriptors The optional list of descriptors.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattCharacteristic sharedCharacteristic(@NonNull final UUID uuid, @CharacteristicProperties final int properties, @CharacteristicPermissions final int permissions, @Nullable final byte[] initialValue, final BluetoothGattDescriptor... descriptors) {
        final BluetoothGattCharacteristic characteristic = characteristic(uuid, properties, permissions, initialValue, descriptors);
        if (sharedCharacteristics == null)
            sharedCharacteristics = new ArrayList<>();
        sharedCharacteristics.add(characteristic);
        return characteristic;
    }

    /**
     * A helper method that creates a characteristic with given UUID, properties and permissions.
     * Optionally, an initial value and a list of descriptors may be set.
     * <p>
     * The Client Characteristic Configuration Descriptor (CCCD) will be added automatically if
     * {@link BluetoothGattCharacteristic#PROPERTY_NOTIFY} or {@link BluetoothGattCharacteristic#PROPERTY_INDICATE}
     * was set, if not added explicitly in the descriptors list.
     * <p>
     * If {@link #reliableWrite()} was added as one of the descriptors or the Characteristic User
     * Description descriptor was created with any of write permissions
     * (see {@link #description(String, boolean)}) the
     * {@link BluetoothGattCharacteristic#PROPERTY_EXTENDED_PROPS} property will be added automatically.
     * <p>
     * The value of the characteristic is shared between clients. A value written by one of the
     * connected clients will be available for all other clients. To create a sandboxed characteristic,
     * use {@link #characteristic(UUID, int, int, byte[], BluetoothGattDescriptor...)} instead.
     *
     * @param uuid The characteristic UUID.
     * @param properties The bit mask of characteristic properties. See {@link BluetoothGattCharacteristic}
     *                   for details.
     * @param permissions The bit mask or characteristic permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param initialValue The optional initial value of the characteristic.
     * @param descriptors The optional list of descriptors.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattCharacteristic sharedCharacteristic(@NonNull final UUID uuid, @CharacteristicProperties final int properties, @CharacteristicPermissions final int permissions, @Nullable final Data initialValue, final BluetoothGattDescriptor... descriptors) {
        return sharedCharacteristic(uuid, properties, permissions, initialValue != null ? initialValue.getValue() : null, descriptors);
    }

    /**
     * A helper method that creates a characteristic with given UUID, properties and permissions.
     * Optionally, a list of descriptors may be set.
     * <p>
     * The Client Characteristic Configuration Descriptor (CCCD) will be added automatically if
     * {@link BluetoothGattCharacteristic#PROPERTY_NOTIFY} or {@link BluetoothGattCharacteristic#PROPERTY_INDICATE}
     * was set, if not added explicitly in the descriptors list.
     * <p>
     * If {@link #reliableWrite()} was added as one of the descriptors or the Characteristic User
     * Description descriptor was created with any of write permissions
     * (see {@link #description(String, boolean)}) the
     * {@link BluetoothGattCharacteristic#PROPERTY_EXTENDED_PROPS} property will be added automatically.
     * <p>
     * The value of the characteristic is shared between clients. A value written by one of the
     * connected clients will be available for all other clients. To create a sandboxed characteristic,
     * use {@link #characteristic(UUID, int, int, byte[], BluetoothGattDescriptor...)} instead.
     *
     * @param uuid The characteristic UUID.
     * @param properties The bit mask of characteristic properties. See {@link BluetoothGattCharacteristic}
     *                   for details.
     * @param permissions The bit mask or characteristic permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param descriptors The optional list of descriptors.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattCharacteristic sharedCharacteristic(@NonNull final UUID uuid, @CharacteristicProperties final int properties, @CharacteristicPermissions final int permissions, final BluetoothGattDescriptor... descriptors) {
        return sharedCharacteristic(uuid, properties, permissions, (byte[]) null, descriptors);
    }

    /**
     * A helper method that creates a descriptor with given UUID and permissions.
     * Optionally, an initial value may be set.
     * <p>
     * The value of the descriptor will NOT be shared between clients. Each client will write
     * and read its own copy. To create a shared descriptor, use
     * {@link #sharedDescriptor(UUID, int, byte[])} instead.
     *
     * @param uuid The characteristic UUID.
     * @param permissions The bit mask or descriptor permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param initialValue The optional initial value of the descriptor.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattDescriptor descriptor(@NonNull final UUID uuid, @DescriptorPermissions final int permissions, @Nullable final byte[] initialValue) {
        final BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(uuid, permissions);
        descriptor.setValue(initialValue);
        return descriptor;
    }

    /**
     * A helper method that creates a descriptor with given UUID and permissions.
     * Optionally, an initial value may be set.
     * <p>
     * The value of the descriptor will NOT be shared between clients. Each client will write
     * and read its own copy. To create a shared descriptor, use
     * {@link #sharedDescriptor(UUID, int, byte[])} instead.
     *
     * @param uuid The characteristic UUID.
     * @param permissions The bit mask or descriptor permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param initialValue The optional initial value of the descriptor.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattDescriptor descriptor(@NonNull final UUID uuid, @DescriptorPermissions final int permissions, @Nullable final Data initialValue) {
        return descriptor(uuid, permissions, initialValue != null ? initialValue.getValue() : null);
    }

    /**
     * A helper method that creates a descriptor with given UUID and permissions.
     * Optionally, an initial value may be set.
     * <p>
     * The value of the characteristic is shared between clients. A value written by one of the
     * connected clients will be available for all other clients. To create a sandboxed characteristic,
     * use {@link #characteristic(UUID, int, int, byte[], BluetoothGattDescriptor...)} instead.
     *
     * @param uuid The characteristic UUID.
     * @param permissions The bit mask or characteristic permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param initialValue The optional initial value of the characteristic.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattDescriptor sharedDescriptor(@NonNull final UUID uuid, @DescriptorPermissions final int permissions, @Nullable final byte[] initialValue) {
        final BluetoothGattDescriptor descriptor = descriptor(uuid, permissions, initialValue);
        if (sharedDescriptors == null)
            sharedDescriptors = new ArrayList<>();
        sharedDescriptors.add(descriptor);
        return descriptor;
    }

    /**
     * A helper method that creates a descriptor with given UUID and permissions.
     * Optionally, an initial value may be set.
     * <p>
     * The value of the characteristic is shared between clients. A value written by one of the
     * connected clients will be available for all other clients. To create a sandboxed characteristic,
     * use {@link #characteristic(UUID, int, int, byte[], BluetoothGattDescriptor...)} instead.
     *
     * @param uuid The characteristic UUID.
     * @param permissions The bit mask or characteristic permissions. See {@link BluetoothGattCharacteristic}
     *                    for details.
     * @param initialValue The optional initial value of the characteristic.
     * @return The characteristic.
     */
    @NonNull
    protected final BluetoothGattDescriptor sharedDescriptor(@NonNull final UUID uuid, @DescriptorPermissions final int permissions, @Nullable final Data initialValue) {
        return sharedDescriptor(uuid, permissions, initialValue != null ? initialValue.getValue() : null);
    }

    /**
     * This helper method returns a new instance of Client Characteristic Configuration Descriptor
     * (CCCD) that can be added to a server characteristic in {@link #initializeServer()}.
     *
     * @return The CCC descriptor used to enable and disable notifications or indications.
     */
    @NonNull
    protected final BluetoothGattDescriptor cccd() {
        return descriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE, new byte[] { 0, 0 });
    }

    /**
     * This helper method returns a new instance of Client Characteristic Configuration Descriptor
     * (CCCD) that can be added to a server characteristic in {@link #initializeServer()}.
     *
     * @return The CCC descriptor used to enable and disable notifications or indications.
     */
    @NonNull
    protected final BluetoothGattDescriptor sharedCccd() {
        return sharedDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE, new byte[] { 0, 0 });
    }

    /**
     * This helper method returns a new instance of Characteristic Extended Properties descriptor
     * that can be added to a server characteristic.
     * This descriptor should be added it {@link BluetoothGattCharacteristic#PROPERTY_EXTENDED_PROPS}
     * property is set.
     * @return The CEP descriptor with Reliable Write bit set.
     */
    @NonNull
    protected final BluetoothGattDescriptor reliableWrite() {
        return sharedDescriptor(CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_READ, new byte[] { 1, 0 });
    }

    /**
     * This helper method returns a new instance of Client User Description Descriptor
     * that can be added to a server characteristic in {@link #initializeServer()}.
     *
     * @param description the UTF-8 string that is a user textual description of the characteristic.
     * @param writableAuxiliaries if true, the descriptor will be writable and the Writable Auxiliaries
     *                            bit in Characteristic Extended Properties descriptor will be set.
     *                            See Vol. 3, Part F, Section 3.3.3.2 in Bluetooth Core specification 5.1.
     * @return The User Description descriptor.
     */
    @NonNull
    protected final BluetoothGattDescriptor description(@Nullable final String description, final boolean writableAuxiliaries) {
        final BluetoothGattDescriptor cud = descriptor(CLIENT_USER_DESCRIPTION_DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_READ | (writableAuxiliaries ? BluetoothGattDescriptor.PERMISSION_WRITE : 0), description != null ? description.getBytes() : null);
        if (!writableAuxiliaries) {
            if (sharedDescriptors == null)
                sharedDescriptors = new ArrayList<>();
            sharedDescriptors.add(cud);
        }
        return cud;
    }

    private final BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onServiceAdded(final int status, @NonNull final BluetoothGattService service) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                try {
                    final BluetoothGattService nextService = serverServices.remove();
                    server.addService(nextService);
                } catch (final Exception e) {
                    log(Log.INFO, "[Server] All services added successfully");
                    if (serverObserver != null)
                        serverObserver.onServerReady();
                    serverServices = null;
                }
            } else {
                log(Log.ERROR, "[Server] Adding service failed with error " + status);
            }
        }

        @Override
        public void onConnectionStateChange(@NonNull final BluetoothDevice device, final int status, final int newState) {
            if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) {
                log(Log.INFO, "[Server] " + device.getAddress() + " is now connected");
                if (serverObserver != null)
                    serverObserver.onDeviceConnectedToServer(device);
            } else {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    log(Log.INFO, "[Server] " + device.getAddress() + " is disconnected");
                } else {
                    log(Log.WARN, "[Server] " + device.getAddress() + " has disconnected connected with status: " + status);
                }
                if (serverObserver != null)
                    serverObserver.onDeviceDisconnectedFromServer(device);
            }
        }

        @Override
        public void onCharacteristicReadRequest(@NonNull final BluetoothDevice device, final int requestId, final int offset, @NonNull final BluetoothGattCharacteristic characteristic) {
            final BleManagerHandler handler = getRequestHandler(device);
            if (handler != null) {
                handler.onCharacteristicReadRequest(server, device, requestId, offset, characteristic);
            }
        }

        @Override
        public void onCharacteristicWriteRequest(@NonNull final BluetoothDevice device, final int requestId, @NonNull final BluetoothGattCharacteristic characteristic, final boolean preparedWrite, final boolean responseNeeded, final int offset, @NonNull final byte[] value) {
            final BleManagerHandler handler = getRequestHandler(device);
            if (handler != null) {
                handler.onCharacteristicWriteRequest(server, device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
            }
        }

        @Override
        public void onDescriptorReadRequest(@NonNull final BluetoothDevice device, final int requestId, final int offset, @NonNull final BluetoothGattDescriptor descriptor) {
            final BleManagerHandler handler = getRequestHandler(device);
            if (handler != null) {
                handler.onDescriptorReadRequest(server, device, requestId, offset, descriptor);
            }
        }

        @Override
        public void onDescriptorWriteRequest(@NonNull final BluetoothDevice device, final int requestId, @NonNull final BluetoothGattDescriptor descriptor, final boolean preparedWrite, final boolean responseNeeded, final int offset, @NonNull final byte[] value) {
            final BleManagerHandler handler = getRequestHandler(device);
            if (handler != null) {
                handler.onDescriptorWriteRequest(server, device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
            }
        }

        @Override
        public void onExecuteWrite(@NonNull final BluetoothDevice device, final int requestId, final boolean execute) {
            final BleManagerHandler handler = getRequestHandler(device);
            if (handler != null) {
                handler.onExecuteWrite(server, device, requestId, execute);
            }
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onNotificationSent(@NonNull final BluetoothDevice device, final int status) {
            final BleManagerHandler handler = getRequestHandler(device);
            if (handler != null) {
                handler.onNotificationSent(server, device, status);
            }
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
        @Override
        public void onMtuChanged(@NonNull final BluetoothDevice device, final int mtu) {
            final BleManagerHandler handler = getRequestHandler(device);
            if (handler != null) {
                handler.onMtuChanged(server, device, mtu);
            }
        }
    };
}

11 Source : MainActivity.java
with MIT License
from thejeshgn

/**
 * It has everything that is needed by GattServer to respond
 * MIT Licnese
 * 2016 - Thejesh GN - https://hejeshgn.com
 */
public clreplaced MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private ILogSession mLogSession;

    private DataManager dataManager = DataManager.getInstance();

    private BluetoothManager mBluetoothManager;

    private BluetoothAdapter mBluetoothAdapter;

    private BluetoothLeAdvertiser mBluetoothLeAdvertiser;

    private BluetoothGattServer mGattServer;

    private ArrayList<BluetoothDevice> mConnectedDevices;

    private ArrayAdapter<BluetoothDevice> mConnectedDevicesAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mLogSession = Logger.newSession(this, "Hello", "BleUARTPeripheral");
        setContentView(R.layout.activity_main);
        ListView list = new ListView(this);
        setContentView(list);
        mConnectedDevices = new ArrayList<BluetoothDevice>();
        mConnectedDevicesAdapter = new ArrayAdapter<BluetoothDevice>(this, android.R.layout.simple_list_item_1, mConnectedDevices);
        list.setAdapter(mConnectedDevicesAdapter);
        mBluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
        mBluetoothAdapter = mBluetoothManager.getAdapter();
        Logger.log(mLogSession, LogContract.Log.Level.DEBUG, "In on create");
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main_tool_bar, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        Toast.makeText(this, "Inside menu item.", Toast.LENGTH_SHORT).show();
        Logger.log(mLogSession, LogContract.Log.Level.INFO, "Inside onOptionsItemSelected");
        switch(item.gereplacedemId()) {
            case R.id.action_about:
                Intent myIntent = new Intent(MainActivity.this, AboutActivity.clreplaced);
                startActivity(myIntent);
                return true;
            case R.id.action_log:
                return true;
            case R.id.action_start_stop:
                // User chose the "Favorite" action, mark the current item
                // as a favorite...
                return true;
            default:
                // If we got here, the user's action was not recognized.
                // Invoke the superclreplaced to handle it.
                return super.onOptionsItemSelected(item);
        }
    }

    protected void onResume() {
        super.onResume();
        /*
         * Make sure bluettoth is enabled
         */
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            // Bluetooth is disabled
            Logger.log(mLogSession, LogContract.Log.Level.DEBUG, "Bluetooth is disabled. Request enable");
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivity(enableBtIntent);
            finish();
            return;
        }
        /*
         * Check for Bluetooth LE Support
         */
        if (!getPackageManager().hreplacedystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Toast.makeText(this, "No LE Support.", Toast.LENGTH_SHORT).show();
            finish();
            return;
        }
        /*
         * Check for advertising support.
         */
        if (!mBluetoothAdapter.isMultipleAdvertisementSupported()) {
            Toast.makeText(this, "No Advertising Support.", Toast.LENGTH_SHORT).show();
            finish();
            return;
        }
        Logger.log(mLogSession, LogContract.Log.Level.DEBUG, "Get Advertiser");
        mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
        Logger.log(mLogSession, LogContract.Log.Level.DEBUG, "Open GattServer");
        mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
        // If everything is okay then start
        initServer();
        startAdvertising();
    }

    /*
       * Callback handles events from the framework describing
       * if we were successful in starting the advertisement requests.
       */
    private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {

        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            Log.i(TAG, "Peripheral Advertise Started.");
            postStatusMessage("GATT Server Ready");
        }

        @Override
        public void onStartFailure(int errorCode) {
            Log.w(TAG, "Peripheral Advertise Failed: " + errorCode);
            postStatusMessage("GATT Server Error " + errorCode);
        }
    };

    private Handler mHandler = new Handler();

    private void postStatusMessage(final String message) {
        mHandler.post(new Runnable() {

            @Override
            public void run() {
                setreplacedle(message);
            }
        });
    }

    /*
 * Create the GATT server instance, attaching all services and
 * characteristics that should be exposed
 */
    private void initServer() {
        BluetoothGattService UART_SERVICE = new BluetoothGattService(UARTProfile.UART_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);
        BluetoothGattCharacteristic TX_READ_CHAR = new BluetoothGattCharacteristic(UARTProfile.TX_READ_CHAR, // Read-only characteristic, supports notifications
        BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
        // Descriptor for read notifications
        BluetoothGattDescriptor TX_READ_CHAR_DESC = new BluetoothGattDescriptor(UARTProfile.TX_READ_CHAR_DESC, UARTProfile.DESCRIPTOR_PERMISSION);
        TX_READ_CHAR.addDescriptor(TX_READ_CHAR_DESC);
        BluetoothGattCharacteristic RX_WRITE_CHAR = new BluetoothGattCharacteristic(UARTProfile.RX_WRITE_CHAR, // write permissions
        BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE);
        UART_SERVICE.addCharacteristic(TX_READ_CHAR);
        UART_SERVICE.addCharacteristic(RX_WRITE_CHAR);
        mGattServer.addService(UART_SERVICE);
    }

    /*
     * Initialize the advertiser
     */
    private void startAdvertising() {
        if (mBluetoothLeAdvertiser == null)
            return;
        AdvertiseSettings settings = new AdvertiseSettings.Builder().setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED).setConnectable(true).setTimeout(0).setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM).build();
        AdvertiseData data = new AdvertiseData.Builder().setIncludeDeviceName(true).addServiceUuid(new ParcelUuid(UARTProfile.UART_SERVICE)).build();
        mBluetoothLeAdvertiser.startAdvertising(settings, data, mAdvertiseCallback);
    }

    private void postDeviceChange(final BluetoothDevice device, final boolean toAdd) {
        mHandler.post(new Runnable() {

            @Override
            public void run() {
                // This will add the item to our list and update the adapter at the same time.
                if (toAdd) {
                    if (mConnectedDevicesAdapter.getPosition(device) < 0) {
                        mConnectedDevicesAdapter.add(device);
                    }
                } else {
                    mConnectedDevicesAdapter.remove(device);
                }
            }
        });
    }

    /*
 * Terminate the server and any running callbacks
 */
    private void shutdownServer() {
        // mHandler.removeCallbacks(mNotifyRunnable);
        if (mGattServer == null)
            return;
        mGattServer.close();
    }

    // private Runnable mNotifyRunnable = new Runnable() {
    // @Override
    // public void run() {
    // mHandler.postDelayed(this, 2000);
    // }
    // };
    /* Callback handles all incoming requests from GATT clients.
     * From connections to read/write requests.
     */
    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            super.onConnectionStateChange(device, status, newState);
            Log.i(TAG, "onConnectionStateChange " + UARTProfile.getStatusDescription(status) + " " + UARTProfile.getStateDescription(newState));
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                postDeviceChange(device, true);
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                postDeviceChange(device, false);
            }
        }

        @Override
        public void onServiceAdded(int status, BluetoothGattService service) {
            Log.d("Start", "Our gatt server service was added.");
            super.onServiceAdded(status, service);
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
            Log.d(TAG, "READ called onCharacteristicReadRequest " + characteristic.getUuid().toString());
            if (UARTProfile.TX_READ_CHAR.equals(characteristic.getUuid())) {
                mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, storage);
            }
        }

        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
            Log.i(TAG, "onCharacteristicWriteRequest " + characteristic.getUuid().toString());
            if (UARTProfile.RX_WRITE_CHAR.equals(characteristic.getUuid())) {
                // IMP: Copy the received value to storage
                if (responseNeeded) {
                    mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, value);
                    Log.d(TAG, "Received  data on " + characteristic.getUuid().toString());
                    Log.d(TAG, "Received data" + bytesToHex(value));
                }
                dataManager.addPacket(value);
                if (dataManager.isMessageComplete()) {
                    storage = dataManager.getTheCompleteMesssage();
                    dataManager.clear();
                    // IMP: Respond
                    sendOurResponse();
                }
                mHandler.post(new Runnable() {

                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this, "We received data", Toast.LENGTH_SHORT).show();
                    }
                });
            }
        }

        @Override
        public void onNotificationSent(BluetoothDevice device, int status) {
            Log.d("GattServer", "onNotificationSent");
            super.onNotificationSent(device, status);
        }

        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            Log.d("HELLO", "Our gatt server descriptor was read.");
            super.onDescriptorReadRequest(device, requestId, offset, descriptor);
            Log.d("DONE", "Our gatt server descriptor was read.");
        }

        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            Log.d("HELLO", "Our gatt server descriptor was written.");
            super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
            Log.d("DONE", "Our gatt server descriptor was written.");
            // NOTE: Its important to send response. It expects response else it will disconnect
            if (responseNeeded) {
                mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, value);
            }
        }
    };

    // Send notification to all the devices once you write
    private void sendOurResponse() {
        for (BluetoothDevice device : mConnectedDevices) {
            BluetoothGattCharacteristic readCharacteristic = mGattServer.getService(UARTProfile.UART_SERVICE).getCharacteristic(UARTProfile.TX_READ_CHAR);
            byte[] notify_msg = storage;
            String hexStorage = bytesToHex(storage);
            Log.d(TAG, "received string = " + bytesToHex(storage));
            if (hexStorage.equals("77686F616D69")) {
                notify_msg = "I am echo an machine".getBytes();
            } else if (bytesToHex(storage).equals("64617465")) {
                DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
                Date date = new Date();
                notify_msg = dateFormat.format(date).getBytes();
            } else {
            // TODO: Do nothing send what you received. Basically echo
            }
            List messages = Util.createPacketsToSend(notify_msg);
            for (int i = 0; i < messages.size(); i++) {
                byte[] message = (byte[]) messages.get(i);
                readCharacteristic.setValue(message);
                Log.d(TAG, "Sending Notifications" + message);
                boolean is_notified = mGattServer.notifyCharacteristicChanged(device, readCharacteristic, false);
                Log.d(TAG, "Notifications =" + is_notified);
            }
        }
    }

    private byte[] storage = hexStringToByteArray("1111");

    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();

    // Helper function converts byte array to hex string
    // for priting
    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

    // Helper function converts hex string into
    // byte array
    public static byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
        }
        return data;
    }
}

11 Source : GattServer.java
with MIT License
from adafruit

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public clreplaced GattServer implements PeripheralService.Listener {

    // Log
    private final static String TAG = GattServer.clreplaced.getSimpleName();

    // Constants
    private final static String kPreferences = "GattServer";

    private final static String kPreferences_includeDeviceName = "includeDeviceName";

    // Config
    // private final static boolean kAddDelayAfterBeforeClosing = true;    // To Fix issue: https://stackoverflow.com/questions/29758890/bluetooth-gatt-callback-not-working-with-new-api-for-lollipop, https://issuetracker.google.com/issues/37057260
    // Listener
    public interface Listener {

        // Connection
        void onCentralConnectionStatusChanged(int status);

        // Advertising
        void onWillStartAdvertising();

        void onDidStartAdvertising();

        void onDidStopAdvertising();

        void onAdvertisingError(int errorCode);
    }

    // Data
    private BluetoothManager mBluetoothManager;

    private BluetoothGattServer mGattServer;

    private BluetoothLeAdvertiser mAdvertiser;

    private int mMtuSize;

    private boolean mShouldStartAdvertising = false;

    private boolean mIsAdvertising = false;

    private Listener mListener;

    private List<PeripheralService> mPeripheralServices = new ArrayList<>();

    private Semapreplaced mAddServicesSemapreplaced;

    // Data - preparedWrite
    clreplaced ServiceCharacteristicKey {

        // Based on https://stackoverflow.com/questions/14677993/how-to-create-a-hashmap-with-two-keys-key-pair-value
        private final UUID serviceUuid;

        private final UUID characteristicUuid;

        ServiceCharacteristicKey(UUID serviceUuid, UUID characteristicUuid) {
            this.serviceUuid = serviceUuid;
            this.characteristicUuid = characteristicUuid;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (!(o instanceof ServiceCharacteristicKey))
                return false;
            ServiceCharacteristicKey key = (ServiceCharacteristicKey) o;
            return serviceUuid.equals(key.serviceUuid) && characteristicUuid.equals(key.characteristicUuid);
        }

        @Override
        public int hashCode() {
            int result = serviceUuid.hashCode();
            result = 31 * result + characteristicUuid.hashCode();
            return result;
        }
    }

    private Map<ServiceCharacteristicKey, byte[]> mPreparedWrites = new HashMap<>();

    // Static methods
    static public boolean isPeripheralModeSupported() {
        return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
    }

    // 
    public GattServer(Context context, Listener listener) {
        mListener = listener;
        mBluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
        // Update internal status
        onBluetoothStateChanged(context);
        // Set Ble status receiver
        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        LocalBroadcastManager.getInstance(context).registerReceiver(mBleAdapterStateReceiver, filter);
    }

    public void removeListener(Context context) {
        // Unregister Ble Status
        LocalBroadcastManager.getInstance(context).unregisterReceiver(mBleAdapterStateReceiver);
        mListener = null;
    }

    private final BroadcastReceiver mBleAdapterStateReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            if (action != null && action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                onBluetoothStateChanged(context);
            }
        }
    };

    private void onBluetoothStateChanged(Context context) {
        // Setup advertiser
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        if (adapter != null) {
            if (!adapter.isEnabled()) {
                Log.w(TAG, "BluetoothLE is disabled");
            }
            if (!adapter.isMultipleAdvertisementSupported()) {
                Log.w(TAG, "Multiple advertisement not supported");
            }
            mAdvertiser = adapter.getBluetoothLeAdvertiser();
            if (mAdvertiser == null) {
                Log.w(TAG, "Device does not support Peripheral Mode");
            }
        } else {
            Log.w(TAG, "Device does not support Bluetooth");
        }
        if (mAdvertiser == null) {
            mIsAdvertising = false;
        }
        if (mShouldStartAdvertising && mAdvertiser != null) {
            startAdvertising(context);
        }
    }

    public boolean isPeripheralModeAvailable() {
        return mAdvertiser != null;
    }

    // region Local Name
    public String getLocalBluetoothName() {
        String name = null;
        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter != null) {
            name = bluetoothAdapter.getName();
        }
        return name;
    }

    public void setLocalBluetoothName(String name) {
        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter != null) {
            bluetoothAdapter.setName(name);
        } else {
            Log.w(TAG, "Trying to set bluetooth name with null adapter");
        }
    }

    // endregion
    // region Service Management
    @NonNull
    public List<PeripheralService> getServices() {
        return mPeripheralServices;
    }

    public void addService(@NonNull PeripheralService service) {
        service.setListener(this);
        mPeripheralServices.add(service);
    }

    public void removeService(@NonNull PeripheralService service) {
        UUID serviceUuid = service.getService().getUuid();
        final int index = indexOfPeripheralServicesWithUuid(serviceUuid);
        if (index >= 0) {
            mPeripheralServices.remove(index);
            service.setListener(null);
        }
    }

    public void removeAllServices() {
        for (PeripheralService service : mPeripheralServices) {
            service.setListener(null);
        }
        mPeripheralServices.clear();
    }

    private int indexOfPeripheralServicesWithUuid(@NonNull UUID uuid) {
        boolean found = false;
        int i = 0;
        while (i < mPeripheralServices.size() && !found) {
            final UUID peripheralServiceUuid = mPeripheralServices.get(i).getService().getUuid();
            if (peripheralServiceUuid.equals(uuid)) {
                found = true;
            } else {
                i++;
            }
        }
        return found ? i : -1;
    }

    // endregion
    // region Advertising
    public synchronized boolean startAdvertising(@NonNull Context context) {
        mShouldStartAdvertising = true;
        if (mBluetoothManager == null || mAdvertiser == null) {
            Log.e(TAG, "startAdvertising with nil objects");
            return false;
        }
        // Clean / Setup
        stopAdvertising(/*context, */
        false);
        // Start Gatt Server
        Log.d(TAG, "startAdvertising");
        mAddServicesSemapreplaced = new Semapreplaced(1, true);
        mGattServer = mBluetoothManager.openGattServer(context.getApplicationContext(), mGattServerCallback);
        if (mGattServer != null) {
            BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
            if (!adapter.isMultipleAdvertisementSupported() && mPeripheralServices.size() > 1) {
                Log.w(TAG, "Trying to advertise multiple services but multipleAdvertisement is no supported on this device");
                return false;
            }
            for (PeripheralService peripheralService : mPeripheralServices) {
                if (peripheralService.isEnabled()) {
                    BluetoothGattService service = peripheralService.getService();
                    try {
                        // Semapreplaced to wait for onServiceAdded callback before adding a new service (check addService docs)
                        mAddServicesSemapreplaced.acquire();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // Note: Wait for onServiceAdded callback before adding another service
                    final boolean isAdded = mGattServer.addService(service);
                    if (!isAdded) {
                        Log.e(TAG, "startGattServer service not added");
                    }
                }
            }
            // mAddServicesSemapreplaced.release();        // Force release any remaining permits (there are no more services that are going to be added)
            // Start advertising
            AdvertiseSettings advertiseSettings = new AdvertiseSettings.Builder().setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY).setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH).setConnectable(true).build();
            AdvertiseData.Builder advertiseDataBuilder = new AdvertiseData.Builder().setIncludeTxPowerLevel(true);
            // Needed to show the local name
            advertiseDataBuilder.setIncludeDeviceName(isIncludingDeviceName(context));
            final boolean isUuartServiceEnabled = indexOfPeripheralServicesWithUuid(UartPeripheralService.kUartServiceUUID) >= 0;
            if (isUuartServiceEnabled) {
                // If UART is enabled, add the UUID to the advertisement packet
                ParcelUuid serviceParcelUuid = new ParcelUuid(UartPeripheralService.kUartServiceUUID);
                advertiseDataBuilder.addServiceUuid(serviceParcelUuid);
            }
            AdvertiseData advertiseData = advertiseDataBuilder.build();
            if (mListener != null) {
                mListener.onWillStartAdvertising();
            }
            mAdvertiser.startAdvertising(advertiseSettings, advertiseData, mAdvertisingCallback);
            return true;
        } else {
            Log.e(TAG, "gatt server is null");
            return false;
        }
    }

    public boolean isIncludingDeviceName(@NonNull Context context) {
        SharedPreferences preferences = context.getSharedPreferences(kPreferences, MODE_PRIVATE);
        return preferences.getBoolean(kPreferences_includeDeviceName, false);
    }

    public void setIncludeDeviceName(@NonNull Context context, boolean enabled) {
        SharedPreferences.Editor preferencesEditor = context.getSharedPreferences(kPreferences, MODE_PRIVATE).edit();
        preferencesEditor.putBoolean(kPreferences_includeDeviceName, enabled);
        preferencesEditor.apply();
    }

    private AdvertiseCallback mAdvertisingCallback = new AdvertiseCallback() {

        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            super.onStartSuccess(settingsInEffect);
            Log.d(TAG, "Advertising onStartSuccess");
            mIsAdvertising = true;
            if (mListener != null) {
                mListener.onDidStartAdvertising();
            }
        }

        @Override
        public void onStartFailure(int errorCode) {
            super.onStartFailure(errorCode);
            /*
            if (errorCode == 3) { // Already advertising
                stopAdvertising();
            }*/
            if (errorCode == ADVERTISE_FAILED_ALREADY_STARTED) {
                Log.d(TAG, "Advertising onStartFailure because it was already advertising. Failure recovered");
                mIsAdvertising = true;
                if (mListener != null) {
                    mListener.onDidStartAdvertising();
                }
            } else {
                Log.e(TAG, "Advertising onStartFailure: " + errorCode);
                mIsAdvertising = false;
                if (mListener != null) {
                    mListener.onAdvertisingError(errorCode);
                }
            }
        }
    };

    private synchronized void stopAdvertising(/*@NonNull Context context, */
    boolean notifyListener) {
        mShouldStartAdvertising = false;
        if (mAdvertiser != null && mIsAdvertising) {
            try {
                mAdvertiser.stopAdvertising(mAdvertisingCallback);
            } catch (IllegalStateException e) {
                // Discard IllegalStateException reported in Android vitals crashes
                Log.w(TAG, "stopAdvertising illegalstate: " + e);
            }
            mIsAdvertising = false;
            Log.d(TAG, "Advertising stopAdvertising");
        }
        if (mGattServer != null) {
            List<BluetoothDevice> devices = mBluetoothManager.getConnectedDevices(BluetoothGattServer.GATT_SERVER);
            for (int i = 0; i < devices.size(); i++) {
                mGattServer.cancelConnection(devices.get(i));
            }
            mGattServer.clearServices();
            /*
            if (kAddDelayAfterBeforeClosing) {      // Hack to avoid Android internal bugs. Exists on Android v6 and v7... maybe more...
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }*/
            mGattServer.close();
            mGattServer = null;
        }
        if (notifyListener && mListener != null) {
            mListener.onDidStopAdvertising();
        }
    }

    public void stopAdvertising() /*@NonNull Context context*/
    {
        stopAdvertising(/*context, */
        true);
    }

    public boolean isAdvertising() {
        return mIsAdvertising;
    }

    // end region
    // region Gatt Server
    private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onConnectionStateChange(final BluetoothDevice device, int status, int newState) {
            super.onConnectionStateChange(device, status, newState);
            Log.d(TAG, "GattServer: onConnectionStateChange status: " + status + "state:" + newState);
            if (status != BluetoothGatt.GATT_SUCCESS) {
                Log.w(TAG, "\tonConnectionStateChange error reported");
            }
            if (mListener != null) {
                mListener.onCentralConnectionStatusChanged(newState);
            }
        }

        @Override
        public void onServiceAdded(int status, BluetoothGattService service) {
            super.onServiceAdded(status, service);
            Log.d(TAG, "GattServer: onServiceAdded");
            mAddServicesSemapreplaced.release();
        }

        @Override
        public void onMtuChanged(BluetoothDevice device, int mtu) {
            super.onMtuChanged(device, mtu);
            Log.d(TAG, "Mtu changed: " + mtu);
            mMtuSize = mtu;
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
            Log.d(TAG, "GattServer: onCharacteristicReadRequest characteristic: " + characteristic.getUuid().toString() + " requestId: " + requestId + " offset: " + offset);
            boolean isCharacteristicValid = false;
            final UUID serviceUuid = characteristic.getService().getUuid();
            final int indexOfPeripheral = indexOfPeripheralServicesWithUuid(serviceUuid);
            if (indexOfPeripheral >= 0) {
                PeripheralService peripheralService = mPeripheralServices.get(indexOfPeripheral);
                final UUID characteristicUuid = characteristic.getUuid();
                BluetoothGattCharacteristic serviceCharacteristic = peripheralService.getCharacteristic(characteristicUuid);
                if (serviceCharacteristic != null) {
                    processReadRequest(device, requestId, offset, serviceCharacteristic.getValue());
                    isCharacteristicValid = true;
                }
            }
            if (!isCharacteristicValid) {
                mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_READ_NOT_PERMITTED, offset, null);
            }
        }

        private void processReadRequest(BluetoothDevice device, int requestId, int offset, byte[] bytes) {
            byte[] value = bytes != null ? bytes : new byte[] { 0 };
            if (offset > value.length) {
                mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_INVALID_OFFSET, 0, new byte[] { 0 });
            } else {
                final int responseSize = Math.min(mMtuSize, value.length - offset);
                byte[] responseChunck = new byte[responseSize];
                System.arraycopy(value, offset, responseChunck, 0, responseSize);
                mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, responseChunck);
            }
        }

        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
            Log.d(TAG, "GattServer: onCharacteristicWriteRequest characteristic: " + characteristic.getUuid().toString() + " requestId: " + requestId + " preparedWrite: " + preparedWrite + " responseNeeded: " + responseNeeded + " offset: " + offset);
            final UUID serviceUuid = characteristic.getService().getUuid();
            for (PeripheralService peripheralService : mPeripheralServices) {
                if (serviceUuid.equals(peripheralService.getService().getUuid())) {
                    final UUID characteristicUuid = characteristic.getUuid();
                    BluetoothGattCharacteristic serviceCharacteristic = peripheralService.getCharacteristic(characteristicUuid);
                    if (serviceCharacteristic != null) {
                        final ServiceCharacteristicKey key = new ServiceCharacteristicKey(serviceUuid, characteristicUuid);
                        byte[] currentValue = preparedWrite ? mPreparedWrites.get(key) : value;
                        currentValue = processWriteRequest(device, requestId, preparedWrite, responseNeeded, offset, value, currentValue);
                        if (preparedWrite) {
                            mPreparedWrites.put(key, currentValue);
                        } else {
                            // characteristic.setValue(currentValue);
                            peripheralService.setCharacteristic(characteristic, currentValue);
                        }
                    }
                }
            }
        }

        @Override
        public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
            super.onExecuteWrite(device, requestId, execute);
            Log.d(TAG, "GattServer: onExecuteWrite requestId: " + requestId + " execute: " + execute);
            if (execute) {
                for (ServiceCharacteristicKey key : mPreparedWrites.keySet()) {
                    byte[] value = mPreparedWrites.get(key);
                    byte[] currentValue = processWriteRequest(device, requestId, false, false, 0, value, null);
                    BluetoothGattCharacteristic characteristic = mGattServer.getService(key.serviceUuid).getCharacteristic(key.characteristicUuid);
                    // characteristic.setValue(currentValue);
                    for (PeripheralService peripheralService : mPeripheralServices) {
                        if (key.serviceUuid.equals(peripheralService.getService().getUuid())) {
                            peripheralService.setCharacteristic(characteristic, currentValue);
                        }
                    }
                }
            }
            mPreparedWrites.clear();
            mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] {});
        }

        @NonNull
        private byte[] processWriteRequest(BluetoothDevice device, int requestId, boolean prepared, boolean responseNeeded, int offset, byte[] value, byte[] currentCharacteristicValue) {
            // Adjust currentValue to make room for the new data
            // Question: should the characteristic value be expanded to accommodate the value? Maybe not...
            if (currentCharacteristicValue == null || offset == 0) {
                currentCharacteristicValue = new byte[value.length];
            } else if (currentCharacteristicValue.length < offset + value.length) {
                byte[] newValue = new byte[offset + value.length];
                System.arraycopy(currentCharacteristicValue, 0, newValue, 0, currentCharacteristicValue.length);
                currentCharacteristicValue = newValue;
            }
            System.arraycopy(value, 0, currentCharacteristicValue, offset, value.length);
            if (responseNeeded) {
                mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
            }
            return currentCharacteristicValue;
        }

        @Override
        public void onNotificationSent(BluetoothDevice device, int status) {
            super.onNotificationSent(device, status);
        }

        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            super.onDescriptorReadRequest(device, requestId, offset, descriptor);
        }

        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
            Log.d(TAG, "GattServer: onDescriptorWriteRequest : " + descriptor.getUuid().toString() + " requestId: " + requestId + " preparedWrite: " + preparedWrite + " responseNeeded: " + responseNeeded + " offset: " + offset);
            // Check if is enabling notification
            if (descriptor.getUuid().equals(BlePeripheral.kClientCharacteristicConfigUUID)) {
                int status = BluetoothGatt.GATT_SUCCESS;
                if (value.length != 2) {
                    status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
                } else {
                    BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
                    boolean isNotify = Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) || Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
                    boolean isNotifySupported = (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0;
                    if (isNotify && isNotifySupported) {
                        descriptor.setValue(value);
                        UUID serviceUUID = descriptor.getCharacteristic().getService().getUuid();
                        if (Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
                            Log.d(TAG, "subscribed to characteristic: " + descriptor.getCharacteristic().getUuid().toString());
                            for (PeripheralService service : mPeripheralServices) {
                                if (serviceUUID.equals(service.getService().getUuid())) {
                                    service.subscribe(characteristic.getUuid(), device);
                                }
                            }
                        } else if (Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
                            Log.d(TAG, "unsubscribed from characteristic: " + descriptor.getCharacteristic().getUuid().toString());
                            for (PeripheralService service : mPeripheralServices) {
                                if (serviceUUID.equals(service.getService().getUuid())) {
                                    service.unsubscribe(characteristic.getUuid(), device);
                                }
                            }
                        }
                    } else {
                        status = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED;
                    }
                // TODO: add support for Indications
                }
                if (responseNeeded) {
                    mGattServer.sendResponse(device, requestId, status, offset, value);
                }
            }
        }
    };

    // end region
    // region PeripheralService.Listener
    @Override
    public void updateValue(@NonNull BluetoothDevice[] devices, @NonNull BluetoothGattCharacteristic characteristic) {
        for (BluetoothDevice device : devices) {
            mGattServer.notifyCharacteristicChanged(device, characteristic, false);
        }
    }
    // endregion
}

8 Source : HidPeripheral.java
with Apache License 2.0
from kshoji

/**
 * BLE HID over GATT base features
 *
 * @author K.Shoji
 */
@TargetApi(VERSION_CODES.LOLLIPOP)
public abstract clreplaced HidPeripheral {

    private static final String TAG = HidPeripheral.clreplaced.getSimpleName();

    /**
     * Main items
     */
    protected static byte INPUT(final int size) {
        return (byte) (0x80 | size);
    }

    protected static byte OUTPUT(final int size) {
        return (byte) (0x90 | size);
    }

    protected static byte COLLECTION(final int size) {
        return (byte) (0xA0 | size);
    }

    protected static byte FEATURE(final int size) {
        return (byte) (0xB0 | size);
    }

    protected static byte END_COLLECTION(final int size) {
        return (byte) (0xC0 | size);
    }

    /**
     * Global items
     */
    protected static byte USAGE_PAGE(final int size) {
        return (byte) (0x04 | size);
    }

    protected static byte LOGICAL_MINIMUM(final int size) {
        return (byte) (0x14 | size);
    }

    protected static byte LOGICAL_MAXIMUM(final int size) {
        return (byte) (0x24 | size);
    }

    protected static byte PHYSICAL_MINIMUM(final int size) {
        return (byte) (0x34 | size);
    }

    protected static byte PHYSICAL_MAXIMUM(final int size) {
        return (byte) (0x44 | size);
    }

    protected static byte UNIT_EXPONENT(final int size) {
        return (byte) (0x54 | size);
    }

    protected static byte UNIT(final int size) {
        return (byte) (0x64 | size);
    }

    protected static byte REPORT_SIZE(final int size) {
        return (byte) (0x74 | size);
    }

    protected static byte REPORT_ID(final int size) {
        return (byte) (0x84 | size);
    }

    protected static byte REPORT_COUNT(final int size) {
        return (byte) (0x94 | size);
    }

    /**
     * Local items
     */
    protected static byte USAGE(final int size) {
        return (byte) (0x08 | size);
    }

    protected static byte USAGE_MINIMUM(final int size) {
        return (byte) (0x18 | size);
    }

    protected static byte USAGE_MAXIMUM(final int size) {
        return (byte) (0x28 | size);
    }

    protected static byte LSB(final int value) {
        return (byte) (value & 0xff);
    }

    protected static byte MSB(final int value) {
        return (byte) (value >> 8 & 0xff);
    }

    /**
     * Device Information Service
     */
    private static final UUID SERVICE_DEVICE_INFORMATION = BleUuidUtils.fromShortValue(0x180A);

    private static final UUID CHARACTERISTIC_MANUFACTURER_NAME = BleUuidUtils.fromShortValue(0x2A29);

    private static final UUID CHARACTERISTIC_MODEL_NUMBER = BleUuidUtils.fromShortValue(0x2A24);

    private static final UUID CHARACTERISTIC_SERIAL_NUMBER = BleUuidUtils.fromShortValue(0x2A25);

    private static final int DEVICE_INFO_MAX_LENGTH = 20;

    private String manufacturer = "kshoji.jp";

    private String deviceName = "BLE HID";

    private String serialNumber = "12345678";

    /**
     * Battery Service
     */
    private static final UUID SERVICE_BATTERY = BleUuidUtils.fromShortValue(0x180F);

    private static final UUID CHARACTERISTIC_BATTERY_LEVEL = BleUuidUtils.fromShortValue(0x2A19);

    /**
     * HID Service
     */
    private static final UUID SERVICE_BLE_HID = BleUuidUtils.fromShortValue(0x1812);

    private static final UUID CHARACTERISTIC_HID_INFORMATION = BleUuidUtils.fromShortValue(0x2A4A);

    private static final UUID CHARACTERISTIC_REPORT_MAP = BleUuidUtils.fromShortValue(0x2A4B);

    private static final UUID CHARACTERISTIC_HID_CONTROL_POINT = BleUuidUtils.fromShortValue(0x2A4C);

    private static final UUID CHARACTERISTIC_REPORT = BleUuidUtils.fromShortValue(0x2A4D);

    private static final UUID CHARACTERISTIC_PROTOCOL_MODE = BleUuidUtils.fromShortValue(0x2A4E);

    /**
     * Represents Report Map byte array
     * @return Report Map data
     */
    protected abstract byte[] getReportMap();

    /**
     * HID Input Report
     */
    private final Queue<byte[]> inputReportQueue = new ConcurrentLinkedQueue<>();

    protected final void addInputReport(final byte[] inputReport) {
        if (inputReport != null && inputReport.length > 0) {
            inputReportQueue.offer(inputReport);
        }
    }

    /**
     * HID Output Report
     *
     * @param outputReport the report data
     */
    protected abstract void onOutputReport(final byte[] outputReport);

    /**
     * Gatt Characteristic Descriptor
     */
    private static final UUID DESCRIPTOR_REPORT_REFERENCE = BleUuidUtils.fromShortValue(0x2908);

    private static final UUID DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION = BleUuidUtils.fromShortValue(0x2902);

    private static final byte[] EMPTY_BYTES = {};

    private static final byte[] RESPONSE_HID_INFORMATION = { 0x11, 0x01, 0x00, 0x03 };

    /**
     * Instances for the peripheral
     */
    private final Context applicationContext;

    private final Handler handler;

    private final BluetoothLeAdvertiser bluetoothLeAdvertiser;

    private BluetoothGattCharacteristic inputReportCharacteristic;

    @Nullable
    private BluetoothGattServer gattServer;

    private final Map<String, BluetoothDevice> bluetoothDevicesMap = new HashMap<>();

    /**
     * Constructor<br />
     * Before constructing the instance, check the Bluetooth availability.
     *
     * @param context the ApplicationContext
     * @param needInputReport true: serves 'Input Report' BLE characteristic
     * @param needOutputReport true: serves 'Output Report' BLE characteristic
     * @param needFeatureReport true: serves 'Feature Report' BLE characteristic
     * @param dataSendingRate sending rate in milliseconds
     * @throws UnsupportedOperationException if starting Bluetooth LE Peripheral failed
     */
    protected HidPeripheral(final Context context, final boolean needInputReport, final boolean needOutputReport, final boolean needFeatureReport, final int dataSendingRate) throws UnsupportedOperationException {
        applicationContext = context.getApplicationContext();
        handler = new Handler(applicationContext.getMainLooper());
        final BluetoothManager bluetoothManager = (BluetoothManager) applicationContext.getSystemService(Context.BLUETOOTH_SERVICE);
        final BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
        if (bluetoothAdapter == null) {
            throw new UnsupportedOperationException("Bluetooth is not available.");
        }
        if (!bluetoothAdapter.isEnabled()) {
            throw new UnsupportedOperationException("Bluetooth is disabled.");
        }
        Log.d(TAG, "isMultipleAdvertisementSupported:" + bluetoothAdapter.isMultipleAdvertisementSupported());
        if (!bluetoothAdapter.isMultipleAdvertisementSupported()) {
            throw new UnsupportedOperationException("Bluetooth LE Advertising not supported on this device.");
        }
        bluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
        Log.d(TAG, "bluetoothLeAdvertiser: " + bluetoothLeAdvertiser);
        if (bluetoothLeAdvertiser == null) {
            throw new UnsupportedOperationException("Bluetooth LE Advertising not supported on this device.");
        }
        gattServer = bluetoothManager.openGattServer(applicationContext, gattServerCallback);
        if (gattServer == null) {
            throw new UnsupportedOperationException("gattServer is null, check Bluetooth is ON.");
        }
        // setup services
        addService(setUpHidService(needInputReport, needOutputReport, needFeatureReport));
        addService(setUpDeviceInformationService());
        addService(setUpBatteryService());
        // send report each dataSendingRate, if data available
        new Timer().scheduleAtFixedRate(new TimerTask() {

            @Override
            public void run() {
                final byte[] polled = inputReportQueue.poll();
                if (polled != null && inputReportCharacteristic != null) {
                    inputReportCharacteristic.setValue(polled);
                    handler.post(new Runnable() {

                        @Override
                        public void run() {
                            final Set<BluetoothDevice> devices = getDevices();
                            for (final BluetoothDevice device : devices) {
                                try {
                                    if (gattServer != null) {
                                        gattServer.notifyCharacteristicChanged(device, inputReportCharacteristic, false);
                                    }
                                } catch (final Throwable ignored) {
                                }
                            }
                        }
                    });
                }
            }
        }, 0, dataSendingRate);
    }

    /**
     * Add GATT service to gattServer
     *
     * @param service the service
     */
    private void addService(final BluetoothGattService service) {
        replacedert gattServer != null;
        boolean serviceAdded = false;
        while (!serviceAdded) {
            try {
                serviceAdded = gattServer.addService(service);
            } catch (final Exception e) {
                Log.d(TAG, "Adding Service failed", e);
            }
        }
        Log.d(TAG, "Service: " + service.getUuid() + " added.");
    }

    /**
     * Setup Device Information Service
     *
     * @return the service
     */
    private static BluetoothGattService setUpDeviceInformationService() {
        final BluetoothGattService service = new BluetoothGattService(SERVICE_DEVICE_INFORMATION, BluetoothGattService.SERVICE_TYPE_PRIMARY);
        {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_MANUFACTURER_NAME, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
            while (!service.addCharacteristic(characteristic)) ;
        }
        {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_MODEL_NUMBER, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
            while (!service.addCharacteristic(characteristic)) ;
        }
        {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_SERIAL_NUMBER, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
            while (!service.addCharacteristic(characteristic)) ;
        }
        return service;
    }

    /**
     * Setup Battery Service
     *
     * @return the service
     */
    private static BluetoothGattService setUpBatteryService() {
        final BluetoothGattService service = new BluetoothGattService(SERVICE_BATTERY, BluetoothGattService.SERVICE_TYPE_PRIMARY);
        // Battery Level
        final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_BATTERY_LEVEL, BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
        final BluetoothGattDescriptor clientCharacteristicConfigurationDescriptor = new BluetoothGattDescriptor(DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
        clientCharacteristicConfigurationDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        characteristic.addDescriptor(clientCharacteristicConfigurationDescriptor);
        while (!service.addCharacteristic(characteristic)) ;
        return service;
    }

    /**
     * Setup HID Service
     *
     * @param isNeedInputReport true: serves 'Input Report' BLE characteristic
     * @param isNeedOutputReport true: serves 'Output Report' BLE characteristic
     * @param isNeedFeatureReport true: serves 'Feature Report' BLE characteristic
     * @return the service
     */
    private BluetoothGattService setUpHidService(final boolean isNeedInputReport, final boolean isNeedOutputReport, final boolean isNeedFeatureReport) {
        final BluetoothGattService service = new BluetoothGattService(SERVICE_BLE_HID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
        // HID Information
        {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_HID_INFORMATION, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
            while (!service.addCharacteristic(characteristic)) ;
        }
        // Report Map
        {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_REPORT_MAP, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
            while (!service.addCharacteristic(characteristic)) ;
        }
        // Protocol Mode
        {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_PROTOCOL_MODE, BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED | BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED);
            characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
            while (!service.addCharacteristic(characteristic)) ;
        }
        // HID Control Point
        {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_HID_CONTROL_POINT, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED);
            characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
            while (!service.addCharacteristic(characteristic)) ;
        }
        // Input Report
        if (isNeedInputReport) {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_REPORT, BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED | BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED);
            final BluetoothGattDescriptor clientCharacteristicConfigurationDescriptor = new BluetoothGattDescriptor(DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, // | BluetoothGattDescriptor.PERMISSION_WRITE
            BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED | BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED);
            clientCharacteristicConfigurationDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            characteristic.addDescriptor(clientCharacteristicConfigurationDescriptor);
            final BluetoothGattDescriptor reportReferenceDescriptor = new BluetoothGattDescriptor(DESCRIPTOR_REPORT_REFERENCE, BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED | BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED);
            characteristic.addDescriptor(reportReferenceDescriptor);
            while (!service.addCharacteristic(characteristic)) ;
            inputReportCharacteristic = characteristic;
        }
        // Output Report
        if (isNeedOutputReport) {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_REPORT, BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED | BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED);
            characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
            final BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(DESCRIPTOR_REPORT_REFERENCE, BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED | BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED);
            characteristic.addDescriptor(descriptor);
            while (!service.addCharacteristic(characteristic)) ;
        }
        // Feature Report
        if (isNeedFeatureReport) {
            final BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_REPORT, BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED | BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED);
            final BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(DESCRIPTOR_REPORT_REFERENCE, BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED | BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED);
            characteristic.addDescriptor(descriptor);
            while (!service.addCharacteristic(characteristic)) ;
        }
        return service;
    }

    /**
     * Starts advertising
     */
    public final void startAdvertising() {
        handler.post(new Runnable() {

            @Override
            public void run() {
                // set up advertising setting
                final AdvertiseSettings advertiseSettings = new AdvertiseSettings.Builder().setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH).setConnectable(true).setTimeout(0).setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY).build();
                // set up advertising data
                final AdvertiseData advertiseData = new Builder().setIncludeTxPowerLevel(false).setIncludeDeviceName(true).addServiceUuid(ParcelUuid.fromString(SERVICE_DEVICE_INFORMATION.toString())).addServiceUuid(ParcelUuid.fromString(SERVICE_BLE_HID.toString())).addServiceUuid(ParcelUuid.fromString(SERVICE_BATTERY.toString())).build();
                // set up scan result
                final AdvertiseData scanResult = new Builder().addServiceUuid(ParcelUuid.fromString(SERVICE_DEVICE_INFORMATION.toString())).addServiceUuid(ParcelUuid.fromString(SERVICE_BLE_HID.toString())).addServiceUuid(ParcelUuid.fromString(SERVICE_BATTERY.toString())).build();
                Log.d(TAG, "advertiseData: " + advertiseData + ", scanResult: " + scanResult);
                bluetoothLeAdvertiser.startAdvertising(advertiseSettings, advertiseData, scanResult, advertiseCallback);
            }
        });
    }

    /**
     * Stops advertising
     */
    public final void stopAdvertising() {
        handler.post(new Runnable() {

            @Override
            public void run() {
                try {
                    bluetoothLeAdvertiser.stopAdvertising(advertiseCallback);
                } catch (final IllegalStateException ignored) {
                // BT Adapter is not turned ON
                }
                try {
                    if (gattServer != null) {
                        final Set<BluetoothDevice> devices = getDevices();
                        for (final BluetoothDevice device : devices) {
                            gattServer.cancelConnection(device);
                        }
                        gattServer.close();
                        gattServer = null;
                    }
                } catch (final IllegalStateException ignored) {
                // BT Adapter is not turned ON
                }
            }
        });
    }

    /**
     * Callback for BLE connection<br />
     * nothing to do.
     */
    private final AdvertiseCallback advertiseCallback = new NullAdvertiseCallback();

    private static clreplaced NullAdvertiseCallback extends AdvertiseCallback {
    }

    /**
     * Obtains connected Bluetooth devices
     *
     * @return the connected Bluetooth devices
     */
    private Set<BluetoothDevice> getDevices() {
        final Set<BluetoothDevice> deviceSet = new HashSet<>();
        synchronized (bluetoothDevicesMap) {
            deviceSet.addAll(bluetoothDevicesMap.values());
        }
        return Collections.unmodifiableSet(deviceSet);
    }

    /**
     * Callback for BLE data transfer
     */
    private final BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onConnectionStateChange(final BluetoothDevice device, final int status, final int newState) {
            super.onConnectionStateChange(device, status, newState);
            Log.d(TAG, "onConnectionStateChange status: " + status + ", newState: " + newState);
            switch(newState) {
                case BluetoothProfile.STATE_CONNECTED:
                    // check bond status
                    Log.d(TAG, "BluetoothProfile.STATE_CONNECTED bondState: " + device.getBondState());
                    if (device.getBondState() == BluetoothDevice.BOND_NONE) {
                        applicationContext.registerReceiver(new BroadcastReceiver() {

                            @Override
                            public void onReceive(final Context context, final Intent intent) {
                                final String action = intent.getAction();
                                Log.d(TAG, "onReceive action: " + action);
                                if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
                                    final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR);
                                    if (state == BluetoothDevice.BOND_BONDED) {
                                        final BluetoothDevice bondedDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                                        // successfully bonded
                                        context.unregisterReceiver(this);
                                        handler.post(new Runnable() {

                                            @Override
                                            public void run() {
                                                if (gattServer != null) {
                                                    gattServer.connect(device, true);
                                                }
                                            }
                                        });
                                        Log.d(TAG, "successfully bonded");
                                    }
                                }
                            }
                        }, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
                        // create bond
                        try {
                            device.setPairingConfirmation(true);
                        } catch (final SecurityException e) {
                            Log.d(TAG, e.getMessage(), e);
                        }
                        device.createBond();
                    } else if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
                        handler.post(new Runnable() {

                            @Override
                            public void run() {
                                if (gattServer != null) {
                                    gattServer.connect(device, true);
                                }
                            }
                        });
                        synchronized (bluetoothDevicesMap) {
                            bluetoothDevicesMap.put(device.getAddress(), device);
                        }
                    }
                    break;
                case BluetoothProfile.STATE_DISCONNECTED:
                    final String deviceAddress = device.getAddress();
                    // try reconnect immediately
                    handler.post(new Runnable() {

                        @Override
                        public void run() {
                            if (gattServer != null) {
                                // gattServer.cancelConnection(device);
                                gattServer.connect(device, true);
                            }
                        }
                    });
                    synchronized (bluetoothDevicesMap) {
                        bluetoothDevicesMap.remove(deviceAddress);
                    }
                    break;
                default:
                    // do nothing
                    break;
            }
        }

        @Override
        public void onCharacteristicReadRequest(final BluetoothDevice device, final int requestId, final int offset, final BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
            if (gattServer == null) {
                return;
            }
            Log.d(TAG, "onCharacteristicReadRequest characteristic: " + characteristic.getUuid() + ", offset: " + offset);
            handler.post(new Runnable() {

                @Override
                public void run() {
                    final UUID characteristicUuid = characteristic.getUuid();
                    if (BleUuidUtils.matches(CHARACTERISTIC_HID_INFORMATION, characteristicUuid)) {
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, RESPONSE_HID_INFORMATION);
                    } else if (BleUuidUtils.matches(CHARACTERISTIC_REPORT_MAP, characteristicUuid)) {
                        if (offset == 0) {
                            gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, getReportMap());
                        } else {
                            final int remainLength = getReportMap().length - offset;
                            if (remainLength > 0) {
                                final byte[] data = new byte[remainLength];
                                System.arraycopy(getReportMap(), offset, data, 0, remainLength);
                                gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, data);
                            } else {
                                gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
                            }
                        }
                    } else if (BleUuidUtils.matches(CHARACTERISTIC_HID_CONTROL_POINT, characteristicUuid)) {
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] { 0 });
                    } else if (BleUuidUtils.matches(CHARACTERISTIC_REPORT, characteristicUuid)) {
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, EMPTY_BYTES);
                    } else if (BleUuidUtils.matches(CHARACTERISTIC_MANUFACTURER_NAME, characteristicUuid)) {
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, manufacturer.getBytes(StandardCharsets.UTF_8));
                    } else if (BleUuidUtils.matches(CHARACTERISTIC_SERIAL_NUMBER, characteristicUuid)) {
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, serialNumber.getBytes(StandardCharsets.UTF_8));
                    } else if (BleUuidUtils.matches(CHARACTERISTIC_MODEL_NUMBER, characteristicUuid)) {
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, deviceName.getBytes(StandardCharsets.UTF_8));
                    } else if (BleUuidUtils.matches(CHARACTERISTIC_BATTERY_LEVEL, characteristicUuid)) {
                        // always 100%
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] { 0x64 });
                    } else {
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, characteristic.getValue());
                    }
                }
            });
        }

        @Override
        public void onDescriptorReadRequest(final BluetoothDevice device, final int requestId, final int offset, final BluetoothGattDescriptor descriptor) {
            super.onDescriptorReadRequest(device, requestId, offset, descriptor);
            Log.d(TAG, "onDescriptorReadRequest requestId: " + requestId + ", offset: " + offset + ", descriptor: " + descriptor.getUuid());
            if (gattServer == null) {
                return;
            }
            handler.post(new Runnable() {

                @Override
                public void run() {
                    if (BleUuidUtils.matches(DESCRIPTOR_REPORT_REFERENCE, descriptor.getUuid())) {
                        final int characteristicProperties = descriptor.getCharacteristic().getProperties();
                        if (characteristicProperties == (BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_NOTIFY)) {
                            // Input Report
                            gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] { 0, 1 });
                        } else if (characteristicProperties == (BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) {
                            // Output Report
                            gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] { 0, 2 });
                        } else if (characteristicProperties == (BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE)) {
                            // Feature Report
                            gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] { 0, 3 });
                        } else {
                            gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, EMPTY_BYTES);
                        }
                    }
                }
            });
        }

        @Override
        public void onCharacteristicWriteRequest(final BluetoothDevice device, final int requestId, final BluetoothGattCharacteristic characteristic, final boolean preparedWrite, final boolean responseNeeded, final int offset, final byte[] value) {
            super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
            Log.d(TAG, "onCharacteristicWriteRequest characteristic: " + characteristic.getUuid() + ", value: " + Arrays.toString(value));
            if (gattServer == null) {
                return;
            }
            if (responseNeeded) {
                if (BleUuidUtils.matches(CHARACTERISTIC_REPORT, characteristic.getUuid())) {
                    if (characteristic.getProperties() == (BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) {
                        // Output Report
                        onOutputReport(value);
                        // send empty
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, EMPTY_BYTES);
                    } else {
                        // send empty
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, EMPTY_BYTES);
                    }
                }
            }
        }

        @Override
        public void onDescriptorWriteRequest(final BluetoothDevice device, final int requestId, final BluetoothGattDescriptor descriptor, final boolean preparedWrite, final boolean responseNeeded, final int offset, final byte[] value) {
            super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
            Log.d(TAG, "onDescriptorWriteRequest descriptor: " + descriptor.getUuid() + ", value: " + Arrays.toString(value) + ", responseNeeded: " + responseNeeded + ", preparedWrite: " + preparedWrite);
            descriptor.setValue(value);
            if (responseNeeded) {
                if (BleUuidUtils.matches(DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, descriptor.getUuid())) {
                    // send empty
                    if (gattServer != null) {
                        gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, EMPTY_BYTES);
                    }
                }
            }
        }

        @Override
        public void onServiceAdded(final int status, final BluetoothGattService service) {
            super.onServiceAdded(status, service);
            Log.d(TAG, "onServiceAdded status: " + status + ", service: " + service.getUuid());
            if (status != 0) {
                Log.d(TAG, "onServiceAdded Adding Service failed..");
            }
        }
    };

    /**
     * Set the manufacturer name
     *
     * @param newManufacturer the name
     */
    public final void setManufacturer(@NonNull final String newManufacturer) {
        // length check
        final byte[] manufacturerBytes = newManufacturer.getBytes(StandardCharsets.UTF_8);
        if (manufacturerBytes.length > DEVICE_INFO_MAX_LENGTH) {
            // shorten
            final byte[] bytes = new byte[DEVICE_INFO_MAX_LENGTH];
            System.arraycopy(manufacturerBytes, 0, bytes, 0, DEVICE_INFO_MAX_LENGTH);
            manufacturer = new String(bytes, StandardCharsets.UTF_8);
        } else {
            manufacturer = newManufacturer;
        }
    }

    /**
     * Set the device name
     *
     * @param newDeviceName the name
     */
    public final void setDeviceName(@NonNull final String newDeviceName) {
        // length check
        final byte[] deviceNameBytes = newDeviceName.getBytes(StandardCharsets.UTF_8);
        if (deviceNameBytes.length > DEVICE_INFO_MAX_LENGTH) {
            // shorten
            final byte[] bytes = new byte[DEVICE_INFO_MAX_LENGTH];
            System.arraycopy(deviceNameBytes, 0, bytes, 0, DEVICE_INFO_MAX_LENGTH);
            deviceName = new String(bytes, StandardCharsets.UTF_8);
        } else {
            deviceName = newDeviceName;
        }
    }

    /**
     * Set the serial number
     *
     * @param newSerialNumber the number
     */
    public final void setSerialNumber(@NonNull final String newSerialNumber) {
        // length check
        final byte[] deviceNameBytes = newSerialNumber.getBytes(StandardCharsets.UTF_8);
        if (deviceNameBytes.length > DEVICE_INFO_MAX_LENGTH) {
            // shorten
            final byte[] bytes = new byte[DEVICE_INFO_MAX_LENGTH];
            System.arraycopy(deviceNameBytes, 0, bytes, 0, DEVICE_INFO_MAX_LENGTH);
            serialNumber = new String(bytes, StandardCharsets.UTF_8);
        } else {
            serialNumber = newSerialNumber;
        }
    }
}

6 Source : CSCService.java
with MIT License
from starryalley

public clreplaced CSCService extends Service {

    private static final String TAG = CSCService.clreplaced.getSimpleName();

    private static final int ONGOING_NOTIFICATION_ID = 9999;

    private static final String CHANNEL_DEFAULT_IMPORTANCE = "csc_ble_channel";

    private static final String MAIN_CHANNEL_NAME = "CscService";

    // Ant+ sensors
    private AntPlusBikeSpeedDistancePcc bsdPcc = null;

    private PccReleaseHandle<AntPlusBikeSpeedDistancePcc> bsdReleaseHandle = null;

    private AntPlusBikeCadencePcc bcPcc = null;

    private PccReleaseHandle<AntPlusBikeCadencePcc> bcReleaseHandle = null;

    private AntPlusHeartRatePcc hrPcc = null;

    private PccReleaseHandle<AntPlusHeartRatePcc> hrReleaseHandle = null;

    private AntPlusStrideSdmPcc ssPcc = null;

    private PccReleaseHandle<AntPlusStrideSdmPcc> ssReleaseHandle = null;

    // Checks that the callback that is done after a BluetoothGattServer.addService() has been complete.
    // More services cannot be added until the callback has completed successfully
    private boolean btServiceInitialized = false;

    // 700x23c cirreplacedference in meter
    private static final BigDecimal cirreplacedference = new BigDecimal("2.095");

    // m/s to km/h ratio
    private static final BigDecimal msToKmSRatio = new BigDecimal("3.6");

    // bluetooth API
    private BluetoothManager mBluetoothManager;

    private BluetoothGattServer mBluetoothGattServer;

    private BluetoothLeAdvertiser mBluetoothLeAdvertiser;

    // notification subscribers
    private Set<BluetoothDevice> mRegisteredDevices = new HashSet<>();

    // last wheel and crank (speed/cadence) information to send to CSCProfile
    private long replacedulativeWheelRevolution = 0;

    private long replacedulativeCrankRevolution = 0;

    private int lastWheelEventTime = 0;

    private int lastCrankEventTime = 0;

    // for UI updates
    private long lastSpeedTimestamp = 0;

    private long lastCadenceTimestamp = 0;

    private long lastHRTimestamp = 0;

    private long lastSSDistanceTimestamp = 0;

    private long lastSSSpeedTimestamp = 0;

    private long lastSSStrideCountTimestamp = 0;

    private float lastSpeed = 0;

    private int lastCadence = 0;

    private int lastHR = 0;

    private long lastSSDistance = 0;

    private float lastSSSpeed = 0;

    private long lastStridePerMinute = 0;

    // for onCreate() failure case
    private boolean initialised = false;

    // Used to flag if we have a combined speed and cadence sensor and have already re-connected as combined
    private boolean combinedSensorConnected = false;

    // Binder for activities wishing to communicate with this service
    private final IBinder binder = new LocalBinder();

    private AntPluginPcc.IPluginAccessResultReceiver<AntPlusBikeSpeedDistancePcc> mBSDResultReceiver = new AntPluginPcc.IPluginAccessResultReceiver<AntPlusBikeSpeedDistancePcc>() {

        @Override
        public void onResultReceived(AntPlusBikeSpeedDistancePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
            if (resultCode == RequestAccessResult.SUCCESS) {
                bsdPcc = result;
                Log.d(TAG, result.getDeviceName() + ": " + initialDeviceState);
                subscribeToEvents();
            } else if (resultCode == RequestAccessResult.USER_CANCELLED) {
                Log.d(TAG, "BSD Closed:" + resultCode);
            } else {
                Log.w(TAG, "BSD state changed:" + initialDeviceState + ", resultCode:" + resultCode);
            }
            // send broadcast
            Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
            i.putExtra("bsd_service_status", initialDeviceState.toString() + "\n(" + resultCode + ")");
            sendBroadcast(i);
        }

        private void subscribeToEvents() {
            bsdPcc.subscribeCalculatedSpeedEvent(new AntPlusBikeSpeedDistancePcc.CalculatedSpeedReceiver(cirreplacedference) {

                @Override
                public void onNewCalculatedSpeed(final long estTimestamp, final EnumSet<EventFlag> eventFlags, final BigDecimal calculatedSpeed) {
                    // convert m/s to km/h
                    lastSpeed = calculatedSpeed.multiply(msToKmSRatio).floatValue();
                // Log.v(TAG, "Speed:" + lastSpeed);
                }
            });
            bsdPcc.subscribeRawSpeedAndDistanceDataEvent(new AntPlusBikeSpeedDistancePcc.IRawSpeedAndDistanceDataReceiver() {

                @Override
                public void onNewRawSpeedAndDistanceData(long estTimestamp, EnumSet<EventFlag> eventFlags, BigDecimal timestampOfLastEvent, long replacedulativeRevolutions) {
                    // estTimestamp - The estimated timestamp of when this event was triggered. Useful for correlating multiple events and determining when data was sent for more accurate data records.
                    // eventFlags - Informational flags about the event.
                    // timestampOfLastEvent - Sensor reported time counter value of last distance or speed computation (up to 1/200s accuracy). Units: s. Rollover: Every ~46 quadrillion s (~1.5 billion years).
                    // replacedulativeRevolutions - Total number of revolutions since the sensor was first connected. Note: If the subscriber is not the first PCC connected to the device the acreplacedulation will probably already be at a value greater than 0 and the subscriber should save the first received value as a relative zero for itself. Units: revolutions. Rollover: Every ~9 quintillion revolutions.
                    Log.v(TAG, "=> BSD: replacedulative revolution:" + replacedulativeRevolutions + ", lastEventTime:" + timestampOfLastEvent);
                    replacedulativeWheelRevolution = replacedulativeRevolutions;
                    lastWheelEventTime = (int) (timestampOfLastEvent.doubleValue() * 1024.0);
                    lastSpeedTimestamp = estTimestamp;
                }
            });
            if (bsdPcc.isSpeedAndCadenceCombinedSensor() && !combinedSensorConnected) {
                // reconnect cadence sensor as combined sensor
                if (bcReleaseHandle != null) {
                    bcReleaseHandle.close();
                }
                combinedSensorConnected = true;
                bcReleaseHandle = AntPlusBikeCadencePcc.requestAccess(getApplicationContext(), bsdPcc.getAntDeviceNumber(), 0, true, mBCResultReceiver, mBCDeviceStateChangeReceiver);
            }
        }
    };

    private AntPluginPcc.IPluginAccessResultReceiver<AntPlusBikeCadencePcc> mBCResultReceiver = new AntPluginPcc.IPluginAccessResultReceiver<AntPlusBikeCadencePcc>() {

        // Handle the result, connecting to events on success or reporting
        // failure to user.
        @Override
        public void onResultReceived(AntPlusBikeCadencePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
            if (resultCode == RequestAccessResult.SUCCESS) {
                bcPcc = result;
                Log.d(TAG, result.getDeviceName() + ": " + initialDeviceState);
                subscribeToEvents();
            } else if (resultCode == RequestAccessResult.USER_CANCELLED) {
                Log.d(TAG, "BC Closed:" + resultCode);
            } else {
                Log.w(TAG, "BC state changed:" + initialDeviceState + ", resultCode:" + resultCode);
            }
            // send broadcast
            Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
            i.putExtra("bc_service_status", initialDeviceState.toString() + "\n(" + resultCode + ")");
            sendBroadcast(i);
        }

        private void subscribeToEvents() {
            bcPcc.subscribeCalculatedCadenceEvent(new AntPlusBikeCadencePcc.ICalculatedCadenceReceiver() {

                @Override
                public void onNewCalculatedCadence(final long estTimestamp, final EnumSet<EventFlag> eventFlags, final BigDecimal calculatedCadence) {
                    // Log.v(TAG, "Cadence:" + calculatedCadence.intValue());
                    lastCadence = calculatedCadence.intValue();
                }
            });
            bcPcc.subscribeRawCadenceDataEvent(new AntPlusBikeCadencePcc.IRawCadenceDataReceiver() {

                @Override
                public void onNewRawCadenceData(final long estTimestamp, final EnumSet<EventFlag> eventFlags, final BigDecimal timestampOfLastEvent, final long replacedulativeRevolutions) {
                    Log.v(TAG, "=> BC: replacedulative revolution:" + replacedulativeRevolutions + ", lastEventTime:" + timestampOfLastEvent);
                    replacedulativeCrankRevolution = replacedulativeRevolutions;
                    lastCrankEventTime = (int) (timestampOfLastEvent.doubleValue() * 1024.0);
                    lastCadenceTimestamp = estTimestamp;
                }
            });
            if (bcPcc.isSpeedAndCadenceCombinedSensor() && !combinedSensorConnected) {
                // reconnect speed sensor as a combined sensor
                if (bsdReleaseHandle != null) {
                    bsdReleaseHandle.close();
                }
                combinedSensorConnected = true;
                bsdReleaseHandle = AntPlusBikeSpeedDistancePcc.requestAccess(getApplicationContext(), bcPcc.getAntDeviceNumber(), 0, true, mBSDResultReceiver, mBSDDeviceStateChangeReceiver);
            }
        }
    };

    private AntPluginPcc.IPluginAccessResultReceiver<AntPlusHeartRatePcc> mHRResultReceiver = new AntPluginPcc.IPluginAccessResultReceiver<AntPlusHeartRatePcc>() {

        @Override
        public void onResultReceived(AntPlusHeartRatePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
            if (resultCode == RequestAccessResult.SUCCESS) {
                hrPcc = result;
                Log.d(TAG, result.getDeviceName() + ": " + initialDeviceState);
                subscribeToEvents();
            } else if (resultCode == RequestAccessResult.USER_CANCELLED) {
                Log.d(TAG, "HR Closed:" + resultCode);
            } else {
                Log.w(TAG, "HR state changed:" + initialDeviceState + ", resultCode:" + resultCode);
            }
            // send broadcast
            Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
            i.putExtra("hr_service_status", initialDeviceState.toString() + "\n(" + resultCode + ")");
            sendBroadcast(i);
        }

        private void subscribeToEvents() {
            hrPcc.subscribeHeartRateDataEvent(new AntPlusHeartRatePcc.IHeartRateDataReceiver() {

                @Override
                public void onNewHeartRateData(final long estTimestamp, EnumSet<EventFlag> eventFlags, final int computedHeartRate, final long heartBeatCount, final BigDecimal heartBeatEventTime, final AntPlusHeartRatePcc.DataState dataState) {
                    lastHR = computedHeartRate;
                    lastHRTimestamp = estTimestamp;
                }
            });
        }
    };

    private AntPluginPcc.IPluginAccessResultReceiver<AntPlusStrideSdmPcc> mSSResultReceiver = new AntPluginPcc.IPluginAccessResultReceiver<AntPlusStrideSdmPcc>() {

        @Override
        public void onResultReceived(AntPlusStrideSdmPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
            if (resultCode == RequestAccessResult.SUCCESS) {
                ssPcc = result;
                Log.d(TAG, result.getDeviceName() + ": " + initialDeviceState);
                subscribeToEvents();
            } else if (resultCode == RequestAccessResult.USER_CANCELLED) {
                Log.d(TAG, "SS Closed:" + resultCode);
            } else {
                Log.w(TAG, "SS state changed: " + initialDeviceState + ", resultCode:" + resultCode);
            }
            // send broadcast
            Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
            i.putExtra("ss_service_status", initialDeviceState.toString() + "\n(" + resultCode + ")");
            sendBroadcast(i);
        }

        private void subscribeToEvents() {
            // https://www.thisisant.com/developer/ant-plus/device-profiles#528_tab
            ssPcc.subscribeStrideCountEvent(new AntPlusStrideSdmPcc.IStrideCountReceiver() {

                private static final int TEN_SECONDS_IN_MS = 10000;

                private static final int FALLBACK_MAX_LIST_SIZE = 500;

                private static final float ONE_MINUTE_IN_MS = 60000f;

                private final LinkedList<Pair<Long, Long>> strideList = new LinkedList<>();

                private final Semapreplaced lock = new Semapreplaced(1);

                @Override
                public void onNewStrideCount(long estTimestamp, EnumSet<EventFlag> eventFlags, final long replacedulativeStrides) {
                    new Thread(() -> {
                        try {
                            lock.acquire();
                            // Calculate number of strides per minute, updates happen around every 500 ms, this number
                            // may be off by that amount but it isn't too significant
                            strideList.addFirst(new Pair<>(estTimestamp, replacedulativeStrides));
                            long strideCount = 0;
                            boolean valueFound = false;
                            int i = 0;
                            for (Pair<Long, Long> p : strideList) {
                                // Cadence over the last 10 seconds
                                if (estTimestamp - p.first >= TEN_SECONDS_IN_MS) {
                                    valueFound = true;
                                    strideCount = calculateStepsPerMin(estTimestamp, replacedulativeStrides, p);
                                    break;
                                } else if (i + 1 == strideList.size()) {
                                    // No value was found yet, it has not been 10 seconds. Give an early rough estimate
                                    strideCount = calculateStepsPerMin(estTimestamp, replacedulativeStrides, p);
                                }
                                i++;
                            }
                            while ((valueFound && strideList.size() >= i + 1) || strideList.size() > FALLBACK_MAX_LIST_SIZE) {
                                strideList.removeLast();
                            }
                            lastSSStrideCountTimestamp = estTimestamp;
                            lastStridePerMinute = strideCount;
                            lock.release();
                        } catch (InterruptedException e) {
                            Log.e(TAG, "Unable to acquire lock to update running cadence", e);
                        }
                    }).start();
                }

                private long calculateStepsPerMin(long estTimestamp, long replacedulativeStrides, Pair<Long, Long> p) {
                    float elapsedTimeMs = estTimestamp - p.first;
                    if (elapsedTimeMs == 0) {
                        return 0;
                    }
                    return (long) ((replacedulativeStrides - p.second) * (ONE_MINUTE_IN_MS / elapsedTimeMs));
                }
            });
            ssPcc.subscribeDistanceEvent(new AntPlusStrideSdmPcc.IDistanceReceiver() {

                @Override
                public void onNewDistance(long estTimestamp, EnumSet<EventFlag> eventFlags, BigDecimal distance) {
                    lastSSDistanceTimestamp = estTimestamp;
                    lastSSDistance = distance.longValue();
                }
            });
            ssPcc.subscribeInstantaneousSpeedEvent(new AntPlusStrideSdmPcc.IInstantaneousSpeedReceiver() {

                @Override
                public void onNewInstantaneousSpeed(long estTimestamp, EnumSet<EventFlag> eventFlags, BigDecimal instantaneousSpeed) {
                    lastSSDistanceTimestamp = estTimestamp;
                    lastSSSpeed = instantaneousSpeed.floatValue();
                }
            });
        }
    };

    private enum AntSensorType {

        CyclingSpeed, CyclingCadence, HR, StrideBasedSpeedAndDistance
    }

    private clreplaced AntDeviceChangeReceiver implements AntPluginPcc.IDeviceStateChangeReceiver {

        private AntSensorType type;

        AntDeviceChangeReceiver(AntSensorType type) {
            this.type = type;
        }

        @Override
        public void onDeviceStateChange(final DeviceState newDeviceState) {
            String extraName = "unknown";
            if (type == AntSensorType.CyclingSpeed) {
                extraName = "bsd_service_status";
                Log.d(TAG, "Speed sensor onDeviceStateChange:" + newDeviceState);
            } else if (type == AntSensorType.CyclingCadence) {
                extraName = "bc_service_status";
                Log.d(TAG, "Cadence sensor onDeviceStateChange:" + newDeviceState);
            } else if (type == AntSensorType.HR) {
                extraName = "hr_service_status";
                Log.d(TAG, "HR sensor onDeviceStateChange:" + newDeviceState);
            } else if (type == AntSensorType.StrideBasedSpeedAndDistance) {
                extraName = "ss_service_status";
                Log.d(TAG, "Stride based speed and distance onDeviceStateChange:" + newDeviceState);
            }
            // send broadcast about device status
            Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
            i.putExtra(extraName, newDeviceState.name());
            sendBroadcast(i);
            // if the device is dead (closed)
            if (newDeviceState == DeviceState.DEAD) {
                bsdPcc = null;
            }
        }
    }

    private AntPluginPcc.IDeviceStateChangeReceiver mBSDDeviceStateChangeReceiver = new AntDeviceChangeReceiver(AntSensorType.CyclingSpeed);

    private AntPluginPcc.IDeviceStateChangeReceiver mBCDeviceStateChangeReceiver = new AntDeviceChangeReceiver(AntSensorType.CyclingCadence);

    private AntPluginPcc.IDeviceStateChangeReceiver mHRDeviceStateChangeReceiver = new AntDeviceChangeReceiver(AntSensorType.HR);

    private AntPluginPcc.IDeviceStateChangeReceiver mSSDeviceStateChangeReceiver = new AntDeviceChangeReceiver(AntSensorType.StrideBasedSpeedAndDistance);

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "Service onStartCommand");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel(CHANNEL_DEFAULT_IMPORTANCE, MAIN_CHANNEL_NAME);
            // Create the PendingIntent
            PendingIntent notifyPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this.getApplicationContext(), MainActivity.clreplaced), PendingIntent.FLAG_UPDATE_CURRENT);
            // build a notification
            Notification notification = new Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE).setContentreplacedle(getText(R.string.app_name)).setContentText("Active").setSmallIcon(R.drawable.ic_notification_icon).setAutoCancel(true).setContentIntent(notifyPendingIntent).setTicker(getText(R.string.app_name)).build();
            startForeground(ONGOING_NOTIFICATION_ID, notification);
        } else {
            Notification notification = new NotificationCompat.Builder(this, CHANNEL_DEFAULT_IMPORTANCE).setContentreplacedle(getString(R.string.app_name)).setContentText("Active").setSmallIcon(R.drawable.ic_notification_icon).setPriority(NotificationCompat.PRIORITY_DEFAULT).setAutoCancel(true).build();
            startForeground(ONGOING_NOTIFICATION_ID, notification);
        }
        return Service.START_NOT_STICKY;
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private void createNotificationChannel(String channelId, String channelName) {
        NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW);
        channel.setLightColor(Color.BLUE);
        channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        manager.createNotificationChannel(channel);
    }

    private boolean checkBluetoothSupport(BluetoothAdapter bluetoothAdapter) {
        if (bluetoothAdapter == null) {
            Log.w(TAG, "Bluetooth is not supported");
            return false;
        }
        if (!getPackageManager().hreplacedystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Log.w(TAG, "Bluetooth LE is not supported");
            return false;
        }
        return true;
    }

    private BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
            switch(state) {
                case BluetoothAdapter.STATE_ON:
                    startAdvertising();
                    startServer();
                    break;
                case BluetoothAdapter.STATE_OFF:
                    stopServer();
                    stopAdvertising();
                    break;
            }
        }
    };

    @Override
    public void onCreate() {
        Log.d(TAG, "Service started");
        super.onCreate();
        // ANT+
        initAntPlus();
        // Bluetooth LE
        mBluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
        replacedert mBluetoothManager != null;
        BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
        // continue without proper Bluetooth support
        if (!checkBluetoothSupport(bluetoothAdapter)) {
            Log.e(TAG, "Bluetooth LE isn't supported. This won't run");
            stopSelf();
            return;
        }
        // Register for system Bluetooth events
        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        registerReceiver(mBluetoothReceiver, filter);
        if (!bluetoothAdapter.isEnabled()) {
            Log.d(TAG, "Bluetooth is currently disabled...enabling");
            bluetoothAdapter.enable();
        } else {
            Log.d(TAG, "Bluetooth enabled...starting services");
            startAdvertising();
            startServer();
        }
        initialised = true;
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        Log.d(TAG, "onTaskRemoved called");
        super.onTaskRemoved(rootIntent);
        stopForeground(true);
        stopSelf();
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "Service destroyed");
        super.onDestroy();
        if (initialised) {
            // stop BLE
            BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
            if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {
                stopServer();
                stopAdvertising();
            }
            unregisterReceiver(mBluetoothReceiver);
            // stop ANT+
            if (bsdReleaseHandle != null)
                bsdReleaseHandle.close();
            if (bcReleaseHandle != null)
                bcReleaseHandle.close();
            if (hrReleaseHandle != null)
                hrReleaseHandle.close();
            if (ssReleaseHandle != null)
                ssReleaseHandle.close();
            combinedSensorConnected = false;
        }
    }

    /**
     * Begin advertising over Bluetooth that this device is connectable
     */
    private void startAdvertising() {
        BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
        if (bluetoothAdapter == null) {
            Log.e(TAG, "Failed to create bluetooth adapter");
            return;
        }
        mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
        if (mBluetoothLeAdvertiser == null) {
            Log.w(TAG, "Failed to create advertiser");
            return;
        }
        AdvertiseSettings settings = new AdvertiseSettings.Builder().setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY).setConnectable(true).setTimeout(0).setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH).build();
        AdvertiseData advData = new AdvertiseData.Builder().setIncludeTxPowerLevel(true).addServiceUuid(new ParcelUuid(CSCProfile.CSC_SERVICE)).addServiceUuid(new ParcelUuid(CSCProfile.HR_SERVICE)).addServiceUuid(new ParcelUuid(CSCProfile.RSC_SERVICE)).build();
        AdvertiseData advScanResponse = new AdvertiseData.Builder().setIncludeDeviceName(true).build();
        mBluetoothLeAdvertiser.startAdvertising(settings, advData, advScanResponse, mAdvertiseCallback);
    }

    /**
     * Stop Bluetooth advertisements
     */
    private void stopAdvertising() {
        if (mBluetoothLeAdvertiser == null)
            return;
        mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
    }

    /**
     * Initialize the GATT server
     */
    private void startServer() {
        mBluetoothGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
        if (mBluetoothGattServer == null) {
            Log.w(TAG, "Unable to create GATT server");
            return;
        }
        btServiceInitialized = false;
        // TODO: enable either 1 of them or both of them according to user selection
        if (mBluetoothGattServer.addService(CSCProfile.createCSCService((byte) (CSCProfile.CSC_FEATURE_WHEEL_REV | CSCProfile.CSC_FEATURE_CRANK_REV)))) {
            Log.d(TAG, "CSCP enabled!");
        } else {
            Log.d(TAG, "Failed to add csc service to bluetooth layer!");
        }
        // We cannot add another service until the callback for the previous service has completed
        while (!btServiceInitialized) ;
        btServiceInitialized = false;
        if (mBluetoothGattServer.addService(CSCProfile.createHRService())) {
            Log.d(TAG, "HR enabled!");
        } else {
            Log.d(TAG, "Failed to add hr service to bluetooth layer!");
        }
        // We cannot add another service until the callback for the previous service has completed
        while (!btServiceInitialized) ;
        if (mBluetoothGattServer.addService(CSCProfile.createRscService())) {
            Log.d(TAG, "RSC enabled!");
        } else {
            Log.d(TAG, "Failed to add rsc service to bluetooth layer");
        }
        Log.d(TAG, "Enumerating (" + mBluetoothGattServer.getServices().size() + ") BT services");
        for (BluetoothGattService b : mBluetoothGattServer.getServices()) {
            Log.d(TAG, "Services registered: " + b.getUuid().toString());
        }
        // start periodicUpdate, sending notification to subscribed device and UI
        handler.post(periodicUpdate);
    }

    /**
     * Shut down the GATT server
     */
    private void stopServer() {
        if (mBluetoothGattServer == null)
            return;
        // stop periodicUpdate
        handler.removeCallbacksAndMessages(null);
        mBluetoothGattServer.close();
    }

    /**
     * Callback to receive information about the advertisement process
     */
    private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {

        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            Log.i(TAG, "LE Advertise Started:" + settingsInEffect);
        }

        @Override
        public void onStartFailure(int errorCode) {
            Log.w(TAG, "LE Advertise Failed: " + errorCode);
        }
    };

    Handler handler = new Handler();

    private Runnable periodicUpdate = new Runnable() {

        @Override
        public void run() {
            // scheduled next run in 1 sec
            handler.postDelayed(periodicUpdate, 1000);
            // send to registered BLE devices. It's a no-op if there is no GATT client
            notifyRegisteredDevices();
            // update UI by sending broadcast to our main activity
            Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
            i.putExtra("speed", lastSpeed);
            i.putExtra("cadence", lastCadence);
            i.putExtra("hr", lastHR);
            i.putExtra("ss_distance", lastSSDistance);
            i.putExtra("ss_speed", lastSSSpeed);
            i.putExtra("ss_stride_count", lastStridePerMinute);
            i.putExtra("speed_timestamp", lastSpeedTimestamp);
            i.putExtra("cadence_timestamp", lastCadenceTimestamp);
            i.putExtra("hr_timestamp", lastHRTimestamp);
            i.putExtra("ss_distance_timestamp", lastSSDistanceTimestamp);
            i.putExtra("ss_speed_timestamp", lastSSSpeedTimestamp);
            i.putExtra("ss_stride_count_timestamp", lastSSStrideCountTimestamp);
            Log.v(TAG, "Updating UI: speed:" + lastSpeed + ", cadence:" + lastCadence + ", hr " + lastHR + ", speed_ts:" + lastSpeedTimestamp + ", cadence_ts:" + lastCadenceTimestamp + ", " + lastHRTimestamp + ", ss_distance: " + lastSSDistance + ", ss_distance_timestamp: " + lastSSDistanceTimestamp + ", ss_speed: " + lastSSSpeed + ", ss_speed_timestamp: " + lastSSSpeedTimestamp + ", ss_stride_count: " + lastStridePerMinute + ", ss_stride_count_timestamp: " + lastSSStrideCountTimestamp);
            sendBroadcast(i);
        }
    };

    /**
     * Send a CSC service notification to any devices that are subscribed
     * to the characteristic
     */
    private void notifyRegisteredDevices() {
        if (mRegisteredDevices.isEmpty()) {
            Log.v(TAG, "No subscribers registered");
            return;
        }
        byte[] data = CSCProfile.getMeasurement(replacedulativeWheelRevolution, lastWheelEventTime, replacedulativeCrankRevolution, lastCrankEventTime);
        Log.v(TAG, "Sending update to " + mRegisteredDevices.size() + " subscribers");
        for (BluetoothDevice device : mRegisteredDevices) {
            BluetoothGattService service = mBluetoothGattServer.getService(CSCProfile.CSC_SERVICE);
            if (service != null) {
                BluetoothGattCharacteristic measurementCharacteristic = mBluetoothGattServer.getService(CSCProfile.CSC_SERVICE).getCharacteristic(CSCProfile.CSC_MEASUREMENT);
                if (!measurementCharacteristic.setValue(data)) {
                    Log.w(TAG, "CSC Measurement data isn't set properly!");
                }
                // false is used to send a notification
                mBluetoothGattServer.notifyCharacteristicChanged(device, measurementCharacteristic, false);
            } else {
                Log.v(TAG, "Service " + CSCProfile.CSC_SERVICE + " was not found as an installed service");
            }
            service = mBluetoothGattServer.getService(CSCProfile.HR_SERVICE);
            if (service != null) {
                Log.v(TAG, "Processing Heart Rate");
                BluetoothGattCharacteristic measurementCharacteristic = mBluetoothGattServer.getService(CSCProfile.HR_SERVICE).getCharacteristic(CSCProfile.HR_MEASUREMENT);
                byte[] hrData = CSCProfile.getHR(lastHR, lastHRTimestamp);
                if (!measurementCharacteristic.setValue(hrData)) {
                    Log.w(TAG, "HR  Measurement data isn't set properly!");
                }
                mBluetoothGattServer.notifyCharacteristicChanged(device, measurementCharacteristic, false);
            } else {
                Log.v(TAG, "Service " + CSCProfile.HR_SERVICE + " was not found as an installed service");
            }
            service = mBluetoothGattServer.getService(CSCProfile.RSC_SERVICE);
            if (service != null) {
                Log.v(TAG, "Processing Running Speed and Cadence sensor");
                BluetoothGattCharacteristic measurementCharacteristic = mBluetoothGattServer.getService(CSCProfile.RSC_SERVICE).getCharacteristic(CSCProfile.RSC_MEASUREMENT);
                byte[] rscData = CSCProfile.getRsc(lastSSDistance, lastSSSpeed, lastStridePerMinute);
                if (!measurementCharacteristic.setValue(rscData)) {
                    Log.w(TAG, "RSC Measurement data isn't set properly!");
                }
                mBluetoothGattServer.notifyCharacteristicChanged(device, measurementCharacteristic, false);
            } else {
                Log.v(TAG, "Service " + CSCProfile.RSC_SERVICE + " was not found as an installed service");
            }
        }
    }

    private final BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onServiceAdded(int status, BluetoothGattService service) {
            Log.i(TAG, "onServiceAdded(): status:" + status + ", service:" + service);
            // Sets up for next service to be added
            btServiceInitialized = true;
        }

        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            if (mRegisteredDevices.contains(device)) {
                if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    Log.i(TAG, "BluetoothDevice DISCONNECTED: " + device.getName() + " [" + device.getAddress() + "]");
                    // Remove device from any active subscriptions
                    mRegisteredDevices.remove(device);
                } else {
                    Log.i(TAG, "onConnectionStateChange() status:" + status + "->" + newState + ", device" + device);
                }
            }
        }

        @Override
        public void onNotificationSent(BluetoothDevice device, int status) {
            Log.v(TAG, "onNotificationSent() result:" + status);
        }

        @Override
        public void onMtuChanged(BluetoothDevice device, int mtu) {
            Log.d(TAG, "onMtuChanged:" + device + " =>" + mtu);
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            if (CSCProfile.CSC_MEASUREMENT.equals(characteristic.getUuid())) {
                Log.i(TAG, "Read CSC Measurement");
                // TODO: this should never happen since this characteristic doesn't support read
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
            } else if (CSCProfile.CSC_FEATURE.equals(characteristic.getUuid())) {
                Log.i(TAG, "Read CSC Feature");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, CSCProfile.getFeature());
            } else if (CSCProfile.RSC_MEASUREMENT.equals(characteristic.getUuid())) {
                Log.i(TAG, "READ RSC Measurement");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
            } else if (CSCProfile.RSC_FEATURE.equals(characteristic.getUuid())) {
                Log.i(TAG, "READ RSC Feature");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, CSCProfile.getRscFeature());
            } else {
                // Invalid characteristic
                Log.w(TAG, "Invalid Characteristic Read: " + characteristic.getUuid());
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
            }
        }

        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            if (CSCProfile.CLIENT_CONFIG.equals(descriptor.getUuid())) {
                Log.d(TAG, "Config descriptor read");
                byte[] returnValue;
                if (mRegisteredDevices.contains(device)) {
                    returnValue = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
                } else {
                    returnValue = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
                }
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, returnValue);
            } else {
                Log.w(TAG, "Unknown descriptor read request");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
            }
        }

        @Override
        public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            if (CSCProfile.CLIENT_CONFIG.equals(descriptor.getUuid())) {
                if (Arrays.equals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, value)) {
                    Log.d(TAG, "Subscribe device to notifications: " + device);
                    mRegisteredDevices.add(device);
                } else if (Arrays.equals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, value)) {
                    Log.d(TAG, "Unsubscribe device from notifications: " + device);
                    mRegisteredDevices.remove(device);
                }
                if (responseNeeded) {
                    mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
                }
            } else {
                Log.w(TAG, "Unknown descriptor write request");
                if (responseNeeded) {
                    mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0, null);
                }
            }
        }
    };

    /**
     * Initialize searching for all supported sensors
     */
    private void initAntPlus() {
        Log.d(TAG, "requesting ANT+ access");
        startSpeedSensorSearch();
        startCadenceSensorSearch();
        startHRSensorSearch();
        startStrideSdmSensorSearch();
    }

    /**
     * Initializes the speed sensor search
     */
    protected void startSpeedSensorSearch() {
        // Release the old access if it exists
        if (bsdReleaseHandle != null)
            bsdReleaseHandle.close();
        combinedSensorConnected = false;
        // starts speed sensor search
        bsdReleaseHandle = AntPlusBikeSpeedDistancePcc.requestAccess(this, 0, 0, false, mBSDResultReceiver, mBSDDeviceStateChangeReceiver);
        // send initial state for UI
        Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
        i.putExtra("bsd_service_status", "SEARCHING");
        sendBroadcast(i);
    }

    /**
     * Initializes the cadence sensor search
     */
    protected void startCadenceSensorSearch() {
        // Release the old access if it exists
        if (bcReleaseHandle != null)
            bcReleaseHandle.close();
        // starts cadence sensor search
        bcReleaseHandle = AntPlusBikeCadencePcc.requestAccess(this, 0, 0, false, mBCResultReceiver, mBCDeviceStateChangeReceiver);
        // send initial state for UI
        Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
        i.putExtra("bc_service_status", "SEARCHING");
        sendBroadcast(i);
    }

    /**
     * Initializes the HR  sensor search
     */
    protected void startHRSensorSearch() {
        // Release the old access if it exists
        if (hrReleaseHandle != null)
            hrReleaseHandle.close();
        // starts hr sensor search
        hrReleaseHandle = AntPlusHeartRatePcc.requestAccess(this, 0, 0, mHRResultReceiver, mHRDeviceStateChangeReceiver);
        // send initial state for UI
        Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
        i.putExtra("hr_service_status", "SEARCHING");
        sendBroadcast(i);
    }

    /**
     * Initialized the Stride SDM (Stride based Speed and Distance Monitor) sensor search
     *
     * ex. Garmin Foot Pod
     */
    protected void startStrideSdmSensorSearch() {
        if (ssReleaseHandle != null)
            ssReleaseHandle.close();
        ssReleaseHandle = AntPlusStrideSdmPcc.requestAccess(this, 0, 0, mSSResultReceiver, mSSDeviceStateChangeReceiver);
        Intent i = new Intent("idv.markkuo.cscblebridge.ANTDATA");
        i.putExtra("ss_service_status", "SEARCHING");
        sendBroadcast(i);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    /**
     * Get the services for communicating with it
     */
    public clreplaced LocalBinder extends Binder {

        CSCService getService() {
            return CSCService.this;
        }
    }
}

4 Source : BluetoothPeripheralManager.java
with MIT License
from weliem

/**
 * This clreplaced represent a peripheral running on the local phone
 */
@SuppressWarnings("UnusedReturnValue")
public clreplaced BluetoothPeripheralManager {

    protected static final UUID CCC_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    private static final String CONTEXT_IS_NULL = "context is null";

    private static final String BLUETOOTH_MANAGER_IS_NULL = "BluetoothManager is null";

    private static final String SERVICE_IS_NULL = "service is null";

    private static final String CHARACTERISTIC_IS_NULL = "characteristic is null";

    private static final String DEVICE_IS_NULL = "device is null";

    private static final String CHARACTERISTIC_VALUE_IS_NULL = "characteristic value is null";

    private static final String CENTRAL_IS_NULL = "central is null";

    private static final String ADDRESS_IS_NULL = "address is null";

    @NotNull
    private final Context context;

    @NotNull
    private final Handler mainHandler = new Handler(Looper.getMainLooper());

    @NotNull
    private final BluetoothManager bluetoothManager;

    @NotNull
    private final BluetoothAdapter bluetoothAdapter;

    @NotNull
    private final BluetoothLeAdvertiser bluetoothLeAdvertiser;

    @NotNull
    private final BluetoothGattServer bluetoothGattServer;

    @NotNull
    private final BluetoothPeripheralManagerCallback callback;

    @NotNull
    protected final Queue<Runnable> commandQueue = new ConcurrentLinkedQueue<>();

    @NotNull
    private final HashMap<BluetoothGattCharacteristic, byte[]> writeLongCharacteristicTemporaryBytes = new HashMap<>();

    @NotNull
    private final HashMap<BluetoothGattDescriptor, byte[]> writeLongDescriptorTemporaryBytes = new HashMap<>();

    @NotNull
    private final Map<String, BluetoothCentral> connectedCentralsMap = new ConcurrentHashMap<>();

    @Nullable
    private BluetoothGattCharacteristic currentNotifyCharacteristic = null;

    @NotNull
    private byte[] currentNotifyValue = new byte[0];

    private volatile boolean commandQueueBusy = false;

    protected final BluetoothGattServerCallback bluetoothGattServerCallback = new BluetoothGattServerCallback() {

        @Override
        public void onConnectionStateChange(final BluetoothDevice device, final int status, final int newState) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    // Call connect() even though we are already connected
                    // It basically tells Android we will really use this connection
                    // If we don't do this, then cancelConnection won't work
                    // See https://issuetracker.google.com/issues/37127644
                    if (connectedCentralsMap.containsKey(device.getAddress())) {
                        return;
                    } else {
                        // This will lead to onConnectionStateChange be called again
                        bluetoothGattServer.connect(device, false);
                    }
                    handleDeviceConnected(device);
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    // Deal is double disconnect messages
                    if (!connectedCentralsMap.containsKey(device.getAddress()))
                        return;
                    handleDeviceDisconnected(device);
                }
            } else {
                Timber.i("Device '%s' disconnected with status %d", device.getName(), status);
                handleDeviceDisconnected(device);
            }
        }

        private void handleDeviceConnected(@NotNull final BluetoothDevice device) {
            Timber.i("Central '%s' (%s) connected", device.getName(), device.getAddress());
            final BluetoothCentral bluetoothCentral = new BluetoothCentral(device.getAddress(), device.getName());
            connectedCentralsMap.put(bluetoothCentral.getAddress(), bluetoothCentral);
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    callback.onCentralConnected(bluetoothCentral);
                }
            });
        }

        private void handleDeviceDisconnected(@NotNull final BluetoothDevice device) {
            final BluetoothCentral bluetoothCentral = getCentral(device);
            Timber.i("Central '%s' (%s) disconnected", bluetoothCentral.getName(), bluetoothCentral.getAddress());
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    callback.onCentralDisconnected(bluetoothCentral);
                }
            });
            removeCentral(device);
        }

        @Override
        public void onServiceAdded(final int status, @NotNull final BluetoothGattService service) {
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    callback.onServiceAdded(GattStatus.fromValue(status), service);
                }
            });
            completedCommand();
        }

        @Override
        public void onCharacteristicReadRequest(@NotNull final BluetoothDevice device, final int requestId, final int offset, @NotNull final BluetoothGattCharacteristic characteristic) {
            Timber.i("read request for characteristic <%s> with offset %d", characteristic.getUuid(), offset);
            final BluetoothCentral bluetoothCentral = getCentral(device);
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    // Call onCharacteristic before any responses are sent, even if it is a long read
                    if (offset == 0) {
                        callback.onCharacteristicRead(bluetoothCentral, characteristic);
                    }
                    // If data is longer than MTU - 1, cut the array. Only ATT_MTU - 1 bytes can be sent in Long Read.
                    final byte[] value = copyOf(nonnullOf(characteristic.getValue()), offset, bluetoothCentral.getCurrentMtu() - 1);
                    bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
                }
            });
        }

        @Override
        public void onCharacteristicWriteRequest(@NotNull final BluetoothDevice device, final int requestId, @NotNull final BluetoothGattCharacteristic characteristic, final boolean preparedWrite, final boolean responseNeeded, final int offset, @Nullable final byte[] value) {
            Timber.i("write characteristic %s request <%s> offset %d for <%s>", responseNeeded ? "WITH_RESPONSE" : "WITHOUT_RESPONSE", bytes2String(value), offset, characteristic.getUuid());
            final byte[] safeValue = nonnullOf(value);
            final BluetoothCentral bluetoothCentral = getCentral(device);
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    GattStatus status = GattStatus.SUCCESS;
                    if (!preparedWrite) {
                        status = callback.onCharacteristicWrite(bluetoothCentral, characteristic, safeValue);
                        if (status == GattStatus.SUCCESS) {
                            characteristic.setValue(safeValue);
                        }
                    } else {
                        if (offset == 0) {
                            writeLongCharacteristicTemporaryBytes.put(characteristic, safeValue);
                        } else {
                            final byte[] temporaryBytes = writeLongCharacteristicTemporaryBytes.get(characteristic);
                            if (temporaryBytes != null && offset == temporaryBytes.length) {
                                writeLongCharacteristicTemporaryBytes.put(characteristic, mergeArrays(temporaryBytes, safeValue));
                            } else {
                                status = GattStatus.INVALID_OFFSET;
                            }
                        }
                    }
                    if (responseNeeded) {
                        bluetoothGattServer.sendResponse(device, requestId, status.value, offset, safeValue);
                    }
                }
            });
        }

        @Override
        public void onDescriptorReadRequest(@NotNull final BluetoothDevice device, final int requestId, final int offset, @NotNull final BluetoothGattDescriptor descriptor) {
            Timber.i("read request for descriptor <%s> with offset %d", descriptor.getUuid(), offset);
            final BluetoothCentral bluetoothCentral = getCentral(device);
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    // Call onDescriptorRead before any responses are sent, even if it is a long read
                    if (offset == 0) {
                        callback.onDescriptorRead(bluetoothCentral, descriptor);
                    }
                    // If data is longer than MTU - 1, cut the array. Only ATT_MTU - 1 bytes can be sent in Long Read.
                    final byte[] value = copyOf(nonnullOf(descriptor.getValue()), offset, bluetoothCentral.getCurrentMtu() - 1);
                    bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
                }
            });
        }

        @Override
        public void onDescriptorWriteRequest(@NotNull final BluetoothDevice device, final int requestId, @NotNull final BluetoothGattDescriptor descriptor, final boolean preparedWrite, final boolean responseNeeded, final int offset, @Nullable final byte[] value) {
            final byte[] safeValue = nonnullOf(value);
            final BluetoothGattCharacteristic characteristic = Objects.requireNonNull(descriptor.getCharacteristic(), "Descriptor does not have characteristic");
            Timber.i("write descriptor %s request <%s> offset %d for <%s>", responseNeeded ? "WITH_RESPONSE" : "WITHOUT_RESPONSE", bytes2String(value), offset, descriptor.getUuid());
            final BluetoothCentral bluetoothCentral = getCentral(device);
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    GattStatus status = GattStatus.SUCCESS;
                    if (descriptor.getUuid().equals(CCC_DESCRIPTOR_UUID)) {
                        status = checkCccDescriptorValue(safeValue, characteristic);
                    } else {
                        if (!preparedWrite) {
                            // Ask callback if value is ok or not
                            status = callback.onDescriptorWrite(bluetoothCentral, descriptor, safeValue);
                        } else {
                            if (offset == 0) {
                                writeLongDescriptorTemporaryBytes.put(descriptor, safeValue);
                            } else {
                                final byte[] temporaryBytes = writeLongDescriptorTemporaryBytes.get(descriptor);
                                if (temporaryBytes != null && offset == temporaryBytes.length) {
                                    writeLongDescriptorTemporaryBytes.put(descriptor, mergeArrays(temporaryBytes, safeValue));
                                } else {
                                    status = GattStatus.INVALID_OFFSET;
                                }
                            }
                        }
                    }
                    if (status == GattStatus.SUCCESS && !preparedWrite) {
                        descriptor.setValue(safeValue);
                    }
                    if (responseNeeded) {
                        bluetoothGattServer.sendResponse(device, requestId, status.value, offset, safeValue);
                    }
                    if (status == GattStatus.SUCCESS && descriptor.getUuid().equals(CCC_DESCRIPTOR_UUID)) {
                        if (Arrays.equals(safeValue, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) || Arrays.equals(safeValue, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
                            Timber.i("notifying enabled for <%s>", characteristic.getUuid());
                            callback.onNotifyingEnabled(bluetoothCentral, characteristic);
                        } else {
                            Timber.i("notifying disabled for <%s>", characteristic.getUuid());
                            callback.onNotifyingDisabled(bluetoothCentral, characteristic);
                        }
                    }
                }
            });
        }

        // Check value to see if it is valid and if matches the characteristic properties
        private GattStatus checkCccDescriptorValue(@NotNull final byte[] safeValue, @NotNull final BluetoothGattCharacteristic characteristic) {
            GattStatus status = GattStatus.SUCCESS;
            if (safeValue.length != 2) {
                status = GattStatus.INVALID_ATTRIBUTE_VALUE_LENGTH;
            } else if (!(Arrays.equals(safeValue, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) || Arrays.equals(safeValue, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) || Arrays.equals(safeValue, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE))) {
                status = GattStatus.VALUE_NOT_ALLOWED;
            } else if (!supportsIndicate(characteristic) && Arrays.equals(safeValue, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)) {
                status = GattStatus.REQUEST_NOT_SUPPORTED;
            } else if (!supportsNotify(characteristic) && Arrays.equals(safeValue, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
                status = GattStatus.REQUEST_NOT_SUPPORTED;
            }
            return status;
        }

        @Override
        public void onExecuteWrite(@NotNull final BluetoothDevice device, final int requestId, final boolean execute) {
            final BluetoothCentral bluetoothCentral = getCentral(device);
            if (execute) {
                mainHandler.post(new Runnable() {

                    @Override
                    public void run() {
                        GattStatus status = GattStatus.SUCCESS;
                        if (!writeLongCharacteristicTemporaryBytes.isEmpty()) {
                            final BluetoothGattCharacteristic characteristic = writeLongCharacteristicTemporaryBytes.keySet().iterator().next();
                            if (characteristic != null) {
                                // Ask callback if value is ok or not
                                status = callback.onCharacteristicWrite(bluetoothCentral, characteristic, writeLongCharacteristicTemporaryBytes.get(characteristic));
                                if (status == GattStatus.SUCCESS) {
                                    characteristic.setValue(writeLongCharacteristicTemporaryBytes.get(characteristic));
                                    writeLongCharacteristicTemporaryBytes.clear();
                                }
                            }
                        } else if (!writeLongDescriptorTemporaryBytes.isEmpty()) {
                            final BluetoothGattDescriptor descriptor = writeLongDescriptorTemporaryBytes.keySet().iterator().next();
                            if (descriptor != null) {
                                // Ask callback if value is ok or not
                                status = callback.onDescriptorWrite(bluetoothCentral, descriptor, writeLongDescriptorTemporaryBytes.get(descriptor));
                                if (status == GattStatus.SUCCESS) {
                                    descriptor.setValue(writeLongDescriptorTemporaryBytes.get(descriptor));
                                    writeLongDescriptorTemporaryBytes.clear();
                                }
                            }
                        }
                        bluetoothGattServer.sendResponse(device, requestId, status.value, 0, null);
                    }
                });
            } else {
                // Long write was cancelled, clean up already received bytes
                writeLongCharacteristicTemporaryBytes.clear();
                writeLongDescriptorTemporaryBytes.clear();
                bluetoothGattServer.sendResponse(device, requestId, GattStatus.SUCCESS.value, 0, null);
            }
        }

        @Override
        public void onNotificationSent(@NotNull final BluetoothDevice device, final int status) {
            final BluetoothCentral bluetoothCentral = getCentral(device);
            @NotNull
            final BluetoothGattCharacteristic characteristic = Objects.requireNonNull(currentNotifyCharacteristic);
            @NotNull
            final byte[] value = Objects.requireNonNull(currentNotifyValue);
            currentNotifyValue = new byte[0];
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    callback.onNotificationSent(bluetoothCentral, value, characteristic, GattStatus.fromValue(status));
                }
            });
            completedCommand();
        }

        @Override
        public void onMtuChanged(@NotNull final BluetoothDevice device, final int mtu) {
            Timber.i("new MTU: %d", mtu);
            BluetoothCentral bluetoothCentral = getCentral(device);
            bluetoothCentral.setCurrentMtu(mtu);
        }

        @Override
        public void onPhyUpdate(@NotNull final BluetoothDevice device, final int txPhy, final int rxPhy, final int status) {
            super.onPhyUpdate(device, txPhy, rxPhy, status);
        }

        @Override
        public void onPhyRead(@NotNull final BluetoothDevice device, final int txPhy, final int rxPhy, final int status) {
            super.onPhyRead(device, txPhy, rxPhy, status);
        }
    };

    protected final AdvertiseCallback advertiseCallback = new AdvertiseCallback() {

        @Override
        public void onStartSuccess(@NotNull final AdvertiseSettings settingsInEffect) {
            Timber.i("advertising started");
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    callback.onAdvertisingStarted(settingsInEffect);
                }
            });
        }

        @Override
        public void onStartFailure(final int errorCode) {
            final AdvertiseError advertiseError = AdvertiseError.fromValue(errorCode);
            Timber.e("advertising failed with error '%s'", advertiseError);
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    callback.onAdvertiseFailure(advertiseError);
                }
            });
        }
    };

    protected void onAdvertisingStopped() {
        Timber.i("advertising stopped");
        mainHandler.post(new Runnable() {

            @Override
            public void run() {
                callback.onAdvertisingStopped();
            }
        });
    }

    /**
     * Create a BluetoothPeripheralManager
     *
     * @param context the application context
     * @param bluetoothManager a valid BluetoothManager
     * @param callback an instance of BluetoothPeripheralManagerCallback where the callbacks will be handled
     */
    public BluetoothPeripheralManager(@NotNull final Context context, @NotNull final BluetoothManager bluetoothManager, @NotNull final BluetoothPeripheralManagerCallback callback) {
        this.context = Objects.requireNonNull(context, CONTEXT_IS_NULL);
        this.callback = Objects.requireNonNull(callback, "Callback is null");
        this.bluetoothManager = Objects.requireNonNull(bluetoothManager, BLUETOOTH_MANAGER_IS_NULL);
        this.bluetoothAdapter = bluetoothManager.getAdapter();
        this.bluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
        this.bluetoothGattServer = bluetoothManager.openGattServer(context, bluetoothGattServerCallback);
        // Register for broadcasts on BluetoothAdapter state change
        final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        context.registerReceiver(adapterStateReceiver, filter);
    }

    /**
     * Close the BluetoothPeripheralManager
     *
     * Application should call this method as early as possible after it is done with
     * this BluetoothPeripheralManager.
     */
    public void close() {
        stopAdvertising();
        context.unregisterReceiver(adapterStateReceiver);
        bluetoothGattServer.close();
    }

    /**
     * Start Bluetooth LE Advertising. The {@code advertiseData} will be broadcasted if the
     * operation succeeds. The {@code scanResponse} is returned when a scanning device sends an
     * active scan request. This method returns immediately, the operation status is delivered
     * through {@link BluetoothPeripheralManagerCallback#onAdvertisingStarted(AdvertiseSettings)} or {@link BluetoothPeripheralManagerCallback#onAdvertiseFailure(AdvertiseError)}.
     *
     * @param settings the AdvertiseSettings
     * @param advertiseData the AdvertiseData
     * @param scanResponse the ScanResponse
     */
    public void startAdvertising(@NotNull final AdvertiseSettings settings, @NotNull final AdvertiseData advertiseData, @NotNull final AdvertiseData scanResponse) {
        if (!bluetoothAdapter.isMultipleAdvertisementSupported()) {
            Timber.e("device does not support advertising");
        } else {
            bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, advertiseCallback);
        }
    }

    /**
     * Stop advertising
     */
    public void stopAdvertising() {
        bluetoothLeAdvertiser.stopAdvertising(advertiseCallback);
        onAdvertisingStopped();
    }

    /**
     * Add a service to the peripheral
     *
     * <p>Once a service has been added to the list, the service and its
     * included characteristics will be provided by the local peripheral.
     *
     * <p>If the local peripheral has already exposed services when this function
     * is called, a service update notification will be sent to all clients.
     *
     * A callback on {@link BluetoothPeripheralManagerCallback#onServiceAdded)} will be received when this operation has completed
     *
     * @param service the service to add
     * @return true if the operation was enqueued, false otherwise
     */
    public boolean add(@NotNull final BluetoothGattService service) {
        Objects.requireNonNull(service, SERVICE_IS_NULL);
        final boolean result = commandQueue.add(new Runnable() {

            @Override
            public void run() {
                if (!bluetoothGattServer.addService(service)) {
                    Timber.e("adding service %s failed", service.getUuid());
                    completedCommand();
                }
            }
        });
        if (result) {
            nextCommand();
        } else {
            Timber.e("could not enqueue add service command");
        }
        return result;
    }

    /**
     * Remove a service
     *
     * @param service the service to remove
     * @return true if the service was removed, otherwise false
     */
    public boolean remove(@NotNull final BluetoothGattService service) {
        Objects.requireNonNull(service, SERVICE_IS_NULL);
        return bluetoothGattServer.removeService(service);
    }

    /**
     * Remove all services
     */
    public void removeAllServices() {
        bluetoothGattServer.clearServices();
    }

    /**
     * Get a list of the all advertised services of this peripheral
     *
     * @return a list of zero or more services
     */
    @NotNull
    public List<BluetoothGattService> getServices() {
        return bluetoothGattServer.getServices();
    }

    /**
     * Send a notification or indication that a local characteristic has been
     * updated
     *
     * <p>A notification or indication is sent to all remote centrals to signal
     * that the characteristic has been updated.
     *
     * @param characteristic the characteristic for which to send a notification
     * @return true if the operation was enqueued, otherwise false
     */
    public boolean notifyCharacteristicChanged(@NotNull final byte[] value, @NotNull final BluetoothGattCharacteristic characteristic) {
        Objects.requireNonNull(value, CHARACTERISTIC_VALUE_IS_NULL);
        Objects.requireNonNull(characteristic, CHARACTERISTIC_IS_NULL);
        if (doesNotSupportNotifying(characteristic))
            return false;
        boolean result = true;
        for (BluetoothDevice device : getConnectedDevices()) {
            if (!notifyCharacteristicChanged(copyOf(value), device, characteristic)) {
                result = false;
            }
        }
        return result;
    }

    private boolean notifyCharacteristicChanged(@NotNull final byte[] value, @NotNull final BluetoothDevice bluetoothDevice, @NotNull final BluetoothGattCharacteristic characteristic) {
        Objects.requireNonNull(value, CHARACTERISTIC_VALUE_IS_NULL);
        Objects.requireNonNull(bluetoothDevice, DEVICE_IS_NULL);
        Objects.requireNonNull(characteristic, CHARACTERISTIC_IS_NULL);
        Objects.requireNonNull(characteristic.getValue(), CHARACTERISTIC_VALUE_IS_NULL);
        if (doesNotSupportNotifying(characteristic))
            return false;
        final boolean confirm = supportsIndicate(characteristic);
        final boolean result = commandQueue.add(new Runnable() {

            @Override
            public void run() {
                currentNotifyValue = value;
                currentNotifyCharacteristic = characteristic;
                characteristic.setValue(value);
                if (!bluetoothGattServer.notifyCharacteristicChanged(bluetoothDevice, characteristic, confirm)) {
                    Timber.e("notifying characteristic changed failed for <%s>", characteristic.getUuid());
                    BluetoothPeripheralManager.this.completedCommand();
                }
            }
        });
        if (result) {
            nextCommand();
        } else {
            Timber.e("could not enqueue notify command");
        }
        return result;
    }

    /**
     * Cancel a connection to a Central
     *
     * @param bluetoothCentral the Central
     */
    public void cancelConnection(@NotNull final BluetoothCentral bluetoothCentral) {
        Objects.requireNonNull(bluetoothCentral, CENTRAL_IS_NULL);
        cancelConnection(bluetoothAdapter.getRemoteDevice(bluetoothCentral.getAddress()));
    }

    private void cancelConnection(@NotNull final BluetoothDevice bluetoothDevice) {
        Objects.requireNonNull(bluetoothDevice, DEVICE_IS_NULL);
        Timber.i("cancelConnection with '%s' (%s)", bluetoothDevice.getName(), bluetoothDevice.getAddress());
        bluetoothGattServer.cancelConnection(bluetoothDevice);
    }

    @NotNull
    private List<BluetoothDevice> getConnectedDevices() {
        return bluetoothManager.getConnectedDevices(BluetoothGattServer.GATT);
    }

    /**
     * Get the set of connected Centrals
     *
     * @return a set with zero or more connected Centrals
     */
    @NotNull
    public Set<BluetoothCentral> getConnectedCentrals() {
        Set<BluetoothCentral> bluetoothCentrals = new HashSet<>(connectedCentralsMap.values());
        return Collections.unmodifiableSet(bluetoothCentrals);
    }

    /**
     * The current command has been completed, move to the next command in the queue (if any)
     */
    private void completedCommand() {
        commandQueue.poll();
        commandQueueBusy = false;
        nextCommand();
    }

    /**
     * Execute the next command in the subscribe queue.
     * A queue is used because some calls have to be executed sequentially.
     */
    private void nextCommand() {
        synchronized (this) {
            // If there is still a command being executed, then bail out
            if (commandQueueBusy)
                return;
            // Check if there is something to do at all
            final Runnable bluetoothCommand = commandQueue.peek();
            if (bluetoothCommand == null)
                return;
            // Execute the next command in the queue
            commandQueueBusy = true;
            mainHandler.post(new Runnable() {

                @Override
                public void run() {
                    try {
                        bluetoothCommand.run();
                    } catch (Exception ex) {
                        Timber.e(ex, "command exception");
                        BluetoothPeripheralManager.this.completedCommand();
                    }
                }
            });
        }
    }

    @Nullable
    public BluetoothCentral getCentral(@NotNull final String address) {
        Objects.requireNonNull(address, ADDRESS_IS_NULL);
        return connectedCentralsMap.get(address);
    }

    @NotNull
    private BluetoothCentral getCentral(@NotNull final BluetoothDevice device) {
        Objects.requireNonNull(device, DEVICE_IS_NULL);
        BluetoothCentral result = connectedCentralsMap.get(device.getAddress());
        if (result == null) {
            result = new BluetoothCentral(device.getAddress(), device.getName());
        }
        return result;
    }

    private void removeCentral(@NotNull final BluetoothDevice device) {
        Objects.requireNonNull(device, DEVICE_IS_NULL);
        connectedCentralsMap.remove(device.getAddress());
    }

    private final BroadcastReceiver adapterStateReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            if (action == null)
                return;
            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
                handleAdapterState(state);
            }
        }
    };

    private void handleAdapterState(final int state) {
        switch(state) {
            case BluetoothAdapter.STATE_OFF:
                Timber.d("bluetooth turned off");
                cancelAllConnectionsWhenBluetoothOff();
                break;
            case BluetoothAdapter.STATE_TURNING_OFF:
                Timber.d("bluetooth turning off");
                break;
            case BluetoothAdapter.STATE_ON:
                Timber.d("bluetooth turned on");
                break;
            case BluetoothAdapter.STATE_TURNING_ON:
                Timber.d("bluetooth turning on");
                break;
        }
    }

    private void cancelAllConnectionsWhenBluetoothOff() {
        final Set<BluetoothCentral> bluetoothCentrals = getConnectedCentrals();
        for (BluetoothCentral bluetoothCentral : bluetoothCentrals) {
            bluetoothGattServerCallback.onConnectionStateChange(bluetoothAdapter.getRemoteDevice(bluetoothCentral.getAddress()), 0, BluetoothProfile.STATE_DISCONNECTED);
        }
        onAdvertisingStopped();
    }

    @NotNull
    private byte[] copyOf(@NotNull final byte[] source, final int offset, final int maxSize) {
        if (source.length > maxSize) {
            final int chunkSize = Math.min(source.length - offset, maxSize);
            return Arrays.copyOfRange(source, offset, offset + chunkSize);
        }
        return Arrays.copyOf(source, source.length);
    }

    /**
     * Make a safe copy of a nullable byte array
     *
     * @param source byte array to copy
     * @return non-null copy of the source byte array or an empty array if source was null
     */
    @NotNull
    byte[] copyOf(@Nullable final byte[] source) {
        return (source == null) ? new byte[0] : Arrays.copyOf(source, source.length);
    }

    /**
     * Make a byte array nonnull by either returning the original byte array if non-null or an empty bytearray
     *
     * @param source byte array to make nonnull
     * @return the source byte array or an empty array if source was null
     */
    @NotNull
    private byte[] nonnullOf(@Nullable final byte[] source) {
        return (source == null) ? new byte[0] : source;
    }

    private boolean supportsNotify(@NotNull final BluetoothGattCharacteristic characteristic) {
        return (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0;
    }

    private boolean supportsIndicate(@NotNull final BluetoothGattCharacteristic characteristic) {
        return (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0;
    }

    private boolean doesNotSupportNotifying(@NotNull final BluetoothGattCharacteristic characteristic) {
        return !(supportsIndicate(characteristic) || supportsNotify(characteristic));
    }
}