/ coding

Android BLE Foreground

How to create a BLE foreground service in Android

Quick summary of steps:

1 - Create a bluetooth object class (BLEForegroundDevice.java)

2 - Create a bluetooth manager class (BLEForegroundManager.java)

3 - Create a service that has a binder

4 - Pass the bluetooth object to the service as an intent extra using the onStartCommand method

5 - Make calls to the bluetooth from the activity using a service connection

1 - Create a bluetooth object class (BLEForegroundDevice.java)

First, create a new file called BLEForegroundDevice.java, and replace with the following code:

package com.baczuk.bleforeground;

import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import java.util.ArrayList;

/**
 * Created by jbaczuk on 4/28/16.
 */
public class BLEForegroundDevice {

    ///////////////////////////////////////////////////////////////////////////
    // BLE Device fields
    ///////////////////////////////////////////////////////////////////////////

    Intent serviceIntent;
    public BluetoothDevice device = null;

    ///////////////////////////////////////////////////////////////////////////
    // Constructors
    ///////////////////////////////////////////////////////////////////////////

    public BLEForegroundDevice(BluetoothDevice device) {
        this.device = device;
    }

    ///////////////////////////////////////////////////////////////////////////
    // Functions
    ///////////////////////////////////////////////////////////////////////////

    // This would be a good place for functions that are specific to one connected
    // device (eg. read and write commands, notifications, etc.)
}

This class would be used to create instances of bluetooth devices and have methods specific to writing/reading data from that device.

2 - Create a bluetooth manager class (BLEForegroundManager.java)

Next, create a new file called BLEForegroundManager.java, and replace with the following code:

private static int SCAN_TIME = 0;
    private Context context = null;
    public static BluetoothAdapter bleAdapter = null;
    private Handler handler = null;
    private boolean isScanning = false;
    private BLEForegroundManager.BLEForegroundManageListener listener = null;
    public static final String deviceName = "_deviceName";
    private BluetoothAdapter.LeScanCallback bleScanCallback = new BluetoothAdapter.LeScanCallback() {
        public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
            BLEForegroundManager.this.handler.post(new Runnable() {
                public void run() {
                    BLEForegroundManager.this.listener.BLEForegroundManagerListener(device, rssi, scanRecord);
                }
            });
        }
    };

    public BLEForegroundManager(Context context) {
        this.handler = new Handler();
        if (!context.getPackageManager().hasSystemFeature("android.hardware.bluetooth_le")) {
//                    Toast.makeText(context, 2131099652, 0).show();
            ((Activity) context).finish();
        } else {
            this.context = context;
            BluetoothManager manager = (BluetoothManager) this.context.getSystemService(Context.BLUETOOTH_SERVICE);
            bleAdapter = manager.getAdapter();
            if (bleAdapter == null) {
//                        Toast.makeText(context, 2131099653, 0).show();
                ((Activity) context).finish();
            }
        }
    }

    public void setScanTime(int scanTime) {
        SCAN_TIME = scanTime;
    }

    public boolean getScanningState() {
        return this.isScanning;
    }

    public void setBLEForegroundManagerListener(BLEForegroundManager.BLEForegroundManageListener listener) {
        this.listener = listener;
    }

    public void startScanBluetoothDevice() {
        // TODO: Implement a way to stop and start scan (to save mobile device battery)
        if(SCAN_TIME != 0) {
            this.handler.postDelayed(new Runnable() {
                public void run() {
                    BLEForegroundManager.this.stopScanBluetoothDevice();
                }
            }, (long) SCAN_TIME);
        }
        this.isScanning = true;
        bleAdapter.startLeScan(this.bleScanCallback);
        this.listener.BLEForegroundManagerStartScan();
    }

    public void stopScanBluetoothDevice() {
        if (this.isScanning) {
            this.isScanning = false;
            bleAdapter.stopLeScan(this.bleScanCallback);
            this.listener.BLEForegroundManagerStopScan();
        }

    }

    public boolean isEnabled(Activity activity) {
        if (!bleAdapter.isEnabled()) {
            if (!bleAdapter.isEnabled()) {
                Intent enableBtIntent = new Intent("android.bluetooth.adapter.action.REQUEST_ENABLE");
                activity.startActivityForResult(enableBtIntent, 1);
            }

            return true;
        } else {
            return false;
        }
    }

    public interface BLEForegroundManageListener {
        void BLEForegroundManagerListener(BluetoothDevice var1, int var2, byte[] var3);

        void BLEForegroundManagerStartScan();

        void BLEForegroundManagerStopScan();
    }

