Library / SDK
nordicsemi/Android-Scanner-Compat-Library avatar
nordicsemi/Android-Scanner-Compat-Library

Android-Scanner-Compat-Library: one BLE scanning API for every Android version

A compat library for Bluetooth Low Energy scanning on Android.

796 stars160 forksJavaBSD-3-Clause

At a glance

What is it?
A Nordic Semiconductor library that emulates the modern Android Bluetooth LE scanning API on older phones, including offloaded filtering, batching and PendingIntent background scanning, at the cost of a background service that uses more battery.
Who is it for?
Adopt the Scanner Compat library if your BLE scanning code must run on pre-Oreo hardware or on devices where offloaded filtering and batching are broken, since it emulates those features rather than dropping them. Do not adopt it for a new app that only targets recent Android versions, where the native BluetoothLeScanner already does everything the compat layer does.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 21 days ago.
What is it written in?
Mainly Java, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Android changed the scanning API four times

The problem this library solves is stated plainly in the documentation. The scanner API, initially created in Android 4.3, changed in Android 5.0, and was extended in 6.0 and 8.0.

That history produces the specific pain. A Bluetooth LE scanning feature in an Android app written today uses features that did not exist on the phone the same app might run on two years earlier. Offloaded filtering moves filter matching into the Bluetooth chipset instead of the CPU, batching accumulates results in hardware so the app is woken less often, and PendingIntent-based scanning lets the platform deliver results without your process running.

The library's answer is to let you use the modern API everywhere. It allows the modern API to be used even on older phones, emulating the features that are not supported. If a feature such as offloaded filtering or batching is not available natively, the compat library emulates it. Native filtering, batching and the first match or match lost reporting can also be disabled if you find them not working on some devices, which is the escape hatch for hardware-specific breakage.

There is one hard boundary. The Advertising Extension, through `ScanSetting#setLegacy(boolean)` or `setPhy(int)`, is available only on Android Oreo or newer, and such calls are ignored on older platforms, where only legacy advertising packets on PHY LE 1M will be reported, due to the Bluetooth chipset capabilities. That is a chipset limit rather than a software one, and no compatibility layer can work around it.

Background scanning is where the emulation costs you

The background scanning section is the part of the documentation that tells you what this library actually costs, and it is worth reading before you adopt it.

`SCAN_MODE_LOW_POWER` or `SCAN_MODE_OPPORTUNISTIC` should be used when scanning in the background, and newer Android versions will enforce low power mode in the background even if another mode has been set.

On pre-Oreo devices the library emulates scanning with PendingIntent by starting a background service that scans with the requested scan mode. The documentation is candid that this is much less battery friendly than the original method, while adding that it works and saves a lot of development time if the feature has to be implemented anyway.

That is the whole trade in one sentence. On old hardware you get the API you wanted at the price of a service that stays alive and keeps the radio busy in a way the platform's own mechanism would have avoided.

A second behaviour in the same section catches people out. For unfiltered scans, scanning is stopped on screen off to save power, and resumed when the screen is turned on again. To avoid this, use scanning with a desired ScanFilter. That is a subtle difference with a large effect on how your app behaves in a pocket, and it is worth knowing before you file a bug about your scanner mysteriously dying.

Getting it into a build, and the version matrix that decides your version

The library is on Maven Central, and adding it is one dependency line:

gradle
implementation 'no.nordicsemi.android.support.v18:scanner:1.6.0'

The version you pick is not a free choice, and the documentation spells out the rules. Projects not targeting API 31, which is Android 12, or newer, should use version 1.5.1. Projects not migrated to Android Jetpack should use version 1.3.1, which is feature-equal to 1.4.0.

There is a repository detail too. As JCenter has shut down, starting from version 1.4.4 the library is available only on Maven Central, so the build needs `mavenCentral()` in the main build.gradle file:

gradle
buildscript {
    repositories {
        mavenCentral()
    }
}
allprojects {
    repositories {
        mavenCentral()
    }
}

And since version 1.5 you need to enable desugaring of Java 8 language features if you have not already done so, and if you are releasing an Android library then anyone who uses that library will also have to enable desugaring. The documentation expects nearly all Android projects to have desugaring enabled already, but points to version 1.4.5 as the fallback if it causes problems.

The repository layout is a small Gradle project with a `scanner/` module directory, a `moustache/` directory, a `fastlane/` directory for release automation, and the standard Gradle wrapper files.

The API looks like Oreo, only the imports move

The migration is deliberately small, and that is the design goal. The Scanner Compat API is very similar to the original one known from Android Oreo.

Instead of getting the scanner from the `BluetoothAdapter`, you acquire the instance with:

java
BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();

And you change the packets for the `ScanSettings`, `ScanFilter` and `ScanCallback` classes to the `no.nordicsemi.android.support.v18.scanner` package.

That is the whole port. Method names, builder patterns and semantics stay as they are, so an existing Oreo-targeted implementation moves over with an import change and a different scanner accessor.

A complete scan setup looks like this:

java
BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
ScanSettings settings = new ScanSettings.Builder()
				.setLegacy(false)
				.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
				.setReportDelay(5000)
				.setUseHardwareBatchingIfSupported(true)
				.build();
