├── .github └── workflows │ └── deploy-all.yml ├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── build.gradle ├── fastlane └── Fastfile ├── gradle.properties ├── gradle ├── git-tag-version.gradle ├── publish-module.gradle ├── publish-root.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── moustache ├── README.mo ├── mo └── split.sh ├── scanner ├── .gitignore ├── build.gradle ├── gradle.properties ├── scanner-proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── no │ │ └── nordicsemi │ │ └── android │ │ └── support │ │ └── v18 │ │ └── scanner │ │ ├── BluetoothLeScannerImplOreoTest.java │ │ ├── BluetoothUuidTest.java │ │ ├── ScanFilterTest.java │ │ ├── ScanRecordTest.java │ │ ├── ScanResultTest.java │ │ └── ScanSettingsTest.java │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── no │ │ └── nordicsemi │ │ └── android │ │ └── support │ │ └── v18 │ │ └── scanner │ │ ├── BluetoothLeScannerCompat.java │ │ ├── BluetoothLeScannerImplJB.java │ │ ├── BluetoothLeScannerImplLollipop.java │ │ ├── BluetoothLeScannerImplMarshmallow.java │ │ ├── BluetoothLeScannerImplOreo.java │ │ ├── BluetoothLeUtils.java │ │ ├── BluetoothUuid.java │ │ ├── Objects.java │ │ ├── PendingIntentExecutor.java │ │ ├── PendingIntentReceiver.java │ │ ├── ScanCallback.java │ │ ├── ScanCallbackWrapperSet.java │ │ ├── ScanFilter.java │ │ ├── ScanRecord.java │ │ ├── ScanResult.java │ │ ├── ScanSettings.java │ │ ├── ScannerService.java │ │ └── UserScanCallbackWrapper.java │ └── test │ └── java │ └── no │ └── nordicsemi │ └── android │ └── support │ └── v18 │ └── scanner │ └── ObjectsTest.java └── settings.gradle /.github/workflows/deploy-all.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Nexus 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | workflow_dispatch: 7 | jobs: 8 | generateReadme: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | ref: master 14 | fetch-depth: 0 15 | - shell: bash 16 | run: | 17 | git config user.email mag@nordicsemi.no 18 | git config user.name "Github Action" 19 | VERSION=`git describe --tags --abbrev=0` 20 | VERSION=`./moustache/split.sh $VERSION` 21 | rm -f ./README.md 22 | VERSION=$VERSION ./moustache/mo ./moustache/README.mo > ./README.md 23 | git add . 24 | git diff-index --quiet HEAD || git commit -m "Update readme to version=$VERSION" && git push 25 | deployAarsToNexus: 26 | needs: generateReadme 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | with: 31 | ref: master 32 | fetch-depth: 0 33 | - shell: bash 34 | env: 35 | GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} 36 | GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }} 37 | OSSR_USERNAME: ${{ secrets.OSSR_USERNAME }} 38 | OSSR_PASSWORD: ${{ secrets.OSSR_PASSWORD }} 39 | SONATYPE_STATING_PROFILE_ID: ${{ secrets.SONATYPE_STATING_PROFILE_ID }} 40 | run: | 41 | echo "${{ secrets.GPG_FILE }}" > sec.gpg.asc 42 | gpg -d --passphrase "${{ secrets.GPG_FILE_PSWD }}" --batch sec.gpg.asc > sec.gpg 43 | fastlane deployNexus 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/androidstudio 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio 4 | 5 | ### AndroidStudio ### 6 | # Covers files to be ignored for android development using Android Studio. 7 | 8 | # Built application files 9 | *.apk 10 | *.ap_ 11 | *.aab 12 | 13 | # Files for the ART/Dalvik VM 14 | *.dex 15 | 16 | # Java class files 17 | *.class 18 | 19 | # Generated files 20 | bin/ 21 | gen/ 22 | out/ 23 | 24 | # Gradle files 25 | .gradle 26 | .gradle/ 27 | build/ 28 | 29 | # Signing files 30 | .signing/ 31 | 32 | # Local configuration file (sdk path, etc) 33 | local.properties 34 | 35 | # Proguard folder generated by Eclipse 36 | proguard/ 37 | 38 | # Log Files 39 | *.log 40 | 41 | # Android Studio 42 | /*/build/ 43 | /*/local.properties 44 | /*/out 45 | /*/*/build 46 | /*/*/production 47 | captures/ 48 | .navigation/ 49 | *.ipr 50 | *~ 51 | *.swp 52 | 53 | # Keystore files 54 | *.jks 55 | *.keystore 56 | 57 | # Google Services (e.g. APIs or Firebase) 58 | # google-services.json 59 | 60 | # Android Patch 61 | gen-external-apklibs 62 | 63 | # External native build folder generated in Android Studio 2.2 and later 64 | .externalNativeBuild 65 | 66 | # NDK 67 | obj/ 68 | 69 | # IntelliJ IDEA 70 | *.iml 71 | *.iws 72 | /out/ 73 | 74 | # User-specific configurations 75 | .idea 76 | .idea/caches/ 77 | .idea/libraries/ 78 | .idea/shelf/ 79 | .idea/workspace.xml 80 | .idea/tasks.xml 81 | .idea/.name 82 | .idea/compiler.xml 83 | .idea/copyright/profiles_settings.xml 84 | .idea/encodings.xml 85 | .idea/misc.xml 86 | .idea/modules.xml 87 | .idea/scopes/scope_settings.xml 88 | .idea/dictionaries 89 | .idea/vcs.xml 90 | .idea/jsLibraryMappings.xml 91 | .idea/datasources.xml 92 | .idea/dataSources.ids 93 | .idea/sqlDataSources.xml 94 | .idea/dynamic.xml 95 | .idea/uiDesigner.xml 96 | .idea/assetWizardSettings.xml 97 | .idea/gradle.xml 98 | .idea/jarRepositories.xml 99 | .idea/navEditor.xml 100 | 101 | # OS-specific files 102 | .DS_Store 103 | .DS_Store? 104 | ._* 105 | .Spotlight-V100 106 | .Trashes 107 | ehthumbs.db 108 | Thumbs.db 109 | 110 | # Legacy Eclipse project files 111 | .classpath 112 | .project 113 | .cproject 114 | .settings/ 115 | 116 | # Mobile Tools for Java (J2ME) 117 | .mtj.tmp/ 118 | 119 | # Package Files # 120 | *.war 121 | *.ear 122 | 123 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 124 | hs_err_pid* 125 | 126 | ## Plugin-specific files: 127 | 128 | # mpeltonen/sbt-idea plugin 129 | .idea_modules/ 130 | 131 | # JIRA plugin 132 | atlassian-ide-plugin.xml 133 | 134 | # Mongo Explorer plugin 135 | .idea/mongoSettings.xml 136 | 137 | # Crashlytics plugin (for Android Studio and IntelliJ) 138 | com_crashlytics_export_strings.xml 139 | crashlytics.properties 140 | crashlytics-build.properties 141 | fabric.properties 142 | 143 | ### AndroidStudio Patch ### 144 | 145 | !/gradle/wrapper/gradle-wrapper.jar 146 | 147 | # End of https://www.toptal.com/developers/gitignore/api/androidstudio 148 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Nordic Semiconductor 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Android-Scanner-Compat-Library nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android BLE Scanner Compat library 2 | 3 | [ ![Download](https://maven-badges.herokuapp.com/maven-central/no.nordicsemi.android.support.v18/scanner/badge.svg?style=plastic) ](https://search.maven.org/artifact/no.nordicsemi.android.support.v18/scanner) 4 | 5 | The Scanner Compat library solves the problem with scanning for Bluetooth Low Energy devices on Android. 6 | The scanner API, initially created in Android 4.3, has changed in Android 5.0 and has been extended in 6.0 and 8.0. 7 | This library allows to use modern API even on older phones, emulating not supported features. If a feature 8 | (for example offloaded filtering or batching) is not available natively, it will be emulated by 9 | the compat library. Also, native filtering, batching and reporting first match or match lost may 10 | be disabled if you find them not working on some devices. Advertising Extension (`ScanSetting#setLegacy(boolean)` 11 | or `setPhy(int)`) is available only on Android Oreo or newer and such calls will be ignored on 12 | older platforms where only legacy advertising packets on PHY LE 1M will be reported, 13 | due to the Bluetooth chipset capabilities. 14 | 15 | ### Background scanning 16 | 17 | `SCAN_MODE_LOW_POWER` or `SCAN_MODE_OPPORTUNISTIC` should be used when scanning in background. 18 | Note, that newer Android versions will enforce using low power mode in background, even if another one has been set. 19 | This library allows to emulate [scanning with PendingIntent](https://developer.android.com/reference/android/bluetooth/le/BluetoothLeScanner.html#startScan(java.util.List%3Candroid.bluetooth.le.ScanFilter%3E,%20android.bluetooth.le.ScanSettings,%20android.app.PendingIntent)) 20 | on pre-Oreo devices by starting a background service that will scan with requested scan mode. 21 | This is much less battery friendly than when the original method is used, but works and saves 22 | a lot of development time if such feature should be implemented anyway. Please read below 23 | for more details. 24 | 25 | Note, that for unfiltered scans, scanning is stopped on screen off to save power. Scanning is 26 | resumed when screen is turned on again. To avoid this, use scanning with desired ScanFilter. 27 | 28 | ## Usage 29 | 30 | The compat library may be found on Maven Central repository. Add it to your project by adding the 31 | following dependency: 32 | 33 | ```Groovy 34 | implementation 'no.nordicsemi.android.support.v18:scanner:1.6.0' 35 | ``` 36 | 37 | Project not targeting API 31 (Android 12) or newer should use version 1.5.1. 38 | 39 | Projects not migrated to Android Jetpack should use version 1.3.1, which is feature-equal to 1.4.0. 40 | 41 | As JCenter has shut down, starting from version 1.4.4 the library is available only on Maven Central. 42 | Make sure you have `mavenCentral()` in your main *build.gradle* file: 43 | ```gradle 44 | buildscript { 45 | repositories { 46 | mavenCentral() 47 | } 48 | } 49 | allprojects { 50 | repositories { 51 | mavenCentral() 52 | } 53 | } 54 | ``` 55 | 56 | Since version 1.5 you will need to [enable desugaring of Java 8 language features](https://developer.android.com/studio/write/java8-support.html#supported_features) 57 | if you have not already done so.(And if you are releasing an Android library, then anyone who uses 58 | that library will also have to enable desugaring.) We expect for nearly all Android projects to have 59 | already enabled desugaring. But if this causes problems for you, please use version 1.4.5. 60 | 61 | ## Permissions 62 | 63 | Following [this](https://developer.android.com/reference/android/bluetooth/le/BluetoothLeScanner#startScan(android.bluetooth.le.ScanCallback)) link: 64 | 65 | > An app must have [ACCESS_COARSE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_COARSE_LOCATION) 66 | permission in order to get results. An App targeting Android Q or later must have 67 | [ACCESS_FINE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION) 68 | permission in order to get results. 69 | For apps targeting [Build.VERSION_CODES#R](https://developer.android.com/reference/android/os/Build.VERSION_CODES#R) 70 | or lower, this requires the [Manifest.permission#BLUETOOTH_ADMIN](https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_ADMIN) 71 | permission which can be gained with a simple `` manifest tag. 72 | For apps targeting [Build.VERSION_CODES#S](https://developer.android.com/reference/android/os/Build.VERSION_CODES#S) 73 | or or higher, this requires the [Manifest.permission#BLUETOOTH_SCAN](https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_SCAN) 74 | permission which can be gained with 75 | [Activity.requestPermissions(String[], int)](https://developer.android.com/reference/android/app/Activity#requestPermissions(java.lang.String[],%20int)). 76 | In addition, this requires either the [Manifest.permission#ACCESS_FINE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION) 77 | permission or a strong assertion that you will never derive the physical location of the device. 78 | You can make this assertion by declaring `usesPermissionFlags="neverForLocation"` on the relevant 79 | `` manifest tag, but it may restrict the types of Bluetooth devices you can interact with. 80 | 81 | ## API 82 | 83 | The Scanner Compat API is very similar to the original one, known from Android Oreo. 84 | 85 | Instead of getting it from the **BluetoothAdapter**, acquire the scanner instance using: 86 | 87 | ```java 88 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 89 | ``` 90 | 91 | You also need to change the packets for **ScanSettings**, **ScanFilter** and **ScanCallback** 92 | classes to: 93 | 94 | ```java 95 | no.nordicsemi.android.support.v18.scanner 96 | ``` 97 | 98 | ## Sample 99 | 100 | To start scanning use (example): 101 | 102 | ```java 103 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 104 | ScanSettings settings = new ScanSettings.Builder() 105 | .setLegacy(false) 106 | .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 107 | .setReportDelay(5000) 108 | .setUseHardwareBatchingIfSupported(true) 109 | .build(); 110 | List filters = new ArrayList<>(); 111 | filters.add(new ScanFilter.Builder().setServiceUuid(mUuid).build()); 112 | scanner.startScan(filters, settings, scanCallback); 113 | ``` 114 | 115 | to stop scanning use: 116 | 117 | ```java 118 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 119 | scanner.stopScan(scanCallback); 120 | ``` 121 | 122 | ### Scanning modes 123 | 124 | There are 4 scanning modes available in native [ScanSettings](https://developer.android.com/reference/android/bluetooth/le/ScanSettings). 125 | 3 of them are available since Android Lollipop while the opportunistic scan mode has been added in Marshmallow. 126 | This library tries to emulate them on platforms where they are not supported natively. 127 | 1. [SCAN_MODE_LOW_POWER](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_LOW_POWER) - 128 | Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes the least power. 129 | The scanner will scan for 0.5 second and rest for 4.5 seconds. A Bluetooth LE device should advertise 130 | very often (at least once per 100 ms) in order to be found with this mode, otherwise the scanning interval may miss some or even all 131 | advertising events. This mode may be enforced if the scanning application is not in foreground. 132 | 2. [SCAN_MODE_BALANCED](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_BALANCED) - 133 | Perform Bluetooth LE scan in balanced power mode. Scan results are returned at a rate that provides a 134 | good trade-off between scan frequency and power consumption. The scanner will scan for 2 seconds followed 135 | by 3 seconds of idle. 136 | 3. [SCAN_MODE_LOW_LATENCY](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_LOW_LATENCY) - 137 | Scan using highest duty cycle. It's recommended to only use this mode when the application is running in the foreground. 138 | 4. [SCAN_MODE_OPPORTUNISTIC](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_OPPORTUNISTIC) - 139 | A special Bluetooth LE scan mode. Applications using this scan mode will passively listen for other scan results 140 | without starting BLE scans themselves. 141 | 142 | 3 first modes are emulated on Android 4.3 and 4.4.x by starting a handler task that scans for a period of time 143 | and rests in between. To set scanning and rest intervals use `Builder#setPowerSave(long,long)`. 144 | 145 | Opportunistic scanning is not possible to emulate and will fallback to `SCAN_MODE_LOW_POWER` on Lollipop and 146 | power save settings on pre-Lollipop devices. That means that this library actually will initiate scanning 147 | on its own. This may have impact on battery consumption and should be used with care. 148 | 149 | ### Scan filters and batching 150 | 151 | Offloaded filtering is available on Lollipop or newer devices where 152 | [BluetoothAdapter#isOffloadedFilteringSupported()](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#isOffloadedFilteringSupported()) 153 | returns *true* (when Bluetooth is enabled). If it is not supported, this library will scan without a filter and 154 | apply the filter to the results. If you find offloaded filtering unreliable you may force using compat filtering by calling 155 | `Builder#useHardwareFilteringIfSupported(false)`. Keep in mind that, newer Android versions may prohibit 156 | background scanning without native filters to save battery, so this method should be used with care. 157 | 158 | Android Scanner Compat Library may also emulate batching. To enable scan batching call `Builder#setScanDelay(interval)` 159 | with an interval greater than 0. For intervals less 5 seconds the actual interval may vary. 160 | If you want to get results in lower intervals, call `Builder#useHardwareBatchingIfSupported(false)`, which will 161 | start a normal scan and report results in given interval. Emulated batching uses significantly more battery 162 | than offloaded as it wakes CPU with every device found. 163 | 164 | ### Scanning with Pending Intent 165 | 166 | Android 8.0 Oreo introduced [Background Execution Limits](https://developer.android.com/about/versions/oreo/background) 167 | which made background running services short-lived. At the same time, to make background scanning possible, a new 168 | [method](https://developer.android.com/reference/android/bluetooth/le/BluetoothLeScanner.html#startScan(java.util.List%3Candroid.bluetooth.le.ScanFilter%3E,%20android.bluetooth.le.ScanSettings,%20android.app.PendingIntent)) 169 | was added to [BluetoothLeScanner](https://developer.android.com/reference/android/bluetooth/le/BluetoothLeScanner.html) 170 | which allows registering a [PendingIntent](https://developer.android.com/reference/android/app/PendingIntent) 171 | that will be sent whenever a device matching filter criteria is found. This will also work after 172 | your application has been killed (the receiver must be added in *AndroidManifest* and the 173 | `PendingIntent` must be created with an explicit Intent). 174 | 175 | Starting from version 1.3.0, this library may emulate such feature on older Android versions. 176 | In order to do that, a background service will be started after calling 177 | `scanner.startScan(filters, settings, context, pendingIntent, requestCode)`, which will be scanning in 178 | background with given settings and will send the given `PendingIntent` when a device 179 | matching filter is found. To lower battery consumption it is recommended to set 180 | `ScanSettings.SCAN_MODE_LOW_POWER` scanning mode and use filter, but even with those conditions fulfilled 181 | **the battery consumption will be significantly higher than on Oreo+**. To stop scanning call 182 | `scanner.stopScan(context, pendingIntent, requestCode)` with 183 | [the same](https://developer.android.com/reference/android/app/PendingIntent) intent in parameter. 184 | The service will be stopped when the last scan was stopped. 185 | 186 | On Android Oreo or newer this library will use the native scanning mechanism. However, as it may also 187 | emulate batching or apply filtering (when `useHardwareBatchingIfSupported` or `useHardwareFilteringIfSupported` 188 | were called with parameter *false*) the library will register its own broadcast 189 | receiver that will translate results from native to compat classes. 190 | 191 | The receiver and service will be added automatically to the manifest even if they are not used by 192 | the application. No changes are required to make it work. 193 | 194 | To use this feature: 195 | 196 | ```java 197 | Intent intent = new Intent(context, MyReceiver.class); // explicit intent 198 | intent.setAction("com.example.ACTION_FOUND"); 199 | intent.putExtra("some.extra", value); // optional 200 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); 201 | 202 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 203 | ScanSettings settings = new ScanSettings.Builder() 204 | .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) 205 | .setReportDelay(10000) 206 | .build(); 207 | List filters = new ArrayList<>(); 208 | filters.add(new ScanFilter.Builder().setServiceUuid(mUuid).build()); 209 | scanner.startScan(filters, settings, context, pendingIntent, requestCode); 210 | ``` 211 | 212 | Add your `MyReceiver` to *AndroidManifest*, as the application context might have been released 213 | and all broadcast receivers registered to it together with it. 214 | 215 | To stop scanning call: 216 | 217 | ```java 218 | // To stop scanning use the same PendingIntent and request code as one used to start scanning. 219 | Intent intent = new Intent(context, MyReceiver.class); 220 | intent.setAction("com.example.ACTION_FOUND"); 221 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT); 222 | 223 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 224 | scanner.stopScan(context, pendingIntent, requestCode); 225 | ``` 226 | 227 | **Note:** Android versions 6 and 7 will not report any advertising packets when in Doze mode. 228 | Read more about it here: https://developer.android.com/training/monitoring-device-state/doze-standby 229 | 230 | **Note 2:** An additional parameter called `requestCode` was added in version 1.4.5 to the above API. 231 | It is to ensure that the scanning would be correctly stopped. If not provided, a request code equal 232 | to 0 will be used preventing from having multiple scanning tasks. 233 | 234 | ## Background scanning guidelines 235 | 236 | To save power it is recommended to use as low power settings as possible and and use filters. 237 | However, the more battery friendly settings are used, the longest time to finding a device. 238 | In general, scanning with `PendingIntent` and `SCAN_MODE_LOW_POWER` or `SCAN_MODE_OPPORTUNISTIC` 239 | should be used, together with report delay set and filters used. 240 | `useHardwareFilteringIfSupported` and `useHardwareBatchingIfSupported` should be set to *true* (default). 241 | 242 | Background scanning on Android 4.3 and 4.4.x will use a lot of power, as all those properties 243 | will have to be emulated. It is recommended to scan in background only on Lollipop or newer, or 244 | even Oreo or newer devices and giving the user an option to disable this feature. 245 | 246 | Note, that for unfiltered scans, scanning is stopped on screen off to save power. Scanning is 247 | resumed when screen is turned on again. To avoid this, use scanning with desired ScanFilter. 248 | 249 | ## License 250 | 251 | The Scanner Compat library is available under BSD 3-Clause license. See the LICENSE file for more info. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | // https://plugins.gradle.org/plugin/io.github.gradle-nexus.publish-plugin 5 | ext.gradle_nexus_publish_plugin = '1.1.0' 6 | 7 | repositories { 8 | google() 9 | mavenCentral() 10 | maven { url "https://plugins.gradle.org/m2/" } 11 | } 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:7.0.4' 14 | classpath "io.github.gradle-nexus:publish-plugin:$gradle_nexus_publish_plugin" 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | 29 | // Maven Central publishing 30 | apply plugin: 'io.github.gradle-nexus.publish-plugin' 31 | apply from: rootProject.file('gradle/publish-root.gradle') -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:android) 17 | 18 | platform :android do 19 | 20 | desc "Deploy libraries to Nexus." 21 | lane :deployNexus do 22 | gradle(task: "publishToSonatype closeAndReleaseSonatypeStagingRepository") 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | android.useAndroidX=true 20 | 21 | GROUP=no.nordicsemi.android.support.v18 22 | 23 | POM_DESCRIPTION=Android Bluetooth LE Scanner Compat library 24 | POM_URL=https://github.com/NordicSemiconductor/Android-Scanner-Compat-Library 25 | POM_SCM_URL=https://github.com/NordicSemiconductor/Android-Scanner-Compat-Library 26 | POM_SCM_CONNECTION=scm:git@github.com:NordicSemiconductor/Android-Scanner-Compat-Library.git 27 | POM_SCM_DEV_CONNECTION=scm:git@github.com:NordicSemiconductor/Android-Scanner-Compat-Library.git 28 | POM_LICENCE=BSD 3-Clause 29 | POM_LICENCE_NAME=The BSD 3-Clause License 30 | POM_LICENCE_URL=http://opensource.org/licenses/BSD-3-Clause 31 | POM_DEVELOPER_ID=philips77 32 | POM_DEVELOPER_NAME=Aleksander Nowakowski 33 | POM_DEVELOPER_EMAIL=aleksander.nowakowski@nordicsemi.no -------------------------------------------------------------------------------- /gradle/git-tag-version.gradle: -------------------------------------------------------------------------------- 1 | ext.getVersionCodeFromTags = { -> 2 | try { 3 | def code = new ByteArrayOutputStream() 4 | exec { 5 | commandLine 'git', 'tag', '--list' 6 | standardOutput = code 7 | } 8 | return 14 + code.toString().split("\n").size() 9 | } 10 | catch (ignored) { 11 | return -1 12 | } 13 | } 14 | 15 | ext.getVersionNameFromTags = { -> 16 | try { 17 | def stdout = new ByteArrayOutputStream() 18 | exec { 19 | commandLine 'git', 'describe', '--tags', '--abbrev=0' 20 | standardOutput = stdout 21 | } 22 | return stdout.toString().trim().split("%")[0] 23 | } 24 | catch (ignored) { 25 | return null 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gradle/publish-module.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | apply from: rootProject.file("gradle/git-tag-version.gradle") 4 | 5 | group = GROUP 6 | version = getVersionNameFromTags() 7 | 8 | task androidSourcesJar(type: Jar) { 9 | archiveClassifier.set('sources') 10 | from android.sourceSets.main.java.srcDirs 11 | // from android.sourceSets.main.kotlin.srcDirs 12 | } 13 | 14 | artifacts { 15 | archives androidSourcesJar 16 | } 17 | 18 | afterEvaluate { 19 | publishing { 20 | publications { 21 | release(MavenPublication) { 22 | from components.release 23 | 24 | artifact androidSourcesJar 25 | 26 | groupId = GROUP 27 | artifactId = POM_ARTIFACT_ID 28 | version = getVersionNameFromTags() 29 | 30 | pom { 31 | name = POM_NAME 32 | packaging = POM_PACKAGING 33 | description = POM_DESCRIPTION 34 | url = POM_URL 35 | 36 | scm { 37 | url = POM_SCM_URL 38 | connection = POM_SCM_CONNECTION 39 | developerConnection = POM_SCM_DEV_CONNECTION 40 | } 41 | 42 | licenses { 43 | license { 44 | name = POM_LICENCE_NAME 45 | url = POM_LICENCE_URL 46 | } 47 | } 48 | 49 | developers { 50 | developer { 51 | id = POM_DEVELOPER_ID 52 | name = POM_DEVELOPER_NAME 53 | email = POM_DEVELOPER_EMAIL 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | ext["signing.keyId"] = System.env.GPG_SIGNING_KEY 63 | ext["signing.password"] = System.env.GPG_PASSWORD 64 | ext["signing.secretKeyRingFile"] = "../sec.gpg" 65 | 66 | signing { 67 | sign publishing.publications 68 | } -------------------------------------------------------------------------------- /gradle/publish-root.gradle: -------------------------------------------------------------------------------- 1 | // Create variables with empty default values 2 | File secretPropsFile = project.rootProject.file('local.properties') 3 | if (secretPropsFile.exists()) { 4 | // Read local.properties file first if it exists 5 | Properties p = new Properties() 6 | new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } 7 | p.each { name, value -> ext[name] = value } 8 | } 9 | 10 | // Set up Sonatype repository 11 | 12 | nexusPublishing { 13 | 14 | repositories { 15 | sonatype { 16 | stagingProfileId = System.env.SONATYPE_STATING_PROFILE_ID 17 | username = System.env.OSSR_USERNAME 18 | password = System.env.OSSR_PASSWORD 19 | } 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NordicSemiconductor/Android-Scanner-Compat-Library/9e4094195ae2ea44617eca851ad208dda8143ec6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Sep 29 10:37:08 CEST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /moustache/README.mo: -------------------------------------------------------------------------------- 1 | # Android BLE Scanner Compat library 2 | 3 | [ ![Download](https://maven-badges.herokuapp.com/maven-central/no.nordicsemi.android.support.v18/scanner/badge.svg?style=plastic) ](https://search.maven.org/artifact/no.nordicsemi.android.support.v18/scanner) 4 | 5 | The Scanner Compat library solves the problem with scanning for Bluetooth Low Energy devices on Android. 6 | The scanner API, initially created in Android 4.3, has changed in Android 5.0 and has been extended in 6.0 and 8.0. 7 | This library allows to use modern API even on older phones, emulating not supported features. If a feature 8 | (for example offloaded filtering or batching) is not available natively, it will be emulated by 9 | the compat library. Also, native filtering, batching and reporting first match or match lost may 10 | be disabled if you find them not working on some devices. Advertising Extension (`ScanSetting#setLegacy(boolean)` 11 | or `setPhy(int)`) is available only on Android Oreo or newer and such calls will be ignored on 12 | older platforms where only legacy advertising packets on PHY LE 1M will be reported, 13 | due to the Bluetooth chipset capabilities. 14 | 15 | ### Background scanning 16 | 17 | `SCAN_MODE_LOW_POWER` or `SCAN_MODE_OPPORTUNISTIC` should be used when scanning in background. 18 | Note, that newer Android versions will enforce using low power mode in background, even if another one has been set. 19 | This library allows to emulate [scanning with PendingIntent](https://developer.android.com/reference/android/bluetooth/le/BluetoothLeScanner.html#startScan(java.util.List%3Candroid.bluetooth.le.ScanFilter%3E,%20android.bluetooth.le.ScanSettings,%20android.app.PendingIntent)) 20 | on pre-Oreo devices by starting a background service that will scan with requested scan mode. 21 | This is much less battery friendly than when the original method is used, but works and saves 22 | a lot of development time if such feature should be implemented anyway. Please read below 23 | for more details. 24 | 25 | Note, that for unfiltered scans, scanning is stopped on screen off to save power. Scanning is 26 | resumed when screen is turned on again. To avoid this, use scanning with desired ScanFilter. 27 | 28 | ## Usage 29 | 30 | The compat library may be found on Maven Central repository. Add it to your project by adding the 31 | following dependency: 32 | 33 | ```Groovy 34 | implementation 'no.nordicsemi.android.support.v18:scanner:{{VERSION}}' 35 | ``` 36 | 37 | Project not targeting API 31 (Android 12) or newer should use version 1.5.1. 38 | 39 | Projects not migrated to Android Jetpack should use version 1.3.1, which is feature-equal to 1.4.0. 40 | 41 | As JCenter has shut down, starting from version 1.4.4 the library is available only on Maven Central. 42 | Make sure you have `mavenCentral()` in your main *build.gradle* file: 43 | ```gradle 44 | buildscript { 45 | repositories { 46 | mavenCentral() 47 | } 48 | } 49 | allprojects { 50 | repositories { 51 | mavenCentral() 52 | } 53 | } 54 | ``` 55 | 56 | Since version 1.5 you will need to [enable desugaring of Java 8 language features](https://developer.android.com/studio/write/java8-support.html#supported_features) 57 | if you have not already done so.(And if you are releasing an Android library, then anyone who uses 58 | that library will also have to enable desugaring.) We expect for nearly all Android projects to have 59 | already enabled desugaring. But if this causes problems for you, please use version 1.4.5. 60 | 61 | ## Permissions 62 | 63 | Following [this](https://developer.android.com/reference/android/bluetooth/le/BluetoothLeScanner#startScan(android.bluetooth.le.ScanCallback)) link: 64 | 65 | > An app must have [ACCESS_COARSE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_COARSE_LOCATION) 66 | permission in order to get results. An App targeting Android Q or later must have 67 | [ACCESS_FINE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION) 68 | permission in order to get results. 69 | For apps targeting [Build.VERSION_CODES#R](https://developer.android.com/reference/android/os/Build.VERSION_CODES#R) 70 | or lower, this requires the [Manifest.permission#BLUETOOTH_ADMIN](https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_ADMIN) 71 | permission which can be gained with a simple `` manifest tag. 72 | For apps targeting [Build.VERSION_CODES#S](https://developer.android.com/reference/android/os/Build.VERSION_CODES#S) 73 | or or higher, this requires the [Manifest.permission#BLUETOOTH_SCAN](https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_SCAN) 74 | permission which can be gained with 75 | [Activity.requestPermissions(String[], int)](https://developer.android.com/reference/android/app/Activity#requestPermissions(java.lang.String[],%20int)). 76 | In addition, this requires either the [Manifest.permission#ACCESS_FINE_LOCATION](https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION) 77 | permission or a strong assertion that you will never derive the physical location of the device. 78 | You can make this assertion by declaring `usesPermissionFlags="neverForLocation"` on the relevant 79 | `` manifest tag, but it may restrict the types of Bluetooth devices you can interact with. 80 | 81 | ## API 82 | 83 | The Scanner Compat API is very similar to the original one, known from Android Oreo. 84 | 85 | Instead of getting it from the **BluetoothAdapter**, acquire the scanner instance using: 86 | 87 | ```java 88 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 89 | ``` 90 | 91 | You also need to change the packets for **ScanSettings**, **ScanFilter** and **ScanCallback** 92 | classes to: 93 | 94 | ```java 95 | no.nordicsemi.android.support.v18.scanner 96 | ``` 97 | 98 | ## Sample 99 | 100 | To start scanning use (example): 101 | 102 | ```java 103 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 104 | ScanSettings settings = new ScanSettings.Builder() 105 | .setLegacy(false) 106 | .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 107 | .setReportDelay(5000) 108 | .setUseHardwareBatchingIfSupported(true) 109 | .build(); 110 | List filters = new ArrayList<>(); 111 | filters.add(new ScanFilter.Builder().setServiceUuid(mUuid).build()); 112 | scanner.startScan(filters, settings, scanCallback); 113 | ``` 114 | 115 | to stop scanning use: 116 | 117 | ```java 118 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 119 | scanner.stopScan(scanCallback); 120 | ``` 121 | 122 | ### Scanning modes 123 | 124 | There are 4 scanning modes available in native [ScanSettings](https://developer.android.com/reference/android/bluetooth/le/ScanSettings). 125 | 3 of them are available since Android Lollipop while the opportunistic scan mode has been added in Marshmallow. 126 | This library tries to emulate them on platforms where they are not supported natively. 127 | 1. [SCAN_MODE_LOW_POWER](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_LOW_POWER) - 128 | Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes the least power. 129 | The scanner will scan for 0.5 second and rest for 4.5 seconds. A Bluetooth LE device should advertise 130 | very often (at least once per 100 ms) in order to be found with this mode, otherwise the scanning interval may miss some or even all 131 | advertising events. This mode may be enforced if the scanning application is not in foreground. 132 | 2. [SCAN_MODE_BALANCED](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_BALANCED) - 133 | Perform Bluetooth LE scan in balanced power mode. Scan results are returned at a rate that provides a 134 | good trade-off between scan frequency and power consumption. The scanner will scan for 2 seconds followed 135 | by 3 seconds of idle. 136 | 3. [SCAN_MODE_LOW_LATENCY](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_LOW_LATENCY) - 137 | Scan using highest duty cycle. It's recommended to only use this mode when the application is running in the foreground. 138 | 4. [SCAN_MODE_OPPORTUNISTIC](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_OPPORTUNISTIC) - 139 | A special Bluetooth LE scan mode. Applications using this scan mode will passively listen for other scan results 140 | without starting BLE scans themselves. 141 | 142 | 3 first modes are emulated on Android 4.3 and 4.4.x by starting a handler task that scans for a period of time 143 | and rests in between. To set scanning and rest intervals use `Builder#setPowerSave(long,long)`. 144 | 145 | Opportunistic scanning is not possible to emulate and will fallback to `SCAN_MODE_LOW_POWER` on Lollipop and 146 | power save settings on pre-Lollipop devices. That means that this library actually will initiate scanning 147 | on its own. This may have impact on battery consumption and should be used with care. 148 | 149 | ### Scan filters and batching 150 | 151 | Offloaded filtering is available on Lollipop or newer devices where 152 | [BluetoothAdapter#isOffloadedFilteringSupported()](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#isOffloadedFilteringSupported()) 153 | returns *true* (when Bluetooth is enabled). If it is not supported, this library will scan without a filter and 154 | apply the filter to the results. If you find offloaded filtering unreliable you may force using compat filtering by calling 155 | `Builder#useHardwareFilteringIfSupported(false)`. Keep in mind that, newer Android versions may prohibit 156 | background scanning without native filters to save battery, so this method should be used with care. 157 | 158 | Android Scanner Compat Library may also emulate batching. To enable scan batching call `Builder#setScanDelay(interval)` 159 | with an interval greater than 0. For intervals less 5 seconds the actual interval may vary. 160 | If you want to get results in lower intervals, call `Builder#useHardwareBatchingIfSupported(false)`, which will 161 | start a normal scan and report results in given interval. Emulated batching uses significantly more battery 162 | than offloaded as it wakes CPU with every device found. 163 | 164 | ### Scanning with Pending Intent 165 | 166 | Android 8.0 Oreo introduced [Background Execution Limits](https://developer.android.com/about/versions/oreo/background) 167 | which made background running services short-lived. At the same time, to make background scanning possible, a new 168 | [method](https://developer.android.com/reference/android/bluetooth/le/BluetoothLeScanner.html#startScan(java.util.List%3Candroid.bluetooth.le.ScanFilter%3E,%20android.bluetooth.le.ScanSettings,%20android.app.PendingIntent)) 169 | was added to [BluetoothLeScanner](https://developer.android.com/reference/android/bluetooth/le/BluetoothLeScanner.html) 170 | which allows registering a [PendingIntent](https://developer.android.com/reference/android/app/PendingIntent) 171 | that will be sent whenever a device matching filter criteria is found. This will also work after 172 | your application has been killed (the receiver must be added in *AndroidManifest* and the 173 | `PendingIntent` must be created with an explicit Intent). 174 | 175 | Starting from version 1.3.0, this library may emulate such feature on older Android versions. 176 | In order to do that, a background service will be started after calling 177 | `scanner.startScan(filters, settings, context, pendingIntent, requestCode)`, which will be scanning in 178 | background with given settings and will send the given `PendingIntent` when a device 179 | matching filter is found. To lower battery consumption it is recommended to set 180 | `ScanSettings.SCAN_MODE_LOW_POWER` scanning mode and use filter, but even with those conditions fulfilled 181 | **the battery consumption will be significantly higher than on Oreo+**. To stop scanning call 182 | `scanner.stopScan(context, pendingIntent, requestCode)` with 183 | [the same](https://developer.android.com/reference/android/app/PendingIntent) intent in parameter. 184 | The service will be stopped when the last scan was stopped. 185 | 186 | On Android Oreo or newer this library will use the native scanning mechanism. However, as it may also 187 | emulate batching or apply filtering (when `useHardwareBatchingIfSupported` or `useHardwareFilteringIfSupported` 188 | were called with parameter *false*) the library will register its own broadcast 189 | receiver that will translate results from native to compat classes. 190 | 191 | The receiver and service will be added automatically to the manifest even if they are not used by 192 | the application. No changes are required to make it work. 193 | 194 | To use this feature: 195 | 196 | ```java 197 | Intent intent = new Intent(context, MyReceiver.class); // explicit intent 198 | intent.setAction("com.example.ACTION_FOUND"); 199 | intent.putExtra("some.extra", value); // optional 200 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); 201 | 202 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 203 | ScanSettings settings = new ScanSettings.Builder() 204 | .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) 205 | .setReportDelay(10000) 206 | .build(); 207 | List filters = new ArrayList<>(); 208 | filters.add(new ScanFilter.Builder().setServiceUuid(mUuid).build()); 209 | scanner.startScan(filters, settings, context, pendingIntent, requestCode); 210 | ``` 211 | 212 | Add your `MyReceiver` to *AndroidManifest*, as the application context might have been released 213 | and all broadcast receivers registered to it together with it. 214 | 215 | To stop scanning call: 216 | 217 | ```java 218 | // To stop scanning use the same PendingIntent and request code as one used to start scanning. 219 | Intent intent = new Intent(context, MyReceiver.class); 220 | intent.setAction("com.example.ACTION_FOUND"); 221 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT); 222 | 223 | BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 224 | scanner.stopScan(context, pendingIntent, requestCode); 225 | ``` 226 | 227 | **Note:** Android versions 6 and 7 will not report any advertising packets when in Doze mode. 228 | Read more about it here: https://developer.android.com/training/monitoring-device-state/doze-standby 229 | 230 | **Note 2:** An additional parameter called `requestCode` was added in version 1.4.5 to the above API. 231 | It is to ensure that the scanning would be correctly stopped. If not provided, a request code equal 232 | to 0 will be used preventing from having multiple scanning tasks. 233 | 234 | ## Background scanning guidelines 235 | 236 | To save power it is recommended to use as low power settings as possible and and use filters. 237 | However, the more battery friendly settings are used, the longest time to finding a device. 238 | In general, scanning with `PendingIntent` and `SCAN_MODE_LOW_POWER` or `SCAN_MODE_OPPORTUNISTIC` 239 | should be used, together with report delay set and filters used. 240 | `useHardwareFilteringIfSupported` and `useHardwareBatchingIfSupported` should be set to *true* (default). 241 | 242 | Background scanning on Android 4.3 and 4.4.x will use a lot of power, as all those properties 243 | will have to be emulated. It is recommended to scan in background only on Lollipop or newer, or 244 | even Oreo or newer devices and giving the user an option to disable this feature. 245 | 246 | Note, that for unfiltered scans, scanning is stopped on screen off to save power. Scanning is 247 | resumed when screen is turned on again. To avoid this, use scanning with desired ScanFilter. 248 | 249 | ## License 250 | 251 | The Scanner Compat library is available under BSD 3-Clause license. See the LICENSE file for more info. -------------------------------------------------------------------------------- /moustache/split.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | str="$1" 4 | 5 | IFS='%' # space is set as delimiter 6 | read -ra ADDR <<< "$str" # str is read into an array as tokens separated by IFS 7 | echo "${ADDR[0]}" 8 | 9 | -------------------------------------------------------------------------------- /scanner/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml -------------------------------------------------------------------------------- /scanner/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 31 5 | 6 | defaultConfig { 7 | minSdkVersion 18 8 | targetSdkVersion 31 9 | versionCode 21 10 | 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | consumerProguardFiles 'scanner-proguard-rules.pro' 17 | } 18 | debug { 19 | testCoverageEnabled true 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation 'androidx.annotation:annotation:1.3.0' 26 | 27 | // Core library 28 | androidTestImplementation 'androidx.test:core:1.4.0' 29 | 30 | // AndroidJUnitRunner 31 | androidTestImplementation 'androidx.test:runner:1.4.0' 32 | 33 | // Assertions & AndroidJUnit4 34 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 35 | 36 | // Truth for Unit Testing 37 | androidTestImplementation "com.google.truth:truth:1.1.3" 38 | testImplementation "com.google.truth:truth:1.1.3" 39 | } 40 | 41 | // === Maven Central configuration === 42 | // The following file exists only when Android Scanner Compat Library project is opened, but not 43 | // when the module is loaded to a different project. 44 | if (rootProject.file('gradle/publish-module.gradle').exists()) { 45 | ext { 46 | POM_ARTIFACT_ID = 'scanner' 47 | POM_NAME = 'Android Bluetooth LE Scanner Compat library' 48 | POM_PACKAGING = 'aar' 49 | } 50 | apply from: rootProject.file('gradle/publish-module.gradle') 51 | } -------------------------------------------------------------------------------- /scanner/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=scanner 2 | POM_NAME=Scanner Compat library 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /scanner/scanner-proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\alno\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /scanner/src/androidTest/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeScannerImplOreoTest.java: -------------------------------------------------------------------------------- 1 | package no.nordicsemi.android.support.v18.scanner; 2 | 3 | import android.bluetooth.BluetoothAdapter; 4 | import android.bluetooth.BluetoothDevice; 5 | 6 | import org.junit.Test; 7 | 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | 11 | import static com.google.common.truth.Truth.assertThat; 12 | 13 | public class BluetoothLeScannerImplOreoTest { 14 | 15 | @Test 16 | public void toImpl() { 17 | // Build mock data 18 | final byte[] bytes = new byte[]{ 19 | 2, 1, 6, // Flags 20 | 5, 8, 'T', 'e', 's', 't', // Shortened Local Name (Test) 21 | 6, (byte) 0xFF, 0x59, 0x00, 1, 2, 3, // Manufacturer Data (Nordic Semi -> 0x010203) 22 | 3, 0x16, 0x09, 0x18, // Service Data - 16-bit UUID (0x1809) 23 | 2, 0x0A, 1 // Tx Power Level (1 dBm) 24 | }; 25 | 26 | final BluetoothDevice device = 27 | BluetoothAdapter.getDefaultAdapter().getRemoteDevice("01:02:03:04:05:06"); 28 | 29 | final android.bluetooth.le.ScanRecord _record = parseScanRecord(bytes); 30 | 31 | android.bluetooth.le.ScanResult _result = new android.bluetooth.le.ScanResult(device, 32 | 0b000001, 1, 2, 0, 33 | android.bluetooth.le.ScanResult.TX_POWER_NOT_PRESENT, -70, 34 | android.bluetooth.le.ScanResult.PERIODIC_INTERVAL_NOT_PRESENT, _record, 35 | 123456789L); 36 | 37 | // Convert to support.v18.ScanResult 38 | final BluetoothLeScannerImplOreo impl = new BluetoothLeScannerImplOreo(); 39 | final ScanResult result = impl.fromNativeScanResult(_result); 40 | 41 | // Validate 42 | assertThat(result).isNotNull(); 43 | assertThat(_record).isNotNull(); 44 | assertThat(_result.isLegacy()).isEqualTo(result.isLegacy()); 45 | assertThat(_result.isConnectable()).isEqualTo(result.isConnectable()); 46 | assertThat(result.getDataStatus()).isEqualTo(ScanResult.DATA_COMPLETE); 47 | assertThat(result.getScanRecord()).isNotNull(); 48 | final ScanRecord record = result.getScanRecord(); 49 | assertThat(record.getAdvertiseFlags()).isEqualTo(6); 50 | assertThat(bytes).isEqualTo(record.getBytes()); 51 | assertThat(record.getManufacturerSpecificData(0x0059)).isNotNull(); 52 | assertThat(_record.getManufacturerSpecificData(0x0059)) 53 | .isEqualTo(record.getManufacturerSpecificData(0x0059)); 54 | assertThat(result.getPeriodicAdvertisingInterval()) 55 | .isEqualTo(ScanResult.PERIODIC_INTERVAL_NOT_PRESENT); 56 | assertThat(result.getTxPower()).isEqualTo(ScanResult.TX_POWER_NOT_PRESENT); 57 | assertThat(result.getTimestampNanos()).isEqualTo(123456789L); 58 | assertThat(_result.getDevice()).isEqualTo(result.getDevice()); 59 | assertThat(device).isEqualTo(result.getDevice()); 60 | } 61 | 62 | /** 63 | * Utility method to call hidden ScanRecord.parseFromBytes method. 64 | */ 65 | static android.bluetooth.le.ScanRecord parseScanRecord(byte[] bytes) { 66 | final Class scanRecordClass = android.bluetooth.le.ScanRecord.class; 67 | try { 68 | final Method method = scanRecordClass.getDeclaredMethod("parseFromBytes", byte[].class); 69 | return (android.bluetooth.le.ScanRecord) method.invoke(null, bytes); 70 | } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException 71 | | InvocationTargetException e) { 72 | return null; 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /scanner/src/androidTest/java/no/nordicsemi/android/support/v18/scanner/BluetoothUuidTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package no.nordicsemi.android.support.v18.scanner; 18 | 19 | import android.os.ParcelUuid; 20 | 21 | import org.junit.Test; 22 | 23 | import static com.google.common.truth.Truth.assertThat; 24 | 25 | public class BluetoothUuidTest { 26 | 27 | @Test 28 | public void testUuidParser() { 29 | final byte[] uuid16 = new byte[]{ 30 | 0x0B, 0x11 31 | }; 32 | assertThat(BluetoothUuid.parseUuidFrom(uuid16)) 33 | .isEqualTo(ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB")); 34 | final byte[] uuid32 = new byte[]{ 35 | 0x0B, 0x11, 0x33, (byte) 0xFE 36 | }; 37 | assertThat(BluetoothUuid.parseUuidFrom(uuid32)) 38 | .isEqualTo(ParcelUuid.fromString("FE33110B-0000-1000-8000-00805F9B34FB")); 39 | final byte[] uuid128 = new byte[]{ 40 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 41 | (byte) 0xFF 42 | }; 43 | assertThat(BluetoothUuid.parseUuidFrom(uuid128)) 44 | .isEqualTo(ParcelUuid.fromString("FF0F0E0D-0C0B-0A09-0807-0060504030201")); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scanner/src/androidTest/java/no/nordicsemi/android/support/v18/scanner/ScanFilterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package no.nordicsemi.android.support.v18.scanner; 18 | 19 | import android.bluetooth.BluetoothAdapter; 20 | import android.bluetooth.BluetoothDevice; 21 | import android.os.Parcel; 22 | import android.os.ParcelUuid; 23 | 24 | import org.junit.Before; 25 | import org.junit.Test; 26 | import org.junit.runner.RunWith; 27 | 28 | import androidx.test.ext.junit.runners.AndroidJUnit4; 29 | 30 | import static com.google.common.truth.Truth.assertThat; 31 | 32 | @RunWith(AndroidJUnit4.class) 33 | public class ScanFilterTest { 34 | 35 | private static final String DEVICE_MAC = "01:02:03:04:05:AB"; 36 | private ScanResult scanResult; 37 | private ScanFilter.Builder filterBuilder; 38 | 39 | @Before 40 | public void setup() { 41 | final byte[] scanRecord = new byte[]{ 42 | 0x02, 0x01, 0x1a, // advertising flags 43 | 0x05, 0x02, 0x0b, 0x11, 0x0a, 0x11, // 16 bit service uuids 44 | 0x04, 0x09, 0x50, 0x65, 0x64, // setName 45 | 0x02, 0x0A, (byte) 0xec, // tx power level 46 | 0x05, 0x16, 0x0b, 0x11, 0x50, 0x64, // service data 47 | 0x05, (byte) 0xff, (byte) 0xe0, 0x00, 0x02, 0x15, // manufacturer specific data 48 | 0x03, 0x50, 0x01, 0x02, // an unknown data type won't cause trouble 49 | }; 50 | 51 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 52 | final BluetoothDevice device = adapter.getRemoteDevice(DEVICE_MAC); 53 | 54 | scanResult = new ScanResult(device, 55 | ScanRecord.parseFromBytes(scanRecord), -10, 1397545200000000L); 56 | filterBuilder = new ScanFilter.Builder(); 57 | } 58 | 59 | @Test 60 | public void testSetNameFilter() { 61 | ScanFilter filter = filterBuilder.setDeviceName("Ped").build(); 62 | assertThat(filter.matches(scanResult)).isTrue(); 63 | filter = filterBuilder.setDeviceName("Pem").build(); 64 | assertThat(filter.matches(scanResult)).isFalse(); 65 | } 66 | 67 | @Test 68 | public void testDeviceFilter() { 69 | ScanFilter filter = filterBuilder.setDeviceAddress(DEVICE_MAC).build(); 70 | assertThat(filter.matches(scanResult)).isTrue(); 71 | filter = filterBuilder.setDeviceAddress("11:22:33:44:55:66").build(); 72 | assertThat(filter.matches(scanResult)).isFalse(); 73 | } 74 | 75 | @Test 76 | public void testSetServiceUuidFilter() { 77 | ScanFilter filter = filterBuilder 78 | .setServiceUuid(ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB")) 79 | .build(); 80 | assertThat(filter.matches(scanResult)).isTrue(); 81 | filter = filterBuilder 82 | .setServiceUuid(ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB")) 83 | .build(); 84 | assertThat(filter.matches(scanResult)).isFalse(); 85 | filter = filterBuilder 86 | .setServiceUuid( 87 | ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"), 88 | ParcelUuid.fromString("FFFFFFF0-FFFF-FFFF-FFFF-FFFFFFFFFFFF") 89 | ) 90 | .build(); 91 | assertThat(filter.matches(scanResult)).isTrue(); 92 | } 93 | 94 | @Test 95 | public void testSetServiceDataFilter() { 96 | final byte[] setServiceData = new byte[]{ 97 | 0x50, 0x64 98 | }; 99 | ParcelUuid serviceDataUuid = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"); 100 | ScanFilter filter = filterBuilder.setServiceData(serviceDataUuid, setServiceData).build(); 101 | assertThat(filter.matches(scanResult)).isTrue(); 102 | byte[] emptyData = new byte[0]; 103 | filter = filterBuilder.setServiceData(serviceDataUuid, emptyData).build(); 104 | assertThat(filter.matches(scanResult)).isTrue(); 105 | byte[] prefixData = new byte[]{ 106 | 0x50 107 | }; 108 | filter = filterBuilder.setServiceData(serviceDataUuid, prefixData).build(); 109 | assertThat(filter.matches(scanResult)).isTrue(); 110 | final byte[] nonMatchData = new byte[]{ 111 | 0x51, 0x64 112 | }; 113 | final byte[] mask = new byte[]{ 114 | (byte) 0x00, (byte) 0xFF 115 | }; 116 | filter = filterBuilder.setServiceData(serviceDataUuid, nonMatchData, mask).build(); 117 | assertThat(filter.matches(scanResult)).isTrue(); 118 | filter = filterBuilder.setServiceData(serviceDataUuid, nonMatchData).build(); 119 | assertThat(filter.matches(scanResult)).isFalse(); 120 | } 121 | 122 | @Test 123 | public void testManufacturerSpecificData() { 124 | final byte[] setManufacturerData = new byte[]{ 125 | 0x02, 0x15 126 | }; 127 | final int manufacturerId = 0xE0; 128 | ScanFilter filter = 129 | filterBuilder.setManufacturerData(manufacturerId, setManufacturerData).build(); 130 | assertThat(filter.matches(scanResult)).isTrue(); 131 | byte[] emptyData = new byte[0]; 132 | filter = filterBuilder.setManufacturerData(manufacturerId, emptyData).build(); 133 | assertThat(filter.matches(scanResult)).isTrue(); 134 | byte[] prefixData = new byte[]{ 135 | 0x02 136 | }; 137 | filter = filterBuilder.setManufacturerData(manufacturerId, prefixData).build(); 138 | assertThat(filter.matches(scanResult)).isTrue(); 139 | // Test data mask 140 | byte[] nonMatchData = new byte[]{ 141 | 0x02, 0x14 142 | }; 143 | filter = filterBuilder.setManufacturerData(manufacturerId, nonMatchData).build(); 144 | assertThat(filter.matches(scanResult)).isFalse(); 145 | byte[] mask = new byte[]{ 146 | (byte) 0xFF, (byte) 0x00 147 | }; 148 | filter = filterBuilder.setManufacturerData(manufacturerId, nonMatchData, mask).build(); 149 | assertThat(filter.matches(scanResult)).isTrue(); 150 | } 151 | 152 | @Test 153 | public void testReadWriteParcel() { 154 | ScanFilter filter = filterBuilder.build(); 155 | testReadWriteParcelForFilter(filter); 156 | filter = filterBuilder.setDeviceName("Ped").build(); 157 | testReadWriteParcelForFilter(filter); 158 | filter = filterBuilder.setDeviceAddress("11:22:33:44:55:66").build(); 159 | testReadWriteParcelForFilter(filter); 160 | filter = 161 | filterBuilder.setServiceUuid(ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB")) 162 | .build(); 163 | testReadWriteParcelForFilter(filter); 164 | filter = 165 | filterBuilder.setServiceUuid(ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"), 166 | ParcelUuid.fromString("FFFFFFF0-FFFF-FFFF-FFFF-FFFFFFFFFFFF")).build(); 167 | testReadWriteParcelForFilter(filter); 168 | final byte[] serviceData = new byte[]{ 169 | 0x50, 0x64 170 | }; 171 | final ParcelUuid serviceDataUuid = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"); 172 | filter = filterBuilder.setServiceData(serviceDataUuid, serviceData).build(); 173 | testReadWriteParcelForFilter(filter); 174 | filter = filterBuilder.setServiceData(serviceDataUuid, new byte[0]).build(); 175 | testReadWriteParcelForFilter(filter); 176 | final byte[] serviceDataMask = new byte[]{ 177 | (byte) 0xFF, (byte) 0xFF 178 | }; 179 | filter = filterBuilder.setServiceData(serviceDataUuid, serviceData, serviceDataMask).build(); 180 | testReadWriteParcelForFilter(filter); 181 | final byte[] manufacturerData = new byte[]{ 182 | 0x02, 0x15 183 | }; 184 | final int manufacturerId = 0xE0; 185 | filter = filterBuilder.setManufacturerData(manufacturerId, manufacturerData).build(); 186 | testReadWriteParcelForFilter(filter); 187 | filter = filterBuilder.setServiceData(serviceDataUuid, new byte[0]).build(); 188 | testReadWriteParcelForFilter(filter); 189 | final byte[] manufacturerDataMask = new byte[]{ 190 | (byte) 0xFF, (byte) 0xFF 191 | }; 192 | filter = filterBuilder 193 | .setManufacturerData(manufacturerId, manufacturerData, manufacturerDataMask) 194 | .build(); 195 | testReadWriteParcelForFilter(filter); 196 | } 197 | 198 | private void testReadWriteParcelForFilter(final ScanFilter filter) { 199 | final Parcel parcel = Parcel.obtain(); 200 | filter.writeToParcel(parcel, 0); 201 | parcel.setDataPosition(0); 202 | final ScanFilter filterFromParcel = ScanFilter.CREATOR.createFromParcel(parcel); 203 | assertThat(filter).isEqualTo(filterFromParcel); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /scanner/src/androidTest/java/no/nordicsemi/android/support/v18/scanner/ScanRecordTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package no.nordicsemi.android.support.v18.scanner; 18 | 19 | import android.os.ParcelUuid; 20 | 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | 24 | import androidx.test.ext.junit.runners.AndroidJUnit4; 25 | 26 | import static com.google.common.truth.Truth.assertThat; 27 | 28 | @RunWith(AndroidJUnit4.class) 29 | public class ScanRecordTest { 30 | 31 | @Test 32 | public void testParser() { 33 | final byte[] scanRecord = new byte[]{ 34 | 0x02, 0x01, 0x1a, // Flags 35 | 0x05, 0x02, 0x0b, 0x11, 0x0a, 0x11, // Incomplete List of 16-bit Service Class UUIDs 36 | 0x04, 0x09, 0x50, 0x65, 0x64, // Complete Local Name 37 | 0x02, 0x0A, (byte) 0xec, // Tx Power Level 38 | 0x05, 0x16, 0x0b, 0x11, 0x50, 0x64, // Service Data - 16-bit UUID 39 | 0x05, (byte) 0xff, (byte) 0xe0, 0x00, 0x02, 0x15, // Manufacturer Specific Data 40 | 0x03, 0x50, 0x01, 0x02, // An unknown data type won't cause trouble 41 | }; 42 | final ScanRecord data = ScanRecord.parseFromBytes(scanRecord); 43 | assertThat(data).isNotNull(); 44 | assertThat(data.getAdvertiseFlags()).isEqualTo(0x1a); 45 | final ParcelUuid uuid1 = ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB"); 46 | final ParcelUuid uuid2 = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"); 47 | assertThat(data.getServiceUuids()).contains(uuid1); 48 | assertThat(data.getServiceUuids()).contains(uuid2); 49 | assertThat(data.getDeviceName()).isEqualTo("Ped"); 50 | assertThat(data.getTxPowerLevel()).isEqualTo(-20); 51 | assertThat(data.getManufacturerSpecificData()).isNotNull(); 52 | assertThat(data.getManufacturerSpecificData().get(0x00E0)).isNotNull(); 53 | assertThat(data.getManufacturerSpecificData().get(0x00E0)) 54 | .isEqualTo(new byte[] { 0x02, 0x15}); 55 | assertThat(data.getServiceData()).isNotNull(); 56 | assertThat(data.getServiceData()).containsKey(uuid2); 57 | assertThat(data.getServiceData().get(uuid2)) 58 | .isEqualTo(new byte[] { 0x50, 0x64}); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scanner/src/androidTest/java/no/nordicsemi/android/support/v18/scanner/ScanResultTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package no.nordicsemi.android.support.v18.scanner; 18 | 19 | import android.bluetooth.BluetoothAdapter; 20 | import android.bluetooth.BluetoothDevice; 21 | import android.os.Parcel; 22 | 23 | import org.junit.Test; 24 | import org.junit.runner.RunWith; 25 | 26 | import androidx.test.ext.junit.runners.AndroidJUnit4; 27 | 28 | import static com.google.common.truth.Truth.assertThat; 29 | 30 | @RunWith(AndroidJUnit4.class) 31 | public class ScanResultTest { 32 | 33 | @Test 34 | public void testScanResultParceling() { 35 | final BluetoothDevice device = BluetoothAdapter.getDefaultAdapter() 36 | .getRemoteDevice("01:02:03:04:05:06"); 37 | final byte[] scanRecord = new byte[] { 2, 1, 3 }; 38 | final ScanResult result = 39 | new ScanResult(device, ScanRecord.parseFromBytes(scanRecord), -10, 10000L); 40 | final Parcel parcel = Parcel.obtain(); 41 | result.writeToParcel(parcel, 0); 42 | // Need to reset parcel data position to the beginning. 43 | parcel.setDataPosition(0); 44 | ScanResult resultFromParcel = ScanResult.CREATOR.createFromParcel(parcel); 45 | assertThat(result).isEqualTo(resultFromParcel); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scanner/src/androidTest/java/no/nordicsemi/android/support/v18/scanner/ScanSettingsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package no.nordicsemi.android.support.v18.scanner; 18 | 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | 22 | import androidx.test.ext.junit.runners.AndroidJUnit4; 23 | 24 | import static org.junit.Assert.assertThrows; 25 | 26 | @RunWith(AndroidJUnit4.class) 27 | public class ScanSettingsTest { 28 | 29 | @Test 30 | public void testCallbackType() { 31 | final ScanSettings.Builder builder = new ScanSettings.Builder(); 32 | builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES); 33 | builder.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH); 34 | builder.setCallbackType(ScanSettings.CALLBACK_TYPE_MATCH_LOST); 35 | builder.setCallbackType( 36 | ScanSettings.CALLBACK_TYPE_FIRST_MATCH | ScanSettings.CALLBACK_TYPE_MATCH_LOST); 37 | assertThrows(IllegalArgumentException.class, () -> 38 | builder.setCallbackType( 39 | ScanSettings.CALLBACK_TYPE_ALL_MATCHES | ScanSettings.CALLBACK_TYPE_MATCH_LOST) 40 | ); 41 | assertThrows(IllegalArgumentException.class, () -> 42 | builder.setCallbackType( 43 | ScanSettings.CALLBACK_TYPE_ALL_MATCHES | ScanSettings.CALLBACK_TYPE_FIRST_MATCH) 44 | ); 45 | assertThrows(IllegalArgumentException.class, () -> 46 | builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES | 47 | ScanSettings.CALLBACK_TYPE_FIRST_MATCH | 48 | ScanSettings.CALLBACK_TYPE_MATCH_LOST) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scanner/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 12 | 15 | 21 | 23 | 24 | 31 | 32 | 33 | 38 | 41 | 42 | 48 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeScannerImplJB.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * 12 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 13 | * software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 20 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | */ 22 | 23 | package no.nordicsemi.android.support.v18.scanner; 24 | 25 | import android.app.PendingIntent; 26 | import android.bluetooth.BluetoothAdapter; 27 | import android.content.Context; 28 | import android.content.Intent; 29 | import android.os.Handler; 30 | import android.os.HandlerThread; 31 | import android.os.SystemClock; 32 | 33 | import androidx.annotation.NonNull; 34 | import androidx.annotation.Nullable; 35 | 36 | import java.util.ArrayList; 37 | import java.util.Collection; 38 | import java.util.List; 39 | 40 | 41 | @SuppressWarnings("deprecation") 42 | /* package */ class BluetoothLeScannerImplJB extends BluetoothLeScannerCompat { 43 | 44 | /** 45 | * A set of scan callback wrappers. Each wrapper contains a weak reference to the scan 46 | * callback given by the user. 47 | */ 48 | @NonNull private final ScanCallbackWrapperSet wrappers = new ScanCallbackWrapperSet<>(); 49 | 50 | @Nullable private HandlerThread handlerThread; 51 | @Nullable private Handler powerSaveHandler; 52 | 53 | private long powerSaveRestInterval; 54 | private long powerSaveScanInterval; 55 | 56 | private final Runnable powerSaveSleepTask = new Runnable() { 57 | @Override 58 | public void run() { 59 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 60 | if (adapter != null && powerSaveRestInterval > 0 && powerSaveScanInterval > 0) { 61 | adapter.stopLeScan(scanCallback); 62 | powerSaveHandler.postDelayed(powerSaveScanTask, powerSaveRestInterval); 63 | } 64 | } 65 | }; 66 | 67 | private final Runnable powerSaveScanTask = new Runnable() { 68 | @Override 69 | public void run() { 70 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 71 | if (adapter != null && powerSaveRestInterval > 0 && powerSaveScanInterval > 0) { 72 | adapter.startLeScan(scanCallback); 73 | powerSaveHandler.postDelayed(powerSaveSleepTask, powerSaveScanInterval); 74 | } 75 | } 76 | }; 77 | 78 | /* package */ BluetoothLeScannerImplJB() {} 79 | 80 | @Override 81 | /* package */ void startScanInternal(@NonNull final List filters, 82 | @NonNull final ScanSettings settings, 83 | @NonNull final ScanCallback callback, 84 | @NonNull final Handler handler) { 85 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 86 | 87 | boolean shouldStart; 88 | 89 | synchronized (wrappers) { 90 | if (wrappers.contains(callback)) { 91 | throw new IllegalArgumentException("scanner already started with given scanCallback"); 92 | } 93 | final UserScanCallbackWrapper callbackWrapper = new UserScanCallbackWrapper(callback); 94 | final ScanCallbackWrapper wrapper = new ScanCallbackWrapper( 95 | false, false, 96 | filters, settings, callbackWrapper, handler); 97 | shouldStart = wrappers.isEmpty(); 98 | wrappers.add(wrapper); 99 | } 100 | 101 | if (handlerThread == null) { 102 | handlerThread = new HandlerThread(BluetoothLeScannerImplJB.class.getName()); 103 | handlerThread.start(); 104 | powerSaveHandler = new Handler(handlerThread.getLooper()); 105 | } 106 | 107 | setPowerSaveSettings(); 108 | 109 | if (shouldStart) { 110 | adapter.startLeScan(scanCallback); 111 | } 112 | } 113 | 114 | @Override 115 | /* package */ void stopScanInternal(@NonNull final ScanCallback callback) { 116 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 117 | 118 | boolean shouldStop; 119 | ScanCallbackWrapper wrapper; 120 | synchronized (wrappers) { 121 | wrapper = wrappers.remove(callback); 122 | shouldStop = wrappers.isEmpty(); 123 | } 124 | if (wrapper == null) 125 | return; 126 | 127 | wrapper.close(); 128 | 129 | setPowerSaveSettings(); 130 | 131 | if (shouldStop) { 132 | adapter.stopLeScan(scanCallback); 133 | 134 | if (powerSaveHandler != null) { 135 | powerSaveHandler.removeCallbacksAndMessages(null); 136 | } 137 | 138 | if (handlerThread != null) { 139 | handlerThread.quitSafely(); 140 | handlerThread = null; 141 | } 142 | } 143 | } 144 | 145 | @Override 146 | /* package */ void startScanInternal(@NonNull final List filters, 147 | @NonNull final ScanSettings settings, 148 | @NonNull final Context context, 149 | @NonNull final PendingIntent callbackIntent, 150 | final int requestCode) { 151 | final Intent service = new Intent(context, ScannerService.class); 152 | service.putParcelableArrayListExtra(ScannerService.EXTRA_FILTERS, new ArrayList<>(filters)); 153 | service.putExtra(ScannerService.EXTRA_SETTINGS, settings); 154 | service.putExtra(ScannerService.EXTRA_PENDING_INTENT, callbackIntent); 155 | service.putExtra(ScannerService.EXTRA_REQUEST_CODE, requestCode); 156 | service.putExtra(ScannerService.EXTRA_START, true); 157 | context.startService(service); 158 | } 159 | 160 | @Override 161 | /* package */ void stopScanInternal(@NonNull final Context context, 162 | @NonNull final PendingIntent callbackIntent, 163 | final int requestCode) { 164 | final Intent service = new Intent(context, ScannerService.class); 165 | service.putExtra(ScannerService.EXTRA_PENDING_INTENT, callbackIntent); 166 | service.putExtra(ScannerService.EXTRA_REQUEST_CODE, requestCode); 167 | service.putExtra(ScannerService.EXTRA_START, false); 168 | context.startService(service); 169 | } 170 | 171 | @Override 172 | public void flushPendingScanResults(@NonNull final ScanCallback callback) { 173 | //noinspection ConstantConditions 174 | if (callback == null) { 175 | throw new IllegalArgumentException("callback cannot be null!"); 176 | } 177 | 178 | ScanCallbackWrapper wrapper; 179 | synchronized (wrappers) { 180 | wrapper = wrappers.get(callback); 181 | } 182 | 183 | if (wrapper == null) { 184 | throw new IllegalArgumentException("callback not registered!"); 185 | } 186 | 187 | wrapper.flushPendingScanResults(); 188 | } 189 | 190 | /** 191 | * This method goes through registered callbacks and sets the power rest and scan intervals 192 | * to next lowest value. 193 | */ 194 | private void setPowerSaveSettings() { 195 | long minRest = Long.MAX_VALUE, minScan = Long.MAX_VALUE; 196 | synchronized (wrappers) { 197 | for (final ScanCallbackWrapper wrapper : wrappers.values()) { 198 | final ScanSettings settings = wrapper.scanSettings; 199 | if (settings.hasPowerSaveMode()) { 200 | if (minRest > settings.getPowerSaveRest()) { 201 | minRest = settings.getPowerSaveRest(); 202 | } 203 | if (minScan > settings.getPowerSaveScan()) { 204 | minScan = settings.getPowerSaveScan(); 205 | } 206 | } 207 | } 208 | } 209 | if (minRest < Long.MAX_VALUE && minScan < Long.MAX_VALUE) { 210 | powerSaveRestInterval = minRest; 211 | powerSaveScanInterval = minScan; 212 | if (powerSaveHandler != null) { 213 | powerSaveHandler.removeCallbacks(powerSaveScanTask); 214 | powerSaveHandler.removeCallbacks(powerSaveSleepTask); 215 | powerSaveHandler.postDelayed(powerSaveSleepTask, powerSaveScanInterval); 216 | } 217 | } else { 218 | powerSaveRestInterval = powerSaveScanInterval = 0; 219 | if (powerSaveHandler != null) { 220 | powerSaveHandler.removeCallbacks(powerSaveScanTask); 221 | powerSaveHandler.removeCallbacks(powerSaveSleepTask); 222 | } 223 | } 224 | } 225 | 226 | private final BluetoothAdapter.LeScanCallback scanCallback = (device, rssi, scanRecord) -> { 227 | final ScanResult scanResult = new ScanResult(device, ScanRecord.parseFromBytes(scanRecord), 228 | rssi, SystemClock.elapsedRealtimeNanos()); 229 | 230 | synchronized (wrappers) { 231 | final Collection scanCallbackWrappers = wrappers.values(); 232 | for (final ScanCallbackWrapper wrapper : scanCallbackWrappers) { 233 | wrapper.handler.post(() -> wrapper.handleScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanResult)); 234 | } 235 | } 236 | }; 237 | } -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeScannerImplLollipop.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * 12 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 13 | * software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 20 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | */ 22 | 23 | package no.nordicsemi.android.support.v18.scanner; 24 | 25 | import android.annotation.TargetApi; 26 | import android.app.PendingIntent; 27 | import android.bluetooth.BluetoothAdapter; 28 | import android.bluetooth.le.BluetoothLeScanner; 29 | import android.content.Context; 30 | import android.content.Intent; 31 | import android.os.Build; 32 | import android.os.Handler; 33 | import android.os.SystemClock; 34 | 35 | import androidx.annotation.NonNull; 36 | 37 | import java.util.ArrayList; 38 | import java.util.List; 39 | 40 | @SuppressWarnings({"deprecation", "WeakerAccess"}) 41 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 42 | /* package */ class BluetoothLeScannerImplLollipop extends BluetoothLeScannerCompat { 43 | 44 | /** 45 | * A set of scan callback wrappers. Each wrapper contains a weak reference to the scan 46 | * callback given by the user. 47 | */ 48 | @NonNull private final ScanCallbackWrapperSet wrappers = new ScanCallbackWrapperSet<>(); 49 | 50 | /* package */ BluetoothLeScannerImplLollipop() {} 51 | 52 | @Override 53 | /* package */ void startScanInternal(@NonNull final List filters, 54 | @NonNull final ScanSettings settings, 55 | @NonNull final ScanCallback callback, 56 | @NonNull final Handler handler) { 57 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 58 | final BluetoothLeScanner scanner = adapter.getBluetoothLeScanner(); 59 | if (scanner == null) 60 | throw new IllegalStateException("BT le scanner not available"); 61 | 62 | final boolean offloadedBatchingSupported = adapter.isOffloadedScanBatchingSupported(); 63 | final boolean offloadedFilteringSupported = adapter.isOffloadedFilteringSupported(); 64 | 65 | ScanCallbackWrapperLollipop wrapper; 66 | 67 | synchronized (wrappers) { 68 | if (wrappers.contains(callback)) { 69 | throw new IllegalArgumentException("scanner already started with given callback"); 70 | } 71 | final UserScanCallbackWrapper callbackWrapper = new UserScanCallbackWrapper(callback); 72 | wrapper = new ScanCallbackWrapperLollipop(offloadedBatchingSupported, 73 | offloadedFilteringSupported, filters, settings, callbackWrapper, handler); 74 | wrappers.add(wrapper); 75 | } 76 | 77 | final android.bluetooth.le.ScanSettings nativeScanSettings = toNativeScanSettings(adapter, settings, false); 78 | List nativeScanFilters = null; 79 | if (!filters.isEmpty() && offloadedFilteringSupported && settings.getUseHardwareFilteringIfSupported()) 80 | nativeScanFilters = toNativeScanFilters(filters); 81 | 82 | scanner.startScan(nativeScanFilters, nativeScanSettings, wrapper.nativeCallback); 83 | } 84 | 85 | @Override 86 | /* package */ void stopScanInternal(@NonNull final ScanCallback callback) { 87 | ScanCallbackWrapperLollipop wrapper; 88 | synchronized (wrappers) { 89 | wrapper = wrappers.remove(callback); 90 | } 91 | if (wrapper == null) 92 | return; 93 | 94 | wrapper.close(); 95 | 96 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 97 | if (adapter != null) { 98 | final BluetoothLeScanner scanner = adapter.getBluetoothLeScanner(); 99 | if (scanner != null) 100 | scanner.stopScan(wrapper.nativeCallback); 101 | } 102 | } 103 | 104 | @Override 105 | /* package */ void startScanInternal(@NonNull final List filters, 106 | @NonNull final ScanSettings settings, 107 | @NonNull final Context context, 108 | @NonNull final PendingIntent callbackIntent, 109 | final int requestCode) { 110 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 111 | final BluetoothLeScanner scanner = adapter.getBluetoothLeScanner(); 112 | if (scanner == null) 113 | throw new IllegalStateException("BT le scanner not available"); 114 | 115 | final Intent service = new Intent(context, ScannerService.class); 116 | service.putParcelableArrayListExtra(ScannerService.EXTRA_FILTERS, new ArrayList<>(filters)); 117 | service.putExtra(ScannerService.EXTRA_SETTINGS, settings); 118 | service.putExtra(ScannerService.EXTRA_PENDING_INTENT, callbackIntent); 119 | service.putExtra(ScannerService.EXTRA_REQUEST_CODE, requestCode); 120 | service.putExtra(ScannerService.EXTRA_START, true); 121 | context.startService(service); 122 | } 123 | 124 | @Override 125 | /* package */ void stopScanInternal(@NonNull final Context context, 126 | @NonNull final PendingIntent callbackIntent, 127 | final int requestCode) { 128 | final Intent service = new Intent(context, ScannerService.class); 129 | service.putExtra(ScannerService.EXTRA_PENDING_INTENT, callbackIntent); 130 | service.putExtra(ScannerService.EXTRA_REQUEST_CODE, requestCode); 131 | service.putExtra(ScannerService.EXTRA_START, false); 132 | context.startService(service); 133 | } 134 | 135 | @Override 136 | public void flushPendingScanResults(@NonNull final ScanCallback callback) { 137 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 138 | //noinspection ConstantConditions 139 | if (callback == null) { 140 | throw new IllegalArgumentException("callback cannot be null!"); 141 | } 142 | 143 | ScanCallbackWrapperLollipop wrapper; 144 | synchronized (wrappers) { 145 | wrapper = wrappers.get(callback); 146 | } 147 | 148 | if (wrapper == null) { 149 | throw new IllegalArgumentException("callback not registered!"); 150 | } 151 | 152 | final ScanSettings settings = wrapper.scanSettings; 153 | if (adapter.isOffloadedScanBatchingSupported() && settings.getUseHardwareBatchingIfSupported()) { 154 | final BluetoothLeScanner scanner = adapter.getBluetoothLeScanner(); 155 | if (scanner == null) 156 | return; 157 | scanner.flushPendingScanResults(wrapper.nativeCallback); 158 | } else { 159 | wrapper.flushPendingScanResults(); 160 | } 161 | } 162 | 163 | @NonNull 164 | /* package */ android.bluetooth.le.ScanSettings toNativeScanSettings(@NonNull final BluetoothAdapter adapter, 165 | @NonNull final ScanSettings settings, 166 | final boolean exactCopy) { 167 | final android.bluetooth.le.ScanSettings.Builder builder = 168 | new android.bluetooth.le.ScanSettings.Builder(); 169 | 170 | if (exactCopy || adapter.isOffloadedScanBatchingSupported() && settings.getUseHardwareBatchingIfSupported()) 171 | builder.setReportDelay(settings.getReportDelayMillis()); 172 | 173 | if (settings.getScanMode() != ScanSettings.SCAN_MODE_OPPORTUNISTIC) { 174 | builder.setScanMode(settings.getScanMode()); 175 | } else { 176 | // SCAN MORE OPPORTUNISTIC is not supported on Lollipop. 177 | // Instead, SCAN_MODE_LOW_POWER will be used. 178 | builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER); 179 | } 180 | 181 | settings.disableUseHardwareCallbackTypes(); // callback types other then CALLBACK_TYPE_ALL_MATCHES are not supported on Lollipop 182 | 183 | return builder.build(); 184 | } 185 | 186 | @NonNull 187 | /* package */ ArrayList toNativeScanFilters(@NonNull final List filters) { 188 | final ArrayList nativeScanFilters = new ArrayList<>(); 189 | for (final ScanFilter filter : filters) 190 | nativeScanFilters.add(toNativeScanFilter(filter)); 191 | return nativeScanFilters; 192 | } 193 | 194 | @NonNull 195 | /* package */ android.bluetooth.le.ScanFilter toNativeScanFilter(@NonNull final ScanFilter filter) { 196 | final android.bluetooth.le.ScanFilter.Builder builder = new android.bluetooth.le.ScanFilter.Builder(); 197 | builder.setServiceUuid(filter.getServiceUuid(), filter.getServiceUuidMask()) 198 | .setManufacturerData(filter.getManufacturerId(), filter.getManufacturerData(), filter.getManufacturerDataMask()); 199 | 200 | if (filter.getDeviceAddress() != null) 201 | builder.setDeviceAddress(filter.getDeviceAddress()); 202 | 203 | if (filter.getDeviceName() != null) 204 | builder.setDeviceName(filter.getDeviceName()); 205 | 206 | if (filter.getServiceDataUuid() != null) 207 | builder.setServiceData(filter.getServiceDataUuid(), filter.getServiceData(), filter.getServiceDataMask()); 208 | 209 | return builder.build(); 210 | } 211 | 212 | @NonNull 213 | /* package */ ScanResult fromNativeScanResult(@NonNull final android.bluetooth.le.ScanResult nativeScanResult) { 214 | final byte[] data = nativeScanResult.getScanRecord() != null ? 215 | nativeScanResult.getScanRecord().getBytes() : null; 216 | return new ScanResult(nativeScanResult.getDevice(), ScanRecord.parseFromBytes(data), 217 | nativeScanResult.getRssi(), nativeScanResult.getTimestampNanos()); 218 | } 219 | 220 | @NonNull 221 | /* package */ ArrayList fromNativeScanResults(@NonNull final List nativeScanResults) { 222 | final ArrayList results = new ArrayList<>(); 223 | for (final android.bluetooth.le.ScanResult nativeScanResult : nativeScanResults) { 224 | final ScanResult result = fromNativeScanResult(nativeScanResult); 225 | results.add(result); 226 | } 227 | return results; 228 | } 229 | 230 | /* package */ static class ScanCallbackWrapperLollipop extends ScanCallbackWrapper { 231 | 232 | private ScanCallbackWrapperLollipop(final boolean offloadedBatchingSupported, 233 | final boolean offloadedFilteringSupported, 234 | @NonNull final List filters, 235 | @NonNull final ScanSettings settings, 236 | @NonNull final ScanCallback callback, 237 | @NonNull final Handler handler) { 238 | super(offloadedBatchingSupported, offloadedFilteringSupported, 239 | filters, settings, callback, handler); 240 | } 241 | 242 | @NonNull 243 | private final android.bluetooth.le.ScanCallback nativeCallback = new android.bluetooth.le.ScanCallback() { 244 | private long lastBatchTimestamp; 245 | 246 | @Override 247 | public void onScanResult(final int callbackType, final android.bluetooth.le.ScanResult nativeScanResult) { 248 | handler.post(() -> { 249 | final BluetoothLeScannerImplLollipop scannerImpl = 250 | (BluetoothLeScannerImplLollipop) BluetoothLeScannerCompat.getScanner(); 251 | final ScanResult result = scannerImpl.fromNativeScanResult(nativeScanResult); 252 | handleScanResult(callbackType, result); 253 | }); 254 | } 255 | 256 | @Override 257 | public void onBatchScanResults(final List nativeScanResults) { 258 | handler.post(() -> { 259 | // On several phones the onBatchScanResults is called twice for every batch. 260 | // Skip the second call if came to early. 261 | final long now = SystemClock.elapsedRealtime(); 262 | if (lastBatchTimestamp > now - scanSettings.getReportDelayMillis() + 5) { 263 | return; 264 | } 265 | lastBatchTimestamp = now; 266 | 267 | final BluetoothLeScannerImplLollipop scannerImpl = 268 | (BluetoothLeScannerImplLollipop) BluetoothLeScannerCompat.getScanner(); 269 | final List results = scannerImpl.fromNativeScanResults(nativeScanResults); 270 | handleScanResults(results); 271 | }); 272 | } 273 | 274 | @Override 275 | public void onScanFailed(final int errorCode) { 276 | handler.post(() -> { 277 | // We were able to determine offloaded batching and filtering before we started scan, 278 | // but there is no method checking if callback types FIRST_MATCH and MATCH_LOST 279 | // are supported. We get an error here it they are not. 280 | if (scanSettings.getUseHardwareCallbackTypesIfSupported() 281 | && scanSettings.getCallbackType() != ScanSettings.CALLBACK_TYPE_ALL_MATCHES) { 282 | // On Nexus 6 with Android 6.0 (MPA44G, M Pre-release 3) the errorCode = 5 (SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES) 283 | // On Pixel 2 with Android 9.0 the errorCode = 4 (SCAN_FAILED_FEATURE_UNSUPPORTED) 284 | 285 | // This feature seems to be not supported on your phone. 286 | // Let's try to do pretty much the same in the code. 287 | scanSettings.disableUseHardwareCallbackTypes(); 288 | 289 | final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 290 | try { 291 | scanner.stopScan(scanCallback); 292 | } catch (final Exception e) { 293 | // Ignore 294 | } 295 | try { 296 | scanner.startScanInternal(filters, scanSettings, scanCallback, handler); 297 | } catch (final Exception e) { 298 | // Ignore 299 | } 300 | return; 301 | } 302 | 303 | // else, notify user application 304 | handleScanError(errorCode); 305 | }); 306 | } 307 | }; 308 | } 309 | } -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeScannerImplMarshmallow.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * 12 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 13 | * software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 20 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | */ 22 | 23 | package no.nordicsemi.android.support.v18.scanner; 24 | 25 | import android.annotation.TargetApi; 26 | import android.bluetooth.BluetoothAdapter; 27 | import android.os.Build; 28 | 29 | import androidx.annotation.NonNull; 30 | 31 | @TargetApi(Build.VERSION_CODES.M) 32 | /* package */ class BluetoothLeScannerImplMarshmallow extends BluetoothLeScannerImplLollipop { 33 | 34 | @NonNull 35 | @Override 36 | /* package */ android.bluetooth.le.ScanSettings toNativeScanSettings(@NonNull final BluetoothAdapter adapter, 37 | @NonNull final ScanSettings settings, 38 | final boolean exactCopy) { 39 | final android.bluetooth.le.ScanSettings.Builder builder = 40 | new android.bluetooth.le.ScanSettings.Builder(); 41 | 42 | if (exactCopy || adapter.isOffloadedScanBatchingSupported() && settings.getUseHardwareBatchingIfSupported()) 43 | builder.setReportDelay(settings.getReportDelayMillis()); 44 | 45 | if (exactCopy || settings.getUseHardwareCallbackTypesIfSupported()) 46 | builder.setCallbackType(settings.getCallbackType()) 47 | .setMatchMode(settings.getMatchMode()) 48 | .setNumOfMatches(settings.getNumOfMatches()); 49 | 50 | builder.setScanMode(settings.getScanMode()); 51 | 52 | return builder.build(); 53 | } 54 | } -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeScannerImplOreo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * 12 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 13 | * software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 20 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | */ 22 | 23 | package no.nordicsemi.android.support.v18.scanner; 24 | 25 | import android.annotation.TargetApi; 26 | import android.app.PendingIntent; 27 | import android.bluetooth.BluetoothAdapter; 28 | import android.bluetooth.le.BluetoothLeScanner; 29 | import android.content.Context; 30 | import android.content.Intent; 31 | import android.os.Build; 32 | import android.os.Handler; 33 | 34 | import androidx.annotation.NonNull; 35 | import androidx.annotation.Nullable; 36 | 37 | import java.util.ArrayList; 38 | import java.util.Collections; 39 | import java.util.HashMap; 40 | import java.util.List; 41 | 42 | @TargetApi(Build.VERSION_CODES.O) 43 | /* package */ class BluetoothLeScannerImplOreo extends BluetoothLeScannerImplMarshmallow { 44 | 45 | /** 46 | * A map that stores {@link PendingIntentExecutorWrapper}s for user's {@link PendingIntent}. 47 | * Each wrapper keeps track of found and lost devices and allows to emulate batching. 48 | */ 49 | // The type is HashMap, not Map, as the Map does not allow to put null as values. 50 | @NonNull private final HashMap wrappers = new HashMap<>(); 51 | 52 | /** 53 | * Returns a wrapper associated with the given {@link PendingIntent}, null when there is 54 | * no such wrapper yet (it has never been created, or the app was killed and the 55 | * {@link BluetoothLeScannerCompat} has been recreated and the previous wrapper was 56 | * destroyed, or throws {@link IllegalStateException} when scanning was stopped for this 57 | * callback intent. 58 | * 59 | * @param callbackIntent User's callback intent used in 60 | * {@link BluetoothLeScannerCompat#startScan(List, ScanSettings, Context, PendingIntent)}. 61 | * @return The wrapper or null if no such wrapper was created yet. 62 | */ 63 | @Nullable 64 | /* package */ PendingIntentExecutorWrapper getWrapper(@NonNull final PendingIntent callbackIntent) { 65 | synchronized (wrappers) { 66 | if (wrappers.containsKey(callbackIntent)) { 67 | final PendingIntentExecutorWrapper wrapper = wrappers.get(callbackIntent); 68 | if (wrapper == null) 69 | throw new IllegalStateException("Scanning has been stopped"); 70 | return wrapper; 71 | } 72 | return null; 73 | } 74 | } 75 | 76 | /* package */ void addWrapper(@NonNull final PendingIntent callbackIntent, 77 | @NonNull final PendingIntentExecutorWrapper wrapper) { 78 | synchronized (wrappers) { 79 | wrappers.put(callbackIntent, wrapper); 80 | } 81 | } 82 | 83 | @Override 84 | /* package */ void startScanInternal(@Nullable final List filters, 85 | @Nullable final ScanSettings settings, 86 | @NonNull final Context context, 87 | @NonNull final PendingIntent callbackIntent, 88 | final int requestCode) { 89 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 90 | final BluetoothLeScanner scanner = adapter.getBluetoothLeScanner(); 91 | if (scanner == null) 92 | throw new IllegalStateException("BT le scanner not available"); 93 | 94 | final ScanSettings nonNullSettings = settings != null ? settings : new ScanSettings.Builder().build(); 95 | final List nonNullFilters = filters != null ? filters : Collections.emptyList(); 96 | 97 | final android.bluetooth.le.ScanSettings nativeSettings = toNativeScanSettings(adapter, nonNullSettings, false); 98 | List nativeFilters = null; 99 | if (filters != null && adapter.isOffloadedFilteringSupported() && nonNullSettings.getUseHardwareFilteringIfSupported()) 100 | nativeFilters = toNativeScanFilters(filters); 101 | 102 | synchronized (wrappers) { 103 | // Make sure there is not such callbackIntent in the map. 104 | // The value could have been set to null when the same intent was used before. 105 | wrappers.remove(callbackIntent); 106 | } 107 | 108 | final PendingIntent pendingIntent = createStartingPendingIntent(nonNullFilters, 109 | nonNullSettings, context, callbackIntent, requestCode); 110 | scanner.startScan(nativeFilters, nativeSettings, pendingIntent); 111 | } 112 | 113 | /* package */ void stopScanInternal(@NonNull final Context context, 114 | @NonNull final PendingIntent callbackIntent, 115 | final int requestCode) { 116 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 117 | final BluetoothLeScanner scanner = adapter.getBluetoothLeScanner(); 118 | if (scanner == null) 119 | throw new IllegalStateException("BT le scanner not available"); 120 | 121 | final PendingIntent pendingIntent = createStoppingPendingIntent(context, requestCode); 122 | scanner.stopScan(pendingIntent); 123 | 124 | synchronized (wrappers) { 125 | // Do not remove the key, just set the value to null. 126 | // Based on that we will know that scanning has been stopped. 127 | // This is used to discard scanning results delivered after the scan was stopped. 128 | // Unfortunately, the callbackIntent will have to be kept and won't he removed, 129 | // despite the fact that reports will eventually stop being broadcast. 130 | wrappers.put(callbackIntent, null); 131 | } 132 | } 133 | 134 | /** 135 | * When scanning with PendingIntent on Android Oreo or newer, the app may get killed 136 | * by the system, but the scan results, when a device is found, will still be delivered. 137 | * To filter or batch devices using compat mode the given filters and settings must be 138 | * saved in the PendingIntent that will be used to start scanning, as the 139 | * BluetoothLeScannerCompat may be disposed as well, together with its any storage. 140 | * 141 | * @return The PendingIntent that is to be used to start scanning. 142 | */ 143 | @NonNull 144 | private PendingIntent createStartingPendingIntent(@NonNull final List filters, 145 | @NonNull final ScanSettings settings, 146 | @NonNull final Context context, 147 | @NonNull final PendingIntent callbackIntent, 148 | final int requestCode) { 149 | // Since Android 8 it has to be an explicit intent 150 | final Intent intent = new Intent(context, PendingIntentReceiver.class); 151 | intent.setAction(PendingIntentReceiver.ACTION); 152 | 153 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 154 | // The caller's callbackIntent will be used to send the intent to the app 155 | intent.putExtra(PendingIntentReceiver.EXTRA_PENDING_INTENT, callbackIntent); 156 | // The following extras will be used to filter and batch data if needed, 157 | // that is when ScanSettings.Builder#use[...]IfSupported were called with false. 158 | // Only native classes may be used here, as they are delivered to another application. 159 | intent.putParcelableArrayListExtra(PendingIntentReceiver.EXTRA_FILTERS, toNativeScanFilters(filters)); 160 | intent.putExtra(PendingIntentReceiver.EXTRA_SETTINGS, toNativeScanSettings(adapter, settings, true)); 161 | intent.putExtra(PendingIntentReceiver.EXTRA_USE_HARDWARE_BATCHING, settings.getUseHardwareBatchingIfSupported()); 162 | intent.putExtra(PendingIntentReceiver.EXTRA_USE_HARDWARE_FILTERING, settings.getUseHardwareFilteringIfSupported()); 163 | intent.putExtra(PendingIntentReceiver.EXTRA_USE_HARDWARE_CALLBACK_TYPES, settings.getUseHardwareCallbackTypesIfSupported()); 164 | intent.putExtra(PendingIntentReceiver.EXTRA_MATCH_MODE, settings.getMatchMode()); 165 | intent.putExtra(PendingIntentReceiver.EXTRA_NUM_OF_MATCHES, settings.getNumOfMatches()); 166 | 167 | int flags = PendingIntent.FLAG_UPDATE_CURRENT; 168 | // Mutable flag has to be set explicitly on Android 12+. Before PendingIntent was mutable by default. 169 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 170 | flags |= PendingIntent.FLAG_MUTABLE; 171 | return PendingIntent.getBroadcast(context, requestCode, intent, flags); 172 | } 173 | 174 | /** 175 | * When scanning with PendingIntent on Android Oreo or newer, the app may get killed 176 | * by the system. To stop scanning, the same {@link PendingIntent} must be used that was 177 | * used to start scanning. Comparing intents is done using {@link Intent#filterEquals(Intent)}. 178 | * 179 | * @return The PendingIntent that is to be used to stop scanning. It is equal to one used to 180 | * start scanning if the requestCode is equal to one used to start scanning. 181 | */ 182 | @NonNull 183 | private PendingIntent createStoppingPendingIntent(@NonNull final Context context, 184 | final int requestCode) { 185 | // Since Android 8 it has to be an explicit intent 186 | final Intent intent = new Intent(context, PendingIntentReceiver.class); 187 | intent.setAction(PendingIntentReceiver.ACTION); 188 | 189 | int flags = PendingIntent.FLAG_UPDATE_CURRENT; 190 | // Immutable flag has to be set explicitly on Android 12+, but can be set from Android 6. 191 | // Stopping scanning does not require the PendingIntent to be mutable. 192 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 193 | flags |= PendingIntent.FLAG_IMMUTABLE; 194 | return PendingIntent.getBroadcast(context, requestCode, intent, flags); 195 | } 196 | 197 | @NonNull 198 | @Override 199 | /* package */ android.bluetooth.le.ScanSettings toNativeScanSettings(@NonNull final BluetoothAdapter adapter, 200 | @NonNull final ScanSettings settings, 201 | final boolean exactCopy) { 202 | final android.bluetooth.le.ScanSettings.Builder builder = 203 | new android.bluetooth.le.ScanSettings.Builder(); 204 | 205 | if (exactCopy || adapter.isOffloadedScanBatchingSupported() && settings.getUseHardwareBatchingIfSupported()) 206 | builder.setReportDelay(settings.getReportDelayMillis()); 207 | 208 | if (exactCopy || settings.getUseHardwareCallbackTypesIfSupported()) 209 | builder.setCallbackType(settings.getCallbackType()) 210 | .setMatchMode(settings.getMatchMode()) 211 | .setNumOfMatches(settings.getNumOfMatches()); 212 | 213 | builder.setScanMode(settings.getScanMode()) 214 | .setLegacy(settings.getLegacy()) 215 | .setPhy(settings.getPhy()); 216 | 217 | return builder.build(); 218 | } 219 | 220 | @NonNull 221 | /* package */ ScanSettings fromNativeScanSettings(@NonNull final android.bluetooth.le.ScanSettings settings, 222 | final boolean useHardwareBatchingIfSupported, 223 | final boolean useHardwareFilteringIfSupported, 224 | final boolean useHardwareCallbackTypesIfSupported, 225 | final long matchLostDeviceTimeout, 226 | final long matchLostTaskInterval, 227 | final int matchMode, final int numOfMatches) { 228 | final ScanSettings.Builder builder = new ScanSettings.Builder() 229 | .setLegacy(settings.getLegacy()) 230 | .setPhy(settings.getPhy()) 231 | .setCallbackType(settings.getCallbackType()) 232 | .setScanMode(settings.getScanMode()) 233 | .setReportDelay(settings.getReportDelayMillis()) 234 | .setUseHardwareBatchingIfSupported(useHardwareBatchingIfSupported) 235 | .setUseHardwareFilteringIfSupported(useHardwareFilteringIfSupported) 236 | .setUseHardwareCallbackTypesIfSupported(useHardwareCallbackTypesIfSupported) 237 | .setMatchOptions(matchLostDeviceTimeout, matchLostTaskInterval) 238 | // Those 2 values are not accessible from the native ScanSettings. 239 | // They need to be transferred separately in intent extras. 240 | .setMatchMode(matchMode).setNumOfMatches(numOfMatches); 241 | 242 | return builder.build(); 243 | } 244 | 245 | @NonNull 246 | /* package */ ArrayList fromNativeScanFilters(@NonNull final List filters) { 247 | final ArrayList nativeScanFilters = new ArrayList<>(); 248 | for (final android.bluetooth.le.ScanFilter filter : filters) 249 | nativeScanFilters.add(fromNativeScanFilter(filter)); 250 | return nativeScanFilters; 251 | } 252 | 253 | @SuppressWarnings("WeakerAccess") 254 | @NonNull 255 | /* package */ ScanFilter fromNativeScanFilter(@NonNull final android.bluetooth.le.ScanFilter filter) { 256 | final ScanFilter.Builder builder = new ScanFilter.Builder(); 257 | builder.setDeviceAddress(filter.getDeviceAddress()) 258 | .setDeviceName(filter.getDeviceName()) 259 | .setServiceUuid(filter.getServiceUuid(), filter.getServiceUuidMask()) 260 | .setManufacturerData(filter.getManufacturerId(), filter.getManufacturerData(), filter.getManufacturerDataMask()); 261 | 262 | if (filter.getServiceDataUuid() != null) 263 | builder.setServiceData(filter.getServiceDataUuid(), filter.getServiceData(), filter.getServiceDataMask()); 264 | 265 | return builder.build(); 266 | } 267 | 268 | @NonNull 269 | @Override 270 | /* package */ ScanResult fromNativeScanResult(@NonNull final android.bluetooth.le.ScanResult result) { 271 | // Calculate the important bits of Event Type 272 | final int eventType = (result.getDataStatus() << 5) 273 | | (result.isLegacy() ? ScanResult.ET_LEGACY_MASK : 0) 274 | | (result.isConnectable() ? ScanResult.ET_CONNECTABLE_MASK : 0); 275 | // Get data as bytes 276 | final byte[] data = result.getScanRecord() != null ? result.getScanRecord().getBytes() : null; 277 | // And return the v18.ScanResult 278 | return new ScanResult(result.getDevice(), eventType, result.getPrimaryPhy(), 279 | result.getSecondaryPhy(), result.getAdvertisingSid(), 280 | result.getTxPower(), result.getRssi(), 281 | result.getPeriodicAdvertisingInterval(), 282 | ScanRecord.parseFromBytes(data), result.getTimestampNanos()); 283 | } 284 | 285 | /* package */ static class PendingIntentExecutorWrapper extends ScanCallbackWrapper { 286 | /* package */ @NonNull final PendingIntentExecutor executor; 287 | 288 | PendingIntentExecutorWrapper(final boolean offloadedBatchingSupported, 289 | final boolean offloadedFilteringSupported, 290 | @NonNull final List filters, 291 | @NonNull final ScanSettings settings, 292 | @NonNull final PendingIntentExecutor executor) { 293 | super(offloadedBatchingSupported, offloadedFilteringSupported, filters, settings, 294 | executor, new Handler()); 295 | this.executor = executor; 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package no.nordicsemi.android.support.v18.scanner; 18 | 19 | import android.util.SparseArray; 20 | 21 | import java.util.Arrays; 22 | import java.util.Iterator; 23 | import java.util.Map; 24 | import java.util.Set; 25 | 26 | import androidx.annotation.Nullable; 27 | 28 | /** 29 | * Helper class for Bluetooth LE utils. 30 | */ 31 | /* package */ 32 | @SuppressWarnings("unused") 33 | class BluetoothLeUtils { 34 | 35 | /** 36 | * Returns a string composed from a {@link SparseArray}. 37 | */ 38 | static String toString(@Nullable final SparseArray array) { 39 | if (array == null) { 40 | return "null"; 41 | } 42 | if (array.size() == 0) { 43 | return "{}"; 44 | } 45 | final StringBuilder buffer = new StringBuilder(); 46 | buffer.append('{'); 47 | for (int i = 0; i < array.size(); ++i) { 48 | buffer.append(array.keyAt(i)).append("=").append(Arrays.toString(array.valueAt(i))); 49 | } 50 | buffer.append('}'); 51 | return buffer.toString(); 52 | } 53 | 54 | /** 55 | * Returns a string composed from a {@link Map}. 56 | */ 57 | static String toString(@Nullable final Map map) { 58 | if (map == null) { 59 | return "null"; 60 | } 61 | if (map.isEmpty()) { 62 | return "{}"; 63 | } 64 | final StringBuilder buffer = new StringBuilder(); 65 | buffer.append('{'); 66 | final Iterator> it = map.entrySet().iterator(); 67 | while (it.hasNext()) { 68 | final Map.Entry entry = it.next(); 69 | final Object key = entry.getKey(); 70 | //noinspection SuspiciousMethodCalls 71 | buffer.append(key).append("=").append(Arrays.toString(map.get(key))); 72 | if (it.hasNext()) { 73 | buffer.append(", "); 74 | } 75 | } 76 | buffer.append('}'); 77 | return buffer.toString(); 78 | } 79 | 80 | /** 81 | * Check whether two {@link SparseArray} equal. 82 | */ 83 | static boolean equals(@Nullable final SparseArray array, 84 | @Nullable final SparseArray otherArray) { 85 | if (array == otherArray) { 86 | return true; 87 | } 88 | if (array == null || otherArray == null) { 89 | return false; 90 | } 91 | if (array.size() != otherArray.size()) { 92 | return false; 93 | } 94 | 95 | // Keys are guaranteed in ascending order when indices are in ascending order. 96 | for (int i = 0; i < array.size(); ++i) { 97 | if (array.keyAt(i) != otherArray.keyAt(i) || 98 | !Arrays.equals(array.valueAt(i), otherArray.valueAt(i))) { 99 | return false; 100 | } 101 | } 102 | return true; 103 | } 104 | 105 | /** 106 | * Check whether two {@link Map} equal. 107 | */ 108 | static boolean equals(@Nullable final Map map, Map otherMap) { 109 | if (map == otherMap) { 110 | return true; 111 | } 112 | if (map == null || otherMap == null) { 113 | return false; 114 | } 115 | if (map.size() != otherMap.size()) { 116 | return false; 117 | } 118 | Set keys = map.keySet(); 119 | if (!keys.equals(otherMap.keySet())) { 120 | return false; 121 | } 122 | for (T key : keys) { 123 | if (!Objects.deepEquals(map.get(key), otherMap.get(key))) { 124 | return false; 125 | } 126 | } 127 | return true; 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothUuid.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package no.nordicsemi.android.support.v18.scanner; 18 | 19 | import android.os.ParcelUuid; 20 | 21 | import java.nio.ByteBuffer; 22 | import java.nio.ByteOrder; 23 | import java.util.UUID; 24 | 25 | /** 26 | * Static helper methods and constants to decode the ParcelUuid of remote devices. 27 | */ 28 | /* package */ final class BluetoothUuid { 29 | 30 | private static final ParcelUuid BASE_UUID = 31 | ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB"); 32 | 33 | /** Length of bytes for 16 bit UUID */ 34 | static final int UUID_BYTES_16_BIT = 2; 35 | /** Length of bytes for 32 bit UUID */ 36 | static final int UUID_BYTES_32_BIT = 4; 37 | /** Length of bytes for 128 bit UUID */ 38 | static final int UUID_BYTES_128_BIT = 16; 39 | 40 | /** 41 | * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID, 42 | * but the returned UUID is always in 128-bit format. 43 | * Note UUID is little endian in Bluetooth. 44 | * 45 | * @param uuidBytes Byte representation of uuid. 46 | * @return {@link ParcelUuid} parsed from bytes. 47 | * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed. 48 | */ 49 | static ParcelUuid parseUuidFrom(final byte[] uuidBytes) { 50 | if (uuidBytes == null) { 51 | throw new IllegalArgumentException("uuidBytes cannot be null"); 52 | } 53 | final int length = uuidBytes.length; 54 | if (length != UUID_BYTES_16_BIT && length != UUID_BYTES_32_BIT && 55 | length != UUID_BYTES_128_BIT) { 56 | throw new IllegalArgumentException("uuidBytes length invalid - " + length); 57 | } 58 | 59 | // Construct a 128 bit UUID. 60 | if (length == UUID_BYTES_128_BIT) { 61 | final ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN); 62 | final long msb = buf.getLong(8); 63 | final long lsb = buf.getLong(0); 64 | return new ParcelUuid(new UUID(msb, lsb)); 65 | } 66 | 67 | // For 16 bit and 32 bit UUID we need to convert them to 128 bit value. 68 | // 128_bit_value = uuid * 2^96 + BASE_UUID 69 | long shortUuid; 70 | if (length == UUID_BYTES_16_BIT) { 71 | shortUuid = uuidBytes[0] & 0xFF; 72 | shortUuid += (uuidBytes[1] & 0xFF) << 8; 73 | } else { 74 | shortUuid = uuidBytes[0] & 0xFF ; 75 | shortUuid += (uuidBytes[1] & 0xFF) << 8; 76 | shortUuid += (uuidBytes[2] & 0xFF) << 16; 77 | shortUuid += (uuidBytes[3] & 0xFF) << 24; 78 | } 79 | final long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32); 80 | final long lsb = BASE_UUID.getUuid().getLeastSignificantBits(); 81 | return new ParcelUuid(new UUID(msb, lsb)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/Objects.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * 12 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 13 | * software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 20 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | */ 22 | 23 | package no.nordicsemi.android.support.v18.scanner; 24 | 25 | import java.util.Arrays; 26 | 27 | /* package */ class Objects { 28 | 29 | /** 30 | * Returns true if both arguments are null, 31 | * the result of {@link Arrays#equals} if both arguments are primitive arrays, 32 | * the result of {@link Arrays#deepEquals} if both arguments are arrays of reference types, 33 | * and the result of {@link #equals} otherwise. 34 | */ 35 | static boolean deepEquals(final Object a, final Object b) { 36 | if (a == null || b == null) { 37 | return a == b; 38 | } else if (a instanceof Object[] && b instanceof Object[]) { 39 | return Arrays.deepEquals((Object[]) a, (Object[]) b); 40 | } else if (a instanceof boolean[] && b instanceof boolean[]) { 41 | return Arrays.equals((boolean[]) a, (boolean[]) b); 42 | } else if (a instanceof byte[] && b instanceof byte[]) { 43 | return Arrays.equals((byte[]) a, (byte[]) b); 44 | } else if (a instanceof char[] && b instanceof char[]) { 45 | return Arrays.equals((char[]) a, (char[]) b); 46 | } else if (a instanceof double[] && b instanceof double[]) { 47 | return Arrays.equals((double[]) a, (double[]) b); 48 | } else if (a instanceof float[] && b instanceof float[]) { 49 | return Arrays.equals((float[]) a, (float[]) b); 50 | } else if (a instanceof int[] && b instanceof int[]) { 51 | return Arrays.equals((int[]) a, (int[]) b); 52 | } else if (a instanceof long[] && b instanceof long[]) { 53 | return Arrays.equals((long[]) a, (long[]) b); 54 | } else if (a instanceof short[] && b instanceof short[]) { 55 | return Arrays.equals((short[]) a, (short[]) b); 56 | } 57 | return a.equals(b); 58 | } 59 | 60 | /** 61 | * Null-safe equivalent of {@code a.equals(b)}. 62 | */ 63 | static boolean equals(final Object a, final Object b) { 64 | return (a == null) ? (b == null) : a.equals(b); 65 | } 66 | 67 | /** 68 | * Convenience wrapper for {@link Arrays#hashCode}, adding varargs. 69 | * This can be used to compute a hash code for an object's fields as follows: 70 | * {@code Objects.hash(a, b, c)}. 71 | */ 72 | static int hash(final Object... values) { 73 | return Arrays.hashCode(values); 74 | } 75 | 76 | /** 77 | * Returns "null" for null or {@code o.toString()}. 78 | */ 79 | static String toString(final Object o) { 80 | return (o == null) ? "null" : o.toString(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentExecutor.java: -------------------------------------------------------------------------------- 1 | package no.nordicsemi.android.support.v18.scanner; 2 | 3 | import android.app.PendingIntent; 4 | import android.app.Service; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.os.Parcelable; 8 | import android.os.SystemClock; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | import androidx.annotation.NonNull; 15 | import androidx.annotation.Nullable; 16 | 17 | /** 18 | * A ScanCallback that will send a {@link PendingIntent} when callback's methods are called. 19 | */ 20 | /* package */ class PendingIntentExecutor extends ScanCallback { 21 | 22 | @NonNull private final PendingIntent callbackIntent; 23 | 24 | /** A temporary context given to the {@link android.content.BroadcastReceiver}. */ 25 | @Nullable private Context context; 26 | /** The service using this executor. */ 27 | @Nullable private Context service; 28 | 29 | private long lastBatchTimestamp; 30 | private final long reportDelay; 31 | 32 | /** 33 | * Creates the {@link PendingIntent} executor that will be used from a 34 | * {@link android.content.BroadcastReceiver}. The {@link Context} may change in every 35 | * {@link android.content.BroadcastReceiver#onReceive(Context, Intent)} call, so it is not 36 | * kept here. Instead, a temporary context must be set with {@link #setTemporaryContext(Context)} 37 | * each time before the received results are handled and released after that to 38 | * prevent from keeping a string reference to the context by a static object. 39 | * 40 | * @param callbackIntent User's {@link PendingIntent} used in 41 | * {@link BluetoothLeScannerCompat#startScan(List, ScanSettings, Context, PendingIntent)}. 42 | * @param settings Scan settings specified by the user. 43 | */ 44 | PendingIntentExecutor(@NonNull final PendingIntent callbackIntent, 45 | @NonNull final ScanSettings settings) { 46 | this.callbackIntent = callbackIntent; 47 | this.reportDelay = settings.getReportDelayMillis(); 48 | } 49 | 50 | /** 51 | * Creates the {@link PendingIntent} executor that will be used from a {@link Service}. 52 | * The service instance will be used as {@link Context} to send intents. 53 | * 54 | * @param callbackIntent User's {@link PendingIntent} used in 55 | * {@link BluetoothLeScannerCompat#startScan(List, ScanSettings, Context, PendingIntent)}. 56 | * @param settings Scan settings specified by the user. 57 | * @param service The service that will scan for Bluetooth LE devices in background. 58 | */ 59 | PendingIntentExecutor(@NonNull final PendingIntent callbackIntent, 60 | @NonNull final ScanSettings settings, 61 | @NonNull final Service service) { 62 | this.callbackIntent = callbackIntent; 63 | this.reportDelay = settings.getReportDelayMillis(); 64 | this.service = service; 65 | } 66 | 67 | /* package */ void setTemporaryContext(@Nullable final Context context) { 68 | this.context = context; 69 | } 70 | 71 | @Override 72 | public void onScanResult(final int callbackType, @NonNull final ScanResult result) { 73 | final Context context = this.context != null ? this.context : this.service; 74 | if (context == null) 75 | return; 76 | 77 | try { 78 | final Intent extrasIntent = new Intent(); 79 | extrasIntent.putExtra(BluetoothLeScannerCompat.EXTRA_CALLBACK_TYPE, callbackType); 80 | extrasIntent.putParcelableArrayListExtra(BluetoothLeScannerCompat.EXTRA_LIST_SCAN_RESULT, 81 | new ArrayList<>(Collections.singletonList(result))); 82 | callbackIntent.send(context, 0, extrasIntent); 83 | } catch (final PendingIntent.CanceledException e) { 84 | // Ignore 85 | } 86 | } 87 | 88 | @Override 89 | public void onBatchScanResults(@NonNull final List results) { 90 | final Context context = this.context != null ? this.context : this.service; 91 | if (context == null) 92 | return; 93 | 94 | // On several phones the broadcast is sent twice for every batch. 95 | // Skip the second call if came to early. 96 | final long now = SystemClock.elapsedRealtime(); 97 | if (lastBatchTimestamp > now - reportDelay + 5) { 98 | return; 99 | } 100 | lastBatchTimestamp = now; 101 | 102 | try { 103 | final Intent extrasIntent = new Intent(); 104 | extrasIntent.putExtra(BluetoothLeScannerCompat.EXTRA_CALLBACK_TYPE, 105 | ScanSettings.CALLBACK_TYPE_ALL_MATCHES); 106 | extrasIntent.putParcelableArrayListExtra(BluetoothLeScannerCompat.EXTRA_LIST_SCAN_RESULT, 107 | new ArrayList(results)); 108 | extrasIntent.setExtrasClassLoader(ScanResult.class.getClassLoader()); 109 | callbackIntent.send(context, 0, extrasIntent); 110 | } catch (final PendingIntent.CanceledException e) { 111 | // Ignore 112 | } 113 | } 114 | 115 | @Override 116 | public void onScanFailed(final int errorCode) { 117 | final Context context = this.context != null ? this.context : this.service; 118 | if (context == null) 119 | return; 120 | 121 | try { 122 | final Intent extrasIntent = new Intent(); 123 | extrasIntent.putExtra(BluetoothLeScannerCompat.EXTRA_ERROR_CODE, errorCode); 124 | callbackIntent.send(context, 0, extrasIntent); 125 | } catch (final PendingIntent.CanceledException e) { 126 | // Ignore 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentReceiver.java: -------------------------------------------------------------------------------- 1 | package no.nordicsemi.android.support.v18.scanner; 2 | 3 | import android.app.PendingIntent; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.le.BluetoothLeScanner; 6 | import android.content.BroadcastReceiver; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.os.Build; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import androidx.annotation.RequiresApi; 15 | 16 | /** 17 | * This receiver, registered in AndroidManifest, will translate received 18 | * {@link android.bluetooth.le.ScanResult}s into compat {@link ScanResult}s and will send 19 | * a {@link PendingIntent} registered by the user with those converted data. It will also apply 20 | * any filters, perform batching or emulate callback types 21 | * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} and 22 | * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} on devices that do not support it. 23 | */ 24 | public class PendingIntentReceiver extends BroadcastReceiver { 25 | 26 | /* package */ static final String ACTION = "no.nordicsemi.android.support.v18.ACTION_FOUND"; 27 | /* package */ static final String EXTRA_PENDING_INTENT = "no.nordicsemi.android.support.v18.EXTRA_PENDING_INTENT"; 28 | /* package */ static final String EXTRA_FILTERS = "no.nordicsemi.android.support.v18.EXTRA_FILTERS"; 29 | /* package */ static final String EXTRA_SETTINGS = "no.nordicsemi.android.support.v18.EXTRA_SETTINGS"; 30 | /* package */ static final String EXTRA_USE_HARDWARE_BATCHING = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_BATCHING"; 31 | /* package */ static final String EXTRA_USE_HARDWARE_FILTERING = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_FILTERING"; 32 | /* package */ static final String EXTRA_USE_HARDWARE_CALLBACK_TYPES = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_CALLBACK_TYPES"; 33 | /* package */ static final String EXTRA_MATCH_LOST_TIMEOUT = "no.nordicsemi.android.support.v18.EXTRA_MATCH_LOST_TIMEOUT"; 34 | /* package */ static final String EXTRA_MATCH_LOST_INTERVAL = "no.nordicsemi.android.support.v18.EXTRA_MATCH_LOST_INTERVAL"; 35 | /* package */ static final String EXTRA_MATCH_MODE = "no.nordicsemi.android.support.v18.EXTRA_MATCH_MODE"; 36 | /* package */ static final String EXTRA_NUM_OF_MATCHES = "no.nordicsemi.android.support.v18.EXTRA_NUM_OF_MATCHES"; 37 | 38 | @RequiresApi(api = Build.VERSION_CODES.O) 39 | @Override 40 | public void onReceive(final Context context, final Intent intent) { 41 | // Ensure we are ok. 42 | if (context == null || intent == null) 43 | return; 44 | 45 | // Find the target pending intent. 46 | final PendingIntent callbackIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); 47 | if (callbackIntent == null) 48 | return; 49 | 50 | // Filters and settings have been set as native objects, otherwise they could not be 51 | // serialized by the system scanner. 52 | final ArrayList nativeScanFilters = 53 | intent.getParcelableArrayListExtra(EXTRA_FILTERS); 54 | final android.bluetooth.le.ScanSettings nativeScanSettings = intent.getParcelableExtra(EXTRA_SETTINGS); 55 | if (nativeScanFilters == null || nativeScanSettings == null) 56 | return; 57 | 58 | // Some ScanSettings parameters are only on compat version and need to be sent separately. 59 | final boolean useHardwareBatchingIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_BATCHING, true); 60 | final boolean useHardwareFilteringIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_FILTERING, true); 61 | final boolean useHardwareCallbackTypesIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_CALLBACK_TYPES, true); 62 | final long matchLostDeviceTimeout = intent.getLongExtra(EXTRA_MATCH_LOST_TIMEOUT, ScanSettings.MATCH_LOST_DEVICE_TIMEOUT_DEFAULT); 63 | final long matchLostTaskInterval = intent.getLongExtra(EXTRA_MATCH_LOST_INTERVAL, ScanSettings.MATCH_LOST_TASK_INTERVAL_DEFAULT); 64 | final int matchMode = intent.getIntExtra(EXTRA_MATCH_MODE, ScanSettings.MATCH_MODE_AGGRESSIVE); 65 | final int numOfMatches = intent.getIntExtra(EXTRA_NUM_OF_MATCHES, ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT); 66 | 67 | // Convert native objects to compat versions. 68 | final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); 69 | final BluetoothLeScannerImplOreo scannerImpl = (BluetoothLeScannerImplOreo) scanner; 70 | final ArrayList filters = scannerImpl.fromNativeScanFilters(nativeScanFilters); 71 | final ScanSettings settings = scannerImpl.fromNativeScanSettings(nativeScanSettings, 72 | useHardwareBatchingIfSupported, 73 | useHardwareFilteringIfSupported, 74 | useHardwareCallbackTypesIfSupported, 75 | matchLostDeviceTimeout, matchLostTaskInterval, 76 | matchMode, numOfMatches); 77 | 78 | // Check device capabilities and create a wrapper that will send a PendingIntent. 79 | final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 80 | final boolean offloadedBatchingSupported = adapter.isOffloadedScanBatchingSupported(); 81 | final boolean offloadedFilteringSupported = adapter.isOffloadedFilteringSupported(); 82 | 83 | // Obtain or create a PendingIntentExecutorWrapper. A static instance (obtained from a 84 | // static BluetoothLeScannerCompat singleton) is necessary as it allows to keeps 85 | // track of found devices and emulate batching and callback types if those are not 86 | // supported or a compat version was forced. 87 | 88 | BluetoothLeScannerImplOreo.PendingIntentExecutorWrapper wrapper; 89 | //noinspection SynchronizationOnLocalVariableOrMethodParameter 90 | synchronized (scanner) { 91 | try { 92 | wrapper = scannerImpl.getWrapper(callbackIntent); 93 | } catch (final IllegalStateException e) { 94 | // Scanning has been stopped. 95 | return; 96 | } 97 | if (wrapper == null) { 98 | // Wrapper has not been created, or was created, but the app was then killed 99 | // and must be created again. Some information will be lost (batched devices). 100 | final PendingIntentExecutor executor = new PendingIntentExecutor(callbackIntent, settings); 101 | wrapper = new BluetoothLeScannerImplOreo.PendingIntentExecutorWrapper( 102 | offloadedBatchingSupported, 103 | offloadedFilteringSupported, 104 | filters, settings, 105 | executor 106 | ); 107 | scannerImpl.addWrapper(callbackIntent, wrapper); 108 | } 109 | } 110 | 111 | // The context may change each time. Set the one time temporary context that will be used 112 | // to send PendingIntent. It will be released after the results were handled. 113 | wrapper.executor.setTemporaryContext(context); 114 | 115 | // Check what results were received and send them to PendingIntent. 116 | final List nativeScanResults = 117 | intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT); 118 | if (nativeScanResults != null) { 119 | final ArrayList results = scannerImpl.fromNativeScanResults(nativeScanResults); 120 | 121 | if (settings.getReportDelayMillis() > 0) { 122 | wrapper.handleScanResults(results); 123 | } else if (!results.isEmpty()) { 124 | final int callbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, 125 | ScanSettings.CALLBACK_TYPE_ALL_MATCHES); 126 | wrapper.handleScanResult(callbackType, results.get(0)); 127 | } 128 | } else { 129 | final int errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0); 130 | if (errorCode != 0) { 131 | wrapper.handleScanError(errorCode); 132 | } 133 | } 134 | 135 | // Release the temporary context reference, so that static executor does not hold a 136 | // reference to a context. 137 | wrapper.executor.setTemporaryContext(null); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * 12 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 13 | * software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 20 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | */ 22 | 23 | package no.nordicsemi.android.support.v18.scanner; 24 | 25 | import java.util.List; 26 | 27 | import androidx.annotation.NonNull; 28 | 29 | /** 30 | * Bluetooth LE scan callbacks. Scan results are reported using these callbacks. 31 | * 32 | * @see BluetoothLeScannerCompat#startScan 33 | */ 34 | @SuppressWarnings({"unused", "WeakerAccess"}) 35 | public abstract class ScanCallback { 36 | /** 37 | * Fails to start scan as BLE scan with the same settings is already started by the app. 38 | */ 39 | public static final int SCAN_FAILED_ALREADY_STARTED = 1; 40 | 41 | /** 42 | * Fails to start scan as app cannot be registered. 43 | */ 44 | public static final int SCAN_FAILED_APPLICATION_REGISTRATION_FAILED = 2; 45 | 46 | /** 47 | * Fails to start scan due an internal error 48 | */ 49 | public static final int SCAN_FAILED_INTERNAL_ERROR = 3; 50 | 51 | /** 52 | * Fails to start power optimized scan as this feature is not supported. 53 | */ 54 | public static final int SCAN_FAILED_FEATURE_UNSUPPORTED = 4; 55 | 56 | /** 57 | * Fails to start scan as it is out of hardware resources. 58 | */ 59 | public static final int SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES = 5; 60 | 61 | /** 62 | * Fails to start scan as application tries to scan too frequently. 63 | */ 64 | public static final int SCAN_FAILED_SCANNING_TOO_FREQUENTLY = 6; 65 | 66 | static final int NO_ERROR = 0; 67 | 68 | /** 69 | * Callback when a BLE advertisement has been found. 70 | * 71 | * @param callbackType Determines how this callback was triggered. Could be one of 72 | * {@link ScanSettings#CALLBACK_TYPE_ALL_MATCHES}, 73 | * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} or 74 | * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} 75 | * @param result A Bluetooth LE scan result. 76 | */ 77 | public void onScanResult(final int callbackType, @NonNull final ScanResult result) { 78 | } 79 | 80 | /** 81 | * Callback when batch results are delivered. 82 | * 83 | * @param results List of scan results that are previously scanned. 84 | */ 85 | public void onBatchScanResults(@NonNull final List results) { 86 | } 87 | 88 | /** 89 | * Callback when scan could not be started. 90 | * 91 | * @param errorCode Error code (one of SCAN_FAILED_*) for scan failure. 92 | */ 93 | public void onScanFailed(final int errorCode) { 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanCallbackWrapperSet.java: -------------------------------------------------------------------------------- 1 | package no.nordicsemi.android.support.v18.scanner; 2 | 3 | import java.util.HashSet; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | 11 | class ScanCallbackWrapperSet { 12 | @NonNull 13 | private final Set wrappers = new HashSet<>(); 14 | 15 | @NonNull 16 | public Set values() { 17 | return wrappers; 18 | } 19 | 20 | boolean isEmpty() { 21 | return wrappers.isEmpty(); 22 | } 23 | 24 | void add(@NonNull final W wrapper) { 25 | wrappers.add(wrapper); 26 | } 27 | 28 | boolean contains(@NonNull final ScanCallback callback) { 29 | for (final W wrapper : wrappers) { 30 | if (wrapper.scanCallback == callback) { 31 | return true; 32 | } 33 | if (wrapper.scanCallback instanceof UserScanCallbackWrapper) { 34 | final UserScanCallbackWrapper callbackWrapper = (UserScanCallbackWrapper) wrapper.scanCallback; 35 | if (callbackWrapper.get() == callback) { 36 | return true; 37 | } 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | @Nullable 44 | W get(@NonNull final ScanCallback callback) { 45 | for (final W wrapper : wrappers) { 46 | if (wrapper.scanCallback == callback) { 47 | return wrapper; 48 | } 49 | if (wrapper.scanCallback instanceof UserScanCallbackWrapper) { 50 | final UserScanCallbackWrapper callbackWrapper = (UserScanCallbackWrapper) wrapper.scanCallback; 51 | if (callbackWrapper.get() == callback) { 52 | return wrapper; 53 | } 54 | } 55 | } 56 | return null; 57 | } 58 | 59 | @Nullable 60 | W remove(@NonNull final ScanCallback callback) { 61 | for (final W wrapper : wrappers) { 62 | if (wrapper.scanCallback == callback) { 63 | return wrapper; 64 | } 65 | if (wrapper.scanCallback instanceof UserScanCallbackWrapper) { 66 | final UserScanCallbackWrapper callbackWrapper = (UserScanCallbackWrapper) wrapper.scanCallback; 67 | if (callbackWrapper.get() == callback) { 68 | wrappers.remove(wrapper); 69 | return wrapper; 70 | } 71 | } 72 | } 73 | cleanUp(); 74 | return null; 75 | } 76 | 77 | private void cleanUp() { 78 | final List deadWrappers = new LinkedList<>(); 79 | for (final W wrapper : wrappers) { 80 | if (wrapper.scanCallback instanceof UserScanCallbackWrapper) { 81 | final UserScanCallbackWrapper callbackWrapper = (UserScanCallbackWrapper) wrapper.scanCallback; 82 | if (callbackWrapper.isDead()) 83 | deadWrappers.add(wrapper); 84 | } 85 | } 86 | for (final W wrapper : deadWrappers) { 87 | wrappers.remove(wrapper); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * 12 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 13 | * software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 20 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | */ 22 | 23 | package no.nordicsemi.android.support.v18.scanner; 24 | 25 | import android.bluetooth.BluetoothAdapter; 26 | import android.bluetooth.BluetoothDevice; 27 | import android.os.Parcel; 28 | import android.os.ParcelUuid; 29 | import android.os.Parcelable; 30 | 31 | import java.util.Arrays; 32 | import java.util.List; 33 | import java.util.UUID; 34 | 35 | import androidx.annotation.NonNull; 36 | import androidx.annotation.Nullable; 37 | 38 | /** 39 | * Criteria for filtering result from Bluetooth LE scans. A {@link ScanFilter} allows clients to 40 | * restrict scan results to only those that are of interest to them. 41 | *

