Android 5.0 Lollipop brings BLE Improvements

June 4, 2016
Box Internal Breakdown

It’s no secret that Android has been difficult to work with when it comes to Bluetooth Low Energy. BLE support in Android has been problematic since Android 4.3 KitKat, with missing features, reliability issues, battery power drain and a lot of headaches for developers. Fortunately, Android 5.0 is better

Google and Smartphone vendors know that it’s critical for them to get BLE stable and working, in the same way that Apple has been able to create a stable and mature BLE stack. The number of accessories using Bluetooth LE is growing, and Android has been treated as second class citizen because of its issues. A perfect example is Kevo, a smart lock from Kwikset. Because Kevo is a BLE Central device, it requires the Smartphone to act as peripheral. But Android doesn’t support BLE Peripheral mode, so until now Kevo only supported iPhones.

The good news is that Android 5.0, officially released in March 2015, brings in a lot of features and fixes to issues that finally makes the BLE experience much better.

A Rough Start for BLE in Android

The story of Android’s Bluetooth support is long. Android originally used the open source BlueZ stack that is common in Linux. But Google removed the bluez stack that was originally in Android because of its GPL license. Instead it decided to develop its own BlueDroid Bluetooth stack with Broadcom.

The GPL license that came with BlueZ caused all kinds of problems. GPL licensed code forces other code using it to be released under the GPL license as well. In order for Google to keep their own license for their code and avoid GPL, it had to isolate BlueZ with another process and D-BUS. This form of communication is one of the commonly accepted exceptions that allows non-GPL code to use GPL code without issues.

[Add image showing communication between components]

But the workarounds didn’t work out so well in practice. They caused a lot of overhead and extra code that wasn’t otherwise needed, which ultimately led Google to drop it. It begain developing Bluedroid when it was offered by Broadcom.

In practice this meant that starting from Android 4.2 when BlueZ was dropped, a brand new and unproven stack was used in Android Smartphones, with all the issues that come with it.

Finally in Android 5 the BlueDroid stack is maturing and adds support for a lot of the missing features like BLE peripheral, improved scanning and lower power. Not all devices running Lollipop will be able to take advantages of these features since some of them depend on the chipset in the phone, but you can expect a lot of improvements going forward.
So now that we understand what happened, let’s see what’s new in the stack for developers and users.

Support for BLE Peripheral and Broadcaster Modes

Up to Android 4.4, Android only supported the typical BLE Central mode. In this mode the smartphone scans for devices and initiates connections with peripherals. But, it can’t act as a peripheral. Now, Android adds support for BLE peripheral mode as well, allowing it to broadcast advertising packets and connect to other central devices. It can also send Beacon or iBeacon packets.
This feature is probably one of the most important changes since it opens the door to working with a lot of devices like the Kevo Smartlock. There are a lot of other applications where it makes sense to use BLE Peripheral and not Central.

Concurrent Advertisements

Your phone may be running multiple applications that run BLE peripheral mode, each with its own advertising requirements. Instead of filling an advertisement with data from multiple apps, each app can have a separate advertisement packet, so each app has a different advertising packet data and format. This simplifies development since you don’t have to worry about what other apps may be doing and how the packets will be sent.

Peripheral Advertising Latency Balancing

Another important aspect for advertising is how often the advertising packets are sent. The faster the packets are transmitted, the faster the smartphone battery is drained. But faster advertising also means your device is found quicker, which is what users love. So, Android now supports better balancing of the urgency of advertisements and latency, allowing low power, low latency or something in between.

Improved Scanning

Scanning, and in particular background scanning, was a big problem in Android for two reasons. The first is that scanning drains a battery tremendously. BLE scanning in Android was stopping the device from going into standby mode and would quickly drain batteries. The normal reaction of users was to disable Bluetooth to save on battery life, but this caused the phone to lose connection with BLE devices. The end result was that communicating with BLE enabled devices and getting data would take much longer, frustrating users.
With Android L, the scanning process has been offloaded to lower layers which allows the smartphone to go to sleep. Users don’t have to disable Bluetooth when not using it actively.

