de.unifreiburg.es.iLitIt.LighterBluetoothService.java Source code

Java tutorial

Introduction

Here is the source code for de.unifreiburg.es.iLitIt.LighterBluetoothService.java

Source

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.unifreiburg.es.iLitIt;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient;
import com.google.android.gms.location.LocationClient;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Date;
import java.util.UUID;

/**
 * Service for managing connection and data communication with a GATT server hosted on a
 * given Bluetooth LE device.
 */
public class LighterBluetoothService extends Service {
    private final static String TAG = LighterBluetoothService.class.getSimpleName();
    public static final String KEY_DEVICEADDR = "device_addr";
    private static final int PROGRESS_ID = 0;
    private static final int BATLOW_ID = 1;
    private static final String KEY_BATEMTPY = "bat_emtpy";
    private static final String KEY_BATVOLTAGE = "bat_voltage";
    private BroadcastReceiver mSmartWatchAnnotationReceiver = null;
    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private String mBluetoothDeviceAddress;
    private BluetoothGatt mBluetoothGatt;
    private Handler mHandler;
    private final CigAnnotationWriter rCigAnnotationWriter = new CigAnnotationWriter(this);
    private final CigIntentBroadcaster rCigIntentBroadcaster = new CigIntentBroadcaster(this);

    public final static UUID UUID_SERVICE = UUID.fromString("595403fb-f50e-4902-a99d-b39ffa4bb134");
    public final static UUID UUID_TIME_MEASUREMENT = UUID.fromString("595403fc-f50e-4902-a99d-b39ffa4bb134");
    public final static UUID UUID_CCC = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    private ObservableLinkedList<CigaretteEvent> mEventList = null;

    private final BluetoothGattCallback rGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            Log.d(TAG, String.format("onConnectionStateChanged status=%d newState=%d", status, newState));

            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.d(TAG, "Connected to GATT server (" + status + "), starting discovery...");
                gatt.discoverServices();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.d(TAG, "Disconnected from GATT server.");
                gatt.close();

                mBluetoothGatt = null;
                mHandler.post(rStartLEScan);
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            Log.d(TAG, String.format("onServicesDiscovery status=%d", status));

            if (status != BluetoothGatt.GATT_SUCCESS)
                return;

            BluetoothGattCharacteristic c = gatt.getService(UUID_SERVICE).getCharacteristic(UUID_TIME_MEASUREMENT);

            if (c == null) {
                Log.d(TAG, "onServiceDiscovered TIME characteristics UUID not found!");
                return;
            }

