Some time ago, I have been in touch with the development of Bluetooth low power Android, which is really too pit. Use is erratic, there are always some weird problems, and Google Documentation is not very clear. Reading other people’s blogs and trying to understand the Gatt protocol is not a good way to get started.

Until I saw an English blog, written in great detail, the original author also shared some of the pits and solutions he encountered. Share with you here. Since the article was written by the original author in 2019, the content may not be very new. I’ll add a few things as well. You are also welcome to add to the discussion.

Original text: medium.com/@martijn.va…

1, the preface

Last year I learned how to develop Bluetooth Low Energy (BLE) applications on iOS. BLE development on iOS is relatively easy. Next we’ll port it to Android… It’s going to be hard to transplant

Now I can say that this is a difficult thing to do. Considerable effort is required to optimize a stable version to run on mainstream models. I read a lot of information in the community, some of it is wrong, some of it is very useful. In this series of articles, I want to summarize my findings so you don’t have to spend as much time googling the information as I did in the beginning

2. Why is BLE development so hard on Android

Mainly due to the following reasons:

2.1 The Description of BLE development in Google documentation is very simple

The documentation is missing some important information, and parts of the API are out of date. The sample application also does not fully show how to do this correctly. There are only open source projects that tell you how to do it right. Stuart Kent’s Presentation is a good introduction to BLE. Nordic’s Guide is a great choice for some more advanced tips.

2.2 Android BLE API’s are written very low

Compared to Apple’s CoreBluetooth API for iOS, which easily implements BLE scanning, connection, and management bug handling, Android’s API design is obviously a bit messier.

On Android, the well-known BLE open source libraries are:

  • SweetBlue
  • RxAndroidBle
  • Android-BLE-Library

It’s easier to develop BLE on iOS but I don’t know, I haven’t touched it. Domestic open source word recommendationFastBle.

2.3 Modification of Android BLE protocol stack by third-party mobile phone manufacturers

Some phone vendors have made magic changes to the AOSP source code to provide their own implementations of Android BLE. As a result, developers’ apps have different implementations in different mobile phones. At the same time, it is difficult to be compatible with BLE APP due to differences in hardware.

2.4 There are some known (unknown) bugs in Android system code

In Android 4, 5, and 6, there are a lot of bugs to deal with. Later versions of Android have fewer bugs, but there are still some bugs that don’t have a good solution. For example, setting up a BLE connection occasionally returns a bizarre 133 error code. It will be mentioned later…

I can’t claim to have completely solved all the problems I encountered. I just optimize them to an “acceptable level”…

Let’s discuss what I have learned…. Start the BLE scan

The minimal version of this article is Android 6.

3. Search equipment

BluetoothAdapter BluetoothLeScanner, and then call startScan to start the search. You can provide filters to filter the scan results. Scanning can be set with scanSettings. Provide scanCallback to receive scan results.

BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); BluetoothLeScanner scanner = adapter.getBluetoothLeScanner(); if (scanner ! = null) { scanner.startScan(filters, scanSettings, scanCallback); Log.d(TAG, "scan started"); } else { Log.e(TAG, "could not get scanner object"); }Copy the code

The scanner will start scanning against your filters and call back to scanCallback when it finds the device