Filtering is another critical part of the equation. Even if Android went to sleep, it would still get advertising packets from other devices that the application didn’t care about, and wake it up needlessly. The solution is to filter and avoid waking up unless these packets match a specific format or have certain data. The latest android release has a lot better filtering and takes better advantage of hardware features that help reduce wakeup when it isn’t needed, saving battery power.

Developing Bluetooth Applications for Android 5.0 and Above

Support for the new features isn’t unified because different phones have different chipsets with different features. Your code needs to specifically check for the features that it needs and make sure that the smartphone supports them. We can see that a big divide will be created by applications from Android 5.0 and earlier because of all the changes. This makes your job to support devices more complicated.

A good example for this issue is dual role support. The Nexus 5 Smartphone has a Bluetooth v4.0 chipset. Although it can support both central and peripheral mode, it can’t run them both simultaneously. So if the user is advertising using the peripheral, it couldn’t maintain a connection to any other device. This issue led Google to disable peripheral mode for the Nexus 5 since BLE Central mode is considered more important (after all you wouldn’t want to disconnect from devices just so one app can advertise). A whole discussion on the issue developed at Google Code which you can read about here. Because initial Android releases didn’t disable BLE peripheral mode for Nexus 5, there was a lot of confusion about why did it suddenly get disabled.

Smartphones like the Nexus 6 with Bluetooth v4.1 chipsets do allow both Central and Peripheral to be running simultaneously, so there’s no limitation.

Box Internal Breakdown

Scanning for devices is one of the first action you’ll do in your app. You can see some of the main changes to the scanning APIs above.

Previous versions of Android use start and stop APIs in the BluetoothAdapter, but these are deprecated starting in Android API 21. They’ll work, but they’re missing features. Let’s look at a code example:

package com.argenox.ble5example;