            // attempting to read characteristic
            boolean r = gatt.readCharacteristic(c);
            Log.d(TAG, "attempted read " + r);
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
                int status) {
            Log.d(TAG, "onCharactersiticRead " + status);

            // subscribing
            boolean rw_ = gatt.setCharacteristicNotification(characteristic, true);
            Log.d(TAG, "attempting to set char notification: " + rw_);

            BluetoothGattDescriptor config = characteristic.getDescriptor(UUID_CCC);
            config.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            boolean rw = gatt.writeDescriptor(config);
            Log.d(TAG, "attempting to subscribe characteristic " + rw);
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic c) {
            Log.d(TAG, "characteristics changed ");

            long send_time = c.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0),
                    evnt_time = c.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 4),
                    diff = send_time - evnt_time;

            Date date = new Date(System.currentTimeMillis() - diff);
            Log.d(TAG, "got event at " + date + " " + Thread.currentThread().getName());

            mEventList.add(new CigaretteEvent(date, "lighter", null));
        }
    };

    private BroadcastReceiver mBluetoothChangeReceiver;
    private LocationClient mLocationClient;
    private Notification mNotification;
    private Notification mLowBatteryWarning;

    public void clear_mac_addr() {
        mBluetoothDeviceAddress = null;
        PreferenceManager.getDefaultSharedPreferences(LighterBluetoothService.this).edit()
                .putString(KEY_DEVICEADDR, mBluetoothDeviceAddress).apply();
        mEventList.fireEvent(null);
    }

    public String get_mac_addr() {
        if (mBluetoothDeviceAddress == null)
            return "none";

        return mBluetoothDeviceAddress;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (mBluetoothChangeReceiver == null) {
            mBluetoothChangeReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);

                    switch (state) {
                    case BluetoothAdapter.STATE_ON:
                        onStartCommand(null, 0, 0);
                        break;
                    case BluetoothAdapter.STATE_OFF:
                        break;
                    default:
                        break;
                    }
                }
            };

            IntentFilter mif = new IntentFilter();
            mif.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
            registerReceiver(mBluetoothChangeReceiver, mif);
        }

        if (mSmartWatchAnnotationReceiver == null) {
            mSmartWatchAnnotationReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (mEventList == null)
                        return;

                    String via = intent.getStringExtra("ess.imu_logger.libs.data_save.extra.annotationVia");
                    mEventList.add(new CigaretteEvent(new Date(), via == null ? "intent" : via, null));
                }
            };

            // create watch ui intent listener
            IntentFilter filter = new IntentFilter("ess.imu_logger.libs.data_save.annotate");
            registerReceiver(mSmartWatchAnnotationReceiver, filter);
        }

        // For API level 18 and above, get a reference to BluetoothAdapter through
        // BluetoothManager.
        //if (serviceIsInitialized)
        //    return START_STICKY;

        mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        if (mBluetoothManager == null) {
            Log.e(TAG, "Unable to initialize BluetoothManager.");
            return START_NOT_STICKY;
        }

        mBluetoothAdapter = mBluetoothManager.getAdapter();
        if (mBluetoothAdapter == null) {
            Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
            return START_NOT_STICKY;
        }

        // for DEBUGGING only
        // PreferenceManager.getDefaultSharedPreferences(this).edit().clear().apply();

        /** check if we are already bound to a device, if not start scanning for one */
        mBluetoothDeviceAddress = PreferenceManager.getDefaultSharedPreferences(this).getString(KEY_DEVICEADDR,
                null);
        mLastBatteryVoltage = PreferenceManager.getDefaultSharedPreferences(this).getFloat(KEY_BATVOLTAGE, 0.0f);
        mBatteryEmpty = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(KEY_BATEMTPY, false);

        super.onStartCommand(intent, flags, startId);

        /** load the stored events */
        if (mEventList == null) {
            mEventList = CigAnnotationWriter.readCigaretteList(this);

            mEventList.register(rCigAnnotationWriter);
            mEventList.register(rCigIntentBroadcaster);

        }

        /** set-up the location service, we need this to run here, since we need to
         *access the location whenever there is a chang to the cigarette model. */
        mLocationClient = new LocationClient(this, mLocationHandler, mLocationHandler);
        mEventList.register(new DelayedObserver(1000, mLocationHandler));

        /** start to scan for LE devices in the area */
        mHandler = new Handler(Looper.getMainLooper());
        mHandler.post(rStartLEScan);

        /** create a notification on a pending connection */
        PendingIntent i = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class),
                PendingIntent.FLAG_UPDATE_CURRENT);
        mNotification = (new NotificationCompat.Builder(this)).setContentText("downloading cigarette events")
                .setContentTitle("iLitIt").setSmallIcon(R.drawable.ic_cigarette_black).setProgress(0, 0, true)
                .setAutoCancel(true).setContentIntent(i).build();

        mLowBatteryWarning = (new NotificationCompat.Builder(this)).setContentTitle("iLitIt - battery low")
                .setContentText("replace battery as soons as possible").setSmallIcon(R.drawable.ic_launcher)
                .build();

        return START_STICKY;
    }

    public ObservableLinkedList<CigaretteEvent> getModel() {
        return mEventList;
    }

    public double get_bat_voltage() {
        return mLastBatteryVoltage;
    }

    public boolean is_bat_empty() {
        return mBatteryEmpty;
    }

    public class LocalBinder extends Binder {
        LighterBluetoothService getService() {
            return LighterBluetoothService.this;
        }
    }

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

    private final IBinder mBinder = new LocalBinder();

    private Runnable rStartLEScan = new Runnable() {
        @Override
        public void run() {
            ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancel(PROGRESS_ID);
            Log.i(TAG, "starting LE/Lighter Scan.");
            mBluetoothAdapter.startLeScan(mFindLighterDevice);
        }
    };

    private double mLastBatteryVoltage = 0.0;
    private boolean mBatteryEmpty = false;
    private final BluetoothAdapter.LeScanCallback mFindLighterDevice = new BluetoothAdapter.LeScanCallback() {
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
            // just make sure we don't hang
            //mHandler.removeCallbacks(rStartLEScan);
            //mHandler.postDelayed(rStartLEScan, 20*1000);

            if (device.getName() == null)
                return;

            Log.d(TAG, "found device " + device.getName() + " with rssi" + rssi);

            if (!device.getName().contains("iLitIt"))
                return; // must be something else

            if (mBluetoothDeviceAddress != null && !mBluetoothDeviceAddress.equals(device.getAddress()))
                return;

            if (mBluetoothDeviceAddress == null) {
                mBluetoothDeviceAddress = device.getAddress();
                PreferenceManager.getDefaultSharedPreferences(LighterBluetoothService.this).edit()
                        .putString(KEY_DEVICEADDR, mBluetoothDeviceAddress).apply();
            }

            try {
                int offset = 0;
                while (offset < scanRecord.length - 1) {
                    int len = (int) scanRecord[offset] & 0xff, typ = (int) scanRecord[offset + 1] & 0xff;

                    if (typ == 0xff)
                        break; // manufacturer data

                    offset += len + 1;
                }

                String s, ss[];
                s = new String(Arrays.copyOfRange(scanRecord, offset + 2, scanRecord.length), "utf-8");
                ss = s.split(" ");

                mLastBatteryVoltage = Double.parseDouble(ss[0]) / 1000.;
                mBatteryEmpty = ss.length > 1 ? ss[1].contains("empty") : false;

                PreferenceManager.getDefaultSharedPreferences(LighterBluetoothService.this).edit()
                        .putBoolean(KEY_BATEMTPY, mBatteryEmpty)
                        .putFloat(KEY_BATVOLTAGE, (float) mLastBatteryVoltage).commit();

                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        mEventList.fireEvent(null);
                    }
                });
            } catch (Exception e) {
                Log.d(TAG, "unable to parse advertisement data", e);
            }

            mHandler.post(new Runnable() { // SAMSUNG workaround
                @Override
                public void run() {
                    if (mBluetoothGatt != null)
                        Log.d(TAG, "previous connection still active?!");

                    Log.d(TAG, "stopping the scan, found connectable device " + mBluetoothDeviceAddress);
                    mBluetoothAdapter.stopLeScan(mFindLighterDevice);

                    mBluetoothGatt = mBluetoothAdapter.getRemoteDevice(mBluetoothDeviceAddress)
                            .connectGatt(LighterBluetoothService.this, true, rGattCallback);

                    ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).notify(PROGRESS_ID,
                            mNotification);

                    if (is_bat_empty())
                        ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).notify(BATLOW_ID,
                                mLowBatteryWarning);
                    else
                        ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancel(BATLOW_ID);
                }
            });
        }
    };

    private class MyLocationHandler implements GooglePlayServicesClient.ConnectionCallbacks,
            GooglePlayServicesClient.OnConnectionFailedListener, Runnable {

        @Override
        public void onConnected(Bundle bundle) {
            // we just go through the list of all cigarettes and fill the ones, where
            // a mock location is set. This way we don't need to get the location everywhere,
            // but just start the location service on a new event.
            boolean has_updated = false;

            if (!mLocationClient.isConnected())
                return; // this seems odd

            for (CigaretteEvent e : mEventList) {
                if (!e.hasValidLocation()) {
                    e.where = mLocationClient.getLastLocation();
                    mEventList.fireEvent(e); // let observer know about the update
                }
            }

            mLocationClient.disconnect();
        }

        @Override
        public void onDisconnected() {

        }

        @Override
        public void onConnectionFailed(ConnectionResult connectionResult) {
            Log.d(TAG, "connecting to LocationService failed " + connectionResult.toString());
        }

        @Override
        public void run() {
            // called when mEventList has changed
            for (CigaretteEvent e : mEventList) {
                if (!e.hasValidLocation()) {
                    Log.d(TAG, "connecting to LocationService");
                    mLocationClient.connect();
                    return;
                }
            }

            // make sure that there is no dangling connection
            mLocationClient.disconnect();
        }
    }

    private MyLocationHandler mLocationHandler = new MyLocationHandler();
}