This class manages connections within the app. You would need to initiate the scan sequence in your MainActivity. Also, you'd need to customize how it finds and connects to your device.

3 - Create a service that has a binder

Next, create a new file called BLEForegroundService.java, and replace with the following code:

///////////////////////////////////////////////////////////////////////////
    // Variable Definitions
    ///////////////////////////////////////////////////////////////////////////
    public static final String ACTION_GATT_CONNECTED = "com.baczuk.bleforeground.service.ACTION_GATT_CONNECTED";
    public static final String ACTION_GATT_CONNECTING = "com.baczuk.bleforeground.service.ACTION_GATT_CONNECTING";
    public static final String ACTION_GATT_DISCONNECTED = "com.baczuk.bleforeground.service.ACTION_GATT_DISCONNECTED";
    public static final String ACTION_GATT_SERVICES_DISCOVERED = "com.baczuk.bleforeground.service.ACTION_GATT_SERVICES_DISCOVERED";
    public static final String ACTION_DATA_AVAILABLE = "com.baczuk.bleforeground.service.ACTION_DATA_AVAILABLE";
    public static final String RSSI = "com.baczuk.bleforeground.service.RSSI";

    public static final String EXTRA_DATA = "com.baczuk.bleforeground.service.EXTRA_DATA";
    public static final String CHARACTERISTIC_ID = "com.baczuk.bleforeground.service.characteristic";

    public static String MAIN_ACTION = "com.baczuk.bleforeground.action.main";
    public static String STARTFOREGROUND_ACTION = "com.baczuk.bleforeground.action.startforeground";
    public static String STOPFOREGROUND_ACTION = "com.baczuk.bleforeground.action.stopforeground";
    public static int FOREGROUND_SERVICE = 101;

    protected boolean isConnected = false;
    protected boolean receiverRegistered = false;

    private static ArrayList<BluetoothGatt> arrayGatt = new ArrayList();

    public BLEForegroundDevice ble_device = null;

    private final IBinder kBinder = new BLEForegroundService.LocalBinder();

    ///////////////////////////////////////////////////////////////////////////
    // Constructors
    ///////////////////////////////////////////////////////////////////////////

    public BLEForegroundService() {
        // Needs to have zero parameters
    }

    ///////////////////////////////////////////////////////////////////////////
    // UI Methods
    ///////////////////////////////////////////////////////////////////////////

    ///////////////////////////////////////////////////////////////////////////
    // Bluetooth Device methods
    ///////////////////////////////////////////////////////////////////////////

    private final BluetoothGattCallback bleGattCallback = new BluetoothGattCallback() {
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String action = null;
            if(newState == 2) {
                action = ACTION_GATT_CONNECTED;
                gatt.discoverServices();
            } else if(newState == 0) {
                action = ACTION_GATT_DISCONNECTED;
            }

            if(action != null && !action.equals("")) {
                BLEForegroundService.this.broadcastUpdate(action);
            }

        }

        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            Log.w("_bleforeground", "onServicesDiscovered received: " + status);
            if(status == 0) {
                BLEForegroundService.this.broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
            } else {
                Log.w("_bleforeground", "onServicesDiscovered received: " + status);
            }

        }

        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            if(status == 0) {
                BLEForegroundService.this.broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
            } else {
                Log.d("_bleforeground", "onCharacteristicRead received: " + status);
            }

        }

        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            BLEForegroundService.this.broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
        }

        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
            if(gatt.connect()) {
                BLEForegroundService.this.broadcastUpdate(RSSI);
            }

        }
    };

    public void initBLEForegroundDevice() {
        // Initialize device here
    }

    public boolean initBluetoothDevice() {
        BluetoothGatt gatt = this.getBluetoothGatt();
        if(gatt != null) {
            if(gatt.connect()) {
                return true;
            } else {
                return false;
            }
        } else {
            gatt = ble_device.device.connectGatt(this, false, this.bleGattCallback);
            arrayGatt.add(gatt);
            return true;
        }
    }

    public void readValue(BluetoothGattCharacteristic characteristic) {
        BluetoothGatt gatt = this.getBluetoothGatt();
        if(gatt == null) {
        } else {
            gatt.readCharacteristic(characteristic);
        }
    }
    // TODO: What does this even do?
    public void writeValue(BluetoothGattCharacteristic characteristic) {
        BluetoothGatt gatt = this.getBluetoothGatt();
        if(gatt == null) {
        } else {
            gatt.writeCharacteristic(characteristic);
        }
    }

    public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enable) {
        BluetoothGatt gatt = this.getBluetoothGatt();
        if(gatt == null) {
        } else {
            UUID localUUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB");
            BluetoothGattDescriptor localBluetoothGattDescriptor = characteristic.getDescriptor(localUUID);
            byte[] arrayOfByte;
            if(enable) {
                arrayOfByte = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
                localBluetoothGattDescriptor.setValue(arrayOfByte);
            } else {
                arrayOfByte = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
                localBluetoothGattDescriptor.setValue(arrayOfByte);
            }

            gatt.setCharacteristicNotification(characteristic, enable);
            gatt.writeDescriptor(localBluetoothGattDescriptor);
        }
    }

    public List<BluetoothGattService> getSupportedGattServices() {
        BluetoothGatt gatt = this.getBluetoothGatt();
        return gatt == null?null:gatt.getServices();
    }

    private BluetoothGatt getBluetoothGatt() {
        BluetoothGatt gatt = null;
        Iterator var4 = arrayGatt.iterator();

        while(var4.hasNext()) {
            BluetoothGatt tmpGatt = (BluetoothGatt)var4.next();
            if(tmpGatt.getDevice().getAddress().equals(ble_device.device.getAddress())) {
                gatt = tmpGatt;
            }
        }

        return gatt;
    }

    protected void discoverCharacteristicsFromService() {
        Log.w("_bleforeground", "discover services size : " + getSupportedGattServices().size());
        Iterator var3 = getSupportedGattServices().iterator();

        while (var3.hasNext()) {
            BluetoothGattService service = (BluetoothGattService) var3.next();
            Iterator var5 = service.getCharacteristics().iterator();

            while (var5.hasNext()) {
                BluetoothGattCharacteristic characteristic = (BluetoothGattCharacteristic) var5.next();
                String tmpUUID = characteristic.getUuid().toString();
                // Here assign UUIDs to device
            }
        }
    }

    public boolean connect() {
        boolean didConnect = false;
        didConnect = this.initBluetoothDevice();
        return didConnect;
    }

    public void disconnect() {
        this.getBluetoothGatt().disconnect();
    }

    ///////////////////////////////////////////////////////////////////////////
    // Binder methods and classes
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Return the communication channel to the service.  May return null if
     * clients can not bind to the service.  The returned
     * {@link IBinder} is usually for a complex interface
     * that has been <a href="{@docRoot}guide/components/aidl.html">described using
     * aidl</a>.
     * <p/>
     * <p><em>Note that unlike other application components, calls on to the
     * IBinder interface returned here may not happen on the main thread
     * of the process</em>.  More information about the main thread can be found in
     * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html">Processes and
     * Threads</a>.</p>
     *
     * @param intent The Intent that was used to bind to this service,
     *               as given to {@link Context#bindService
     *               Context.bindService}.  Note that any extras that were included with
     *               the Intent at that point will <em>not</em> be seen here.
     * @return Return an IBinder through which clients can call on to the
     * service.
     */
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return this.kBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
    }

    @Override
    public void onRebind(Intent intent) {
        super.onRebind(intent);
    }


    public class LocalBinder extends Binder {
        public LocalBinder() {
        }

        public BLEForegroundService getService() {
            return BLEForegroundService.this;
        }
    }

    ///////////////////////////////////////////////////////////////////////////
    // Broadcasting methods
    ///////////////////////////////////////////////////////////////////////////

    private void broadcastUpdate(String action) {
        Intent intent = new Intent(action);
        this.sendBroadcast(intent);
    }

    private void broadcastUpdate(String action, BluetoothGattCharacteristic characteristic) {
        Intent intent = new Intent(action);
        byte[] data = characteristic.getValue();
        if(data != null && data.length > 0) {
            intent.putExtra("com.baczuk.bleforeground.service.EXTRA_DATA", characteristic.getValue());
            intent.putExtra("com.baczuk.bleforeground.service.characteristic", characteristic.getUuid().toString());
        }

        this.sendBroadcast(intent);
    }

    private BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if(BLEForegroundService.this.ble_device == null) {
                Log.d("BP: ", "sorry still isn't working...");
            }
            else {
                Log.d("onReceive: ", "something received");
            }
        }
    };

    protected IntentFilter bleIntentFilter() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(ACTION_GATT_CONNECTED);
        intentFilter.addAction(ACTION_GATT_DISCONNECTED);
        intentFilter.addAction(ACTION_GATT_SERVICES_DISCOVERED);
        intentFilter.addAction(ACTION_DATA_AVAILABLE);
        intentFilter.addAction(RSSI);
        return intentFilter;
    }

    ///////////////////////////////////////////////////////////////////////////
    // Custom Notification created when service starts
    ///////////////////////////////////////////////////////////////////////////

    ///////////////////////////////////////////////////////////////////////////
    // NOTE: This method is called each time you call startService, however, only onCreate
    // is called the first time the service actually starts.  This method can be used
    // for multiple actions.
    ///////////////////////////////////////////////////////////////////////////

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        // Create the Foreground notification
        if (intent.getAction().equals(STARTFOREGROUND_ACTION)) {
            // Get BluetoothDevice object
            if(this.ble_device == null) {
                // Initialize the device object
                BluetoothDevice bleDevice = intent.getParcelableExtra(BLEForegroundManager.deviceName);
                this.ble_device = new BLEForegroundDevice(bleDevice);
            }
            // Connect to the bluetooth device first thing:
            if(!this.isConnected) {
                this.connect();
            }

            // Start up the broadcast receiver here
            if(!this.receiverRegistered) {
                registerReceiver(this.gattUpdateReceiver, this.bleIntentFilter());
                this.receiverRegistered = true;
            }

            Intent notificationIntent = new Intent(BLEForegroundService.this, MainActivity.class);
            notificationIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
                    notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);

            Bitmap icon = BitmapFactory.decodeResource(getResources(),
                    R.drawable.profile_short);

            // TODO: Add close button
            Notification notification = new NotificationCompat.Builder(this)
                    .setContentTitle("BLE Foreground")
                    .setTicker("BLE Foreground")
                    .setContentText("BLE Foreground")
                    // TODO; This isn't appearing correctly the first time
                    .setSmallIcon(R.drawable.profile_short)
                    //.setLargeIcon(
                    //        Bitmap.createScaledBitmap(icon, 128, 128, false))
                    .setContentIntent(pendingIntent)
                    // Note: this doesn't seem to do anything (setting it to false doesn't change it either)
                    .setOngoing(true).build();

            startForeground(FOREGROUND_SERVICE, notification);
        } else if (intent.getAction().equals(STOPFOREGROUND_ACTION)) {
            if(this.receiverRegistered) {
                unregisterReceiver(this.gattUpdateReceiver);
                this.receiverRegistered = false;
            }
            if(this.isConnected) {
                // FIXME: Should this power off the device or just disconnect?
                this.disconnect();
            }
            stopForeground(true);
            stopSelf();
        }
        return START_STICKY;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

This service allows the bluetooth connection to run in the foreground and be associated with a push notification so it runs even when the app is minimized. The service can be bound to in an activity in order to manipulate that instance of the service.

4 - Pass the bluetooth object to the service as an intent extra using the onStartCommand method

This is how the service keeps track of which bluetooth object it is associated with.

5 - Make calls to the bluetooth from the activity using a service connection

Inside your MainActivity.java class, insert the following:

BLEForegroundService mBoundService;
    boolean mServiceBound = false;

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

    @Override
    protected void onResume() {
        super.onResume();

        // Bind to Device Service
        Intent serviceIntent = new Intent(this, BLEForegroundService.class);
        bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
        mServiceBound = true;
    }

    ///////////////////////////////////////////////////////////////////////////
    // Binder Methods
    ///////////////////////////////////////////////////////////////////////////
    private ServiceConnection mServiceConnection = new ServiceConnection() {

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mServiceBound = false;
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            BLEForegroundService.LocalBinder localBinder = (BLEForegroundService.LocalBinder) service;
            mBoundService = localBinder.getService();
            mServiceBound = true;
        }
    };

This is how you access the instance of the service that is running.

Note you need to add bluetooth permissions to your android manifest file:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>