List<ScanFilter> filters = new ArrayList<>();
filters.add(new ScanFilter.Builder().setServiceUuid(mUuid).build());
scanner.startScan(filters, settings, scanCallback);

And stopping is the mirror image:

java
BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
scanner.stopScan(scanCallback);

Note that `setUseHardwareBatchingIfSupported(true)` is the compat library's way of asking for batching where it exists, and the compat layer handles the case where it does not.

Permissions differ by target version, and the newest rule bites

The permissions section is the longest part of the documentation, because the rules genuinely differ by target SDK and getting them wrong means no scan results at all rather than an error.

An app must have `ACCESS_COARSE_LOCATION` in order to get results, and an app targeting Android Q or later must have `ACCESS_FINE_LOCATION` in order to get results. For apps targeting `Build.VERSION_CODES#R` or lower, that requires the `Manifest.permission#BLUETOOTH_ADMIN` permission, gained with a simple `<uses-permission>` manifest tag. For apps targeting `Build.VERSION_CODES#S` or higher, it requires the `Manifest.permission#BLUETOOTH_SCAN` permission, gained with `Activity.requestPermissions(String[], int)`.

The Android 12 rule has an extra condition. `BLUETOOTH_SCAN` additionally requires either `ACCESS_FINE_LOCATION` or a strong assertion that you will never derive the physical location of the device. You can make that assertion by declaring `usesPermissionFlags="neverForLocation"` on the relevant `<uses-permission>` tag, but the documentation warns that it may restrict the types of Bluetooth devices you can interact with.

That warning is the part to take seriously. The neverForLocation assertion is a promise about your app's behaviour, and the platform enforces it by filtering what your scanner can see, so a scan for a particular kind of beacon may simply stop returning it on Android 12 and later.

Where this library stops being the right answer

Three cases are clear.

The first is an app that only targets recent Android. If your minimum SDK is at or above Oreo and your target is at or above API 31, the native `BluetoothLeScanner` already offers offloaded filtering, batching, report delay and PendingIntent scanning, and the compat layer is an extra dependency that emulates features your hardware already has.

The second is a scan that has to be extremely power efficient on old hardware. The background service approach is documented as much less battery friendly, and the difference between the platform mechanism and an emulated one is the difference between the system scheduling the radio and your service holding it. For a continuously scanning beacon app, that is not a small difference.

The third is a device where the modern advertising extensions matter. If you need `setPhy(int)` or non-legacy advertising, the compat library ignores those calls below Oreo and only legacy advertising on PHY LE 1M is reported. The library is honest about this because the Bluetooth chipset is the limit, but it does mean the library cannot deliver a feature on the devices where it would be most useful.

The alternative to name is the native API itself, with the version checks and fallback code written by hand. The difference in approach is that the compat library moves that branching into a maintained library with one API surface, while the native route leaves you maintaining a matrix of platform conditions and, for older devices, a background service of your own. The native route is the right answer if you control the minimum Android version and can drop the features you do not need on old hardware; the compat library is the right answer if you need those features everywhere.

Editorial conclusion

Adopt the Scanner Compat library if your BLE scanning code must run on pre-Oreo hardware or on devices where offloaded filtering and batching are broken, since it emulates those features rather than dropping them. Do not adopt it for a new app that only targets recent Android versions, where the native BluetoothLeScanner already does everything the compat layer does. Verify first which version you need, since the documentation points projects not targeting API 31 to 1.5.1 and projects not migrated to Jetpack to 1.3.1, and check whether your build already enables Java 8 desugaring, which version 1.5 and later require.

Frequently asked questions

What does the Android Scanner Compat library do?

It lets you use the modern Bluetooth LE scanning API on older Android phones, emulating features such as offloaded filtering, batching and first match or match lost reporting when they are not available natively, and emulating PendingIntent scanning on pre-Oreo devices with a background service.

Which version of the Scanner Compat library should I use?

Projects not targeting API 31 or newer should use version 1.5.1, and projects not migrated to Android Jetpack should use version 1.3.1, which is feature-equal to 1.4.0. Since version 1.5 you also need to enable desugaring of Java 8 language features, with 1.4.5 as the documented fallback.

How do I add the Scanner Compat library to an Android project?

Add `implementation 'no.nordicsemi.android.support.v18:scanner:1.6.0'` and make sure `mavenCentral()` is in the repositories of both the buildscript and allprojects blocks, since JCenter has shut down and the library has been Maven Central only since version 1.4.4.

What permissions does BLE scanning need on Android 12 and later?

Apps targeting Build.VERSION_CODES#S or higher need the BLUETOOTH_SCAN permission requested through Activity.requestPermissions, plus either ACCESS_FINE_LOCATION or a neverForLocation assertion on the uses-permission tag, though that assertion may restrict which Bluetooth devices you can interact with.

Does the Scanner Compat library work on the newest Android versions?

The Advertising Extension calls setLegacy and setPhy are only available on Android Oreo or newer and are ignored on older platforms. The library also notes that native filtering, batching and match reporting can be disabled if they do not work on a given device.

Official sources

  1. Issues
  2. License: BSD-3-Clause
  3. nordicsemi/Android-Scanner-Compat-Library on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nordicsemi-android-scanner-compat-library.svg)](https://hysenlabs.com/projects/nordicsemi-android-scanner-compat-library)

Was this useful?