private final ScanCallback scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device = result.getDevice(); / /... do whatever you want with this found device } @Override public void onBatchScanResults(List<ScanResult> results) { // Ignore for now } @Override public void onScanFailed(int errorCode) { // Ignore for now } };Copy the code

When you get ScanResult, you can get the BluetoothDevice object from it. You can try to connect the device. Before we connect, let’s talk about scanning.

In addition to the BluetoothDevice object, ScanResult also contains additional information about some devices

  • Advertisement data An array of byte streams related to devices. Most devices contain “name” and “service UUIDs” information. You can use these characteristics to filter information during search.
  • RSSI value (Signal strength), which can determine the distance of the device around.
  • There are a few other things you can learn more about in Google Docs.

Note: Do not perform BLE related operations in the Activity. The Activity is constantly rebuilt by the system and may be launched more than once if scanned in the Activity. To make matters worse, the BLE connection can also be broken during Activity rebuilding.

3.1 Setting Scan Filters

Before you scan, you need to set some filters. If you don’t set filters, you will scan all the devices around you. This may also be what you want, but sometimes you’ll run into a search for a specific device, perhaps a specific name or MAC address.

3.1.1 Searching for devices by the Specific Service UUID

Service UUIDs can help you find the device you want.

For example, a Blood Pressure monitor uses UUID 1810 for “Blood Pressure Service” in the BLE standard. During device manufacturing, hardware manufacturers inform users of the service UUID of the device. Developers can quickly search for devices using the Service UUID.

For example: how to search for the blood pressure monitor service?

UUID BLP_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb"); UUID[] serviceUUIDs = new UUID[]{BLP_SERVICE_UUID}; List<ScanFilter> filters = null; if(serviceUUIDs ! = null) { filters = new ArrayList<>(); for (UUID serviceUUID : serviceUUIDs) { ScanFilter filter = new ScanFilter.Builder() .setServiceUuid(new ParcelUuid(serviceUUID)) .build(); filters.add(filter); } } scanner.startScan(filters, scanSettings, scanCallback);Copy the code

3.1.2 Searching by Device Name

There are two scenarios for searching by device name: searching for a specified device or searching for a specified device Model. My Polar H7 advertises itself as “Polar H7 391BB014″, the latter part (” 391BB014 “) is a unique number and the front part is the prefix for all devices of Polar H7. This is a very common practice.

Unfortunately, searching for devices by device name only searches for specific devices and only matches the full name.

For example

String[] names = new String[]{"Polar H7 391BB014"}; List<ScanFilter> filters = null; if(names ! = null) { filters = new ArrayList<>(); for (String name : names) { ScanFilter filter = new ScanFilter.Builder() .setDeviceName(name) .build(); filters.add(filter); } } scanner.startScan(filters, scanSettings, scanCallback);Copy the code

3.1.3 Searching by Mac Address

Filtering Mac addresses has some special features. Usually, you do not know the Mac address of the device. Unless you’ve scanned the device before and saved the Mac address.

But sometimes, the Mac address of the device you buy is written on the box. Especially medical equipment.

To reconnect to known devices, you need to reconnect to a new Mac address.

Here’s an example:

String[] peripheralAddresses = new String[]{"01:0A:5C:7D:D0:1A"}; // Build filters list List<ScanFilter> filters = null; if (peripheralAddresses ! = null) { filters = new ArrayList<>(); for (String address : peripheralAddresses) { ScanFilter filter = new ScanFilter.Builder() .setDeviceAddress(address) .build(); filters.add(filter); } } scanner.startScan(filters, scanSettings, scanByServiceUUIDCallback);Copy the code

You’ve probably figured out if you use these conditional filters.

4. Define ScanSettings

ScanSettings controls the behavior of Android scanning. There are many properties that can be set. Here is an example of ScanSettings. In this setup we use “low power” but very sensitive to finding devices. In other words, it scans intermittently. But when he finds out the device is, he responds immediately.

ScanSettings scanSettings = new ScanSettings.Builder()
        .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
        .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
        .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
        .setReportDelay(0L)
        .build();
Copy the code

Let’s see what they mean:

4.1 ScanMode

This is by far the most important property, controlling the timing and timing of BLE scans.

Since BLE scanning is a heavy battery drain on your phone, you have to control it so you don’t run out of juice too quickly.

There are four main scan modes:

4.1.1 SCAN_MODE_LOW_POWER

This is the default Scanning mode on Android and consumes the least power. If the scan is no longer foreground, this mode is enforced.

In this mode, Android will scan for 0.5 seconds and pause for 4.5 seconds.

4.1.2 SCAN_MODE_BALANCED

Balance mode, balance scan frequency and power consumption.

In this mode, Android scans for 2s and pauses for 3s. It’s a compromise model.

4.1.3 SCAN_MODE_LOW_LATENCY

Continuous scanning, recommended for use in the foreground. But it uses a lot more power. The scan results will also be faster.

4.1.4 SCAN_MODE_OPPORTUNISTIC

In this mode, only callbacks to scan results from other apps are listened for. It can’t find the device you want to find.

4.2 the Callback type

This property determines the strategy for finding devices and calling back

  • CALLBACK_TYPE_ALL_MATCHES an immediate callback is performed every time it searches for a new device.
  • The CALLBACK_TYPE_FIRST_MATCH callback is performed only when the first device is searched and no callback is performed when other devices are scanned.
  • CALLBACK_TYPE_MATCH_LOST this is a bit weird. It is called when the first device is found and no more devices are searched.

In practice, only CALLBACK_TYPE_ALL_MATCHES and CALLBACK_TYPE_FIRST_MATCH will be used, depending on your scenario.

4.3 the Match mode

This property determines how to define the search for “match” to a device

  • MATCH_MODE_AGGRESSIVE matches devices even if the signal is weak.
  • MATCH_MODE_STICKY In contrast to MATCH_MODE_AGGRESSIVE, a MATCH_MODE_AGGRESSIVE node can be searched only when it receives strong signals from devices. …

4.4 Number of matches

This property controls how many devices need to be matched.

  • MATCH_NUM_ONE_ADVERTISEMENT
  • MATCH_NUM_FEW_ADVERTISEMENT
  • MATCH_NUM_FEW_ADVERTISEMENT

4.5 the Report delay

You can set the delay for calling back the scan results. If the delay is greater than zero, Android will collect the search results and notify you when the delay is reached. In this case, the onBatchScanResults method is called back instead of the onScanResult method.

5. Cache Android Bluetooth stack

BLE search doesn’t just call back some device information around you, it’s also cached at the bottom of the Bluetooth protocol stack. Caches some important information about the device, such as name, Mac address, device type (Classic Bluetooth, Low Power Bluetooth). This information is cached in a file, and when you need to connect to a device, the Bluetooth protocol stack will first read the device information in the file, and then try to connect to the device. The point is that you cannot successfully connect to a device with a single Mac address.

5.1 Clearing Cache Information

Search cached device information does not persist. There are three cases in which the cache is cleared.

  • Restart the bluetooth
  • Restart the phone
  • Manually clear the cache in Settings

This is a pain point for developers, and rebooting the phone comes up a lot. Bluetooth is also turned off when the user turns on airplane mode. Other than that. There are also differences among handset makers. I tried some Samsung phones and restarting Bluetooth didn’t clear the cache.

This means you can’t rely on Android’s cache of device information alone. You may not find the device you want in the cache. If you are reconnecting to a device using its Mac address, you can check if the device type is DEVICE_TYPE_UNKNOWN. If so, the device does not exist in the cache.

// Get device object for a mac address
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(peripheralAddress)
// Check if the peripheral is cached or not
int deviceType = device.getType();
if(deviceType == BluetoothDevice.DEVICE_TYPE_UNKNOWN) {
    // The peripheral is not cached
} else {
    // The peripheral is cached
}
Copy the code

If you don’t do a search before you connect to the device. This information needs attention

6. Continuous scanning?

In short, you can’t keep sweeping non-stop because it’s a very energy-intensive task. It’s going to consume a lot of users’ power.

If you want to keep scanning to keep finding devices. Use SCAN_MODE_LOW_POWER scan mode and limit the scan. For example, the application scans only in the foreground and does not scan when it is backward. Or use intermittent scanning.

Google has made some restrictions on persistent scanning. Here are some of the changes:

  • Starting from Android 8.1, if Scan Filter is not set, the mobile phone will stop scanning when the screen is off. That is, if you do not set Scan Filter, the phone will automatically stop scanning when the screen goes out. Resume scanning when the screen is on. This is about the submission of changes. This is clearly Google’s attempt to optimize the power consumption of a scan.
  • On Android 7, you can only scan continuously for 30 minutes, after which Android will change ScanSettings to SCAN_MODE_OPPORTUNISTIC. So the optimal scan window should be 30 minutes, and you should close the scan and restart it within 30 minutes.
  • On Android 7, repeat enabling scanning five times within 30 seconds and Android will temporarily disable scanning.

7, continuous scanning in the background

Google has put a lot of restrictions on continuous foreground scanning, but if you want to continuously scan in the background, it will be more challenging.

The main problem is that the new Android operating system limits the running time of background services. Typically, background services survive for 10 minutes before being killed by the system.

Here are some suggestions:

  • Stackoverflow discussion
  • David Young ‘s blog post

8. Check your Bluetooth status and permissions

Bluetooth scan requires the following permissions:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
Copy the code

Make sure your app has these permissions. ACCESS_COARSE_LOCATION is a dangerous permission, so you need to get it dynamically.

private boolean hasPermissions() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (getApplicationContext().checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ! = PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[] { Manifest.permission.ACCESS_COARSE_LOCATION }, ACCESS_COARSE_LOCATION_REQUEST); return false; } } return true; }Copy the code

Also make sure the Bluetooth status is turned on, if not, ask the user to turn on Bluetooth:

BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (! bluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }Copy the code