com.wolkabout.hexiwear.service.BluetoothService.java Source code

Java tutorial

Introduction

Here is the source code for com.wolkabout.hexiwear.service.BluetoothService.java

Source

/**
 *  Hexiwear application is used to pair with Hexiwear BLE devices
 *  and send sensor readings to WolkSense sensor data cloud
 *
 *  Copyright (C) 2016 WolkAbout Technology s.r.o.
 *
 *  Hexiwear is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Hexiwear is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package com.wolkabout.hexiwear.service;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import android.graphics.Color;
import android.os.Binder;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import com.wolkabout.hexiwear.R;
import com.wolkabout.hexiwear.activity.MainActivity_;
import com.wolkabout.hexiwear.activity.ReadingsActivity_;
import com.wolkabout.hexiwear.model.Characteristic;
import com.wolkabout.hexiwear.model.HexiwearDevice;
import com.wolkabout.hexiwear.model.ManufacturerInfo;
import com.wolkabout.hexiwear.model.Mode;
import com.wolkabout.hexiwear.util.ByteUtils;
import com.wolkabout.hexiwear.util.DataConverter;
import com.wolkabout.hexiwear.util.HexiwearDevices;
import com.wolkabout.wolk.Logger;
import com.wolkabout.wolk.ReadingType;
import com.wolkabout.wolk.Wolk;
import com.wolkabout.wolkrestandroid.Credentials_;

import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EService;
import org.androidannotations.annotations.Receiver;
import org.androidannotations.annotations.SystemService;
import org.androidannotations.annotations.sharedpreferences.Pref;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.TimeZone;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

@EService
public class BluetoothService extends Service {

    private static final String TAG = BluetoothService.class.getSimpleName();

    public static final String SERVICES_AVAILABLE = "servicesAvailable";
    public static final String DATA_AVAILABLE = "dataAvailable";
    public static final String READING_TYPE = "readingType";
    public static final String CONNECTION_STATE_CHANGED = "ConnectionStateChange";
    public static final String CONNECTION_STATE = "connectionState";
    public static final String STRING_DATA = "stringData";
    public static final String STOP = "stop";
    public static final String ACTION_NEEDS_BOND = "noBond";
    public static final String PREFERENCE_CHANGED = "preferenceChanged";
    public static final String PREFERENCE_NAME = "preferenceName";
    public static final String PREFERENCE_ENABLED = "preferenceEnabled";
    public static final String PUBLISH_TIME_CHANGED = "publishTimeChanged";
    public static final String SHOULD_PUBLISH_CHANGED = "shouldPublishChanged";
    public static final String MODE_CHANGED = "modeChanged";
    public static final String MODE = "mode";

    // Notification types
    private static final byte MISSED_CALLS = 2;
    private static final byte UNREAD_MESSAGES = 4;
    private static final byte UNREAD_EMAILS = 6;

    // Alert In commands
    private static final byte WRITE_NOTIFICATION = 1;
    private static final byte WRITE_TIME = 3;

    private static final Map<String, BluetoothGattCharacteristic> readableCharacteristics = new HashMap<>();
    private static final ManufacturerInfo manufacturerInfo = new ManufacturerInfo();
    private static final Queue<String> readingQueue = new ArrayBlockingQueue<>(12);
    private static final Queue<byte[]> notificationsQueue = new LinkedBlockingDeque<>();

    private volatile boolean isConnected;
    private BluetoothDevice bluetoothDevice;
    private HexiwearDevice hexiwearDevice;
    private BluetoothGattCharacteristic alertIn;
    private BluetoothGatt bluetoothGatt;
    private Wolk wolk;
    private Mode mode;

    @Bean
    HexiwearDevices hexiwearDevices;

    @SystemService
    NotificationManager notificationManager;

    @Pref
    Credentials_ credentials;

    @Receiver(actions = BluetoothDevice.ACTION_BOND_STATE_CHANGED)
    void onBondStateChanged(Intent intent) {
        final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
        final int previousBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);

        Log.d(TAG, "Bond state changed for: " + device.getAddress() + " new state: " + bondState + " previous: "
                + previousBondState);

        if (bondState == BluetoothDevice.BOND_BONDED) {
            Log.i(TAG, "Bonded");
            createGATT(device);
        } else if (bondState == BluetoothDevice.BOND_NONE) {
            device.createBond();
        }
    }

    @Receiver(actions = STOP)
    void onStopCommand() {
        Log.i(TAG, "Stop command received.");
        stopForeground(true);
        stopSelf();
    }

    @Override
    public void onDestroy() {
        Log.i(TAG, "Stopping service...");
        if (bluetoothGatt != null) {
            bluetoothGatt.close();
            NotificationService_.intent(this).stop();
        }
        if (wolk != null && hexiwearDevices.shouldTransmit(hexiwearDevice)) {
            wolk.stopAutoPublishing();
        }
    }

    public void startReading(BluetoothDevice device) {
        Log.i(TAG, "Starting to read data for device: " + device.getName());
        hexiwearDevice = hexiwearDevices.getDevice(device.getAddress());
        bluetoothDevice = device;
        createGATT(device);

        if (credentials.username().get().equals("Demo")) {
            return;
        }

        wolk = new Wolk(hexiwearDevice);
        wolk.setLogger(new Logger() {
            @Override
            public void info(final String message) {
                Log.i(TAG, message);
            }

            @Override
            public void error(final String message, final Throwable e) {
                Log.e(TAG, message, e);
            }
        });

        if (hexiwearDevices.shouldTransmit(device)) {
            final int publishInterval = hexiwearDevices.getPublishInterval(hexiwearDevice);
            wolk.startAutoPublishing(publishInterval);
        }
    }

    private void createGATT(final BluetoothDevice device) {
        bluetoothGatt = device.connectGatt(this, true, new BluetoothGattCallback() {
            @Override
            public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
                isConnected = BluetoothProfile.STATE_CONNECTED == newState;
                if (isConnected) {
                    Log.i(TAG, "GATT connected.");
                    startForeground(442, getNotification(device));
                    gatt.discoverServices();
                } else {
                    Log.i(TAG, "GATT disconnected.");
                    NotificationService_.intent(BluetoothService.this).stop();
                    notificationManager.notify(442, getNotification(device));
                    gatt.connect();
                }

                final Intent connectionStateChanged = new Intent(CONNECTION_STATE_CHANGED);
                connectionStateChanged.putExtra(CONNECTION_STATE, isConnected);
                sendBroadcast(connectionStateChanged);
            }

            @Override
            public void onServicesDiscovered(BluetoothGatt gatt, int status) {
                Log.i(TAG, "Services discovered.");
                if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
                    handleAuthenticationError(gatt);
                    return;
                }

                discoverCharacteristics(gatt);
            }

            @Override
            public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
                    int status) {
                Log.i(TAG, "Characteristic written: " + status);

                if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
                    handleAuthenticationError(gatt);
                    return;
                }

                final byte command = characteristic.getValue()[0];
                switch (command) {
                case WRITE_TIME:
                    Log.i(TAG, "Time written.");
                    final BluetoothGattCharacteristic batteryCharacteristic = readableCharacteristics
                            .get(Characteristic.BATTERY.getUuid());
                    gatt.setCharacteristicNotification(batteryCharacteristic, true);
                    for (BluetoothGattDescriptor descriptor : batteryCharacteristic.getDescriptors()) {
                        if (descriptor.getUuid().toString().startsWith("00002904")) {
                            descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
                            gatt.writeDescriptor(descriptor);
                        }
                    }
                    break;
                case WRITE_NOTIFICATION:
                    Log.i(TAG, "Notification sent.");
                    if (notificationsQueue.isEmpty()) {
                        Log.i(TAG, "Reading characteristics...");
                        readNextCharacteristics(gatt);
                    } else {
                        Log.i(TAG, "writing next notification...");
                        alertIn.setValue(notificationsQueue.poll());
                        gatt.writeCharacteristic(alertIn);
                    }
                    break;
                default:
                    Log.w(TAG, "No such ALERT IN command: " + command);
                    break;
                }
            }

            @Override
            public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
                readCharacteristic(gatt, Characteristic.MANUFACTURER);
            }

            @Override
            public void onCharacteristicRead(BluetoothGatt gatt,
                    final BluetoothGattCharacteristic gattCharacteristic, int status) {
                if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) {
                    handleAuthenticationError(gatt);
                    return;
                }

                final String characteristicUuid = gattCharacteristic.getUuid().toString();
                final Characteristic characteristic = Characteristic.byUuid(characteristicUuid);
                switch (characteristic) {
                case MANUFACTURER:
                    manufacturerInfo.manufacturer = gattCharacteristic.getStringValue(0);
                    readCharacteristic(gatt, Characteristic.FW_REVISION);
                    break;
                case FW_REVISION:
                    manufacturerInfo.firmwareRevision = gattCharacteristic.getStringValue(0);
                    readCharacteristic(gatt, Characteristic.MODE);
                    break;
                default:
                    Log.v(TAG, "Characteristic read: " + characteristic.name());
                    if (characteristic == Characteristic.MODE) {
                        final Mode newMode = Mode.bySymbol(gattCharacteristic.getValue()[0]);
                        if (mode != newMode) {
                            onModeChanged(newMode);
                        }
                    } else {
                        onBluetoothDataReceived(characteristic, gattCharacteristic.getValue());
                    }

                    if (notificationsQueue.isEmpty()) {
                        readNextCharacteristics(gatt);
                    } else {
                        alertIn.setValue(notificationsQueue.poll());
                        gatt.writeCharacteristic(alertIn);
                    }

                    break;
                }
            }

            @Override
            public void onCharacteristicChanged(BluetoothGatt gatt,
                    BluetoothGattCharacteristic gattCharacteristic) {
                final String characteristicUuid = gattCharacteristic.getUuid().toString();
                final Characteristic characteristic = Characteristic.byUuid(characteristicUuid);
                Log.d(TAG, "Characteristic changed: " + characteristic);

                if (characteristic == Characteristic.BATTERY) {
                    onBluetoothDataReceived(Characteristic.BATTERY, gattCharacteristic.getValue());
                }
            }
        });
    }

    private void onModeChanged(final Mode newMode) {
        Log.i(TAG, "Mode changed. New mode is: " + mode);
        mode = newMode;

        setReadingQueue();

        final Intent modeChanged = new Intent(MODE_CHANGED);
        modeChanged.putExtra(MODE, newMode);
        LocalBroadcastManager.getInstance(this).sendBroadcast(modeChanged);
    }

    private void setReadingQueue() {
        readingQueue.clear();
        readingQueue.add(Characteristic.MODE.name());
        final List<String> enabledPreferences = hexiwearDevices.getEnabledPreferences(bluetoothDevice.getAddress());
        for (String characteristic : enabledPreferences) {
            if (mode.hasCharacteristic(characteristic)) {
                readingQueue.add(characteristic);
            }
        }
    }

    @Receiver(actions = PREFERENCE_CHANGED, local = true)
    void preferenceChanged(@Receiver.Extra String preferenceName, @Receiver.Extra boolean preferenceEnabled) {
        if (!mode.hasCharacteristic(preferenceName)) {
            return;
        }

        if (preferenceEnabled) {
            readingQueue.add(preferenceName);
        } else {
            readingQueue.remove(preferenceName);
        }
    }

    @Receiver(actions = PUBLISH_TIME_CHANGED, local = true)
    void onPublishTimeChanged() {
        if (hexiwearDevices.shouldTransmit(hexiwearDevice)) {
            setTracking(false);
            setTracking(true);
        }
    }

    @Receiver(actions = SHOULD_PUBLISH_CHANGED, local = true)
    void onShouldPublishChanged() {
        setTracking(hexiwearDevices.shouldTransmit(hexiwearDevice));
    }

    @Receiver(actions = NotificationService.MISSED_CALLS_AMOUNT_CHANGED, local = true)
    void onMissedCallsAmountChanged(@Receiver.Extra final int value) {
        queueNotification(MISSED_CALLS, value);
    }

    @Receiver(actions = NotificationService.UNREAD_MESSAGES_AMOUNT_CHANGED, local = true)
    void onUnreadMessagesAmountChanged(@Receiver.Extra final int value) {
        queueNotification(UNREAD_MESSAGES, value);
    }

    @Receiver(actions = NotificationService.UNREAD_EMAILS_AMOUNT_CHANGED, local = true)
    void onUnreadEmailAmountChanged(@Receiver.Extra final int value) {
        queueNotification(UNREAD_EMAILS, value);
    }

    private void queueNotification(final byte type, final int amount) {
        final byte[] notification = new byte[20];
        notification[0] = WRITE_NOTIFICATION;
        notification[1] = type;
        notification[2] = ByteUtils.intToByte(amount);
        notificationsQueue.add(notification);
    }

    private void onBluetoothDataReceived(final Characteristic type, final byte[] data) {
        if (wolk != null && hexiwearDevices.shouldTransmit(hexiwearDevice) && type != Characteristic.BATTERY) {
            final ReadingType readingType = ReadingType.valueOf(type.name());
            wolk.addReading(readingType, DataConverter.formatForPublushing(type, data));
        }

        final Intent dataRead = new Intent(DATA_AVAILABLE);
        dataRead.putExtra(READING_TYPE, type.getUuid());
        dataRead.putExtra(STRING_DATA, DataConverter.parseBluetoothData(type, data));
        sendBroadcast(dataRead);
    }

    void readNextCharacteristics(final BluetoothGatt gatt) {
        final String characteristicUuid = readingQueue.poll();
        readingQueue.add(characteristicUuid);
        readCharacteristic(gatt, Characteristic.valueOf(characteristicUuid));
    }

    private void readCharacteristic(final BluetoothGatt gatt, final Characteristic characteristic) {
        if (!isConnected) {
            return;
        }

        final BluetoothGattCharacteristic gattCharacteristic = readableCharacteristics
                .get(characteristic.getUuid());
        if (gattCharacteristic != null) {
            gatt.readCharacteristic(gattCharacteristic);
        }
    }

    private void discoverCharacteristics(final BluetoothGatt gatt) {
        if (gatt.getServices().size() == 0) {
            Log.i(TAG, "No services found.");
        }

        for (BluetoothGattService gattService : gatt.getServices()) {
            storeCharacteristicsFromService(gattService);
        }

        sendBroadcast(new Intent(SERVICES_AVAILABLE));
    }

    private void storeCharacteristicsFromService(BluetoothGattService gattService) {
        for (BluetoothGattCharacteristic gattCharacteristic : gattService.getCharacteristics()) {
            final String characteristicUuid = gattCharacteristic.getUuid().toString();
            final Characteristic characteristic = Characteristic.byUuid(characteristicUuid);

            if (characteristic == Characteristic.ALERT_IN) {
                Log.d(TAG, "ALERT_IN DISCOVERED");
                alertIn = gattCharacteristic;
                setTime();
                NotificationService_.intent(BluetoothService.this).start();
            } else if (characteristic != null) {
                Log.v(TAG, characteristic.getType() + ": " + characteristic.name());
                readableCharacteristics.put(characteristicUuid, gattCharacteristic);
            } else {
                Log.v(TAG, "UNKNOWN: " + characteristicUuid);
            }
        }
    }

    public void setTime() {
        Log.d(TAG, "Setting time...");
        if (!isConnected || alertIn == null) {
            Log.w(TAG, "Time not set.");
            return;
        }

        final byte[] time = new byte[20];
        final long currentTime = System.currentTimeMillis();
        final long currentTimeWithTimeZoneOffset = (currentTime + TimeZone.getDefault().getOffset(currentTime))
                / 1000;

        final ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.order(ByteOrder.LITTLE_ENDIAN).asLongBuffer().put(currentTimeWithTimeZoneOffset);
        final byte[] utcBytes = buffer.array();

        final byte length = 0x04;

        time[0] = WRITE_TIME;
        time[1] = length;
        time[2] = utcBytes[0];
        time[3] = utcBytes[1];
        time[4] = utcBytes[2];
        time[5] = utcBytes[3];

        alertIn.setValue(time);
        alertIn.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
        bluetoothGatt.writeCharacteristic(alertIn);
    }

    public void setTracking(final boolean enabled) {
        if (enabled) {
            final int publishInterval = hexiwearDevices.getPublishInterval(hexiwearDevice);
            wolk.startAutoPublishing(publishInterval);
        } else {
            wolk.stopAutoPublishing();
        }
    }

    public boolean isConnected() {
        return isConnected;
    }

    public Mode getCurrentMode() {
        return mode;
    }

    public BluetoothDevice getCurrentDevice() {
        return bluetoothDevice;
    }

    private void handleAuthenticationError(final BluetoothGatt gatt) {
        gatt.close();
        sendBroadcast(new Intent(BluetoothService.ACTION_NEEDS_BOND));
        gatt.getDevice().createBond();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return new ServiceBinder(this);
    }

    public ManufacturerInfo getManufacturerInfo() {
        return manufacturerInfo;
    }

    protected Notification getNotification(final BluetoothDevice device) {
        final boolean connectionEstablished = isConnected && bluetoothGatt != null
                && bluetoothGatt.getDevice() != null;
        final String text = connectionEstablished ? "Device is connected to " + hexiwearDevice.getWolkName()
                : "Device is not connected.";
        final int icon = connectionEstablished ? R.drawable.ic_bluetooth_connected_white_48dp
                : R.drawable.ic_bluetooth_searching_white_48dp;

        final Intent readingsActivityIntent = ReadingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
                .device(device).get();
        final Intent mainActivityIntent = MainActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_SINGLE_TOP).get();
        final Intent launchActivity = connectionEstablished ? readingsActivityIntent : mainActivityIntent;
        final PendingIntent pendingIntentMain = PendingIntent.getActivity(this, 71, launchActivity,
                PendingIntent.FLAG_UPDATE_CURRENT);

        final Intent stopIntent = new Intent(STOP);
        final PendingIntent pendingStopIntent = PendingIntent.getBroadcast(this, 0, stopIntent, 0);

        return new NotificationCompat.Builder(this).setSmallIcon(icon)
                .setContentTitle(getResources().getString(R.string.app_name)).setContentIntent(pendingIntentMain)
                .setLights(Color.GREEN, 100, 5000)
                .addAction(R.drawable.ic_cloud_off_black_24dp, "Stop", pendingStopIntent).setContentText(text)
                .build();
    }

    @Override
    public void sendBroadcast(Intent intent) {
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
    }

    public class ServiceBinder extends Binder {

        private BluetoothService service;

        public ServiceBinder(BluetoothService service) {
            this.service = service;
        }

        public BluetoothService getService() {
            return service;
        }

    }
}