Current filtering on the following fields are supported:

42 | *
    43 | *
  • Service UUIDs which identify the bluetooth gatt services running on the device.
  • 44 | *
  • Name of remote Bluetooth LE device.
  • 45 | *
  • Mac address of the remote device.
  • 46 | *
  • Service data which is the data associated with a service.
  • 47 | *
  • Manufacturer specific data which is the data associated with a particular manufacturer.
  • 48 | *
49 | * 50 | * @see ScanResult 51 | * @see BluetoothLeScannerCompat 52 | */ 53 | @SuppressWarnings("WeakerAccess") 54 | public final class ScanFilter implements Parcelable { 55 | 56 | @Nullable 57 | private final String deviceName; 58 | @Nullable 59 | private final String deviceAddress; 60 | 61 | @Nullable 62 | private final ParcelUuid serviceUuid; 63 | @Nullable 64 | private final ParcelUuid serviceUuidMask; 65 | 66 | @Nullable 67 | private final ParcelUuid serviceDataUuid; 68 | @Nullable 69 | private final byte[] serviceData; 70 | @Nullable 71 | private final byte[] serviceDataMask; 72 | 73 | private final int manufacturerId; 74 | @Nullable 75 | private final byte[] manufacturerData; 76 | @Nullable 77 | private final byte[] manufacturerDataMask; 78 | 79 | private static final ScanFilter EMPTY = new ScanFilter.Builder().build() ; 80 | 81 | private ScanFilter(@Nullable final String name, @Nullable final String deviceAddress, 82 | @Nullable final ParcelUuid uuid, @Nullable final ParcelUuid uuidMask, 83 | @Nullable final ParcelUuid serviceDataUuid, @Nullable final byte[] serviceData, 84 | @Nullable final byte[] serviceDataMask, final int manufacturerId, 85 | @Nullable final byte[] manufacturerData, 86 | @Nullable final byte[] manufacturerDataMask) { 87 | this.deviceName = name; 88 | this.serviceUuid = uuid; 89 | this.serviceUuidMask = uuidMask; 90 | this.deviceAddress = deviceAddress; 91 | this.serviceDataUuid = serviceDataUuid; 92 | this.serviceData = serviceData; 93 | this.serviceDataMask = serviceDataMask; 94 | this.manufacturerId = manufacturerId; 95 | this.manufacturerData = manufacturerData; 96 | this.manufacturerDataMask = manufacturerDataMask; 97 | } 98 | 99 | @Override 100 | public int describeContents() { 101 | return 0; 102 | } 103 | 104 | @Override 105 | public void writeToParcel(final Parcel dest, final int flags) { 106 | dest.writeInt(deviceName == null ? 0 : 1); 107 | if (deviceName != null) { 108 | dest.writeString(deviceName); 109 | } 110 | dest.writeInt(deviceAddress == null ? 0 : 1); 111 | if (deviceAddress != null) { 112 | dest.writeString(deviceAddress); 113 | } 114 | dest.writeInt(serviceUuid == null ? 0 : 1); 115 | if (serviceUuid != null) { 116 | dest.writeParcelable(serviceUuid, flags); 117 | dest.writeInt(serviceUuidMask == null ? 0 : 1); 118 | if (serviceUuidMask != null) { 119 | dest.writeParcelable(serviceUuidMask, flags); 120 | } 121 | } 122 | dest.writeInt(serviceDataUuid == null ? 0 : 1); 123 | if (serviceDataUuid != null) { 124 | dest.writeParcelable(serviceDataUuid, flags); 125 | dest.writeInt(serviceData == null ? 0 : 1); 126 | if (serviceData != null) { 127 | dest.writeInt(serviceData.length); 128 | dest.writeByteArray(serviceData); 129 | 130 | dest.writeInt(serviceDataMask == null ? 0 : 1); 131 | if (serviceDataMask != null) { 132 | dest.writeInt(serviceDataMask.length); 133 | dest.writeByteArray(serviceDataMask); 134 | } 135 | } 136 | } 137 | dest.writeInt(manufacturerId); 138 | dest.writeInt(manufacturerData == null ? 0 : 1); 139 | if (manufacturerData != null) { 140 | dest.writeInt(manufacturerData.length); 141 | dest.writeByteArray(manufacturerData); 142 | 143 | dest.writeInt(manufacturerDataMask == null ? 0 : 1); 144 | if (manufacturerDataMask != null) { 145 | dest.writeInt(manufacturerDataMask.length); 146 | dest.writeByteArray(manufacturerDataMask); 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * A {@link android.os.Parcelable.Creator} to create {@link ScanFilter} from parcel. 153 | */ 154 | public static final Creator CREATOR = new Creator() { 155 | 156 | @Override 157 | public ScanFilter[] newArray(final int size) { 158 | return new ScanFilter[size]; 159 | } 160 | 161 | @Override 162 | public ScanFilter createFromParcel(final Parcel in) { 163 | final Builder builder = new Builder(); 164 | if (in.readInt() == 1) { 165 | builder.setDeviceName(in.readString()); 166 | } 167 | if (in.readInt() == 1) { 168 | builder.setDeviceAddress(in.readString()); 169 | } 170 | if (in.readInt() == 1) { 171 | ParcelUuid uuid = in.readParcelable(ParcelUuid.class.getClassLoader()); 172 | builder.setServiceUuid(uuid); 173 | if (in.readInt() == 1) { 174 | ParcelUuid uuidMask = in.readParcelable( 175 | ParcelUuid.class.getClassLoader()); 176 | builder.setServiceUuid(uuid, uuidMask); 177 | } 178 | } 179 | if (in.readInt() == 1) { 180 | ParcelUuid serviceDataUuid = in.readParcelable(ParcelUuid.class.getClassLoader()); 181 | if (in.readInt() == 1) { 182 | final int serviceDataLength = in.readInt(); 183 | final byte[] serviceData = new byte[serviceDataLength]; 184 | in.readByteArray(serviceData); 185 | if (in.readInt() == 0) { 186 | //noinspection ConstantConditions 187 | builder.setServiceData(serviceDataUuid, serviceData); 188 | } else { 189 | final int serviceDataMaskLength = in.readInt(); 190 | final byte[] serviceDataMask = new byte[serviceDataMaskLength]; 191 | in.readByteArray(serviceDataMask); 192 | //noinspection ConstantConditions 193 | builder.setServiceData(serviceDataUuid, serviceData, serviceDataMask); 194 | } 195 | } 196 | } 197 | 198 | final int manufacturerId = in.readInt(); 199 | if (in.readInt() == 1) { 200 | final int manufacturerDataLength = in.readInt(); 201 | final byte[] manufacturerData = new byte[manufacturerDataLength]; 202 | in.readByteArray(manufacturerData); 203 | if (in.readInt() == 0) { 204 | builder.setManufacturerData(manufacturerId, manufacturerData); 205 | } else { 206 | final int manufacturerDataMaskLength = in.readInt(); 207 | final byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength]; 208 | in.readByteArray(manufacturerDataMask); 209 | builder.setManufacturerData(manufacturerId, manufacturerData, 210 | manufacturerDataMask); 211 | } 212 | } 213 | 214 | return builder.build(); 215 | } 216 | }; 217 | 218 | /** 219 | * Returns the filter set the device name field of Bluetooth advertisement data. 220 | */ 221 | @Nullable 222 | public String getDeviceName() { 223 | return deviceName; 224 | } 225 | 226 | /** 227 | * Returns the filter set on the service uuid. 228 | */ 229 | @Nullable 230 | public ParcelUuid getServiceUuid() { 231 | return serviceUuid; 232 | } 233 | 234 | @Nullable 235 | public ParcelUuid getServiceUuidMask() { 236 | return serviceUuidMask; 237 | } 238 | 239 | @Nullable 240 | public String getDeviceAddress() { 241 | return deviceAddress; 242 | } 243 | 244 | @Nullable 245 | public byte[] getServiceData() { 246 | return serviceData; 247 | } 248 | 249 | @Nullable 250 | public byte[] getServiceDataMask() { 251 | return serviceDataMask; 252 | } 253 | 254 | @Nullable 255 | public ParcelUuid getServiceDataUuid() { 256 | return serviceDataUuid; 257 | } 258 | 259 | /** 260 | * Returns the manufacturer id. -1 if the manufacturer filter is not set. 261 | */ 262 | public int getManufacturerId() { 263 | return manufacturerId; 264 | } 265 | 266 | @Nullable 267 | public byte[] getManufacturerData() { 268 | return manufacturerData; 269 | } 270 | 271 | @Nullable 272 | public byte[] getManufacturerDataMask() { 273 | return manufacturerDataMask; 274 | } 275 | 276 | /** 277 | * Check if the scan filter matches a {@code scanResult}. A scan result is considered as a match 278 | * if it matches all the field filters. 279 | */ 280 | public boolean matches(@Nullable final ScanResult scanResult) { 281 | if (scanResult == null) { 282 | return false; 283 | } 284 | final BluetoothDevice device = scanResult.getDevice(); 285 | // Device match. 286 | if (deviceAddress != null && !deviceAddress.equals(device.getAddress())) { 287 | return false; 288 | } 289 | 290 | final ScanRecord scanRecord = scanResult.getScanRecord(); 291 | 292 | // Scan record is null but there exist filters on it. 293 | if (scanRecord == null 294 | && (deviceName != null || serviceUuid != null || manufacturerData != null 295 | || serviceData != null)) { 296 | return false; 297 | } 298 | 299 | // Local name match. 300 | if (deviceName != null && !deviceName.equals(scanRecord.getDeviceName())) { 301 | return false; 302 | } 303 | 304 | // UUID match. 305 | if (serviceUuid != null && !matchesServiceUuids(serviceUuid, serviceUuidMask, 306 | scanRecord.getServiceUuids())) { 307 | return false; 308 | } 309 | 310 | // Service data match 311 | if (serviceDataUuid != null && scanRecord != null) { 312 | if (!matchesPartialData(serviceData, serviceDataMask, 313 | scanRecord.getServiceData(serviceDataUuid))) { 314 | return false; 315 | } 316 | } 317 | 318 | // Manufacturer data match. 319 | if (manufacturerId >= 0 && scanRecord != null) { 320 | //noinspection RedundantIfStatement 321 | if (!matchesPartialData(manufacturerData, manufacturerDataMask, 322 | scanRecord.getManufacturerSpecificData(manufacturerId))) { 323 | return false; 324 | } 325 | } 326 | // All filters match. 327 | return true; 328 | } 329 | 330 | /** 331 | * Check if the uuid pattern is contained in a list of parcel uuids. 332 | */ 333 | private static boolean matchesServiceUuids(@Nullable final ParcelUuid uuid, 334 | @Nullable final ParcelUuid parcelUuidMask, 335 | @Nullable final List uuids) { 336 | if (uuid == null) { 337 | return true; 338 | } 339 | if (uuids == null) { 340 | return false; 341 | } 342 | 343 | for (final ParcelUuid parcelUuid : uuids) { 344 | final UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid(); 345 | if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) { 346 | return true; 347 | } 348 | } 349 | return false; 350 | } 351 | 352 | // Check if the uuid pattern matches the particular service uuid. 353 | private static boolean matchesServiceUuid(@NonNull final UUID uuid, 354 | @Nullable final UUID mask, 355 | @NonNull final UUID data) { 356 | if (mask == null) { 357 | return uuid.equals(data); 358 | } 359 | if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits()) != 360 | (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) { 361 | return false; 362 | } 363 | return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits()) == 364 | (data.getMostSignificantBits() & mask.getMostSignificantBits())); 365 | } 366 | 367 | // Check whether the data pattern matches the parsed data. 368 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") 369 | private boolean matchesPartialData(@Nullable final byte[] data, 370 | @Nullable final byte[] dataMask, 371 | @Nullable final byte[] parsedData) { 372 | if (data == null) { 373 | // If filter data is null it means it doesn't matter. 374 | // We return true if any data matching the manufacturerId were found. 375 | return parsedData != null; 376 | } 377 | if (parsedData == null || parsedData.length < data.length) { 378 | return false; 379 | } 380 | if (dataMask == null) { 381 | for (int i = 0; i < data.length; ++i) { 382 | if (parsedData[i] != data[i]) { 383 | return false; 384 | } 385 | } 386 | return true; 387 | } 388 | for (int i = 0; i < data.length; ++i) { 389 | if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) { 390 | return false; 391 | } 392 | } 393 | return true; 394 | } 395 | 396 | @Override 397 | public String toString() { 398 | return "BluetoothLeScanFilter [deviceName=" + deviceName + ", deviceAddress=" 399 | + deviceAddress 400 | + ", mUuid=" + serviceUuid + ", uuidMask=" + serviceUuidMask 401 | + ", serviceDataUuid=" + Objects.toString(serviceDataUuid) + ", serviceData=" 402 | + Arrays.toString(serviceData) + ", serviceDataMask=" 403 | + Arrays.toString(serviceDataMask) + ", manufacturerId=" + manufacturerId 404 | + ", manufacturerData=" + Arrays.toString(manufacturerData) 405 | + ", manufacturerDataMask=" + Arrays.toString(manufacturerDataMask) + "]"; 406 | } 407 | 408 | @Override 409 | public int hashCode() { 410 | return Objects.hash(deviceName, deviceAddress, manufacturerId, 411 | Arrays.hashCode(manufacturerData), 412 | Arrays.hashCode(manufacturerDataMask), 413 | serviceDataUuid, 414 | Arrays.hashCode(serviceData), 415 | Arrays.hashCode(serviceDataMask), 416 | serviceUuid, serviceUuidMask); 417 | } 418 | 419 | @Override 420 | public boolean equals(final Object obj) { 421 | if (this == obj) { 422 | return true; 423 | } 424 | if (obj == null || getClass() != obj.getClass()) { 425 | return false; 426 | } 427 | final ScanFilter other = (ScanFilter) obj; 428 | return Objects.equals(deviceName, other.deviceName) && 429 | Objects.equals(deviceAddress, other.deviceAddress) && 430 | manufacturerId == other.manufacturerId && 431 | Objects.deepEquals(manufacturerData, other.manufacturerData) && 432 | Objects.deepEquals(manufacturerDataMask, other.manufacturerDataMask) && 433 | Objects.equals(serviceDataUuid, other.serviceDataUuid) && 434 | Objects.deepEquals(serviceData, other.serviceData) && 435 | Objects.deepEquals(serviceDataMask, other.serviceDataMask) && 436 | Objects.equals(serviceUuid, other.serviceUuid) && 437 | Objects.equals(serviceUuidMask, other.serviceUuidMask); 438 | } 439 | 440 | /** 441 | * Checks if the scan filter is empty. 442 | */ 443 | @SuppressWarnings("unused") 444 | /* package */ boolean isAllFieldsEmpty() { 445 | return EMPTY.equals(this); 446 | } 447 | 448 | /** 449 | * Builder class for {@link ScanFilter}. 450 | */ 451 | public static final class Builder { 452 | 453 | private String deviceName; 454 | private String deviceAddress; 455 | 456 | private ParcelUuid serviceUuid; 457 | private ParcelUuid uuidMask; 458 | 459 | private ParcelUuid serviceDataUuid; 460 | private byte[] serviceData; 461 | private byte[] serviceDataMask; 462 | 463 | private int manufacturerId = -1; 464 | private byte[] manufacturerData; 465 | private byte[] manufacturerDataMask; 466 | 467 | /** 468 | * Set filter on device name. 469 | */ 470 | public Builder setDeviceName(@Nullable final String deviceName) { 471 | this.deviceName = deviceName; 472 | return this; 473 | } 474 | 475 | /** 476 | * Set filter on device address. 477 | * 478 | * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the 479 | * format of "01:02:03:AB:CD:EF". The device address can be validated using 480 | * {@link BluetoothAdapter#checkBluetoothAddress}. 481 | * @throws IllegalArgumentException If the {@code deviceAddress} is invalid. 482 | */ 483 | public Builder setDeviceAddress(@Nullable final String deviceAddress) { 484 | if (deviceAddress != null && !BluetoothAdapter.checkBluetoothAddress(deviceAddress)) { 485 | throw new IllegalArgumentException("invalid device address " + deviceAddress); 486 | } 487 | this.deviceAddress = deviceAddress; 488 | return this; 489 | } 490 | 491 | /** 492 | * Set filter on service uuid. 493 | */ 494 | public Builder setServiceUuid(@Nullable final ParcelUuid serviceUuid) { 495 | this.serviceUuid = serviceUuid; 496 | this.uuidMask = null; // clear uuid mask 497 | return this; 498 | } 499 | 500 | /** 501 | * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the 502 | * {@code serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the 503 | * bit in {@code serviceUuid}, and 0 to ignore that bit. 504 | * 505 | * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but 506 | * {@code uuidMask} is not {@code null}. 507 | */ 508 | public Builder setServiceUuid(@Nullable final ParcelUuid serviceUuid, 509 | @Nullable final ParcelUuid uuidMask) { 510 | if (uuidMask != null && serviceUuid == null) { 511 | throw new IllegalArgumentException("uuid is null while uuidMask is not null!"); 512 | } 513 | this.serviceUuid = serviceUuid; 514 | this.uuidMask = uuidMask; 515 | return this; 516 | } 517 | 518 | /** 519 | * Set filtering on service data. 520 | * 521 | * @throws IllegalArgumentException If {@code serviceDataUuid} is null. 522 | */ 523 | public Builder setServiceData(@NonNull final ParcelUuid serviceDataUuid, 524 | @Nullable final byte[] serviceData) { 525 | //noinspection ConstantConditions 526 | if (serviceDataUuid == null) { 527 | throw new IllegalArgumentException("serviceDataUuid is null!"); 528 | } 529 | this.serviceDataUuid = serviceDataUuid; 530 | this.serviceData = serviceData; 531 | this.serviceDataMask = null; // clear service data mask 532 | return this; 533 | } 534 | 535 | /** 536 | * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to 537 | * match the one in service data, otherwise set it to 0 to ignore that bit. 538 | *

539 | * The {@code serviceDataMask} must have the same length of the {@code serviceData}. 540 | * 541 | * @throws IllegalArgumentException If {@code serviceDataUuid} is null or 542 | * {@code serviceDataMask} is {@code null} while {@code serviceData} is not or 543 | * {@code serviceDataMask} and {@code serviceData} has different length. 544 | */ 545 | public Builder setServiceData(@NonNull final ParcelUuid serviceDataUuid, 546 | @Nullable final byte[] serviceData, 547 | @Nullable final byte[] serviceDataMask) { 548 | //noinspection ConstantConditions 549 | if (serviceDataUuid == null) { 550 | throw new IllegalArgumentException("serviceDataUuid is null"); 551 | } 552 | if (serviceDataMask != null) { 553 | if (serviceData == null) { 554 | throw new IllegalArgumentException( 555 | "serviceData is null while serviceDataMask is not null"); 556 | } 557 | // Since the serviceDataMask is a bit mask for serviceData, the lengths of the two 558 | // byte array need to be the same. 559 | if (serviceData.length != serviceDataMask.length) { 560 | throw new IllegalArgumentException( 561 | "size mismatch for service data and service data mask"); 562 | } 563 | } 564 | this.serviceDataUuid = serviceDataUuid; 565 | this.serviceData = serviceData; 566 | this.serviceDataMask = serviceDataMask; 567 | return this; 568 | } 569 | 570 | /** 571 | * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id. 572 | *

573 | * Note the first two bytes of the {@code manufacturerData} is the manufacturerId. 574 | * 575 | * @throws IllegalArgumentException If the {@code manufacturerId} is invalid. 576 | */ 577 | public Builder setManufacturerData(final int manufacturerId, 578 | @Nullable final byte[] manufacturerData) { 579 | if (manufacturerData != null && manufacturerId < 0) { 580 | throw new IllegalArgumentException("invalid manufacture id"); 581 | } 582 | this.manufacturerId = manufacturerId; 583 | this.manufacturerData = manufacturerData; 584 | this.manufacturerDataMask = null; // clear manufacturer data mask 585 | return this; 586 | } 587 | 588 | /** 589 | * Set filter on partial manufacture data. For any bit in the mask, set it the 1 if it needs 590 | * to match the one in manufacturer data, otherwise set it to 0. 591 | *

592 | * The {@code manufacturerDataMask} must have the same length of {@code manufacturerData}. 593 | * 594 | * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or 595 | * {@code manufacturerData} is null while {@code manufacturerDataMask} is not, 596 | * or {@code manufacturerData} and {@code manufacturerDataMask} have different 597 | * length. 598 | */ 599 | public Builder setManufacturerData(final int manufacturerId, 600 | @Nullable final byte[] manufacturerData, 601 | @Nullable final byte[] manufacturerDataMask) { 602 | if (manufacturerData != null && manufacturerId < 0) { 603 | throw new IllegalArgumentException("invalid manufacture id"); 604 | } 605 | if (manufacturerDataMask != null) { 606 | if (manufacturerData == null) { 607 | throw new IllegalArgumentException( 608 | "manufacturerData is null while manufacturerDataMask is not null"); 609 | } 610 | // Since the manufacturerDataMask is a bit mask for manufacturerData, the lengths 611 | // of the two byte array need to be the same. 612 | if (manufacturerData.length != manufacturerDataMask.length) { 613 | throw new IllegalArgumentException( 614 | "size mismatch for manufacturerData and manufacturerDataMask"); 615 | } 616 | } 617 | this.manufacturerId = manufacturerId; 618 | this.manufacturerData = manufacturerData; 619 | this.manufacturerDataMask = manufacturerDataMask; 620 | return this; 621 | } 622 | 623 | /** 624 | * Build {@link ScanFilter}. 625 | * 626 | * @throws IllegalArgumentException If the filter cannot be built. 627 | */ 628 | public ScanFilter build() { 629 | return new ScanFilter(deviceName, deviceAddress, serviceUuid, uuidMask, 630 | serviceDataUuid, serviceData, serviceDataMask, 631 | manufacturerId, manufacturerData, manufacturerDataMask); 632 | } 633 | } 634 | } 635 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * 12 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 13 | * software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 20 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | */ 22 | 23 | 24 | package no.nordicsemi.android.support.v18.scanner; 25 | 26 | import android.os.ParcelUuid; 27 | import android.util.Log; 28 | import android.util.SparseArray; 29 | 30 | import java.util.ArrayList; 31 | import java.util.Arrays; 32 | import java.util.HashMap; 33 | import java.util.List; 34 | import java.util.Map; 35 | 36 | import androidx.annotation.NonNull; 37 | import androidx.annotation.Nullable; 38 | 39 | /** 40 | * Represents a scan record from Bluetooth LE scan. 41 | */ 42 | @SuppressWarnings("WeakerAccess") 43 | public final class ScanRecord { 44 | 45 | private static final String TAG = "ScanRecord"; 46 | 47 | // The following data type values are assigned by Bluetooth SIG. 48 | // For more details refer to Bluetooth 4.1 specification, Volume 3, Part C, Section 18. 49 | private static final int DATA_TYPE_FLAGS = 0x01; 50 | private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02; 51 | private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03; 52 | private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04; 53 | private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05; 54 | private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06; 55 | private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07; 56 | private static final int DATA_TYPE_LOCAL_NAME_SHORT = 0x08; 57 | private static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09; 58 | private static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A; 59 | private static final int DATA_TYPE_SERVICE_DATA_16_BIT = 0x16; 60 | private static final int DATA_TYPE_SERVICE_DATA_32_BIT = 0x20; 61 | private static final int DATA_TYPE_SERVICE_DATA_128_BIT = 0x21; 62 | private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF; 63 | 64 | // Flags of the advertising data. 65 | private final int advertiseFlags; 66 | 67 | @Nullable private final List serviceUuids; 68 | 69 | @Nullable private final SparseArray manufacturerSpecificData; 70 | 71 | @Nullable private final Map serviceData; 72 | 73 | // Transmission power level(in dB). 74 | private final int txPowerLevel; 75 | 76 | // Local name of the Bluetooth LE device. 77 | private final String deviceName; 78 | 79 | // Raw bytes of scan record. 80 | private final byte[] bytes; 81 | 82 | /** 83 | * Returns the advertising flags indicating the discoverable mode and capability of the device. 84 | * Returns -1 if the flag field is not set. 85 | */ 86 | public int getAdvertiseFlags() { 87 | return advertiseFlags; 88 | } 89 | 90 | /** 91 | * Returns a list of service UUIDs within the advertisement that are used to identify the 92 | * bluetooth GATT services. 93 | */ 94 | @Nullable 95 | public List getServiceUuids() { 96 | return serviceUuids; 97 | } 98 | 99 | /** 100 | * Returns a sparse array of manufacturer identifier and its corresponding manufacturer specific 101 | * data. 102 | */ 103 | @Nullable 104 | public SparseArray getManufacturerSpecificData() { 105 | return manufacturerSpecificData; 106 | } 107 | 108 | /** 109 | * Returns the manufacturer specific data associated with the manufacturer id. Returns 110 | * {@code null} if the {@code manufacturerId} is not found. 111 | */ 112 | @Nullable 113 | public byte[] getManufacturerSpecificData(final int manufacturerId) { 114 | if (manufacturerSpecificData == null) { 115 | return null; 116 | } 117 | return manufacturerSpecificData.get(manufacturerId); 118 | } 119 | 120 | /** 121 | * Returns a map of service UUID and its corresponding service data. 122 | */ 123 | @Nullable 124 | public Map getServiceData() { 125 | return serviceData; 126 | } 127 | 128 | /** 129 | * Returns the service data byte array associated with the {@code serviceUuid}. Returns 130 | * {@code null} if the {@code serviceDataUuid} is not found. 131 | */ 132 | @Nullable 133 | public byte[] getServiceData(@NonNull final ParcelUuid serviceDataUuid) { 134 | //noinspection ConstantConditions 135 | if (serviceDataUuid == null || serviceData == null) { 136 | return null; 137 | } 138 | return serviceData.get(serviceDataUuid); 139 | } 140 | 141 | /** 142 | * Returns the transmission power level of the packet in dBm. Returns {@link Integer#MIN_VALUE} 143 | * if the field is not set. This value can be used to calculate the path loss of a received 144 | * packet using the following equation: 145 | *

146 | * pathloss = txPowerLevel - rssi 147 | */ 148 | public int getTxPowerLevel() { 149 | return txPowerLevel; 150 | } 151 | 152 | /** 153 | * Returns the local name of the BLE device. The is a UTF-8 encoded string. 154 | */ 155 | @Nullable 156 | public String getDeviceName() { 157 | return deviceName; 158 | } 159 | 160 | /** 161 | * Returns raw bytes of scan record. 162 | */ 163 | @Nullable 164 | public byte[] getBytes() { 165 | return bytes; 166 | } 167 | 168 | private ScanRecord(@Nullable final List serviceUuids, 169 | @Nullable final SparseArray manufacturerData, 170 | @Nullable final Map serviceData, 171 | final int advertiseFlags, final int txPowerLevel, 172 | final String localName, final byte[] bytes) { 173 | this.serviceUuids = serviceUuids; 174 | this.manufacturerSpecificData = manufacturerData; 175 | this.serviceData = serviceData; 176 | this.deviceName = localName; 177 | this.advertiseFlags = advertiseFlags; 178 | this.txPowerLevel = txPowerLevel; 179 | this.bytes = bytes; 180 | } 181 | 182 | /** 183 | * Parse scan record bytes to {@link ScanRecord}. 184 | *

185 | * The format is defined in Bluetooth 4.1 specification, Volume 3, Part C, Section 11 and 18. 186 | *

187 | * All numerical multi-byte entities and values shall use little-endian byte 188 | * order. 189 | * 190 | * @param scanRecord The scan record of Bluetooth LE advertisement and/or scan response. 191 | */ 192 | @Nullable 193 | /* package */ static ScanRecord parseFromBytes(@Nullable final byte[] scanRecord) { 194 | if (scanRecord == null) { 195 | return null; 196 | } 197 | 198 | int currentPos = 0; 199 | int advertiseFlag = -1; 200 | int txPowerLevel = Integer.MIN_VALUE; 201 | String localName = null; 202 | List serviceUuids = null; 203 | SparseArray manufacturerData = null; 204 | Map serviceData = null; 205 | 206 | try { 207 | while (currentPos < scanRecord.length) { 208 | // length is unsigned int. 209 | final int length = scanRecord[currentPos++] & 0xFF; 210 | if (length == 0) { 211 | break; 212 | } 213 | // Note the length includes the length of the field type itself. 214 | final int dataLength = length - 1; 215 | // fieldType is unsigned int. 216 | final int fieldType = scanRecord[currentPos++] & 0xFF; 217 | switch (fieldType) { 218 | case DATA_TYPE_FLAGS: 219 | advertiseFlag = scanRecord[currentPos] & 0xFF; 220 | break; 221 | case DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL: 222 | case DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE: 223 | if (serviceUuids == null) 224 | serviceUuids = new ArrayList<>(); 225 | parseServiceUuid(scanRecord, currentPos, 226 | dataLength, BluetoothUuid.UUID_BYTES_16_BIT, serviceUuids); 227 | break; 228 | case DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL: 229 | case DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE: 230 | if (serviceUuids == null) 231 | serviceUuids = new ArrayList<>(); 232 | parseServiceUuid(scanRecord, currentPos, dataLength, 233 | BluetoothUuid.UUID_BYTES_32_BIT, serviceUuids); 234 | break; 235 | case DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL: 236 | case DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE: 237 | if (serviceUuids == null) 238 | serviceUuids = new ArrayList<>(); 239 | parseServiceUuid(scanRecord, currentPos, dataLength, 240 | BluetoothUuid.UUID_BYTES_128_BIT, serviceUuids); 241 | break; 242 | case DATA_TYPE_LOCAL_NAME_SHORT: 243 | case DATA_TYPE_LOCAL_NAME_COMPLETE: 244 | localName = new String( 245 | extractBytes(scanRecord, currentPos, dataLength)); 246 | break; 247 | case DATA_TYPE_TX_POWER_LEVEL: 248 | txPowerLevel = scanRecord[currentPos]; 249 | break; 250 | case DATA_TYPE_SERVICE_DATA_16_BIT: 251 | case DATA_TYPE_SERVICE_DATA_32_BIT: 252 | case DATA_TYPE_SERVICE_DATA_128_BIT: 253 | int serviceUuidLength = BluetoothUuid.UUID_BYTES_16_BIT; 254 | if (fieldType == DATA_TYPE_SERVICE_DATA_32_BIT) { 255 | serviceUuidLength = BluetoothUuid.UUID_BYTES_32_BIT; 256 | } else if (fieldType == DATA_TYPE_SERVICE_DATA_128_BIT) { 257 | serviceUuidLength = BluetoothUuid.UUID_BYTES_128_BIT; 258 | } 259 | 260 | final byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos, 261 | serviceUuidLength); 262 | final ParcelUuid serviceDataUuid = BluetoothUuid.parseUuidFrom( 263 | serviceDataUuidBytes); 264 | final byte[] serviceDataArray = extractBytes(scanRecord, 265 | currentPos + serviceUuidLength, dataLength - serviceUuidLength); 266 | if (serviceData == null) 267 | serviceData = new HashMap<>(); 268 | serviceData.put(serviceDataUuid, serviceDataArray); 269 | break; 270 | case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA: 271 | // The first two bytes of the manufacturer specific data are 272 | // manufacturer ids in little endian. 273 | final int manufacturerId = ((scanRecord[currentPos + 1] & 0xFF) << 8) + 274 | (scanRecord[currentPos] & 0xFF); 275 | final byte[] manufacturerDataBytes = extractBytes(scanRecord, currentPos + 2, 276 | dataLength - 2); 277 | if (manufacturerData == null) 278 | manufacturerData = new SparseArray<>(); 279 | manufacturerData.put(manufacturerId, manufacturerDataBytes); 280 | break; 281 | default: 282 | // Just ignore, we don't handle such data type. 283 | break; 284 | } 285 | currentPos += dataLength; 286 | } 287 | 288 | return new ScanRecord(serviceUuids, manufacturerData, serviceData, 289 | advertiseFlag, txPowerLevel, localName, scanRecord); 290 | } catch (final Exception e) { 291 | Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord)); 292 | // As the record is invalid, ignore all the parsed results for this packet 293 | // and return an empty record with raw scanRecord bytes in results 294 | return new ScanRecord(null, null, null, 295 | -1, Integer.MIN_VALUE, null, scanRecord); 296 | } 297 | } 298 | 299 | @Override 300 | public boolean equals(final Object obj) { 301 | if (this == obj) { 302 | return true; 303 | } 304 | if (obj == null || getClass() != obj.getClass()) { 305 | return false; 306 | } 307 | ScanRecord other = (ScanRecord) obj; 308 | return Arrays.equals(bytes, other.bytes); 309 | } 310 | 311 | @Override 312 | public String toString() { 313 | return "ScanRecord [advertiseFlags=" + advertiseFlags + ", serviceUuids=" + serviceUuids 314 | + ", manufacturerSpecificData=" + BluetoothLeUtils.toString(manufacturerSpecificData) 315 | + ", serviceData=" + BluetoothLeUtils.toString(serviceData) 316 | + ", txPowerLevel=" + txPowerLevel + ", deviceName=" + deviceName + "]"; 317 | } 318 | 319 | // Parse service UUIDs. 320 | @SuppressWarnings("UnusedReturnValue") 321 | private static int parseServiceUuid(@NonNull final byte[] scanRecord, 322 | int currentPos, int dataLength, 323 | final int uuidLength, 324 | @NonNull final List serviceUuids) { 325 | while (dataLength > 0) { 326 | final byte[] uuidBytes = extractBytes(scanRecord, currentPos, 327 | uuidLength); 328 | serviceUuids.add(BluetoothUuid.parseUuidFrom(uuidBytes)); 329 | dataLength -= uuidLength; 330 | currentPos += uuidLength; 331 | } 332 | return currentPos; 333 | } 334 | 335 | // Helper method to extract bytes from byte array. 336 | private static byte[] extractBytes(@NonNull final byte[] scanRecord, 337 | final int start, final int length) { 338 | byte[] bytes = new byte[length]; 339 | System.arraycopy(scanRecord, start, bytes, 0, length); 340 | return bytes; 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * 12 | * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 13 | * software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 20 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | */ 22 | 23 | package no.nordicsemi.android.support.v18.scanner; 24 | 25 | import android.bluetooth.BluetoothDevice; 26 | import android.os.Parcel; 27 | import android.os.Parcelable; 28 | 29 | import androidx.annotation.NonNull; 30 | import androidx.annotation.Nullable; 31 | 32 | /** 33 | * ScanResult for Bluetooth LE scan. 34 | */ 35 | @SuppressWarnings({"WeakerAccess", "unused"}) 36 | public final class ScanResult implements Parcelable { 37 | 38 | /** 39 | * For chained advertisements, indicates that the data contained in this 40 | * scan result is complete. 41 | */ 42 | public static final int DATA_COMPLETE = 0x00; 43 | 44 | /** 45 | * For chained advertisements, indicates that the controller was 46 | * unable to receive all chained packets and the scan result contains 47 | * incomplete truncated data. 48 | */ 49 | public static final int DATA_TRUNCATED = 0x02; 50 | 51 | /** 52 | * Indicates that the secondary physical layer was not used. 53 | */ 54 | public static final int PHY_UNUSED = 0x00; 55 | 56 | /** 57 | * Advertising Set ID is not present in the packet. 58 | */ 59 | public static final int SID_NOT_PRESENT = 0xFF; 60 | 61 | /** 62 | * TX power is not present in the packet. 63 | */ 64 | public static final int TX_POWER_NOT_PRESENT = 0x7F; 65 | 66 | /** 67 | * Periodic advertising interval is not present in the packet. 68 | */ 69 | public static final int PERIODIC_INTERVAL_NOT_PRESENT = 0x00; 70 | 71 | /** 72 | * Mask for checking whether event type represents legacy advertisement. 73 | */ 74 | static final int ET_LEGACY_MASK = 0x10; 75 | 76 | /** 77 | * Mask for checking whether event type represents connectable advertisement. 78 | */ 79 | static final int ET_CONNECTABLE_MASK = 0x01; 80 | 81 | // Remote Bluetooth device. 82 | @NonNull 83 | private final BluetoothDevice device; 84 | 85 | // Scan record, including advertising data and scan response data. 86 | @Nullable 87 | private ScanRecord scanRecord; 88 | 89 | // Received signal strength. 90 | private final int rssi; 91 | 92 | // Device timestamp when the result was last seen. 93 | private final long timestampNanos; 94 | 95 | private final int eventType; 96 | private final int primaryPhy; 97 | private final int secondaryPhy; 98 | private final int advertisingSid; 99 | private final int txPower; 100 | private final int periodicAdvertisingInterval; 101 | 102 | /** 103 | * Constructs a new ScanResult. 104 | * 105 | * @param device Remote Bluetooth device found. 106 | * @param scanRecord Scan record including both advertising data and scan response data. 107 | * @param rssi Received signal strength. 108 | * @param timestampNanos Timestamp at which the scan result was observed. 109 | * @deprecated use {@link #ScanResult(BluetoothDevice, int, int, int, int, int, int, int, ScanRecord, long)} 110 | */ 111 | public ScanResult(@NonNull final BluetoothDevice device, @Nullable final ScanRecord scanRecord, 112 | int rssi, long timestampNanos) { 113 | this.device = device; 114 | this.scanRecord = scanRecord; 115 | this.rssi = rssi; 116 | this.timestampNanos = timestampNanos; 117 | this.eventType = (DATA_COMPLETE << 5) | ET_LEGACY_MASK | ET_CONNECTABLE_MASK; 118 | this.primaryPhy = 1; // BluetoothDevice.PHY_LE_1M; 119 | this.secondaryPhy = PHY_UNUSED; 120 | this.advertisingSid = SID_NOT_PRESENT; 121 | this.txPower = 127; 122 | this.periodicAdvertisingInterval = 0; 123 | } 124 | 125 | /** 126 | * Constructs a new ScanResult. 127 | * 128 | * @param device Remote Bluetooth device found. 129 | * @param eventType Event type. 130 | * @param primaryPhy Primary advertising phy. 131 | * @param secondaryPhy Secondary advertising phy. 132 | * @param advertisingSid Advertising set ID. 133 | * @param txPower Transmit power. 134 | * @param rssi Received signal strength. 135 | * @param periodicAdvertisingInterval Periodic advertising interval. 136 | * @param scanRecord Scan record including both advertising data and scan response data. 137 | * @param timestampNanos Timestamp at which the scan result was observed. 138 | */ 139 | public ScanResult(@NonNull final BluetoothDevice device, final int eventType, 140 | final int primaryPhy, final int secondaryPhy, 141 | final int advertisingSid, final int txPower, final int rssi, 142 | final int periodicAdvertisingInterval, 143 | @Nullable final ScanRecord scanRecord, final long timestampNanos) { 144 | this.device = device; 145 | this.eventType = eventType; 146 | this.primaryPhy = primaryPhy; 147 | this.secondaryPhy = secondaryPhy; 148 | this.advertisingSid = advertisingSid; 149 | this.txPower = txPower; 150 | this.rssi = rssi; 151 | this.periodicAdvertisingInterval = periodicAdvertisingInterval; 152 | this.scanRecord = scanRecord; 153 | this.timestampNanos = timestampNanos; 154 | } 155 | 156 | private ScanResult(final Parcel in) { 157 | device = BluetoothDevice.CREATOR.createFromParcel(in); 158 | if (in.readInt() == 1) { 159 | scanRecord = ScanRecord.parseFromBytes(in.createByteArray()); 160 | } 161 | rssi = in.readInt(); 162 | timestampNanos = in.readLong(); 163 | eventType = in.readInt(); 164 | primaryPhy = in.readInt(); 165 | secondaryPhy = in.readInt(); 166 | advertisingSid = in.readInt(); 167 | txPower = in.readInt(); 168 | periodicAdvertisingInterval = in.readInt(); 169 | } 170 | 171 | @Override 172 | public void writeToParcel(final Parcel dest, final int flags) { 173 | device.writeToParcel(dest, flags); 174 | if (scanRecord != null) { 175 | dest.writeInt(1); 176 | dest.writeByteArray(scanRecord.getBytes()); 177 | } else { 178 | dest.writeInt(0); 179 | } 180 | dest.writeInt(rssi); 181 | dest.writeLong(timestampNanos); 182 | dest.writeInt(eventType); 183 | dest.writeInt(primaryPhy); 184 | dest.writeInt(secondaryPhy); 185 | dest.writeInt(advertisingSid); 186 | dest.writeInt(txPower); 187 | dest.writeInt(periodicAdvertisingInterval); 188 | } 189 | 190 | @Override 191 | public int describeContents() { 192 | return 0; 193 | } 194 | 195 | /** 196 | * Returns the remote Bluetooth device identified by the Bluetooth device address. 197 | */ 198 | @NonNull 199 | public BluetoothDevice getDevice() { 200 | return device; 201 | } 202 | 203 | /** 204 | * Returns the scan record, which is a combination of advertisement and scan response. 205 | */ 206 | @Nullable 207 | public ScanRecord getScanRecord() { 208 | return scanRecord; 209 | } 210 | 211 | /** 212 | * Returns the received signal strength in dBm. The valid range is [-127, 126]. 213 | */ 214 | public int getRssi() { 215 | return rssi; 216 | } 217 | 218 | /** 219 | * Returns timestamp since boot when the scan record was observed. 220 | */ 221 | public long getTimestampNanos() { 222 | return timestampNanos; 223 | } 224 | 225 | /** 226 | * Returns true if this object represents legacy scan result. 227 | * Legacy scan results do not contain advanced advertising information 228 | * as specified in the Bluetooth Core Specification v5. 229 | */ 230 | public boolean isLegacy() { 231 | return (eventType & ET_LEGACY_MASK) != 0; 232 | } 233 | 234 | /** 235 | * Returns true if this object represents connectable scan result. 236 | */ 237 | public boolean isConnectable() { 238 | return (eventType & ET_CONNECTABLE_MASK) != 0; 239 | } 240 | 241 | /** 242 | * Returns the data status. 243 | * Can be one of {@link ScanResult#DATA_COMPLETE} or 244 | * {@link ScanResult#DATA_TRUNCATED}. 245 | */ 246 | public int getDataStatus() { 247 | // return bit 5 and 6 248 | return (eventType >> 5) & 0x03; 249 | } 250 | 251 | /** 252 | * Returns the primary Physical Layer 253 | * on which this advertisement was received. 254 | * Can be one of {@link BluetoothDevice#PHY_LE_1M} or 255 | * {@link BluetoothDevice#PHY_LE_CODED}. 256 | */ 257 | public int getPrimaryPhy() { return primaryPhy; } 258 | 259 | /** 260 | * Returns the secondary Physical Layer 261 | * on which this advertisement was received. 262 | * Can be one of {@link BluetoothDevice#PHY_LE_1M}, 263 | * {@link BluetoothDevice#PHY_LE_2M}, {@link BluetoothDevice#PHY_LE_CODED} 264 | * or {@link ScanResult#PHY_UNUSED} - if the advertisement 265 | * was not received on a secondary physical channel. 266 | */ 267 | public int getSecondaryPhy() { return secondaryPhy; } 268 | 269 | /** 270 | * Returns the advertising set id. 271 | * May return {@link ScanResult#SID_NOT_PRESENT} if 272 | * no set id was is present. 273 | */ 274 | public int getAdvertisingSid() { return advertisingSid; } 275 | 276 | /** 277 | * Returns the transmit power in dBm. 278 | * Valid range is [-127, 126]. A value of {@link ScanResult#TX_POWER_NOT_PRESENT} 279 | * indicates that the TX power is not present. 280 | */ 281 | public int getTxPower() { return txPower; } 282 | 283 | /** 284 | * Returns the periodic advertising interval in units of 1.25ms. 285 | * Valid range is 6 (7.5ms) to 65536 (81918.75ms). A value of 286 | * {@link ScanResult#PERIODIC_INTERVAL_NOT_PRESENT} means periodic 287 | * advertising interval is not present. 288 | */ 289 | public int getPeriodicAdvertisingInterval() { 290 | return periodicAdvertisingInterval; 291 | } 292 | 293 | @Override 294 | public int hashCode() { 295 | return Objects.hash(device, rssi, scanRecord, timestampNanos, 296 | eventType, primaryPhy, secondaryPhy, 297 | advertisingSid, txPower, 298 | periodicAdvertisingInterval); 299 | } 300 | 301 | @Override 302 | public boolean equals(final Object obj) { 303 | if (this == obj) { 304 | return true; 305 | } 306 | if (obj == null || getClass() != obj.getClass()) { 307 | return false; 308 | } 309 | final ScanResult other = (ScanResult) obj; 310 | return Objects.equals(device, other.device) && (rssi == other.rssi) && 311 | Objects.equals(scanRecord, other.scanRecord) && 312 | (timestampNanos == other.timestampNanos) && 313 | eventType == other.eventType && 314 | primaryPhy == other.primaryPhy && 315 | secondaryPhy == other.secondaryPhy && 316 | advertisingSid == other.advertisingSid && 317 | txPower == other.txPower && 318 | periodicAdvertisingInterval == other.periodicAdvertisingInterval; 319 | } 320 | 321 | @Override 322 | public String toString() { 323 | return "ScanResult{" + "device=" + device + ", scanRecord=" + 324 | Objects.toString(scanRecord) + ", rssi=" + rssi + 325 | ", timestampNanos=" + timestampNanos + ", eventType=" + eventType + 326 | ", primaryPhy=" + primaryPhy + ", secondaryPhy=" + secondaryPhy + 327 | ", advertisingSid=" + advertisingSid + ", txPower=" + txPower + 328 | ", periodicAdvertisingInterval=" + periodicAdvertisingInterval + '}'; 329 | } 330 | 331 | public static final Parcelable.Creator CREATOR = new Creator() { 332 | @Override 333 | public ScanResult createFromParcel(final Parcel source) { 334 | return new ScanResult(source); 335 | } 336 | 337 | @Override 338 | public ScanResult[] newArray(final int size) { 339 | return new ScanResult[size]; 340 | } 341 | }; 342 | 343 | } 344 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScannerService.java: -------------------------------------------------------------------------------- 1 | package no.nordicsemi.android.support.v18.scanner; 2 | 3 | import android.Manifest; 4 | import android.app.PendingIntent; 5 | import android.app.Service; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.os.Handler; 9 | import android.os.IBinder; 10 | import android.util.Log; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | 17 | import androidx.annotation.NonNull; 18 | import androidx.annotation.Nullable; 19 | import androidx.annotation.RequiresPermission; 20 | 21 | /** 22 | * A service that will emulate 23 | * {@link android.bluetooth.le.BluetoothLeScanner#startScan(List, android.bluetooth.le.ScanSettings, PendingIntent)} 24 | * on Android versions before Oreo. 25 | *

26 | * To start the service call 27 | * {@link BluetoothLeScannerCompat#startScan(List, ScanSettings, Context, PendingIntent)}. 28 | * It will be stopped automatically when the last scan has been stopped using 29 | * {@link BluetoothLeScannerCompat#stopScan(Context, PendingIntent)}. 30 | *

31 | * As this service will run and scan in background it is recommended to use 32 | * {@link ScanSettings#SCAN_MODE_LOW_POWER} mode and set filter to lower power consumption. 33 | */ 34 | public class ScannerService extends Service { 35 | private static final String TAG = "ScannerService"; 36 | 37 | /* package */ static final String EXTRA_PENDING_INTENT = "no.nordicsemi.android.support.v18.EXTRA_PENDING_INTENT"; 38 | /* package */ static final String EXTRA_REQUEST_CODE = "no.nordicsemi.android.support.v18.REQUEST_CODE"; 39 | /* package */ static final String EXTRA_FILTERS = "no.nordicsemi.android.support.v18.EXTRA_FILTERS"; 40 | /* package */ static final String EXTRA_SETTINGS = "no.nordicsemi.android.support.v18.EXTRA_SETTINGS"; 41 | /* package */ static final String EXTRA_START = "no.nordicsemi.android.support.v18.EXTRA_START"; 42 | 43 | @NonNull private final Object LOCK = new Object(); 44 | 45 | private HashMap callbacks; 46 | private Handler handler; 47 | 48 | @Override 49 | public void onCreate() { 50 | super.onCreate(); 51 | callbacks = new HashMap<>(); 52 | handler = new Handler(); 53 | } 54 | 55 | @Override 56 | @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) 57 | public int onStartCommand(final Intent intent, final int flags, final int startId) { 58 | // 59 | // null intent observed on Samsung Galaxy J7 Android 6.0.1 60 | // 61 | if (intent != null) { 62 | final PendingIntent callbackIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); 63 | final int requestCode = intent.getIntExtra(EXTRA_REQUEST_CODE, 0); 64 | final boolean start = intent.getBooleanExtra(EXTRA_START, false); 65 | final boolean stop = !start; 66 | 67 | if (callbackIntent == null) { 68 | boolean shouldStop; 69 | synchronized (LOCK) { 70 | shouldStop = callbacks.isEmpty(); 71 | } 72 | if (shouldStop) 73 | stopSelf(); 74 | return START_NOT_STICKY; 75 | } 76 | 77 | boolean knownCallback; 78 | synchronized (LOCK) { 79 | knownCallback = callbacks.containsKey(requestCode); 80 | } 81 | 82 | if (start && !knownCallback) { 83 | final ArrayList filters = intent.getParcelableArrayListExtra(EXTRA_FILTERS); 84 | final ScanSettings settings = intent.getParcelableExtra(EXTRA_SETTINGS); 85 | startScan(filters != null ? filters : Collections.emptyList(), 86 | settings != null ? settings : new ScanSettings.Builder().build(), 87 | callbackIntent, requestCode); 88 | } else if (stop && knownCallback) { 89 | stopScan(requestCode); 90 | } 91 | } 92 | 93 | return START_NOT_STICKY; 94 | } 95 | 96 | @Nullable 97 | @Override 98 | public IBinder onBind(final Intent intent) { 99 | // Forbid binding 100 | return null; 101 | } 102 | 103 | @Override 104 | public void onTaskRemoved(final Intent rootIntent) { 105 | super.onTaskRemoved(rootIntent); 106 | // Stopping self here would cause the service to be killed when user removes the task 107 | // from Recents. This is not the behavior found in Oreo+. 108 | // Related issue: https://github.com/NordicSemiconductor/Android-Scanner-Compat-Library/issues/45 109 | 110 | // Even with this line removed, the service will stop receiving devices when the phone 111 | // enters Doze mode. 112 | // Find out more here: https://developer.android.com/training/monitoring-device-state/doze-standby 113 | 114 | // stopSelf(); 115 | } 116 | 117 | @Override 118 | @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) 119 | public void onDestroy() { 120 | final BluetoothLeScannerCompat scannerCompat = BluetoothLeScannerCompat.getScanner(); 121 | for (final ScanCallback callback : callbacks.values()) { 122 | try { 123 | scannerCompat.stopScan(callback); 124 | } catch (final Exception e) { 125 | // Ignore 126 | } 127 | } 128 | callbacks.clear(); 129 | callbacks = null; 130 | handler = null; 131 | super.onDestroy(); 132 | } 133 | 134 | @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) 135 | private void startScan(@NonNull final List filters, 136 | @NonNull final ScanSettings settings, 137 | @NonNull final PendingIntent callbackIntent, 138 | final int requestCode) { 139 | final PendingIntentExecutor executor = 140 | new PendingIntentExecutor(callbackIntent, settings, this); 141 | synchronized (LOCK) { 142 | callbacks.put(requestCode, executor); 143 | } 144 | 145 | try { 146 | final BluetoothLeScannerCompat scannerCompat = BluetoothLeScannerCompat.getScanner(); 147 | scannerCompat.startScanInternal(filters, settings, executor, handler); 148 | } catch (final Exception e) { 149 | Log.w(TAG, "Starting scanning failed", e); 150 | } 151 | } 152 | 153 | @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) 154 | private void stopScan(final int requestCode) { 155 | ScanCallback callback; 156 | boolean shouldStop; 157 | synchronized (LOCK) { 158 | callback = callbacks.remove(requestCode); 159 | shouldStop = callbacks.isEmpty(); 160 | } 161 | if (callback == null) 162 | return; 163 | 164 | try { 165 | final BluetoothLeScannerCompat scannerCompat = BluetoothLeScannerCompat.getScanner(); 166 | scannerCompat.stopScan(callback); 167 | } catch (final Exception e) { 168 | Log.w(TAG, "Stopping scanning failed", e); 169 | } 170 | 171 | if (shouldStop) 172 | stopSelf(); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/UserScanCallbackWrapper.java: -------------------------------------------------------------------------------- 1 | package no.nordicsemi.android.support.v18.scanner; 2 | 3 | import java.lang.ref.WeakReference; 4 | import java.util.List; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | 9 | /** 10 | * This class wraps the {@link ScanCallback} object given by the user and holds a weak reference 11 | * to it. This prevents from leaking object if the callbacks are held by Activities, 12 | * Fragments or View Models. 13 | * 14 | * See https://github.com/NordicSemiconductor/Android-Scanner-Compat-Library/issues/109 15 | */ 16 | /* package */ class UserScanCallbackWrapper extends ScanCallback { 17 | private final WeakReference weakScanCallback; 18 | 19 | UserScanCallbackWrapper(@NonNull final ScanCallback userCallback) { 20 | weakScanCallback = new WeakReference<>(userCallback); 21 | } 22 | 23 | boolean isDead() { 24 | return weakScanCallback.get() == null; 25 | } 26 | 27 | @Nullable 28 | ScanCallback get() { 29 | return weakScanCallback.get(); 30 | } 31 | 32 | @Override 33 | public void onScanResult(final int callbackType, @NonNull final ScanResult result) { 34 | final ScanCallback userCallback = weakScanCallback.get(); 35 | if (userCallback != null) 36 | userCallback.onScanResult(callbackType, result); 37 | } 38 | 39 | @Override 40 | public void onBatchScanResults(@NonNull final List results) { 41 | final ScanCallback userCallback = weakScanCallback.get(); 42 | if (userCallback != null) 43 | userCallback.onBatchScanResults(results); 44 | } 45 | 46 | @Override 47 | public void onScanFailed(final int errorCode) { 48 | final ScanCallback userCallback = weakScanCallback.get(); 49 | if (userCallback != null) 50 | userCallback.onScanFailed(errorCode); 51 | } 52 | } -------------------------------------------------------------------------------- /scanner/src/test/java/no/nordicsemi/android/support/v18/scanner/ObjectsTest.java: -------------------------------------------------------------------------------- 1 | package no.nordicsemi.android.support.v18.scanner; 2 | 3 | import org.junit.Test; 4 | 5 | import static com.google.common.truth.Truth.assertThat; 6 | 7 | public class ObjectsTest { 8 | 9 | @Test public void toString_nullValueAsParam_returnNullString() { 10 | // Given 11 | final Object objectA = null; 12 | 13 | // When 14 | final String nullString = Objects.toString(objectA); 15 | 16 | // Then 17 | assertThat(nullString).isEqualTo("null"); 18 | } 19 | 20 | @Test public void toString_nonNullValueAsParam_returnObjectToStringValue() { 21 | // Given 22 | final Object objectA = new Object() { 23 | @Override public String toString() { 24 | return "notNull"; 25 | } 26 | }; 27 | 28 | // When 29 | final String nonNullString = Objects.toString(objectA); 30 | 31 | // Then 32 | assertThat(nonNullString).isEqualTo("notNull"); 33 | } 34 | 35 | @Test public void equals_nullValueAsFirstParam_returnFalse() { 36 | // Given 37 | final Object objectA = null; 38 | final Object objectB = new Object(); 39 | 40 | // When 41 | //noinspection ConstantConditions 42 | final boolean result = Objects.equals(objectA, objectB); 43 | 44 | // Then 45 | assertThat(result).isFalse(); 46 | } 47 | 48 | @Test public void equals_nullValueAsSecondParam_returnFalse() { 49 | // Given 50 | final Object objectA = new Object(); 51 | final Object objectB = null; 52 | 53 | // When 54 | final boolean result = Objects.equals(objectA, objectB); 55 | 56 | // Then 57 | assertThat(result).isFalse(); 58 | } 59 | 60 | @Test public void equals_nullValueAsBothParams_returnTrue() { 61 | // Given 62 | final Object objectA = null; 63 | final Object objectB = null; 64 | 65 | // When 66 | //noinspection ConstantConditions 67 | final boolean result = Objects.equals(objectA, objectB); 68 | 69 | // Then 70 | assertThat(result).isTrue(); 71 | } 72 | 73 | @Test public void equals_differentBooleanParams_returnFalse() { 74 | // Given 75 | final boolean paramA = true; 76 | final boolean paramB = false; 77 | 78 | // When 79 | final boolean result = Objects.equals(paramA, paramB); 80 | 81 | // Then 82 | assertThat(result).isFalse(); 83 | } 84 | 85 | @Test public void equals_sameBooleanParams_returnTrue() { 86 | // Given 87 | final boolean paramA = true; 88 | final boolean paramB = true; 89 | 90 | // When 91 | final boolean result = Objects.equals(paramA, paramB); 92 | 93 | // Then 94 | assertThat(result).isTrue(); 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':scanner' 2 | --------------------------------------------------------------------------------