import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends ActionBarActivity
{
 private BluetoothAdapter mBluetoothAdapter;
 private int REQUEST_ENABLE_BT = 1;
 private Handler mHandler;
 private static final long SCAN_PERIOD = 10000;
 private BluetoothLeScanner mLEScanner;
 private ScanSettings settings;
 private List<ScanFilter> filters;
 private BluetoothGatt mGatt;

 @Override
 protected void onCreate(Bundle savedInstanceState)
 {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  mHandler = new Handler();

  /* Check to make sure BLE is supported */
  if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE))
  {
   Toast.makeText(this, "BLE Not Supported",
     Toast.LENGTH_SHORT).show();
   finish();
  }
  /* Get a Bluetooth Adapter Object */
  final BluetoothManager bluetoothManager =
    (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
  mBluetoothAdapter = bluetoothManager.getAdapter();

 }

 @Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data)
 {
  if (requestCode == REQUEST_ENABLE_BT)
  {
   if (resultCode == Activity.RESULT_CANCELED)
   {
    //Bluetooth not enabled.
    finish();
    return;
   }
  }
  super.onActivityResult(requestCode, resultCode, data);
 }

 private void scanLeDevice(final boolean enable)
 {
  if (enable)
  {
   mHandler.postDelayed(new Runnable()
   {
    @Override
    public void run()
    {
     if (Build.VERSION.SDK_INT < 21)
     {
      mBluetoothAdapter.stopLeScan(mLeScanCallback);
     }
     else
     {
      mLEScanner.stopScan(mScanCallback);

     }
    }
   }, SCAN_PERIOD);
   if (Build.VERSION.SDK_INT < 21)
   {
    mBluetoothAdapter.startLeScan(mLeScanCallback);
   }
   else
   {
    mLEScanner.startScan(filters, settings, mScanCallback);
   }
  }
  else
  {
   if (Build.VERSION.SDK_INT < 21)
   {
    mBluetoothAdapter.stopLeScan(mLeScanCallback);
   }
   else
   {
    mLEScanner.stopScan(mScanCallback);
   }
  }
 }


 private ScanCallback mScanCallback = new ScanCallback()
 {
  @Override
  public void onScanResult(int callbackType, ScanResult result)
  {
   /* Connect to device found */
   Log.i("callbackType", String.valueOf(callbackType));

   BluetoothDevice btDevice = result.getDevice();
   connectToDevice(btDevice);
  }

  @Override
  public void onBatchScanResults(List<ScanResult> results)
  {
   /* Process a batch scan results */
   for (ScanResult sr : results)
   {
    Log.i("Scan Item: ", sr.toString());
   }
  }
 };

 private BluetoothAdapter.LeScanCallback mLeScanCallback =
   new BluetoothAdapter.LeScanCallback()
   {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi,
          byte[] scanRecord)
    {
     runOnUiThread(new Runnable()
     {
      @Override
      public void run()
      {
       Log.i("onLeScan", device.toString());
       connectToDevice(device);
      }
     });
    }
   };

 public void connectToDevice(BluetoothDevice device)
 {
  if (mGatt == null) {
   mGatt = device.connectGatt(this, false, mainGattCallback);
   scanLeDevice(false);// will stop after first device detection
  }
 }

 private final BluetoothGattCallback mainGattCallback = new BluetoothGattCallback()
 {
  @Override
  public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
  {
   switch (newState)
   {
    case BluetoothProfile.STATE_CONNECTED:
     Log.i("mainGattCallback", "CONNECTED");
     gatt.discoverServices();
     break;
    case BluetoothProfile.STATE_DISCONNECTED:
     Log.e("mainGattCallback", "DISCONNECTED");
     break;
    default:
     Log.e("mainGattCallback", "STATE_OTHER");
   }

  }

  @Override
  public void onServicesDiscovered(BluetoothGatt gatt, int status)
  {
   List<BluetoothGattService> services = gatt.getServices();
   Log.i("onServicesDiscovered", services.toString());
   gatt.readCharacteristic(services.get(1).getCharacteristics().get
     (0));
  }

  @Override
  public void onCharacteristicRead(BluetoothGatt gatt,
           BluetoothGattCharacteristic
             characteristic, int status)
  {
   Log.i("onCharacteristicRead", characteristic.toString());
  }
 };
}

The example above goes through quite a few things, so let’s look at them.
onCreate is the initial function called by Android when the activity begins. It’s here that the first call to hasSystemFeature() is made to check whether BLE is supported. In reality, adding Bluetooth permissions to the manifest file is the right approach to prevent users from downloading the App if there’s no BLE Support.

Once we know BLE is supported, we need to get a Bluetooth Adapter from the Bluetooth Manager.

We’ve added the scanLeDevice function which will perform the scanning for BLE devices, but it checks to see the API version. For APIs less than 21 (versions less than 5.0 Lollipop), we tell the Bluetooth Adapter to start the LE scan. Otherwise, we use the BluetoothLeScanner object mLEScanner which is the new approach.

If you compare both approaches you’ll see that both take a ScanCallback parameter, which is how Android lets us know of the devices it found. But, BluetoothLeScanner allows us to call a variant with ScanFilter list and ScanSettings.

Scan Filter is a powerful approach that allows us to filter devices based on the following:

  • Service UUIDs which identify the Bluetooth GATT services running on the device.
  • Name of remote Bluetooth LE device.
  • Mac address of the remote device.
  • Service data which is the data associated with a service.
  • Manufacturer specific data which is the data associated with a particular manufacturer.

This is a powerful way to avoid wading through a lot of devices, and it also pushes the heavy lifting to the low levels which improves energy consumption.

When the callback is fired, we can print information about the device, and the code above connects to it. Once connected, we get a Connection State change an we can discover services, read characteristics etc.

SUBSCRIBE

Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Latest Posts