From 5a4c6bfb0be0edf500e478f88f5904e597781be8 Mon Sep 17 00:00:00 2001 From: gu_tian_le Date: Sat, 8 May 2021 14:20:45 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E3=80=91#AR000000=EF=BC=9A=E9=A6=96=E6=AC=A1=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 41 +- build.gradle | 31 +- gradle.properties | 30 +- gradle/wrapper/gradle-wrapper.jar | Bin 49896 -> 58694 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 115 +- gradlew.bat | 33 +- scanner/.gitignore | 1 - scanner/build.gradle | 46 +- .../v18/scanner/BluetoothLeScannerCompat.java | 965 ++++++------- .../support/v18/scanner/BluetoothLeUtils.java | 181 ++- .../support/v18/scanner/BluetoothUuid.java | 108 +- .../android/support/v18/scanner/Objects.java | 102 +- .../v18/scanner/PendingIntentExecutor.java | 201 +-- .../v18/scanner/PendingIntentReceiver.java | 245 ++-- .../support/v18/scanner/ScanCallback.java | 103 +- .../support/v18/scanner/ScanFilter.java | 1190 ++++++++-------- .../support/v18/scanner/ScanRecord.java | 65 +- .../support/v18/scanner/ScanResult.java | 654 ++++----- .../support/v18/scanner/ScanSettings.java | 1220 +++++++++-------- .../support/v18/scanner/ScannerService.java | 171 ++- settings.gradle | 2 +- 22 files changed, 2781 insertions(+), 2726 deletions(-) diff --git a/.gitignore b/.gitignore index 9c9035b..603b140 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,14 @@ -# Built application files -*.apk -*.ap_ - -# Files for the Dalvik VM -*.dex - -# Java class files -*.class - -# Generated files -bin/ -gen/ - -# Gradle files -.gradle/ -build/ -/*/build/ - -# Local configuration file (sdk path, etc) -local.properties -/.idea *.iml - -# Proguard folder generated by Eclipse -proguard/ - -# Log Files -*.log +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/build.gradle b/build.gradle index 8da322c..0f37d1b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,22 +1,37 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +apply plugin: 'com.huawei.ohos.app' + +ohos { + compileSdkVersion 5 + defaultConfig { + compatibleSdkVersion 5 + } +} buildscript { repositories { - google() + maven { + url 'https://mirrors.huaweicloud.com/repository/maven/' + } + maven { + url 'https://developer.huawei.com/repo/' + } jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.2' - - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files + classpath 'com.huawei.ohos:hap:2.4.2.5' + classpath 'com.huawei.ohos:decctest:1.0.0.6' } } allprojects { repositories { - google() + maven { + url 'https://mirrors.huaweicloud.com/repository/maven/' + } + maven { + url 'https://developer.huawei.com/repo/' + } jcenter() } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 82c67a0..0daf183 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,34 +1,10 @@ # Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: +# IDE (e.g. DevEco Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. - # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html - # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -android.enableJetifier=true -android.useAndroidX=true - -VERSION_NAME=1.4.3 -GROUP=no.nordicsemi.android.support.v18 - -POM_DESCRIPTION=Android Bluetooth LE Scanner Compat library -POM_URL=https://github.com/NordicSemiconductor/Android-Scanner-Compat-Library -POM_SCM_URL=https://github.com/NordicSemiconductor/Android-Scanner-Compat-Library -POM_SCM_CONNECTION=scm:git@github.com:NordicSemiconductor/Android-Scanner-Compat-Library.git -POM_SCM_DEV_CONNECTION=scm:git@github.com:NordicSemiconductor/Android-Scanner-Compat-Library.git -POM_LICENCE=BSD 3-Clause -POM_LICENCE_NAME=The BSD 3-Clause License -POM_LICENCE_URL=http://opensource.org/licenses/BSD-3-Clause -POM_DEVELOPER_ID=nordic -POM_DEVELOPER_NAME=Nordic Semiconductor ASA \ No newline at end of file +# If the Chinese output is garbled, please configure the following parameter. +# org.gradle.jvmargs=-Dfile.encoding=GBK diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8c0fb64a8698b08ecc4158d828ca593c4928e9dd..490fda8577df6c95960ba7077c43220e5bb2c0d9 100644 GIT binary patch literal 58694 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 literal 49896 zcmagFb986H(k`5d^NVfUwr$(C?M#x1ZQHiZiEVpg+jrjgoQrerx!>1o_ul)D>ebz~ zs=Mmxr&>W81QY-S1PKWQ%N-;H^tS;2*XwVA`dej1RRn1z<;3VgfE4~kaG`A%QSPsR z#ovnZe+tS9%1MfeDyz`RirvdjPRK~p(#^q2(^5@O&NM19EHdvN-A&StN>0g6QA^VN z0Gx%Gq#PD$QMRFzmK+utjS^Y1F0e8&u&^=w5K<;4Rz|i3A=o|IKLY+g`iK6vfr9?+ z-`>gmU&i?FGSL5&F?TXFu`&Js6h;15QFkXp2M1H9|Eq~bpov-GU(uz%mH0n55wUl- zv#~ccAz`F5wlQ>e_KlJS3@{)B?^v*EQM=IxLa&76^y51a((wq|2-`qON>+4dLc{Oo z51}}o^Zen(oAjxDK7b++9_Yg`67p$bPo3~BCpGM7uAWmvIhWc5Gi+gQZ|Pwa-Gll@<1xmcPy z|NZmu6m)g5Ftu~BG&Xdxclw7Cij{xbBMBn-LMII#Slp`AElb&2^Hw+w>(3crLH!;I zN+Vk$D+wP1#^!MDCiad@vM>H#6+`Ct#~6VHL4lzmy;lSdk>`z6)=>Wh15Q2)dQtGqvn0vJU@+(B5{MUc*qs4!T+V=q=wy)<6$~ z!G>e_4dN@lGeF_$q9`Ju6Ncb*x?O7=l{anm7Eahuj_6lA{*#Gv*TaJclevPVbbVYu z(NY?5q+xxbO6%g1xF0r@Ix8fJ~u)VRUp`S%&rN$&e!Od`~s+64J z5*)*WSi*i{k%JjMSIN#X;jC{HG$-^iX+5f5BGOIHWAl*%15Z#!xntpk($-EGKCzKa zT7{siZ9;4TICsWQ$pu&wKZQTCvpI$Xvzwxoi+XkkpeE&&kFb!B?h2hi%^YlXt|-@5 zHJ~%AN!g_^tmn1?HSm^|gCE#!GRtK2(L{9pL#hp0xh zME}|DB>(5)`iE7CM)&_+S}-Bslc#@B5W4_+k4Cp$l>iVyg$KP>CN?SVGZ(&02>iZK zB<^HP$g$Lq*L$BWd?2(F?-MUbNWTJVQdW7$#8a|k_30#vHAD1Z{c#p;bETk0VnU5A zBgLe2HFJ3032$G<`m*OB!KM$*sdM20jm)It5OSru@tXpK5LT>#8)N!*skNu1$TpIw zufjjdp#lyH5bZ%|Iuo|iu9vG1HrIVWLH>278xo>aVBkPN3V$~!=KnlXQ4eDqS7%E% zQ!z^$Q$b^6Q)g#cLpwur(|<0gWHo6A6jc;n`t(V9T;LzTAU{IAu*uEQ%Ort1k+Kn+f_N`9|bxYC+~Z1 zCC1UCWv*Orx$_@ydv9mIe(liLfOr7mhbV@tKw{6)q^1DH1nmvZ0cj215R<~&I<4S| zgnr;9Cdjqpz#o8i0CQjtl`}{c*P)aSdH|abxGdrR)-3z+02-eX(k*B)Uqv6~^nh** z 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +75,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -110,10 +125,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -138,27 +154,30 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282..62bd9b9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,14 +24,17 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +65,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +78,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/scanner/.gitignore b/scanner/.gitignore index d0b97c6..796b96d 100644 --- a/scanner/.gitignore +++ b/scanner/.gitignore @@ -1,2 +1 @@ /build -*.iml \ No newline at end of file diff --git a/scanner/build.gradle b/scanner/build.gradle index 152ec5f..76741ae 100644 --- a/scanner/build.gradle +++ b/scanner/build.gradle @@ -1,44 +1,14 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - +apply plugin: 'com.huawei.ohos.library' +ohos { + compileSdkVersion 5 defaultConfig { - minSdkVersion 18 - targetSdkVersion 29 - versionCode 21 - versionName VERSION_NAME - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - buildTypes { - release { - minifyEnabled false - consumerProguardFiles 'scanner-proguard-rules.pro' - } - debug { - testCoverageEnabled true - } + compatibleSdkVersion 5 } + } dependencies { - implementation 'androidx.annotation:annotation:1.1.0' - - androidTestImplementation 'androidx.test:runner:1.3.0-alpha02' - androidTestImplementation 'androidx.test:rules:1.3.0-alpha02' - androidTestImplementation 'org.hamcrest:hamcrest-library:2.1' - androidTestImplementation ('junit:junit:4.13-beta-3') { - exclude module: 'hamcrest-core' - } - - testImplementation 'org.hamcrest:hamcrest-library:2.1' - testImplementation ('junit:junit:4.13-beta-3') { - exclude module: 'hamcrest-core' - } - testImplementation "org.mockito:mockito-core:2.18.0" - testImplementation "org.powermock:powermock-module-junit4:1.7.4" - testImplementation "org.powermock:powermock-api-mockito:1.7.4" + implementation fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + implementation 'org.jetbrains:annotations:15.0' } - -//apply from: rootProject.file('gradle/gradle-bintray-push.gradle') \ No newline at end of file diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeScannerCompat.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeScannerCompat.java index 65ee77d..a5be468 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeScannerCompat.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeScannerCompat.java @@ -22,26 +22,16 @@ package no.nordicsemi.android.support.v18.scanner; -import android.Manifest; -import android.app.PendingIntent; -import android.content.Context; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresPermission; + +import ohos.agp.components.Clock; +import ohos.app.Context; +import ohos.bluetooth.ble.BleScanResult; +import ohos.event.intentagent.IntentAgent; +import ohos.eventhandler.EventHandler; +import ohos.eventhandler.EventRunner; +import org.jetbrains.annotations.Nullable; + +import java.util.*; /** * This class provides methods to perform scan related operations for Bluetooth LE devices. An @@ -52,472 +42,493 @@ import androidx.annotation.RequiresPermission; *

* Since version 1.3.0 of the Scanner Compar Library library, * {@link PendingIntentReceiver} and {@link ScannerService} will be added to AndroidManifest - * whether the scanning with {@link PendingIntent} feature is used or not. + * whether the scanning with {@link IntentAgent} feature is used or not. * The {@link ScannerService} is used to emulate such scanning on platforms * before Oreo, while {@link PendingIntentReceiver} is used to translate from native - * {@link android.bluetooth.le.ScanResult} to compat {@link ScanResult} and apply additional + * {@link ScanResult} to compat {@link ScanResult} and apply additional * filtering. *

* Note: Most of the scan methods here require - * {@link Manifest.permission#BLUETOOTH_ADMIN} permission. * * @see ScanFilter */ @SuppressWarnings("WeakerAccess") public abstract class BluetoothLeScannerCompat { - /** - * Extra containing a list of ScanResults. It can have one or more results if there was no - * error. In case of error, {@link #EXTRA_ERROR_CODE} will contain the error code and this - * extra will not be available. - */ - public static final String EXTRA_LIST_SCAN_RESULT = - "android.bluetooth.le.extra.LIST_SCAN_RESULT"; - - /** - * Optional extra indicating the error code, if any. The error code will be one of the - * SCAN_FAILED_* codes in {@link android.bluetooth.le.ScanCallback}. - */ - public static final String EXTRA_ERROR_CODE = "android.bluetooth.le.extra.ERROR_CODE"; - - /** - * Optional extra indicating the callback type, which will be one of - * CALLBACK_TYPE_* constants in {@link android.bluetooth.le.ScanSettings}. - * - * @see android.bluetooth.le.ScanCallback#onScanResult(int, android.bluetooth.le.ScanResult) - */ - public static final String EXTRA_CALLBACK_TYPE = "android.bluetooth.le.extra.CALLBACK_TYPE"; - - private static BluetoothLeScannerCompat instance; - - /** - * Returns the scanner compat object - * @return scanner implementation - */ - @NonNull - public synchronized static BluetoothLeScannerCompat getScanner() { - if (instance != null) - return instance; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - return instance = new BluetoothLeScannerImplOreo(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - return instance = new BluetoothLeScannerImplMarshmallow(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - return instance = new BluetoothLeScannerImplLollipop(); - return instance = new BluetoothLeScannerImplJB(); - } - - /* package */ BluetoothLeScannerCompat() {} - - /** - * Start Bluetooth LE scan with default parameters and no filters. The scan results will be - * delivered through {@code callback}. - *

- * Requires {@link Manifest.permission#BLUETOOTH_ADMIN} permission. - * An app must hold - * {@link Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_FINE_LOCATION} permission - * in order to get results. - * - * @param callback Callback used to deliver scan results. - * @throws IllegalArgumentException If {@code callback} is null. - */ - @SuppressWarnings("WeakerAccess") - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - public final void startScan(@NonNull final ScanCallback callback) { - //noinspection ConstantConditions - if (callback == null) { - throw new IllegalArgumentException("callback is null"); - } - final Handler handler = new Handler(Looper.getMainLooper()); - startScanInternal(Collections.emptyList(), new ScanSettings.Builder().build(), - callback, handler); - } - - /** - * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}. - *

- * Requires {@link Manifest.permission#BLUETOOTH_ADMIN} permission. - * An app must hold - * {@link Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_FINE_LOCATION} permission - * in order to get results. - * - * @param filters {@link ScanFilter}s for finding exact BLE devices. - * @param settings Optional settings for the scan. - * @param callback Callback used to deliver scan results. - * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. - */ - @SuppressWarnings("unused") - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - public final void startScan(@Nullable final List filters, - @Nullable final ScanSettings settings, - @NonNull final ScanCallback callback) { - //noinspection ConstantConditions - if (callback == null) { - throw new IllegalArgumentException("callback is null"); - } - final Handler handler = new Handler(Looper.getMainLooper()); - startScanInternal(filters != null ? filters : Collections.emptyList(), - settings != null ? settings : new ScanSettings.Builder().build(), - callback, handler); - } - - /** - * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}. - *

- * Requires {@link Manifest.permission#BLUETOOTH_ADMIN} permission. - * An app must hold - * {@link Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_FINE_LOCATION} permission - * in order to get results. - * - * @param filters {@link ScanFilter}s for finding exact BLE devices. - * @param settings Optional settings for the scan. - * @param callback Callback used to deliver scan results. - * @param handler Optional handler used to deliver results. - * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. - */ - @SuppressWarnings("unused") - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - public final void startScan(@Nullable final List filters, - @Nullable final ScanSettings settings, - @NonNull final ScanCallback callback, - @Nullable final Handler handler) { - //noinspection ConstantConditions - if (callback == null) { - throw new IllegalArgumentException("callback is null"); - } - startScanInternal(filters != null ? filters : Collections.emptyList(), - settings != null ? settings : new ScanSettings.Builder().build(), - callback, handler != null ? handler : new Handler(Looper.getMainLooper())); - } - - /** - * Stops an ongoing Bluetooth LE scan. - *

- * Requires {@link Manifest.permission#BLUETOOTH_ADMIN} permission. - * - * @param callback The callback used to start scanning. - */ - @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) - public final void stopScan(@NonNull final ScanCallback callback) { - //noinspection ConstantConditions - if (callback == null) { - throw new IllegalArgumentException("callback is null"); - } - stopScanInternal(callback); - } - - /** - * Starts Bluetooth LE scan. Its implementation depends on the Android version. - * - * @param filters {@link ScanFilter}s for finding exact BLE devices. - * @param settings Settings for the scan. - * @param callback Callback used to deliver scan results. - * @param handler Handler used to deliver results. - */ - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - /* package */ abstract void startScanInternal(@NonNull List filters, - @NonNull ScanSettings settings, - @NonNull ScanCallback callback, - @NonNull Handler handler); - - /** - * Stops an ongoing Bluetooth LE scan. Its implementation depends on the Android version. - * - * @param callback The callback used to start scanning. - */ - @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) - /* package */ abstract void stopScanInternal(@NonNull ScanCallback callback); - - /** - * Start Bluetooth LE scan using a {@link PendingIntent}. The scan results will be delivered - * via the PendingIntent. On platforms before Oreo this will start {@link ScannerService} - * which will scan in background using given settings. - *

- * This method of scanning is intended to work in background. Long running scanning may - * consume a lot of battery, so it is recommended to use low power mode in settings, - * offloaded filtering and batching. However, the library may emulate batching, filtering or - * callback types {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} and - * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} if they are not supported. - *

- * A {@link PendingIntentReceiver} and {@link ScannerService} will be added to AndroidManifest - * whether this feature is used or not. - *

- * Requires {@link Manifest.permission#BLUETOOTH_ADMIN} permission. - * An app must hold - * {@link Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_FINE_LOCATION} permission - * in order to get results. - *

- * When the PendingIntent is delivered, the Intent passed to the receiver or activity will - * contain one or more of the extras {@link #EXTRA_CALLBACK_TYPE}, {@link #EXTRA_ERROR_CODE} and - * {@link #EXTRA_LIST_SCAN_RESULT} to indicate the result of the scan. - * - * @param filters {@link ScanFilter}s for finding exact BLE devices. - * @param settings Optional settings for the scan. - * @param context Context used to start {@link ScannerService}. - * @param callbackIntent The PendingIntent to deliver the result to. - * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. - */ - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - public final void startScan(@Nullable final List filters, - @Nullable final ScanSettings settings, - @NonNull final Context context, - @NonNull final PendingIntent callbackIntent) { - //noinspection ConstantConditions - if (callbackIntent == null) { - throw new IllegalArgumentException("callbackIntent is null"); - } - //noinspection ConstantConditions - if (context == null) { - throw new IllegalArgumentException("context is null"); - } - startScanInternal(filters != null ? filters : Collections.emptyList(), - settings != null ? settings : new ScanSettings.Builder().build(), - context, callbackIntent); - } - - /** - * Stops an ongoing Bluetooth LE scan. - *

- * Requires {@link Manifest.permission#BLUETOOTH_ADMIN} permission. - * - * @param context Context used to stop {@link ScannerService}. - * @param callbackIntent The PendingIntent that was used to start the scan. - */ - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - public final void stopScan(@NonNull final Context context, - @NonNull final PendingIntent callbackIntent) { - //noinspection ConstantConditions - if (callbackIntent == null) { - throw new IllegalArgumentException("callbackIntent is null"); - } - //noinspection ConstantConditions - if (context == null) { - throw new IllegalArgumentException("context is null"); - } - stopScanInternal(context, callbackIntent); - } - - /** - * Starts Bluetooth LE scan using PendingIntent. - * Its implementation depends on the Android version. - * - * @param filters {@link ScanFilter}s for finding exact BLE devices. - * @param settings Settings for the scan. - * @param context Context used to start {@link ScannerService}. - * @param callbackIntent The PendingIntent to deliver the result to. - */ - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - /* package */ abstract void startScanInternal(@NonNull List filters, - @NonNull ScanSettings settings, - @NonNull Context context, - @NonNull PendingIntent callbackIntent); - - /** - * Stops an ongoing Bluetooth LE scan. - * - * @param context Context used to stop {@link ScannerService}. - * @param callbackIntent The PendingIntent that was used to start the scan. - */ - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - /* package */ abstract void stopScanInternal(@NonNull Context context, - @NonNull PendingIntent callbackIntent); - - /** - * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth - * LE scan results batched on Bluetooth controller. Returns immediately, batch scan results data - * will be delivered through the {@code callback}. - * - * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one - * used to start scan. - */ - @SuppressWarnings("unused") - public abstract void flushPendingScanResults(@NonNull ScanCallback callback); - - /* package */ static class ScanCallbackWrapper { - - @NonNull private final Object LOCK = new Object(); - - private final boolean emulateFiltering; - private final boolean emulateBatching; - private final boolean emulateFoundOrLostCallbackType; - private boolean scanningStopped; - - @NonNull final List filters; - @NonNull final ScanSettings scanSettings; - @NonNull final ScanCallback scanCallback; - @NonNull final Handler handler; - - @NonNull private final List scanResults = new ArrayList<>(); - - @NonNull private final Set devicesInBatch = new HashSet<>(); - - /** A collection of scan result of devices in range. */ - @NonNull private final Map devicesInRange = new HashMap<>(); - - @NonNull - private final Runnable flushPendingScanResultsTask = new Runnable() { - @Override - public void run() { - if (!scanningStopped) { - flushPendingScanResults(); - handler.postDelayed(this, scanSettings.getReportDelayMillis()); - } - } - }; - - /** A task, called periodically, that notifies about match lost. */ - @NonNull - private final Runnable matchLostNotifierTask = new Runnable() { - @Override - public void run() { - final long now = SystemClock.elapsedRealtimeNanos(); - - synchronized (LOCK) { - final Iterator iterator = devicesInRange.values().iterator(); - while (iterator.hasNext()) { - final ScanResult result = iterator.next(); - if (result.getTimestampNanos() < now - scanSettings.getMatchLostDeviceTimeout()) { - iterator.remove(); - handler.post(new Runnable() { - @Override - public void run() { - scanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST, result); - } - }); - } - } - - if (!devicesInRange.isEmpty()) { - handler.postDelayed(this, scanSettings.getMatchLostTaskInterval()); - } - } - } - }; - - /* package */ ScanCallbackWrapper(final boolean offloadedBatchingSupported, - final boolean offloadedFilteringSupported, - @NonNull final List filters, - @NonNull final ScanSettings settings, - @NonNull final ScanCallback callback, - @NonNull final Handler handler) { - this.filters = Collections.unmodifiableList(filters); - this.scanSettings = settings; - this.scanCallback = callback; - this.handler = handler; - this.scanningStopped = false; - - // Emulate other callback types - final boolean callbackTypesSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; - emulateFoundOrLostCallbackType = settings.getCallbackType() != ScanSettings.CALLBACK_TYPE_ALL_MATCHES - && (!callbackTypesSupported || !settings.getUseHardwareCallbackTypesIfSupported()); - - // Emulate filtering - emulateFiltering = !filters.isEmpty() && (!offloadedFilteringSupported || !settings.getUseHardwareFilteringIfSupported()); - - // Emulate batching - final long delay = settings.getReportDelayMillis(); - emulateBatching = delay > 0 && (!offloadedBatchingSupported || !settings.getUseHardwareBatchingIfSupported()); - if (emulateBatching) { - handler.postDelayed(flushPendingScanResultsTask, delay); - } - } - - /* package */ void close() { - scanningStopped = true; - handler.removeCallbacksAndMessages(null); - synchronized (LOCK) { - devicesInRange.clear(); - devicesInBatch.clear(); - scanResults.clear(); - } - } - - /* package */ void flushPendingScanResults() { - if (emulateBatching && !scanningStopped) { - synchronized (LOCK) { - scanCallback.onBatchScanResults(new ArrayList<>(scanResults)); - scanResults.clear(); - devicesInBatch.clear(); - } - } - } - - /* package */ void handleScanResult(final int callbackType, - @NonNull final ScanResult scanResult) { - if (scanningStopped || !filters.isEmpty() && !matches(scanResult)) - return; - - final String deviceAddress = scanResult.getDevice().getAddress(); - - // Notify if a new device was found and callback type is FIRST MATCH - if (emulateFoundOrLostCallbackType) { // -> Callback type != ScanSettings.CALLBACK_TYPE_ALL_MATCHES - ScanResult previousResult; - boolean firstResult; - synchronized (devicesInRange) { - // The periodic task will be started only on the first result - firstResult = devicesInRange.isEmpty(); - // Save the first result or update the old one with new data - previousResult = devicesInRange.put(deviceAddress, scanResult); - } - - if (previousResult == null) { - if ((scanSettings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) > 0) { - scanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH, scanResult); - } - } - - // In case user wants to be notified about match lost, we need to start a task that - // will check the timestamp periodically - if (firstResult) { - if ((scanSettings.getCallbackType() & ScanSettings.CALLBACK_TYPE_MATCH_LOST) > 0) { - handler.removeCallbacks(matchLostNotifierTask); - handler.postDelayed(matchLostNotifierTask, scanSettings.getMatchLostTaskInterval()); - } - } - } else { - // A callback type may not contain CALLBACK_TYPE_ALL_MATCHES and any other value. - // If devicesInRange is empty, report delay > 0 means we are emulating hardware - // batching. Otherwise handleScanResults(List) is called, not this method. - if (emulateBatching) { - synchronized (LOCK) { - if (!devicesInBatch.contains(deviceAddress)) { // add only the first record from the device, others will be skipped - scanResults.add(scanResult); - devicesInBatch.add(deviceAddress); - } - } - return; - } - - scanCallback.onScanResult(callbackType, scanResult); - } - } - - /* package */ void handleScanResults(@NonNull final List results) { - if (scanningStopped) - return; - - List filteredResults = results; - - if (emulateFiltering) { - filteredResults = new ArrayList<>(); - for (final ScanResult result : results) - if (matches(result)) - filteredResults.add(result); - } - - scanCallback.onBatchScanResults(filteredResults); - } - - /* package */ void handleScanError(final int errorCode) { - scanCallback.onScanFailed(errorCode); - } - - private boolean matches(@NonNull final ScanResult result) { - for (final ScanFilter filter : filters) { - if (filter.matches(result)) - return true; - } - return false; - } - } + /** + * Extra containing a list of ScanResults. It can have one or more results if there was no + * error. In case of error, {@link #EXTRA_ERROR_CODE} will contain the error code and this + * extra will not be available. + */ + public static final String EXTRA_LIST_SCAN_RESULT = + "android.bluetooth.le.extra.LIST_SCAN_RESULT"; + + /** + * Optional extra indicating the error code, if any. The error code will be one of the + * SCAN_FAILED_* codes in {@link ScanCallback}. + */ + public static final String EXTRA_ERROR_CODE = "android.bluetooth.le.extra.ERROR_CODE"; + + /** + * Optional extra indicating the callback type, which will be one of + * CALLBACK_TYPE_* constants in {@link ScanSettings}. + */ + public static final String EXTRA_CALLBACK_TYPE = "android.bluetooth.le.extra.CALLBACK_TYPE"; + + private static BluetoothLeScannerCompat instance; + + /** + * Returns the scanner compat object + * + * @return scanner implementation + */ + +// public synchronized static BluetoothLeScannerCompat getScanner() { +// if (instance != null) +// return instance; +//// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) +//// return instance = new BluetoothLeScannerImplOreo(); +//// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) +//// return instance = new BluetoothLeScannerImplMarshmallow(); +//// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) +//// return instance = new BluetoothLeScannerImplLollipop(); +//// return instance = new BluetoothLeScannerImplJB(); +// } + + /* package */ BluetoothLeScannerCompat() { + } + + public synchronized static BluetoothLeScannerCompat getScanner() { + if (instance != null) + return instance; + instance = new BluetoothScannerImplV5(); + return instance; + } + + /** + * Start Bluetooth LE scan with default parameters and no filters. The scan results will be + * delivered through {@code callback}. + *

+ * An app must hold + * in order to get results. + * + * @param callback Callback used to deliver scan results. + * @throws IllegalArgumentException If {@code callback} is null. + */ + @SuppressWarnings("WeakerAccess") +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + public final void startScan(final ScanCallback callback) { + //noinspection ConstantConditions + if (callback == null) { + throw new IllegalArgumentException("callback is null"); + } +// EventRunner Looper = EventRunner.create(true); + EventRunner eventRunner = EventRunner.create(true); + final EventHandler handler = new EventHandler(eventRunner); + startScanInternal(Collections.emptyList(), new ScanSettings.Builder().build(), + callback, handler); + } + + /** + * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}. + *

+ * An app must hold + * in order to get results. + * + * @param filters {@link ScanFilter}s for finding exact BLE devices. + * @param settings Optional settings for the scan. + * @param callback Callback used to deliver scan results. + * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. + */ + @SuppressWarnings("unused") +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + public final void startScan(@Nullable final List filters, + @Nullable final ScanSettings settings, + final ScanCallback callback) { + //noinspection ConstantConditions + if (callback == null) { + throw new IllegalArgumentException("callback is null"); + } + EventRunner eventRunner = EventRunner.create(true); + final EventHandler handler = new EventHandler(eventRunner); + startScanInternal(filters != null ? filters : Collections.emptyList(), + settings != null ? settings : new ScanSettings.Builder().build(), + callback, handler); + } + + /** + * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}. + *

+ * An app must hold + * in order to get results. + * + * @param filters {@link ScanFilter}s for finding exact BLE devices. + * @param settings Optional settings for the scan. + * @param callback Callback used to deliver scan results. + * @param handler Optional handler used to deliver results. + * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. + */ + @SuppressWarnings("unused") +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + public final void startScan(@Nullable final List filters, + @Nullable final ScanSettings settings, + final ScanCallback callback, + @Nullable final EventHandler handler) { + //noinspection ConstantConditions + if (callback == null) { + throw new IllegalArgumentException("callback is null"); + } + EventRunner eventRunner = EventRunner.create(true); + startScanInternal(filters != null ? filters : Collections.emptyList(), + settings != null ? settings : new ScanSettings.Builder().build(), + callback, handler != null ? handler : new EventHandler(eventRunner)); + } + + protected abstract void startScanInternal(List scanFilters, ScanSettings scanSettings, ScanCallback callback, EventHandler handler); + + /** + * Stops an ongoing Bluetooth LE scan. + *

+ * + * @param callback The callback used to start scanning. + */ +// @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) + public final void stopScan(final ScanCallback callback) { + //noinspection ConstantConditions + if (callback == null) { + throw new IllegalArgumentException("callback is null"); + } + stopScanInternal(callback); + } + + /** + * Starts Bluetooth LE scan. Its implementation depends on the Android version. + * + * @param filters {@link ScanFilter}s for finding exact BLE devices. + * @param settings Settings for the scan. + * @param callback Callback used to deliver scan results. + * @param EventHandler Handler used to deliver results. + */ +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) +// /* package */ abstract void startScanInternal(List filters, +// ScanSettings settings, +// ScanCallback callback, +// EventHandler handler); + + /** + * Stops an ongoing Bluetooth LE scan. Its implementation depends on the Android version. + * + * @param callback The callback used to start scanning. + */ +// @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) + /* package */ + abstract void stopScanInternal(ScanCallback callback); + + /** + * via the PendingIntent. On platforms before Oreo this will start {@link ScannerService} + * which will scan in background using given settings. + *

+ * This method of scanning is intended to work in background. Long running scanning may + * consume a lot of battery, so it is recommended to use low power mode in settings, + * offloaded filtering and batching. However, the library may emulate batching, filtering or + * callback types {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} and + * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} if they are not supported. + *

+ * A {@link PendingIntentReceiver} and {@link ScannerService} will be added to AndroidManifest + * whether this feature is used or not. + *

+ * An app must hold + * in order to get results. + *

+ * When the PendingIntent is delivered, the Intent passed to the receiver or activity will + * contain one or more of the extras {@link #EXTRA_CALLBACK_TYPE}, {@link #EXTRA_ERROR_CODE} and + * {@link #EXTRA_LIST_SCAN_RESULT} to indicate the result of the scan. + * + * @param filters {@link ScanFilter}s for finding exact BLE devices. + * @param settings Optional settings for the scan. + * @param context Context used to start {@link ScannerService}. + * @param callbackIntent The PendingIntent to deliver the result to. + * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. + */ +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + public final void startScan(@Nullable final List filters, + @Nullable final ScanSettings settings, + final Context context, + final IntentAgent callbackIntent) { + //noinspection ConstantConditions + if (callbackIntent == null) { + throw new IllegalArgumentException("callbackIntent is null"); + } + //noinspection ConstantConditions + if (context == null) { + throw new IllegalArgumentException("context is null"); + } + startScanInternal(filters != null ? filters : Collections.emptyList(), + settings != null ? settings : new ScanSettings.Builder().build(), + context, callbackIntent); + } + + /** + * Stops an ongoing Bluetooth LE scan. + *

+ * + * @param context Context used to stop {@link ScannerService}. + * @param callbackIntent The PendingIntent that was used to start the scan. + */ +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + public final void stopScan(final Context context, + final IntentAgent callbackIntent) { + //noinspection ConstantConditions + if (callbackIntent == null) { + throw new IllegalArgumentException("callbackIntent is null"); + } + //noinspection ConstantConditions + if (context == null) { + throw new IllegalArgumentException("context is null"); + } + stopScanInternal(context, callbackIntent); + } + + /** + * Starts Bluetooth LE scan using PendingIntent. + * Its implementation depends on the Android version. + * @param filters {@link ScanFilter}s for finding exact BLE devices. + * @param settings Settings for the scan. + * @param context Context used to start {@link ScannerService}. + * @param callbackIntent The PendingIntent to deliver the result to. + */ +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + /* package */ + abstract void startScanInternal(List filters, + ScanSettings settings, + Context context, + IntentAgent callbackIntent); + + /** + * Stops an ongoing Bluetooth LE scan. + * + * @param context Context used to stop {@link ScannerService}. + * @param callbackIntent The PendingIntent that was used to start the scan. + */ +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + /* package */ + abstract void stopScanInternal(Context context, + IntentAgent callbackIntent); + + /** + * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth + * LE scan results batched on Bluetooth controller. Returns immediately, batch scan results data + * will be delivered through the {@code callback}. + * + * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one + * used to start scan. + */ + @SuppressWarnings("unused") + public abstract void flushPendingScanResults(ScanCallback callback); + + /* package */ static class ScanCallbackWrapper { + + private final Object LOCK = new Object(); + + private final boolean emulateFiltering; + private final boolean emulateBatching; + // private final boolean emulateFoundOrLostCallbackType; + private boolean scanningStopped; + + final List filters; + final ScanSettings scanSettings; + final ScanCallback scanCallback; + final EventHandler handler; + Context context; + + private final List scanResults = new ArrayList<>(); + + private final Set devicesInBatch = new HashSet<>(); + + /** + * A collection of scan result of devices in range. + */ + private final Map devicesInRange = new HashMap<>(); + + + private final Runnable flushPendingScanResultsTask = new Runnable() { + @Override + public void run() { + if (!scanningStopped) { + flushPendingScanResults(); + handler.postTask(this, scanSettings.getReportDelayMillis()); +// handler.postDelayed(this, scanSettings.getReportDelayMillis()); + } + } + }; + + /** + * A task, called periodically, that notifies about match lost. + */ + + private final Runnable matchLostNotifierTask = new Runnable() { + @Override + public void run() { +// final long now = SystemClock.elapsedRealtimeNanos(); + Clock clock = new Clock(context); + long now = clock.getTime(); + synchronized (LOCK) { + final Iterator iterator = devicesInRange.values().iterator(); + while (iterator.hasNext()) { + final BleScanResult result = iterator.next(); + if (result.getTime() < now - scanSettings.getMatchLostDeviceTimeout()) { + iterator.remove(); + handler.postTask(new Runnable() { + @Override + public void run() { + scanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST, result); + } + }); +// handler.post(new Runnable() { +// @Override +// public void run() { +// scanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST, result); +// } +// }); + } + } + + if (!devicesInRange.isEmpty()) { + handler.postTask(this, scanSettings.getMatchLostTaskInterval()); +// handler.postDelayed(this, scanSettings.getMatchLostTaskInterval()); + } + } + } + }; + + /* package */ ScanCallbackWrapper(final boolean offloadedBatchingSupported, + final boolean offloadedFilteringSupported, + final List filters, + final ScanSettings settings, + final ScanCallback callback, + final EventHandler handler) { + this.filters = Collections.unmodifiableList(filters); + this.scanSettings = settings; + this.scanCallback = callback; + this.handler = handler; + this.scanningStopped = false; + // Emulate other callback types +// final boolean callbackTypesSupported = Build.VERSION_CODES.M; +// emulateFoundOrLostCallbackType = settings.getCallbackType() != ScanSettings.CALLBACK_TYPE_ALL_MATCHES +// && (!callbackTypesSupported || !settings.getUseHardwareCallbackTypesIfSupported()); + + // Emulate filtering + emulateFiltering = !filters.isEmpty() && (!offloadedFilteringSupported || !settings.getUseHardwareFilteringIfSupported()); + + // Emulate batching + final long delay = settings.getReportDelayMillis(); + emulateBatching = delay > 0 && (!offloadedBatchingSupported || !settings.getUseHardwareBatchingIfSupported()); + if (emulateBatching) { + handler.postTask(flushPendingScanResultsTask, delay); +// handler.postDelayed(flushPendingScanResultsTask, delay); + } + } + + /* package */ void close() { + scanningStopped = true; + +// handler.removeCallbacksAndMessages(null); + + synchronized (LOCK) { + devicesInRange.clear(); + devicesInBatch.clear(); + scanResults.clear(); + } + } + + /* package */ void flushPendingScanResults() { + if (emulateBatching && !scanningStopped) { + synchronized (LOCK) { + scanCallback.onBatchScanResults(new ArrayList<>(scanResults)); + scanResults.clear(); + devicesInBatch.clear(); + } + } + } + + /* package */ void handleScanResult(final int callbackType, + final BleScanResult scanResult) { + if (scanningStopped || !filters.isEmpty() && !matches(scanResult)) + return; + final String deviceAddress = scanResult.getPeripheralDevice().getDeviceAddr(); +// final String deviceAddress = scanResult.getDevice().getAddress(); + + // Notify if a new device was found and callback type is FIRST MATCH +// if (emulateFoundOrLostCallbackType) { // -> Callback type != ScanSettings.CALLBACK_TYPE_ALL_MATCHES + BleScanResult previousResult; + boolean firstResult; + synchronized (devicesInRange) { + // The periodic task will be started only on the first result + firstResult = devicesInRange.isEmpty(); + // Save the first result or update the old one with new data + previousResult = devicesInRange.put(deviceAddress, scanResult); +// } + + if (previousResult == null) { + if ((scanSettings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) > 0) { + scanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH, scanResult); + } + } + + // In case user wants to be notified about match lost, we need to start a task that + // will check the timestamp periodically + if (firstResult) { + if ((scanSettings.getCallbackType() & ScanSettings.CALLBACK_TYPE_MATCH_LOST) > 0) { + +// handler.removeCallbacks(matchLostNotifierTask); + handler.removeTask(matchLostNotifierTask); + handler.postTask(matchLostNotifierTask, scanSettings.getMatchLostTaskInterval()); +// handler.postDelayed(matchLostNotifierTask, scanSettings.getMatchLostTaskInterval()); + } + } + // A callback type may not contain CALLBACK_TYPE_ALL_MATCHES and any other value. + // If devicesInRange is empty, report delay > 0 means we are emulating hardware + // batching. Otherwise handleScanResults(List) is called, not this method. + if (emulateBatching) { + synchronized (LOCK) { + if (!devicesInBatch.contains(deviceAddress)) { // add only the first record from the device, others will be skipped + scanResults.add(scanResult); + devicesInBatch.add(deviceAddress); + } + } + return; + } + + scanCallback.onScanResult(callbackType, scanResult); + } + } + + /* package */ void handleScanResults(final List results) { + if (scanningStopped) + return; + + List filteredResults = results; + + if (emulateFiltering) { + filteredResults = new ArrayList<>(); + for (final BleScanResult result : results) + if (matches(result)) + filteredResults.add(result); + } + + scanCallback.onBatchScanResults(filteredResults); + } + + /* package */ void handleScanError(final int errorCode) { + scanCallback.onScanFailed(errorCode); + } + + private boolean matches(final BleScanResult result) { + for (final ScanFilter filter : filters) { + if (filter.matches(result)) + return true; + } + return false; + } + } } diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeUtils.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeUtils.java index 14a947d..44d1beb 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeUtils.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothLeUtils.java @@ -16,14 +16,13 @@ package no.nordicsemi.android.support.v18.scanner; -import android.util.SparseArray; - import java.util.Arrays; import java.util.Iterator; import java.util.Map; import java.util.Set; -import androidx.annotation.Nullable; +import ohos.utils.PlainArray; +import org.jetbrains.annotations.Nullable; /** * Helper class for Bluetooth LE utils. @@ -32,98 +31,96 @@ import androidx.annotation.Nullable; @SuppressWarnings("unused") class BluetoothLeUtils { - /** - * Returns a string composed from a {@link SparseArray}. - */ - static String toString(@Nullable final SparseArray array) { - if (array == null) { - return "null"; - } - if (array.size() == 0) { - return "{}"; - } - final StringBuilder buffer = new StringBuilder(); - buffer.append('{'); - for (int i = 0; i < array.size(); ++i) { - buffer.append(array.keyAt(i)).append("=").append(Arrays.toString(array.valueAt(i))); - } - buffer.append('}'); - return buffer.toString(); - } + /** + */ + static String toString(@Nullable final PlainArray array) { + if (array == null) { + return "null"; + } + if (array.size() == 0) { + return "{}"; + } + final StringBuilder buffer = new StringBuilder(); + buffer.append('{'); + for (int i = 0; i < array.size(); ++i) { + buffer.append(array.keyAt(i)).append("=").append(Arrays.toString(array.valueAt(i))); + } + buffer.append('}'); + return buffer.toString(); + } - /** - * Returns a string composed from a {@link Map}. - */ - static String toString(@Nullable final Map map) { - if (map == null) { - return "null"; - } - if (map.isEmpty()) { - return "{}"; - } - final StringBuilder buffer = new StringBuilder(); - buffer.append('{'); - final Iterator> it = map.entrySet().iterator(); - while (it.hasNext()) { - final Map.Entry entry = it.next(); - final Object key = entry.getKey(); - buffer.append(key).append("=").append(Arrays.toString(map.get(key))); - if (it.hasNext()) { - buffer.append(", "); - } - } - buffer.append('}'); - return buffer.toString(); - } + /** + * Returns a string composed from a {@link Map}. + */ + static String toString(@Nullable final Map map) { + if (map == null) { + return "null"; + } + if (map.isEmpty()) { + return "{}"; + } + final StringBuilder buffer = new StringBuilder(); + buffer.append('{'); + final Iterator> it = map.entrySet().iterator(); + while (it.hasNext()) { + final Map.Entry entry = it.next(); + final Object key = entry.getKey(); + buffer.append(key).append("=").append(Arrays.toString(map.get(key))); + if (it.hasNext()) { + buffer.append(", "); + } + } + buffer.append('}'); + return buffer.toString(); + } - /** - * Check whether two {@link SparseArray} equal. - */ - static boolean equals(@Nullable final SparseArray array, - @Nullable final SparseArray otherArray) { - if (array == otherArray) { - return true; - } - if (array == null || otherArray == null) { - return false; - } - if (array.size() != otherArray.size()) { - return false; - } + /** + */ + static boolean equals(@Nullable final PlainArray array, + @Nullable final PlainArray otherArray) { + if (array == otherArray) { + return true; + } + if (array == null || otherArray == null) { + return false; + } + if (array.size() != otherArray.size()) { + return false; + } - // Keys are guaranteed in ascending order when indices are in ascending order. - for (int i = 0; i < array.size(); ++i) { - if (array.keyAt(i) != otherArray.keyAt(i) || - !Arrays.equals(array.valueAt(i), otherArray.valueAt(i))) { - return false; - } - } - return true; - } + // Keys are guaranteed in ascending order when indices are in ascending order. + for (int i = 0; i < array.size(); ++i) { + if (array.keyAt(i) != otherArray.keyAt(i) || + !Arrays.equals(array.valueAt(i), otherArray.valueAt(i))) { + return false; + } + } + return true; + } - /** - * Check whether two {@link Map} equal. - */ - static boolean equals(@Nullable final Map map, Map otherMap) { - if (map == otherMap) { - return true; - } - if (map == null || otherMap == null) { - return false; - } - if (map.size() != otherMap.size()) { - return false; - } - Set keys = map.keySet(); - if (!keys.equals(otherMap.keySet())) { - return false; - } - for (T key : keys) { - if (!Objects.deepEquals(map.get(key), otherMap.get(key))) { - return false; - } - } - return true; - } + /** + * Check whether two {@link Map} equal. + */ + static boolean equals(@Nullable final Map map, Map otherMap) { + if (map == otherMap) { + return true; + } + if (map == null || otherMap == null) { + return false; + } + if (map.size() != otherMap.size()) { + return false; + } + Set keys = map.keySet(); + if (!keys.equals(otherMap.keySet())) { + return false; + } + for (T key : keys) { + if (!Objects.deepEquals(map.get(key), otherMap.get(key))) { + return false; + } + } + return true; + } } \ No newline at end of file diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothUuid.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothUuid.java index 167a45b..3b5ca0b 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothUuid.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothUuid.java @@ -16,7 +16,8 @@ package no.nordicsemi.android.support.v18.scanner; -import android.os.ParcelUuid; + +import ohos.utils.SequenceUuid; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -25,59 +26,64 @@ import java.util.UUID; /** * Static helper methods and constants to decode the ParcelUuid of remote devices. */ -/* package */ final class BluetoothUuid { +/* package */ public final class BluetoothUuid { - private static final ParcelUuid BASE_UUID = - ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB"); + private static final SequenceUuid BASE_UUID = + SequenceUuid.uuidFromString("00000000-0000-1000-8000-00805F9B34FB"); - /** Length of bytes for 16 bit UUID */ - static final int UUID_BYTES_16_BIT = 2; - /** Length of bytes for 32 bit UUID */ - static final int UUID_BYTES_32_BIT = 4; - /** Length of bytes for 128 bit UUID */ - static final int UUID_BYTES_128_BIT = 16; + /** + * Length of bytes for 16 bit UUID + */ + static final int UUID_BYTES_16_BIT = 2; + /** + * Length of bytes for 32 bit UUID + */ + static final int UUID_BYTES_32_BIT = 4; + /** + * Length of bytes for 128 bit UUID + */ + static final int UUID_BYTES_128_BIT = 16; - /** - * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID, - * but the returned UUID is always in 128-bit format. - * Note UUID is little endian in Bluetooth. - * - * @param uuidBytes Byte representation of uuid. - * @return {@link ParcelUuid} parsed from bytes. - * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed. - */ - static ParcelUuid parseUuidFrom(final byte[] uuidBytes) { - if (uuidBytes == null) { - throw new IllegalArgumentException("uuidBytes cannot be null"); - } - final int length = uuidBytes.length; - if (length != UUID_BYTES_16_BIT && length != UUID_BYTES_32_BIT && - length != UUID_BYTES_128_BIT) { - throw new IllegalArgumentException("uuidBytes length invalid - " + length); - } + /** + * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID, + * but the returned UUID is always in 128-bit format. + * Note UUID is little endian in Bluetooth. + * + * @param uuidBytes Byte representation of uuid. + * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed. + */ + static SequenceUuid parseUuidFrom(final byte[] uuidBytes) { + if (uuidBytes == null) { + throw new IllegalArgumentException("uuidBytes cannot be null"); + } + final int length = uuidBytes.length; + if (length != UUID_BYTES_16_BIT && length != UUID_BYTES_32_BIT && + length != UUID_BYTES_128_BIT) { + throw new IllegalArgumentException("uuidBytes length invalid - " + length); + } - // Construct a 128 bit UUID. - if (length == UUID_BYTES_128_BIT) { - final ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN); - final long msb = buf.getLong(8); - final long lsb = buf.getLong(0); - return new ParcelUuid(new UUID(msb, lsb)); - } + // Construct a 128 bit UUID. + if (length == UUID_BYTES_128_BIT) { + final ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN); + final long msb = buf.getLong(8); + final long lsb = buf.getLong(0); + return new SequenceUuid(new UUID(msb, lsb)); + } - // For 16 bit and 32 bit UUID we need to convert them to 128 bit value. - // 128_bit_value = uuid * 2^96 + BASE_UUID - long shortUuid; - if (length == UUID_BYTES_16_BIT) { - shortUuid = uuidBytes[0] & 0xFF; - shortUuid += (uuidBytes[1] & 0xFF) << 8; - } else { - shortUuid = uuidBytes[0] & 0xFF ; - shortUuid += (uuidBytes[1] & 0xFF) << 8; - shortUuid += (uuidBytes[2] & 0xFF) << 16; - shortUuid += (uuidBytes[3] & 0xFF) << 24; - } - final long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32); - final long lsb = BASE_UUID.getUuid().getLeastSignificantBits(); - return new ParcelUuid(new UUID(msb, lsb)); - } + // For 16 bit and 32 bit UUID we need to convert them to 128 bit value. + // 128_bit_value = uuid * 2^96 + BASE_UUID + long shortUuid; + if (length == UUID_BYTES_16_BIT) { + shortUuid = uuidBytes[0] & 0xFF; + shortUuid += (uuidBytes[1] & 0xFF) << 8; + } else { + shortUuid = uuidBytes[0] & 0xFF; + shortUuid += (uuidBytes[1] & 0xFF) << 8; + shortUuid += (uuidBytes[2] & 0xFF) << 16; + shortUuid += (uuidBytes[3] & 0xFF) << 24; + } + final long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32); + final long lsb = BASE_UUID.getUuid().getLeastSignificantBits(); + return new SequenceUuid(new UUID(msb, lsb)); + } } diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/Objects.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/Objects.java index d54c0c7..35ac421 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/Objects.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/Objects.java @@ -22,61 +22,63 @@ package no.nordicsemi.android.support.v18.scanner; +import org.jetbrains.annotations.Nullable; + import java.util.Arrays; /* package */ class Objects { - /** - * Returns true if both arguments are null, - * the result of {@link Arrays#equals} if both arguments are primitive arrays, - * the result of {@link Arrays#deepEquals} if both arguments are arrays of reference types, - * and the result of {@link #equals} otherwise. - */ - static boolean deepEquals(final Object a, final Object b) { - if (a == null || b == null) { - return a == b; - } else if (a instanceof Object[] && b instanceof Object[]) { - return Arrays.deepEquals((Object[]) a, (Object[]) b); - } else if (a instanceof boolean[] && b instanceof boolean[]) { - return Arrays.equals((boolean[]) a, (boolean[]) b); - } else if (a instanceof byte[] && b instanceof byte[]) { - return Arrays.equals((byte[]) a, (byte[]) b); - } else if (a instanceof char[] && b instanceof char[]) { - return Arrays.equals((char[]) a, (char[]) b); - } else if (a instanceof double[] && b instanceof double[]) { - return Arrays.equals((double[]) a, (double[]) b); - } else if (a instanceof float[] && b instanceof float[]) { - return Arrays.equals((float[]) a, (float[]) b); - } else if (a instanceof int[] && b instanceof int[]) { - return Arrays.equals((int[]) a, (int[]) b); - } else if (a instanceof long[] && b instanceof long[]) { - return Arrays.equals((long[]) a, (long[]) b); - } else if (a instanceof short[] && b instanceof short[]) { - return Arrays.equals((short[]) a, (short[]) b); - } - return a.equals(b); - } + /** + * Returns true if both arguments are null, + * the result of {@link Arrays#equals} if both arguments are primitive arrays, + * the result of {@link Arrays#deepEquals} if both arguments are arrays of reference types, + * and the result of {@link #equals} otherwise. + */ + static boolean deepEquals(final Object a, final Object b) { + if (a == null || b == null) { + return a == b; + } else if (a instanceof Object[] && b instanceof Object[]) { + return Arrays.deepEquals((Object[]) a, (Object[]) b); + } else if (a instanceof boolean[] && b instanceof boolean[]) { + return Arrays.equals((boolean[]) a, (boolean[]) b); + } else if (a instanceof byte[] && b instanceof byte[]) { + return Arrays.equals((byte[]) a, (byte[]) b); + } else if (a instanceof char[] && b instanceof char[]) { + return Arrays.equals((char[]) a, (char[]) b); + } else if (a instanceof double[] && b instanceof double[]) { + return Arrays.equals((double[]) a, (double[]) b); + } else if (a instanceof float[] && b instanceof float[]) { + return Arrays.equals((float[]) a, (float[]) b); + } else if (a instanceof int[] && b instanceof int[]) { + return Arrays.equals((int[]) a, (int[]) b); + } else if (a instanceof long[] && b instanceof long[]) { + return Arrays.equals((long[]) a, (long[]) b); + } else if (a instanceof short[] && b instanceof short[]) { + return Arrays.equals((short[]) a, (short[]) b); + } + return a.equals(b); + } - /** - * Null-safe equivalent of {@code a.equals(b)}. - */ - static boolean equals(final Object a, final Object b) { - return (a == null) ? (b == null) : a.equals(b); - } + /** + * Null-safe equivalent of {@code a.equals(b)}. + */ + static boolean equals(final Object a, final Object b) { + return (a == null) ? (b == null) : a.equals(b); + } - /** - * Convenience wrapper for {@link Arrays#hashCode}, adding varargs. - * This can be used to compute a hash code for an object's fields as follows: - * {@code Objects.hash(a, b, c)}. - */ - static int hash(final Object... values) { - return Arrays.hashCode(values); - } + /** + * Convenience wrapper for {@link Arrays#hashCode}, adding varargs. + * This can be used to compute a hash code for an object's fields as follows: + * {@code Objects.hash(a, b, c)}. + */ + static int hash(final Object... values) { + return Arrays.hashCode(values); + } - /** - * Returns "null" for null or {@code o.toString()}. - */ - static String toString(final Object o) { - return (o == null) ? "null" : o.toString(); - } + /** + * Returns "null" for null or {@code o.toString()}. + */ + static String toString(final Object o) { + return (o == null) ? "null" : o.toString(); + } } diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentExecutor.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentExecutor.java index 06e9308..2b5a2cc 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentExecutor.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentExecutor.java @@ -1,129 +1,130 @@ package no.nordicsemi.android.support.v18.scanner; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Parcelable; -import android.os.SystemClock; - +import java.security.Provider; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import ohos.aafwk.content.Intent; +import ohos.aafwk.content.IntentParams; +import ohos.agp.components.Clock; +import ohos.ai.asr.AsrIntent; +import ohos.app.Context; +import ohos.bluetooth.ble.BleScanResult; +import ohos.event.intentagent.IntentAgent; +import ohos.utils.Sequenceable; +import org.jetbrains.annotations.Nullable; /** - * A ScanCallback that will send a {@link PendingIntent} when callback's methods are called. + * */ /* package */ class PendingIntentExecutor extends ScanCallback { - @NonNull private final PendingIntent callbackIntent; - - /** A temporary context given to the {@link android.content.BroadcastReceiver}. */ - @Nullable private Context context; - /** The service using this executor. */ - @Nullable private Context service; + private final IntentAgent callbackIntent; + /** + * + */ + @Nullable + private Context context; + /** + * The service using this executor. + */ + @Nullable + private Context service; private long lastBatchTimestamp; private long reportDelay; /** - * Creates the {@link PendingIntent} executor that will be used from a - * {@link android.content.BroadcastReceiver}. The {@link Context} may change in every - * {@link android.content.BroadcastReceiver#onReceive(Context, Intent)} call, so it is not * kept here. Instead, a temporary context must be set with {@link #setTemporaryContext(Context)} * each time before the received results are handled and released after that to * prevent from keeping a string reference to the context by a static object. * - * @param callbackIntent User's {@link PendingIntent} used in - * {@link BluetoothLeScannerCompat#startScan(List, ScanSettings, Context, PendingIntent)}. - * @param settings Scan settings specified by the user. + * @param settings Scan settings specified by the user. */ - PendingIntentExecutor(@NonNull final PendingIntent callbackIntent, - @NonNull final ScanSettings settings) { - this.callbackIntent = callbackIntent; - this.reportDelay = settings.getReportDelayMillis(); - } - - /** - * Creates the {@link PendingIntent} executor that will be used from a {@link Service}. - * The service instance will be used as {@link Context} to send intents. - * - * @param callbackIntent User's {@link PendingIntent} used in - * {@link BluetoothLeScannerCompat#startScan(List, ScanSettings, Context, PendingIntent)}. - * @param settings Scan settings specified by the user. - * @param service The service that will scan for Bluetooth LE devices in background. - */ - PendingIntentExecutor(@NonNull final PendingIntent callbackIntent, - @NonNull final ScanSettings settings, - @NonNull final Service service) { - this.callbackIntent = callbackIntent; - this.reportDelay = settings.getReportDelayMillis(); - this.service = service; - } - - /* package */ void setTemporaryContext(@Nullable final Context context) { - this.context = context; - } - - @Override - public void onScanResult(final int callbackType, @NonNull final ScanResult result) { - final Context context = this.context != null ? this.context : this.service; - if (context == null) - return; - - try { - final Intent extrasIntent = new Intent(); - extrasIntent.putExtra(BluetoothLeScannerCompat.EXTRA_CALLBACK_TYPE, callbackType); - extrasIntent.putParcelableArrayListExtra(BluetoothLeScannerCompat.EXTRA_LIST_SCAN_RESULT, - new ArrayList<>(Collections.singletonList(result))); - callbackIntent.send(context, 0, extrasIntent); - } catch (final PendingIntent.CanceledException e) { - // Ignore - } - } - - @Override - public void onBatchScanResults(@NonNull final List results) { - final Context context = this.context != null ? this.context : this.service; - if (context == null) - return; + PendingIntentExecutor(final IntentAgent callbackIntent, + final ScanSettings settings) { + this.callbackIntent = callbackIntent; + this.reportDelay = settings.getReportDelayMillis(); + } + + /** + * The service instance will be used as {@link Context} to send intents. + * + * @param callbackIntent User's {@link IntentAgent} used in + * {@link BluetoothLeScannerCompat#startScan(List, ScanSettings, Context, IntentAgent)}. + * @param settings Scan settings specified by the user. + * @param service The service that will scan for Bluetooth LE devices in background. + */ + PendingIntentExecutor(final IntentAgent callbackIntent, + final ScanSettings settings, + final Provider.Service service) { + this.callbackIntent = callbackIntent; + this.reportDelay = settings.getReportDelayMillis(); +// this.service = service; + this.context = context; + } + + /* package */ void setTemporaryContext(@Nullable final Context context) { + this.context = context; + } + +// @Override + public void onScanResult(final int callbackType, final ScanResult result) { + final Context context = this.context != null ? this.context : this.service; + if (context == null) + return; + + + final Intent extrasIntent = new Intent(); + extrasIntent.setParam(BluetoothLeScannerCompat.EXTRA_CALLBACK_TYPE, callbackType); +// extrasIntent.putExtra(BluetoothLeScannerCompat.EXTRA_CALLBACK_TYPE, callbackType); +// extrasIntent.putParcelableArrayListExtra(BluetoothLeScannerCompat.EXTRA_LIST_SCAN_RESULT,new ArrayList<>(Collections.singletonList(result))); + extrasIntent.setSequenceableArrayListParam(BluetoothLeScannerCompat.EXTRA_LIST_SCAN_RESULT, new ArrayList<>(Collections.singletonList(result))); +// callbackIntent.send(context, 0, extrasIntent); + callbackIntent.equals(extrasIntent); + } + + @Override + public void onBatchScanResults(final List results) { + final Context context = this.context != null ? this.context : this.service; + if (context == null) + return; // On several phones the broadcast is sent twice for every batch. // Skip the second call if came to early. - final long now = SystemClock.elapsedRealtime(); - if (lastBatchTimestamp > now - reportDelay + 5) { +// final long now = Clock.elapsedRealtime(); + Clock clock = new Clock(context); + final long now = clock.getTime(); + if (lastBatchTimestamp > now - reportDelay + 5) { return; } lastBatchTimestamp = now; - try { - final Intent extrasIntent = new Intent(); - extrasIntent.putExtra(BluetoothLeScannerCompat.EXTRA_CALLBACK_TYPE, - ScanSettings.CALLBACK_TYPE_ALL_MATCHES); - extrasIntent.putParcelableArrayListExtra(BluetoothLeScannerCompat.EXTRA_LIST_SCAN_RESULT, - new ArrayList(results)); - extrasIntent.setExtrasClassLoader(ScanResult.class.getClassLoader()); - callbackIntent.send(context, 0, extrasIntent); - } catch (final PendingIntent.CanceledException e) { - // Ignore - } - } - - @Override - public void onScanFailed(final int errorCode) { - final Context context = this.context != null ? this.context : this.service; - if (context == null) - return; - - try { - final Intent extrasIntent = new Intent(); - extrasIntent.putExtra(BluetoothLeScannerCompat.EXTRA_ERROR_CODE, errorCode); - callbackIntent.send(context, 0, extrasIntent); - } catch (final PendingIntent.CanceledException e) { - // Ignore - } - } +// try { + final Intent extrasIntent = new Intent(); + final IntentParams Intent = new IntentParams(); + extrasIntent.setParam(BluetoothLeScannerCompat.EXTRA_CALLBACK_TYPE, ScanSettings.CALLBACK_TYPE_ALL_MATCHES); +// extrasIntent.putParcelableArrayListExtra(BluetoothLeScannerCompat.EXTRA_LIST_SCAN_RESULT, new ArrayList(results)); + extrasIntent.setSequenceableArrayListParam(BluetoothLeScannerCompat.EXTRA_LIST_SCAN_RESULT, new ArrayList<>(results)); + Intent.setClassLoader(ScanResult.class.getClassLoader()); +// callbackIntent.send(context, 0, extrasIntent); + callbackIntent.equals(extrasIntent); +// } catch (final IntentAgent.CanceledException e) { +// // Ignore +// } + } + + @Override + public void onScanFailed(final int errorCode) { + final Context context = this.context != null ? this.context : this.service; + if (context == null) + return; + + final Intent extrasIntent = new Intent(); + extrasIntent.setParam(BluetoothLeScannerCompat.EXTRA_ERROR_CODE, errorCode); +// callbackIntent.send(context, 0, extrasIntent); + callbackIntent.equals(extrasIntent); + + } } \ No newline at end of file diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentReceiver.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentReceiver.java index e9ff998..ef1a724 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentReceiver.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentReceiver.java @@ -1,134 +1,137 @@ package no.nordicsemi.android.support.v18.scanner; -import android.app.PendingIntent; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.le.BluetoothLeScanner; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Build; - import java.util.ArrayList; import java.util.List; -import androidx.annotation.RequiresApi; +import ohos.aafwk.content.Intent; +import ohos.app.Context; +import ohos.app.GeneralReceiver; +import ohos.bluetooth.ble.BleScanFilter; +import ohos.bluetooth.ble.BleScanResult; +import ohos.event.intentagent.IntentAgent; /** * This receiver, registered in AndroidManifest, will translate received - * {@link android.bluetooth.le.ScanResult}s into compat {@link ScanResult}s and will send - * a {@link PendingIntent} registered by the user with those converted data. It will also apply + * {@link ScanResult}s into compat {@link ScanResult}s and will send * any filters, perform batching or emulate callback types * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} and * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} on devices that do not support it. */ -public class PendingIntentReceiver extends BroadcastReceiver { - - /* package */ static final String ACTION = "no.nordicsemi.android.support.v18.ACTION_FOUND"; - /* package */ static final String EXTRA_PENDING_INTENT = "no.nordicsemi.android.support.v18.EXTRA_PENDING_INTENT"; - /* package */ static final String EXTRA_FILTERS = "no.nordicsemi.android.support.v18.EXTRA_FILTERS"; - /* package */ static final String EXTRA_SETTINGS = "no.nordicsemi.android.support.v18.EXTRA_SETTINGS"; - /* package */ static final String EXTRA_USE_HARDWARE_BATCHING = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_BATCHING"; - /* package */ static final String EXTRA_USE_HARDWARE_FILTERING = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_FILTERING"; - /* package */ static final String EXTRA_USE_HARDWARE_CALLBACK_TYPES = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_CALLBACK_TYPES"; - /* package */ static final String EXTRA_MATCH_LOST_TIMEOUT = "no.nordicsemi.android.support.v18.EXTRA_MATCH_LOST_TIMEOUT"; - /* package */ static final String EXTRA_MATCH_LOST_INTERVAL = "no.nordicsemi.android.support.v18.EXTRA_MATCH_LOST_INTERVAL"; - /* package */ static final String EXTRA_MATCH_MODE = "no.nordicsemi.android.support.v18.EXTRA_MATCH_MODE"; - /* package */ static final String EXTRA_NUM_OF_MATCHES = "no.nordicsemi.android.support.v18.EXTRA_NUM_OF_MATCHES"; - - @RequiresApi(api = Build.VERSION_CODES.O) - @Override - public void onReceive(final Context context, final Intent intent) { - // Ensure we are ok. - if (context == null || intent == null) - return; - - // Find the target pending intent. - final PendingIntent callbackIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); - if (callbackIntent == null) - return; - - // Filters and settings have been set as native objects, otherwise they could not be - // serialized by the system scanner. - final ArrayList nativeScanFilters = - intent.getParcelableArrayListExtra(EXTRA_FILTERS); - final android.bluetooth.le.ScanSettings nativeScanSettings = intent.getParcelableExtra(EXTRA_SETTINGS); - if (nativeScanFilters == null || nativeScanSettings == null) - return; - - // Some ScanSettings parameters are only on compat version and need to be sent separately. - final boolean useHardwareBatchingIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_BATCHING, true); - final boolean useHardwareFilteringIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_FILTERING, true); - final boolean useHardwareCallbackTypesIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_CALLBACK_TYPES, true); - final long matchLostDeviceTimeout = intent.getLongExtra(EXTRA_MATCH_LOST_TIMEOUT, ScanSettings.MATCH_LOST_DEVICE_TIMEOUT_DEFAULT); - final long matchLostTaskInterval = intent.getLongExtra(EXTRA_MATCH_LOST_INTERVAL, ScanSettings.MATCH_LOST_TASK_INTERVAL_DEFAULT); - final int matchMode = intent.getIntExtra(EXTRA_MATCH_MODE, ScanSettings.MATCH_MODE_AGGRESSIVE); - final int numOfMatches = intent.getIntExtra(EXTRA_NUM_OF_MATCHES, ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT); - - // Convert native objects to compat versions. - final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); - final BluetoothLeScannerImplOreo scannerImpl = (BluetoothLeScannerImplOreo) scanner; - final ArrayList filters = scannerImpl.fromNativeScanFilters(nativeScanFilters); - final ScanSettings settings = scannerImpl.fromNativeScanSettings(nativeScanSettings, - useHardwareBatchingIfSupported, - useHardwareFilteringIfSupported, - useHardwareCallbackTypesIfSupported, - matchLostDeviceTimeout, matchLostTaskInterval, - matchMode, numOfMatches); - - // Check device capabilities and create a wrapper that will send a PendingIntent. - final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - final boolean offloadedBatchingSupported = adapter.isOffloadedScanBatchingSupported(); - final boolean offloadedFilteringSupported = adapter.isOffloadedFilteringSupported(); - - // Obtain or create a PendingIntentExecutorWrapper. A static instance (obtained from a - // static BluetoothLeScannerCompat singleton) is necessary as it allows to keeps - // track of found devices and emulate batching and callback types if those are not - // supported or a compat version was forced. - - BluetoothLeScannerImplOreo.PendingIntentExecutorWrapper wrapper; - //noinspection SynchronizationOnLocalVariableOrMethodParameter - synchronized (scanner) { - try { - wrapper = scannerImpl.getWrapper(callbackIntent); - } catch (final IllegalStateException e) { - // Scanning has been stopped. - return; - } - if (wrapper == null) { - // Wrapper has not been created, or was created, but the app was then killed - // and must be created again. Some information will be lost (batched devices). - wrapper = new BluetoothLeScannerImplOreo.PendingIntentExecutorWrapper(offloadedBatchingSupported, - offloadedFilteringSupported, filters, settings, callbackIntent); - scannerImpl.addWrapper(callbackIntent, wrapper); - } - } - - // The context may change each time. Set the one time temporary context that will be used - // to send PendingIntent. It will be released after the results were handled. - wrapper.executor.setTemporaryContext(context); - - // Check what results were received and send them to PendingIntent. - final List nativeScanResults = - intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT); - if (nativeScanResults != null) { - final ArrayList results = scannerImpl.fromNativeScanResults(nativeScanResults); - - if (settings.getReportDelayMillis() > 0) { - wrapper.handleScanResults(results); - } else if (!results.isEmpty()) { - final int callbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, - ScanSettings.CALLBACK_TYPE_ALL_MATCHES); - wrapper.handleScanResult(callbackType, results.get(0)); - } - } else { - final int errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0); - if (errorCode != 0) { - wrapper.handleScanError(errorCode); - } - } - - // Release the temporary context reference, so that static executor does not hold a - // reference to a context. - wrapper.executor.setTemporaryContext(null); - } +public class +PendingIntentReceiver extends GeneralReceiver { + + /* package */ static final String ACTION = "no.nordicsemi.android.support.v18.ACTION_FOUND"; + /* package */ static final String EXTRA_PENDING_INTENT = "no.nordicsemi.android.support.v18.EXTRA_PENDING_INTENT"; + /* package */ static final String EXTRA_FILTERS = "no.nordicsemi.android.support.v18.EXTRA_FILTERS"; + /* package */ static final String EXTRA_SETTINGS = "no.nordicsemi.android.support.v18.EXTRA_SETTINGS"; + /* package */ static final String EXTRA_USE_HARDWARE_BATCHING = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_BATCHING"; + /* package */ static final String EXTRA_USE_HARDWARE_FILTERING = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_FILTERING"; + /* package */ static final String EXTRA_USE_HARDWARE_CALLBACK_TYPES = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_CALLBACK_TYPES"; + /* package */ static final String EXTRA_MATCH_LOST_TIMEOUT = "no.nordicsemi.android.support.v18.EXTRA_MATCH_LOST_TIMEOUT"; + /* package */ static final String EXTRA_MATCH_LOST_INTERVAL = "no.nordicsemi.android.support.v18.EXTRA_MATCH_LOST_INTERVAL"; + /* package */ static final String EXTRA_MATCH_MODE = "no.nordicsemi.android.support.v18.EXTRA_MATCH_MODE"; + /* package */ static final String EXTRA_NUM_OF_MATCHES = "no.nordicsemi.android.support.v18.EXTRA_NUM_OF_MATCHES"; + +// @RequiresApi(api = Build.VERSION_CODES.O) + public void onReceive(final Context context, final Intent intent) { + // Ensure we are ok. + if (context == null || intent == null) + return; + + // Find the target pending intent. +// final IntentAgent callbackIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); + final IntentAgent callbackIntent = intent.getSequenceableParam(EXTRA_PENDING_INTENT); + if (callbackIntent == null) + return; + + // Filters and settings have been set as native objects, otherwise they could not be + // serialized by the system scanner. + final ArrayList nativeScanFilters = +// intent.getParcelableArrayListExtra(EXTRA_FILTERS); + intent.getSequenceableArrayListParam(EXTRA_FILTERS); +// final ScanSettings nativeScanSettings = intent.getParcelableExtra(EXTRA_SETTINGS); + final ScanSettings nativeScanSettings = intent.getSequenceableParam(EXTRA_SETTINGS); + if (nativeScanFilters == null || nativeScanSettings == null) + return; + + // Some ScanSettings parameters are only on compat version and need to be sent separately. +// final boolean useHardwareBatchingIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_BATCHING, true); + final boolean useHardwareBatchingIfSupported = intent.getBooleanParam(EXTRA_USE_HARDWARE_BATCHING, true); + final boolean useHardwareFilteringIfSupported = intent.getBooleanParam(EXTRA_USE_HARDWARE_FILTERING, true); + final boolean useHardwareCallbackTypesIfSupported = intent.getBooleanParam(EXTRA_USE_HARDWARE_CALLBACK_TYPES, true); + final long matchLostDeviceTimeout = intent.getLongParam(EXTRA_MATCH_LOST_TIMEOUT, ScanSettings.MATCH_LOST_DEVICE_TIMEOUT_DEFAULT); + final long matchLostTaskInterval = intent.getLongParam(EXTRA_MATCH_LOST_INTERVAL, ScanSettings.MATCH_LOST_TASK_INTERVAL_DEFAULT); + final int matchMode = intent.getIntParam(EXTRA_MATCH_MODE, ScanSettings.MATCH_MODE_AGGRESSIVE); + final int numOfMatches = intent.getIntParam(EXTRA_NUM_OF_MATCHES, ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT); + + // Convert native objects to compat versions. + final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); +// final BluetoothLeScannerImplOreo scannerImpl = (BluetoothLeScannerImplOreo) scanner; +// final ArrayList filters = scannerImpl.fromNativeScanFilters(nativeScanFilters); +// final ScanSettings settings = scannerImpl.fromNativeScanSettings(nativeScanSettings, +// useHardwareBatchingIfSupported, +// useHardwareFilteringIfSupported, +// useHardwareCallbackTypesIfSupported, +// matchLostDeviceTimeout, matchLostTaskInterval, +// matchMode, numOfMatches); + + // Check device capabilities and create a wrapper that will send a PendingIntent. +// final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); +// final boolean offloadedBatchingSupported = adapter.isOffloadedScanBatchingSupported(); +// final boolean offloadedFilteringSupported = adapter.isOffloadedFilteringSupported(); + + // Obtain or create a PendingIntentExecutorWrapper. A static instance (obtained from a + // static BluetoothLeScannerCompat singleton) is necessary as it allows to keeps + // track of found devices and emulate batching and callback types if those are not + // supported or a compat version was forced. + +// BluetoothLeScannerImplOreo.PendingIntentExecutorWrapper wrapper; + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (scanner) { + try { +// wrapper = scannerImpl.getWrapper(callbackIntent); + } catch (final IllegalStateException e) { + // Scanning has been stopped. + return; + } +// if (wrapper == null) { +// // Wrapper has not been created, or was created, but the app was then killed +// // and must be created again. Some information will be lost (batched devices). +// wrapper = new BluetoothLeScannerImplOreo.PendingIntentExecutorWrapper(offloadedBatchingSupported, +// offloadedFilteringSupported, filters, settings, callbackIntent); +// scannerImpl.addWrapper(callbackIntent, wrapper); +// } + } + + // The context may change each time. Set the one time temporary context that will be used + // to send PendingIntent. It will be released after the results were handled. +// wrapper.executor.setTemporaryContext(context); + + // Check what results were received and send them to PendingIntent. +// final List nativeScanResults = +// intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT); +// intent.getParcelableArrayListParam(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT); +// if (nativeScanResults != null) { +// final ArrayList results = scannerImpl.fromNativeScanResults(nativeScanResults); +// if (settings.getReportDelayMillis() > 0) { +// wrapper.handleScanResults(results); +// } else if (!results.isEmpty()) { +//// final int callbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, +//// ScanSettings.CALLBACK_TYPE_ALL_MATCHES); +// intent.getIntParam(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, +// ScanSettings.CALLBACK_TYPE_ALL_MATCHES); +// wrapper.handleScanResult(callbackType, results.get(0)); +// } +// } else { +//// final int errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0); +// final int errorCode = intent.getIntParam(BluetoothLeScanner.EXTRA_ERROR_CODE, 0); +// if (errorCode != 0) { +// wrapper.handleScanError(errorCode); +// } +// } + + // Release the temporary context reference, so that static executor does not hold a + // reference to a context. +// wrapper.executor.setTemporaryContext(null); + } } diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanCallback.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanCallback.java index 799e430..3bc8a6c 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanCallback.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanCallback.java @@ -22,9 +22,10 @@ package no.nordicsemi.android.support.v18.scanner; +import ohos.bluetooth.ble.BleScanResult; + import java.util.List; -import androidx.annotation.NonNull; /** * Bluetooth LE scan callbacks. Scan results are reported using these callbacks. @@ -33,63 +34,63 @@ import androidx.annotation.NonNull; */ @SuppressWarnings({"unused", "WeakerAccess"}) public abstract class ScanCallback { - /** - * Fails to start scan as BLE scan with the same settings is already started by the app. - */ - public static final int SCAN_FAILED_ALREADY_STARTED = 1; + /** + * Fails to start scan as BLE scan with the same settings is already started by the app. + */ + public static final int SCAN_FAILED_ALREADY_STARTED = 1; - /** - * Fails to start scan as app cannot be registered. - */ - public static final int SCAN_FAILED_APPLICATION_REGISTRATION_FAILED = 2; + /** + * Fails to start scan as app cannot be registered. + */ + public static final int SCAN_FAILED_APPLICATION_REGISTRATION_FAILED = 2; - /** - * Fails to start scan due an internal error - */ - public static final int SCAN_FAILED_INTERNAL_ERROR = 3; + /** + * Fails to start scan due an internal error + */ + public static final int SCAN_FAILED_INTERNAL_ERROR = 3; - /** - * Fails to start power optimized scan as this feature is not supported. - */ - public static final int SCAN_FAILED_FEATURE_UNSUPPORTED = 4; + /** + * Fails to start power optimized scan as this feature is not supported. + */ + public static final int SCAN_FAILED_FEATURE_UNSUPPORTED = 4; - /** - * Fails to start scan as it is out of hardware resources. - */ - public static final int SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES = 5; + /** + * Fails to start scan as it is out of hardware resources. + */ + public static final int SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES = 5; - /** - * Fails to start scan as application tries to scan too frequently. - */ - public static final int SCAN_FAILED_SCANNING_TOO_FREQUENTLY = 6; + /** + * Fails to start scan as application tries to scan too frequently. + */ + public static final int SCAN_FAILED_SCANNING_TOO_FREQUENTLY = 6; - static final int NO_ERROR = 0; + static final int NO_ERROR = 0; - /** - * Callback when a BLE advertisement has been found. - * - * @param callbackType Determines how this callback was triggered. Could be one of - * {@link ScanSettings#CALLBACK_TYPE_ALL_MATCHES}, - * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} or - * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} - * @param result A Bluetooth LE scan result. - */ - public void onScanResult(final int callbackType, @NonNull final ScanResult result) { - } + /** + * Callback when a BLE advertisement has been found. + * + * @param callbackType Determines how this callback was triggered. Could be one of + * {@link ScanSettings#CALLBACK_TYPE_ALL_MATCHES}, + * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} or + * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} + * @param result A Bluetooth LE scan result. + */ + public void onScanResult(final int callbackType, final BleScanResult result) { + } - /** - * Callback when batch results are delivered. - * - * @param results List of scan results that are previously scanned. - */ - public void onBatchScanResults(@NonNull final List results) { - } + /** + * Callback when batch results are delivered. + * + * @param results List of scan results that are previously scanned. + */ + public void onBatchScanResults(final List results) { + } - /** - * Callback when scan could not be started. - * - * @param errorCode Error code (one of SCAN_FAILED_*) for scan failure. - */ - public void onScanFailed(final int errorCode) { - } + /** + * Callback when scan could not be started. + * + * @param errorCode Error code (one of SCAN_FAILED_*) for scan failure. + */ + public void onScanFailed(final int errorCode) { + } } diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanFilter.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanFilter.java index f47b746..f5986dd 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanFilter.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanFilter.java @@ -22,18 +22,19 @@ package no.nordicsemi.android.support.v18.scanner; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.os.Parcel; -import android.os.ParcelUuid; -import android.os.Parcelable; + +import ohos.bluetooth.ble.BlePeripheralDevice; +import ohos.bluetooth.ble.BleScanResult; +import ohos.interwork.utils.ParcelableEx; +import ohos.utils.Parcel; +import ohos.utils.SequenceUuid; +import ohos.utils.Sequenceable; +import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.List; import java.util.UUID; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; /** * Criteria for filtering result from Bluetooth LE scans. A {@link ScanFilter} allows clients to @@ -51,584 +52,599 @@ import androidx.annotation.Nullable; * @see BluetoothLeScannerCompat */ @SuppressWarnings("WeakerAccess") -public final class ScanFilter implements Parcelable { - - @Nullable - private final String deviceName; - @Nullable - private final String deviceAddress; - - @Nullable - private final ParcelUuid serviceUuid; - @Nullable - private final ParcelUuid serviceUuidMask; - - @Nullable - private final ParcelUuid serviceDataUuid; - @Nullable - private final byte[] serviceData; - @Nullable - private final byte[] serviceDataMask; - - private final int manufacturerId; - @Nullable - private final byte[] manufacturerData; - @Nullable - private final byte[] manufacturerDataMask; - - private static final ScanFilter EMPTY = new ScanFilter.Builder().build() ; - - private ScanFilter(@Nullable final String name, @Nullable final String deviceAddress, - @Nullable final ParcelUuid uuid, @Nullable final ParcelUuid uuidMask, - @Nullable final ParcelUuid serviceDataUuid, @Nullable final byte[] serviceData, - @Nullable final byte[] serviceDataMask, final int manufacturerId, - @Nullable final byte[] manufacturerData, - @Nullable final byte[] manufacturerDataMask) { - this.deviceName = name; - this.serviceUuid = uuid; - this.serviceUuidMask = uuidMask; - this.deviceAddress = deviceAddress; - this.serviceDataUuid = serviceDataUuid; - this.serviceData = serviceData; - this.serviceDataMask = serviceDataMask; - this.manufacturerId = manufacturerId; - this.manufacturerData = manufacturerData; - this.manufacturerDataMask = manufacturerDataMask; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(final Parcel dest, final int flags) { - dest.writeInt(deviceName == null ? 0 : 1); - if (deviceName != null) { - dest.writeString(deviceName); - } - dest.writeInt(deviceAddress == null ? 0 : 1); - if (deviceAddress != null) { - dest.writeString(deviceAddress); - } - dest.writeInt(serviceUuid == null ? 0 : 1); - if (serviceUuid != null) { - dest.writeParcelable(serviceUuid, flags); - dest.writeInt(serviceUuidMask == null ? 0 : 1); - if (serviceUuidMask != null) { - dest.writeParcelable(serviceUuidMask, flags); - } - } - dest.writeInt(serviceDataUuid == null ? 0 : 1); - if (serviceDataUuid != null) { - dest.writeParcelable(serviceDataUuid, flags); - dest.writeInt(serviceData == null ? 0 : 1); - if (serviceData != null) { - dest.writeInt(serviceData.length); - dest.writeByteArray(serviceData); - - dest.writeInt(serviceDataMask == null ? 0 : 1); - if (serviceDataMask != null) { - dest.writeInt(serviceDataMask.length); - dest.writeByteArray(serviceDataMask); - } - } - } - dest.writeInt(manufacturerId); - dest.writeInt(manufacturerData == null ? 0 : 1); - if (manufacturerData != null) { - dest.writeInt(manufacturerData.length); - dest.writeByteArray(manufacturerData); - - dest.writeInt(manufacturerDataMask == null ? 0 : 1); - if (manufacturerDataMask != null) { - dest.writeInt(manufacturerDataMask.length); - dest.writeByteArray(manufacturerDataMask); - } - } - } - - /** - * A {@link android.os.Parcelable.Creator} to create {@link ScanFilter} from parcel. - */ - public static final Creator CREATOR = new Creator() { - - @Override - public ScanFilter[] newArray(final int size) { - return new ScanFilter[size]; - } - - @Override - public ScanFilter createFromParcel(final Parcel in) { - final Builder builder = new Builder(); - if (in.readInt() == 1) { - builder.setDeviceName(in.readString()); - } - if (in.readInt() == 1) { - builder.setDeviceAddress(in.readString()); - } - if (in.readInt() == 1) { - ParcelUuid uuid = in.readParcelable(ParcelUuid.class.getClassLoader()); - builder.setServiceUuid(uuid); - if (in.readInt() == 1) { - ParcelUuid uuidMask = in.readParcelable( - ParcelUuid.class.getClassLoader()); - builder.setServiceUuid(uuid, uuidMask); - } - } - if (in.readInt() == 1) { - ParcelUuid serviceDataUuid = in.readParcelable(ParcelUuid.class.getClassLoader()); - if (in.readInt() == 1) { - final int serviceDataLength = in.readInt(); - final byte[] serviceData = new byte[serviceDataLength]; - in.readByteArray(serviceData); - if (in.readInt() == 0) { - //noinspection ConstantConditions - builder.setServiceData(serviceDataUuid, serviceData); - } else { - final int serviceDataMaskLength = in.readInt(); - final byte[] serviceDataMask = new byte[serviceDataMaskLength]; - in.readByteArray(serviceDataMask); - //noinspection ConstantConditions - builder.setServiceData(serviceDataUuid, serviceData, serviceDataMask); - } - } - } - - final int manufacturerId = in.readInt(); - if (in.readInt() == 1) { - final int manufacturerDataLength = in.readInt(); - final byte[] manufacturerData = new byte[manufacturerDataLength]; - in.readByteArray(manufacturerData); - if (in.readInt() == 0) { - builder.setManufacturerData(manufacturerId, manufacturerData); - } else { - final int manufacturerDataMaskLength = in.readInt(); - final byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength]; - in.readByteArray(manufacturerDataMask); - builder.setManufacturerData(manufacturerId, manufacturerData, - manufacturerDataMask); - } - } - - return builder.build(); - } - }; - - /** - * Returns the filter set the device name field of Bluetooth advertisement data. - */ - @Nullable - public String getDeviceName() { - return deviceName; - } - - /** - * Returns the filter set on the service uuid. - */ - @Nullable - public ParcelUuid getServiceUuid() { - return serviceUuid; - } - - @Nullable - public ParcelUuid getServiceUuidMask() { - return serviceUuidMask; - } - - @Nullable - public String getDeviceAddress() { - return deviceAddress; - } - - @Nullable - public byte[] getServiceData() { - return serviceData; - } - - @Nullable - public byte[] getServiceDataMask() { - return serviceDataMask; - } - - @Nullable - public ParcelUuid getServiceDataUuid() { - return serviceDataUuid; - } - - /** - * Returns the manufacturer id. -1 if the manufacturer filter is not set. - */ - public int getManufacturerId() { - return manufacturerId; - } - - @Nullable - public byte[] getManufacturerData() { - return manufacturerData; - } - - @Nullable - public byte[] getManufacturerDataMask() { - return manufacturerDataMask; - } - - /** - * Check if the scan filter matches a {@code scanResult}. A scan result is considered as a match - * if it matches all the field filters. - */ - public boolean matches(@Nullable final ScanResult scanResult) { - if (scanResult == null) { - return false; - } - final BluetoothDevice device = scanResult.getDevice(); - // Device match. - if (deviceAddress != null && !deviceAddress.equals(device.getAddress())) { - return false; - } - - final ScanRecord scanRecord = scanResult.getScanRecord(); - - // Scan record is null but there exist filters on it. - if (scanRecord == null - && (deviceName != null || serviceUuid != null || manufacturerData != null - || serviceData != null)) { - return false; - } - - // Local name match. - if (deviceName != null && !deviceName.equals(scanRecord.getDeviceName())) { - return false; - } - - // UUID match. - if (serviceUuid != null && !matchesServiceUuids(serviceUuid, serviceUuidMask, - scanRecord.getServiceUuids())) { - return false; - } - - // Service data match - if (serviceDataUuid != null && scanRecord != null) { - if (!matchesPartialData(serviceData, serviceDataMask, - scanRecord.getServiceData(serviceDataUuid))) { - return false; - } - } - - // Manufacturer data match. - if (manufacturerId >= 0 && scanRecord != null) { - //noinspection RedundantIfStatement - if (!matchesPartialData(manufacturerData, manufacturerDataMask, - scanRecord.getManufacturerSpecificData(manufacturerId))) { - return false; - } - } - // All filters match. - return true; - } - - /** - * Check if the uuid pattern is contained in a list of parcel uuids. - */ - private static boolean matchesServiceUuids(@Nullable final ParcelUuid uuid, - @Nullable final ParcelUuid parcelUuidMask, - @Nullable final List uuids) { - if (uuid == null) { - return true; - } - if (uuids == null) { - return false; - } - - for (final ParcelUuid parcelUuid : uuids) { - final UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid(); - if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) { - return true; - } - } - return false; - } - - // Check if the uuid pattern matches the particular service uuid. - private static boolean matchesServiceUuid(@NonNull final UUID uuid, - @Nullable final UUID mask, - @NonNull final UUID data) { - if (mask == null) { - return uuid.equals(data); - } - if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits()) != - (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) { - return false; - } - return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits()) == - (data.getMostSignificantBits() & mask.getMostSignificantBits())); - } - - // Check whether the data pattern matches the parsed data. - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private boolean matchesPartialData(@Nullable final byte[] data, - @Nullable final byte[] dataMask, - @Nullable final byte[] parsedData) { - if (data == null) { - // If filter data is null it means it doesn't matter. - // We return true if any data matching the manufacturerId were found. - return parsedData != null; - } - if (parsedData == null || parsedData.length < data.length) { - return false; - } - if (dataMask == null) { - for (int i = 0; i < data.length; ++i) { - if (parsedData[i] != data[i]) { - return false; - } - } - return true; - } - for (int i = 0; i < data.length; ++i) { - if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) { - return false; - } - } - return true; - } - - @Override - public String toString() { - return "BluetoothLeScanFilter [deviceName=" + deviceName + ", deviceAddress=" - + deviceAddress - + ", mUuid=" + serviceUuid + ", uuidMask=" + serviceUuidMask - + ", serviceDataUuid=" + Objects.toString(serviceDataUuid) + ", serviceData=" - + Arrays.toString(serviceData) + ", serviceDataMask=" - + Arrays.toString(serviceDataMask) + ", manufacturerId=" + manufacturerId - + ", manufacturerData=" + Arrays.toString(manufacturerData) - + ", manufacturerDataMask=" + Arrays.toString(manufacturerDataMask) + "]"; - } - - @Override - public int hashCode() { - return Objects.hash(deviceName, deviceAddress, manufacturerId, - Arrays.hashCode(manufacturerData), - Arrays.hashCode(manufacturerDataMask), - serviceDataUuid, - Arrays.hashCode(serviceData), - Arrays.hashCode(serviceDataMask), - serviceUuid, serviceUuidMask); - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - final ScanFilter other = (ScanFilter) obj; - return Objects.equals(deviceName, other.deviceName) && - Objects.equals(deviceAddress, other.deviceAddress) && - manufacturerId == other.manufacturerId && - Objects.deepEquals(manufacturerData, other.manufacturerData) && - Objects.deepEquals(manufacturerDataMask, other.manufacturerDataMask) && - Objects.equals(serviceDataUuid, other.serviceDataUuid) && - Objects.deepEquals(serviceData, other.serviceData) && - Objects.deepEquals(serviceDataMask, other.serviceDataMask) && - Objects.equals(serviceUuid, other.serviceUuid) && - Objects.equals(serviceUuidMask, other.serviceUuidMask); - } - - /** - * Checks if the scan filter is empty. - */ - @SuppressWarnings("unused") - /* package */ boolean isAllFieldsEmpty() { - return EMPTY.equals(this); - } - - /** - * Builder class for {@link ScanFilter}. - */ - public static final class Builder { - - private String deviceName; - private String deviceAddress; - - private ParcelUuid serviceUuid; - private ParcelUuid uuidMask; - - private ParcelUuid serviceDataUuid; - private byte[] serviceData; - private byte[] serviceDataMask; - - private int manufacturerId = -1; - private byte[] manufacturerData; - private byte[] manufacturerDataMask; - - /** - * Set filter on device name. - */ - public Builder setDeviceName(@Nullable final String deviceName) { - this.deviceName = deviceName; - return this; - } - - /** - * Set filter on device address. - * - * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the - * format of "01:02:03:AB:CD:EF". The device address can be validated using - * {@link BluetoothAdapter#checkBluetoothAddress}. - * @throws IllegalArgumentException If the {@code deviceAddress} is invalid. - */ - public Builder setDeviceAddress(@Nullable final String deviceAddress) { - if (deviceAddress != null && !BluetoothAdapter.checkBluetoothAddress(deviceAddress)) { - throw new IllegalArgumentException("invalid device address " + deviceAddress); - } - this.deviceAddress = deviceAddress; - return this; - } - - /** - * Set filter on service uuid. - */ - public Builder setServiceUuid(@Nullable final ParcelUuid serviceUuid) { - this.serviceUuid = serviceUuid; - this.uuidMask = null; // clear uuid mask - return this; - } - - /** - * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the - * {@code serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the - * bit in {@code serviceUuid}, and 0 to ignore that bit. - * - * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but - * {@code uuidMask} is not {@code null}. - */ - public Builder setServiceUuid(@Nullable final ParcelUuid serviceUuid, - @Nullable final ParcelUuid uuidMask) { - if (uuidMask != null && serviceUuid == null) { - throw new IllegalArgumentException("uuid is null while uuidMask is not null!"); - } - this.serviceUuid = serviceUuid; - this.uuidMask = uuidMask; - return this; - } - - /** - * Set filtering on service data. - * - * @throws IllegalArgumentException If {@code serviceDataUuid} is null. - */ - public Builder setServiceData(@NonNull final ParcelUuid serviceDataUuid, - @Nullable final byte[] serviceData) { - //noinspection ConstantConditions - if (serviceDataUuid == null) { - throw new IllegalArgumentException("serviceDataUuid is null!"); - } - this.serviceDataUuid = serviceDataUuid; - this.serviceData = serviceData; - this.serviceDataMask = null; // clear service data mask - return this; - } - - /** - * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to - * match the one in service data, otherwise set it to 0 to ignore that bit. - *

- * The {@code serviceDataMask} must have the same length of the {@code serviceData}. - * - * @throws IllegalArgumentException If {@code serviceDataUuid} is null or - * {@code serviceDataMask} is {@code null} while {@code serviceData} is not or - * {@code serviceDataMask} and {@code serviceData} has different length. - */ - public Builder setServiceData(@NonNull final ParcelUuid serviceDataUuid, - @Nullable final byte[] serviceData, - @Nullable final byte[] serviceDataMask) { - //noinspection ConstantConditions - if (serviceDataUuid == null) { - throw new IllegalArgumentException("serviceDataUuid is null"); - } - if (serviceDataMask != null) { - if (serviceData == null) { - throw new IllegalArgumentException( - "serviceData is null while serviceDataMask is not null"); - } - // Since the serviceDataMask is a bit mask for serviceData, the lengths of the two - // byte array need to be the same. - if (serviceData.length != serviceDataMask.length) { - throw new IllegalArgumentException( - "size mismatch for service data and service data mask"); - } - } - this.serviceDataUuid = serviceDataUuid; - this.serviceData = serviceData; - this.serviceDataMask = serviceDataMask; - return this; - } - - /** - * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id. - *

- * Note the first two bytes of the {@code manufacturerData} is the manufacturerId. - * - * @throws IllegalArgumentException If the {@code manufacturerId} is invalid. - */ - public Builder setManufacturerData(final int manufacturerId, - @Nullable final byte[] manufacturerData) { - if (manufacturerData != null && manufacturerId < 0) { - throw new IllegalArgumentException("invalid manufacture id"); - } - this.manufacturerId = manufacturerId; - this.manufacturerData = manufacturerData; - this.manufacturerDataMask = null; // clear manufacturer data mask - return this; - } - - /** - * Set filter on partial manufacture data. For any bit in the mask, set it the 1 if it needs - * to match the one in manufacturer data, otherwise set it to 0. - *

- * The {@code manufacturerDataMask} must have the same length of {@code manufacturerData}. - * - * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or - * {@code manufacturerData} is null while {@code manufacturerDataMask} is not, - * or {@code manufacturerData} and {@code manufacturerDataMask} have different - * length. - */ - public Builder setManufacturerData(final int manufacturerId, - @Nullable final byte[] manufacturerData, - @Nullable final byte[] manufacturerDataMask) { - if (manufacturerData != null && manufacturerId < 0) { - throw new IllegalArgumentException("invalid manufacture id"); - } - if (manufacturerDataMask != null) { - if (manufacturerData == null) { - throw new IllegalArgumentException( - "manufacturerData is null while manufacturerDataMask is not null"); - } - // Since the manufacturerDataMask is a bit mask for manufacturerData, the lengths - // of the two byte array need to be the same. - if (manufacturerData.length != manufacturerDataMask.length) { - throw new IllegalArgumentException( - "size mismatch for manufacturerData and manufacturerDataMask"); - } - } - this.manufacturerId = manufacturerId; - this.manufacturerData = manufacturerData; - this.manufacturerDataMask = manufacturerDataMask; - return this; - } - - /** - * Build {@link ScanFilter}. - * - * @throws IllegalArgumentException If the filter cannot be built. - */ - public ScanFilter build() { - return new ScanFilter(deviceName, deviceAddress, serviceUuid, uuidMask, - serviceDataUuid, serviceData, serviceDataMask, - manufacturerId, manufacturerData, manufacturerDataMask); - } - } +public final class ScanFilter implements Sequenceable { + + @Nullable + private final String deviceName; + @Nullable + private final String deviceAddress; + + private final Object serviceUuid; + private final Object serviceUuidMask; + + private final Object serviceDataUuid; + @Nullable + private final byte[] serviceData; + @Nullable + private final byte[] serviceDataMask; + + private final int manufacturerId; + @Nullable + private final byte[] manufacturerData; + @Nullable + private final byte[] manufacturerDataMask; + + private static final ScanFilter EMPTY = new Builder().build(); + + private ScanFilter(@Nullable final String name, @Nullable final String deviceAddress, + final Object uuid, final Object uuidMask, + final Object serviceDataUuid, @Nullable final byte[] serviceData, + @Nullable final byte[] serviceDataMask, final int manufacturerId, + @Nullable final byte[] manufacturerData, + @Nullable final byte[] manufacturerDataMask) { + this.deviceName = name; + this.serviceUuid = uuid; + this.serviceUuidMask = uuidMask; + this.deviceAddress = deviceAddress; + this.serviceDataUuid = serviceDataUuid; + this.serviceData = serviceData; + this.serviceDataMask = serviceDataMask; + this.manufacturerId = manufacturerId; + this.manufacturerData = manufacturerData; + this.manufacturerDataMask = manufacturerDataMask; + } + +// @Override + public int describeContents() { + return 0; + } + +// @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeInt(deviceName == null ? 0 : 1); + if (deviceName != null) { + dest.writeString(deviceName); + } + dest.writeInt(deviceAddress == null ? 0 : 1); + if (deviceAddress != null) { + dest.writeString(deviceAddress); + } + dest.writeInt(serviceUuid == null ? 0 : 1); + if (serviceUuid != null) { +// dest.writeParcelable(serviceUuid, flags); + dest.writeParcelableEx((ParcelableEx) serviceUuid); + dest.writeInt(serviceUuidMask == null ? 0 : 1); + if (serviceUuidMask != null) { +// dest.writeParcelable(serviceUuidMask, flags); + dest.writeParcelableEx((ParcelableEx) serviceUuidMask); + } + } + dest.writeInt(serviceDataUuid == null ? 0 : 1); + if (serviceDataUuid != null) { +// dest.writeParcelable(serviceDataUuid, flags); + dest.writeParcelableEx((ParcelableEx) serviceUuid); + dest.writeInt(serviceData == null ? 0 : 1); + if (serviceData != null) { + dest.writeInt(serviceData.length); + dest.writeByteArray(serviceData); + + dest.writeInt(serviceDataMask == null ? 0 : 1); + if (serviceDataMask != null) { + dest.writeInt(serviceDataMask.length); + dest.writeByteArray(serviceDataMask); + } + } + } + dest.writeInt(manufacturerId); + dest.writeInt(manufacturerData == null ? 0 : 1); + if (manufacturerData != null) { + dest.writeInt(manufacturerData.length); + dest.writeByteArray(manufacturerData); + + dest.writeInt(manufacturerDataMask == null ? 0 : 1); + if (manufacturerDataMask != null) { + dest.writeInt(manufacturerDataMask.length); + dest.writeByteArray(manufacturerDataMask); + } + } + } + + + + public static final Sequenceable.Producer CREATOR = new Producer() { + +// @Override + public ScanFilter[] newArray(final int size) { + return new ScanFilter[size]; + } + +// @Override + public ScanFilter createFromParcel(final Parcel in) { + final Builder builder = new Builder(); + if (in.readInt() == 1) { + builder.setDeviceName(in.readString()); + } + if (in.readInt() == 1) { + builder.setDeviceAddress(in.readString()); + } + if (in.readInt() == 1) { +// Object uuid = in.readParcelable(ParcelUuid.class.getClassLoader()); + ParcelableEx uuid = in.readParcelableEx(UUID.class.getClassLoader()); + builder.setServiceUuid(uuid); + if (in.readInt() == 1) { +// Object uuidMask = in.readParcelable(SequenceUuid.class.getClassLoader()); + ParcelableEx uuidMask = in.readParcelableEx(SequenceUuid.class.getClassLoader()); + builder.setServiceUuid(uuid, uuidMask); + } + } + if (in.readInt() == 1) { +// Object serviceDataUuid = in.readParcelable(SequenceUuid.class.getClassLoader()); + ParcelableEx serviceDataUuid = in.readParcelableEx(SequenceUuid.class.getClassLoader()); + if (in.readInt() == 1) { + final int serviceDataLength = in.readInt(); + final byte[] serviceData = new byte[serviceDataLength]; + in.readByteArray(serviceData); + if (in.readInt() == 0) { + //noinspection ConstantConditions + builder.setServiceData(serviceDataUuid, serviceData); + } else { + final int serviceDataMaskLength = in.readInt(); + final byte[] serviceDataMask = new byte[serviceDataMaskLength]; + in.readByteArray(serviceDataMask); + //noinspection ConstantConditions + builder.setServiceData(serviceDataUuid, serviceData, serviceDataMask); + } + } + } + + final int manufacturerId = in.readInt(); + if (in.readInt() == 1) { + final int manufacturerDataLength = in.readInt(); + final byte[] manufacturerData = new byte[manufacturerDataLength]; + in.readByteArray(manufacturerData); + if (in.readInt() == 0) { + builder.setManufacturerData(manufacturerId, manufacturerData); + } else { + final int manufacturerDataMaskLength = in.readInt(); + final byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength]; + in.readByteArray(manufacturerDataMask); + builder.setManufacturerData(manufacturerId, manufacturerData, + manufacturerDataMask); + } + } + + return builder.build(); + } + }; + + /** + * Returns the filter set the device name field of Bluetooth advertisement data. + */ + @Nullable + public String getDeviceName() { + return deviceName; + } + + /** + * Returns the filter set on the service uuid. + */ + public Object getServiceUuid() { + return serviceUuid; + } + + public Object getServiceUuidMask() { + return serviceUuidMask; + } + + @Nullable + public String getDeviceAddress() { + return deviceAddress; + } + + @Nullable + public byte[] getServiceData() { + return serviceData; + } + + @Nullable + public byte[] getServiceDataMask() { + return serviceDataMask; + } + + public Object getServiceDataUuid() { + return serviceDataUuid; + } + + /** + * Returns the manufacturer id. -1 if the manufacturer filter is not set. + */ + public int getManufacturerId() { + return manufacturerId; + } + + @Nullable + public byte[] getManufacturerData() { + return manufacturerData; + } + + @Nullable + public byte[] getManufacturerDataMask() { + return manufacturerDataMask; + } + + /** + * Check if the scan filter matches a {@code scanResult}. A scan result is considered as a match + * if it matches all the field filters. + */ + public boolean matches(@Nullable final BleScanResult scanResult) { + if (scanResult == null) { + return false; + } + BlePeripheralDevice device = scanResult.getPeripheralDevice(); + // Device match. + if (deviceAddress != null && !deviceAddress.equals(device.getDeviceAddr())) { + return false; + } + + final BlePeripheralDevice scanRecord = scanResult.getPeripheralDevice(); + // Scan record is null but there exist filters on it. + if (scanRecord == null + && (deviceName != null || serviceUuid != null || manufacturerData != null + || serviceData != null)) { + return false; + } + + // Local name match. +// if (deviceName != null && !deviceName.equals(scanRecord.getDeviceName())) { + if (deviceName != null && !deviceName.equals(scanRecord.getDeviceName())) { + return false; + } + +// // UUID match. +// if (serviceUuid != null && !matchesServiceUuids(serviceUuid, serviceUuidMask, +//// scanRecord.getServiceUuids())) { +// scanRecord.getServices())) { +// return false; +// } + + // Service data match + if (serviceDataUuid != null && scanRecord != null) { +// if (!matchesPartialData(serviceData, serviceDataMask, +// scanRecord.getServiceData(serviceDataUuid))) { +// return false; +// } + } + + // Manufacturer data match. + if (manufacturerId >= 0 && scanRecord != null) { + //noinspection RedundantIfStatement +// if (!matchesPartialData(manufacturerData, manufacturerDataMask, +// scanRecord.getManufacturerSpecificData(manufacturerId))) { +// return false; +// } + } + // All filters match. + return true; + } + + /** + * Check if the uuid pattern is contained in a list of parcel uuids. + */ + private static boolean matchesServiceUuids(final SequenceUuid + uuid, + final SequenceUuid + parcelUuidMask, + @Nullable final List uuids) { + if (uuid == null) { + return true; + } + if (uuids == null) { + return false; + } + + for (final SequenceUuid parcelUuid : uuids) { + final UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid(); + if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) { + return true; + } + } + return false; + } + + // Check if the uuid pattern matches the particular service uuid. + private static boolean matchesServiceUuid(final UUID uuid, + @Nullable final UUID mask, + final UUID data) { + if (mask == null) { + return uuid.equals(data); + } + if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits()) != + (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) { + return false; + } + return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits()) == + (data.getMostSignificantBits() & mask.getMostSignificantBits())); + } + + // Check whether the data pattern matches the parsed data. + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean matchesPartialData(@Nullable final byte[] data, + @Nullable final byte[] dataMask, + @Nullable final byte[] parsedData) { + if (data == null) { + // If filter data is null it means it doesn't matter. + // We return true if any data matching the manufacturerId were found. + return parsedData != null; + } + if (parsedData == null || parsedData.length < data.length) { + return false; + } + if (dataMask == null) { + for (int i = 0; i < data.length; ++i) { + if (parsedData[i] != data[i]) { + return false; + } + } + return true; + } + for (int i = 0; i < data.length; ++i) { + if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return "BluetoothLeScanFilter [deviceName=" + deviceName + ", deviceAddress=" + + deviceAddress + + ", mUuid=" + serviceUuid + ", uuidMask=" + serviceUuidMask + + ", serviceDataUuid=" + Objects.toString(serviceDataUuid) + ", serviceData=" + + Arrays.toString(serviceData) + ", serviceDataMask=" + + Arrays.toString(serviceDataMask) + ", manufacturerId=" + manufacturerId + + ", manufacturerData=" + Arrays.toString(manufacturerData) + + ", manufacturerDataMask=" + Arrays.toString(manufacturerDataMask) + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(deviceName, deviceAddress, manufacturerId, + Arrays.hashCode(manufacturerData), + Arrays.hashCode(manufacturerDataMask), + serviceDataUuid, + Arrays.hashCode(serviceData), + Arrays.hashCode(serviceDataMask), + serviceUuid, serviceUuidMask); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final ScanFilter other = (ScanFilter) obj; + return Objects.equals(deviceName, other.deviceName) && + Objects.equals(deviceAddress, other.deviceAddress) && + manufacturerId == other.manufacturerId && + Objects.deepEquals(manufacturerData, other.manufacturerData) && + Objects.deepEquals(manufacturerDataMask, other.manufacturerDataMask) && + Objects.equals(serviceDataUuid, other.serviceDataUuid) && + Objects.deepEquals(serviceData, other.serviceData) && + Objects.deepEquals(serviceDataMask, other.serviceDataMask) && + Objects.equals(serviceUuid, other.serviceUuid) && + Objects.equals(serviceUuidMask, other.serviceUuidMask); + } + + /** + * Checks if the scan filter is empty. + */ + @SuppressWarnings("unused") + /* package */ boolean isAllFieldsEmpty() { + return EMPTY.equals(this); + } + + @Override + public boolean hasFileDescriptor() { + return false; + } + + @Override + public boolean marshalling(Parcel parcel) { + return false; + } + + @Override + public boolean unmarshalling(Parcel parcel) { + return false; + } + + /** + * Builder class for {@link ScanFilter}. + */ + public static final class Builder { + + private String deviceName; + private String deviceAddress; + + private Object serviceUuid; + private Object uuidMask; + + private Object serviceDataUuid; + private byte[] serviceData; + private byte[] serviceDataMask; + + private int manufacturerId = -1; + private byte[] manufacturerData; + private byte[] manufacturerDataMask; + + /** + * Set filter on device name. + */ + public Builder setDeviceName(@Nullable final String deviceName) { + this.deviceName = deviceName; + return this; + } + + /** + * Set filter on device address. + * + * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the + * format of "01:02:03:AB:CD:EF". The device address can be validated using + * @throws IllegalArgumentException If the {@code deviceAddress} is invalid. + */ + public Builder setDeviceAddress(@Nullable final String deviceAddress) { +// if (deviceAddress != null && !BluetoothAdapter.checkBluetoothAddress(deviceAddress)) { +// throw new IllegalArgumentException("invalid device address " + deviceAddress); +// } + this.deviceAddress = deviceAddress; + return this; + } + + /** + * Set filter on service uuid. + */ + public Builder setServiceUuid(final Object serviceUuid) { + this.serviceUuid = serviceUuid; + this.uuidMask = null; // clear uuid mask + return this; + } + + /** + * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the + * {@code serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the + * bit in {@code serviceUuid}, and 0 to ignore that bit. + * + * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but + * {@code uuidMask} is not {@code null}. + */ + public Builder setServiceUuid(final Object serviceUuid, + final Object uuidMask) { + if (uuidMask != null && serviceUuid == null) { + throw new IllegalArgumentException("uuid is null while uuidMask is not null!"); + } + this.serviceUuid = serviceUuid; + this.uuidMask = uuidMask; + return this; + } + + /** + * Set filtering on service data. + * + * @throws IllegalArgumentException If {@code serviceDataUuid} is null. + */ + public Builder setServiceData(final Object serviceDataUuid, + @Nullable final byte[] serviceData) { + //noinspection ConstantConditions + if (serviceDataUuid == null) { + throw new IllegalArgumentException("serviceDataUuid is null!"); + } + this.serviceDataUuid = serviceDataUuid; + this.serviceData = serviceData; + this.serviceDataMask = null; // clear service data mask + return this; + } + + /** + * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to + * match the one in service data, otherwise set it to 0 to ignore that bit. + *

+ * The {@code serviceDataMask} must have the same length of the {@code serviceData}. + * + * @throws IllegalArgumentException If {@code serviceDataUuid} is null or + * {@code serviceDataMask} is {@code null} while {@code serviceData} is not or + * {@code serviceDataMask} and {@code serviceData} has different length. + */ + public Builder setServiceData(final Object serviceDataUuid, + @Nullable final byte[] serviceData, + @Nullable final byte[] serviceDataMask) { + //noinspection ConstantConditions + if (serviceDataUuid == null) { + throw new IllegalArgumentException("serviceDataUuid is null"); + } + if (serviceDataMask != null) { + if (serviceData == null) { + throw new IllegalArgumentException( + "serviceData is null while serviceDataMask is not null"); + } + // Since the serviceDataMask is a bit mask for serviceData, the lengths of the two + // byte array need to be the same. + if (serviceData.length != serviceDataMask.length) { + throw new IllegalArgumentException( + "size mismatch for service data and service data mask"); + } + } + this.serviceDataUuid = serviceDataUuid; + this.serviceData = serviceData; + this.serviceDataMask = serviceDataMask; + return this; + } + + /** + * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id. + *

+ * Note the first two bytes of the {@code manufacturerData} is the manufacturerId. + * + * @throws IllegalArgumentException If the {@code manufacturerId} is invalid. + */ + public Builder setManufacturerData(final int manufacturerId, + @Nullable final byte[] manufacturerData) { + if (manufacturerData != null && manufacturerId < 0) { + throw new IllegalArgumentException("invalid manufacture id"); + } + this.manufacturerId = manufacturerId; + this.manufacturerData = manufacturerData; + this.manufacturerDataMask = null; // clear manufacturer data mask + return this; + } + + /** + * Set filter on partial manufacture data. For any bit in the mask, set it the 1 if it needs + * to match the one in manufacturer data, otherwise set it to 0. + *

+ * The {@code manufacturerDataMask} must have the same length of {@code manufacturerData}. + * + * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or + * {@code manufacturerData} is null while {@code manufacturerDataMask} is not, + * or {@code manufacturerData} and {@code manufacturerDataMask} have different + * length. + */ + public Builder setManufacturerData(final int manufacturerId, + @Nullable final byte[] manufacturerData, + @Nullable final byte[] manufacturerDataMask) { + if (manufacturerData != null && manufacturerId < 0) { + throw new IllegalArgumentException("invalid manufacture id"); + } + if (manufacturerDataMask != null) { + if (manufacturerData == null) { + throw new IllegalArgumentException( + "manufacturerData is null while manufacturerDataMask is not null"); + } + // Since the manufacturerDataMask is a bit mask for manufacturerData, the lengths + // of the two byte array need to be the same. + if (manufacturerData.length != manufacturerDataMask.length) { + throw new IllegalArgumentException( + "size mismatch for manufacturerData and manufacturerDataMask"); + } + } + this.manufacturerId = manufacturerId; + this.manufacturerData = manufacturerData; + this.manufacturerDataMask = manufacturerDataMask; + return this; + } + + /** + * Build {@link ScanFilter}. + * + * @throws IllegalArgumentException If the filter cannot be built. + */ + public ScanFilter build() { + return new ScanFilter(deviceName, deviceAddress, serviceUuid, uuidMask, + serviceDataUuid, serviceData, serviceDataMask, + manufacturerId, manufacturerData, manufacturerDataMask); + } + } } diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanRecord.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanRecord.java index 3818756..2c8c0fe 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanRecord.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanRecord.java @@ -23,18 +23,15 @@ package no.nordicsemi.android.support.v18.scanner; -import android.os.ParcelUuid; -import android.util.Log; -import android.util.SparseArray; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import ohos.hiviewdfx.HiLog; +import ohos.utils.PlainArray; +import ohos.utils.SequenceUuid; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + /** * Represents a scan record from Bluetooth LE scan. @@ -60,15 +57,17 @@ public final class ScanRecord { private static final int DATA_TYPE_SERVICE_DATA_32_BIT = 0x20; private static final int DATA_TYPE_SERVICE_DATA_128_BIT = 0x21; private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF; + private static final String LABEL_LOG ="ScanRecord" ; // Flags of the advertising data. private final int advertiseFlags; - @Nullable private final List serviceUuids; + @Nullable + private final List serviceUuids; - @Nullable private final SparseArray manufacturerSpecificData; + @Nullable private final PlainArray manufacturerSpecificData; - @Nullable private final Map serviceData; + @Nullable private final Map serviceData; // Transmission power level(in dB). private final int txPowerLevel; @@ -76,7 +75,7 @@ public final class ScanRecord { // Local name of the Bluetooth LE device. private final String deviceName; - // Raw bytes of scan record. + // Raw bytes of scan ecord. private final byte[] bytes; /** @@ -92,7 +91,7 @@ public final class ScanRecord { * bluetooth GATT services. */ @Nullable - public List getServiceUuids() { + public List getServiceUuids() { return serviceUuids; } @@ -101,16 +100,16 @@ public final class ScanRecord { * data. */ @Nullable - public SparseArray getManufacturerSpecificData() { + public PlainArray getManufacturerSpecificData() { return manufacturerSpecificData; } /** * Returns the manufacturer specific data associated with the manufacturer id. Returns * {@code null} if the {@code manufacturerId} is not found. + * @return */ - @Nullable - public byte[] getManufacturerSpecificData(final int manufacturerId) { + public Optional getManufacturerSpecificData(final int manufacturerId) { if (manufacturerSpecificData == null) { return null; } @@ -121,7 +120,7 @@ public final class ScanRecord { * Returns a map of service UUID and its corresponding service data. */ @Nullable - public Map getServiceData() { + public Map getServiceData() { return serviceData; } @@ -130,7 +129,7 @@ public final class ScanRecord { * {@code null} if the {@code serviceDataUuid} is not found. */ @Nullable - public byte[] getServiceData(@NonNull final ParcelUuid serviceDataUuid) { + public byte[] getServiceData( final Object serviceDataUuid) { //noinspection ConstantConditions if (serviceDataUuid == null || serviceData == null) { return null; @@ -165,9 +164,9 @@ public final class ScanRecord { return bytes; } - private ScanRecord(@Nullable final List serviceUuids, - @Nullable final SparseArray manufacturerData, - @Nullable final Map serviceData, + private ScanRecord(@Nullable final List serviceUuids, + @Nullable final PlainArray manufacturerData, + @Nullable final Map serviceData, final int advertiseFlags, final int txPowerLevel, final String localName, final byte[] bytes) { this.serviceUuids = serviceUuids; @@ -199,9 +198,9 @@ public final class ScanRecord { int advertiseFlag = -1; int txPowerLevel = Integer.MIN_VALUE; String localName = null; - List serviceUuids = null; - SparseArray manufacturerData = null; - Map serviceData = null; + List serviceUuids = null; + PlainArray manufacturerData = null; + Map serviceData = null; try { while (currentPos < scanRecord.length) { @@ -259,7 +258,7 @@ public final class ScanRecord { final byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos, serviceUuidLength); - final ParcelUuid serviceDataUuid = BluetoothUuid.parseUuidFrom( + final SequenceUuid serviceDataUuid = BluetoothUuid.parseUuidFrom( serviceDataUuidBytes); final byte[] serviceDataArray = extractBytes(scanRecord, currentPos + serviceUuidLength, dataLength - serviceUuidLength); @@ -275,7 +274,7 @@ public final class ScanRecord { final byte[] manufacturerDataBytes = extractBytes(scanRecord, currentPos + 2, dataLength - 2); if (manufacturerData == null) - manufacturerData = new SparseArray<>(); + manufacturerData = new PlainArray<>(); manufacturerData.put(manufacturerId, manufacturerDataBytes); break; default: @@ -288,7 +287,9 @@ public final class ScanRecord { return new ScanRecord(serviceUuids, manufacturerData, serviceData, advertiseFlag, txPowerLevel, localName, scanRecord); } catch (final Exception e) { - Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord)); +// Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord)); +// HiLog.error(LABEL_LOG, "unable to parse scan record:"+ Arrays.toString(scanRecord)); +// HiLog.error(LABEL_LOG, "Starting scanning failed" + Arrays.toString(scanRecord), e); // As the record is invalid, ignore all the parsed results for this packet // and return an empty record with raw scanRecord bytes in results return new ScanRecord(null, null, null, @@ -318,10 +319,10 @@ public final class ScanRecord { // Parse service UUIDs. @SuppressWarnings("UnusedReturnValue") - private static int parseServiceUuid(@NonNull final byte[] scanRecord, + private static int parseServiceUuid( final byte[] scanRecord, int currentPos, int dataLength, final int uuidLength, - @NonNull final List serviceUuids) { + final List serviceUuids) { while (dataLength > 0) { final byte[] uuidBytes = extractBytes(scanRecord, currentPos, uuidLength); @@ -333,7 +334,7 @@ public final class ScanRecord { } // Helper method to extract bytes from byte array. - private static byte[] extractBytes(@NonNull final byte[] scanRecord, + private static byte[] extractBytes( final byte[] scanRecord, final int start, final int length) { byte[] bytes = new byte[length]; System.arraycopy(scanRecord, start, bytes, 0, length); diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanResult.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanResult.java index faf47fc..4891651 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanResult.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanResult.java @@ -22,327 +22,347 @@ package no.nordicsemi.android.support.v18.scanner; -import android.bluetooth.BluetoothDevice; -import android.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import ohos.bluetooth.ble.BleScanResult; +import ohos.utils.Parcel; +import ohos.utils.Sequenceable; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; /** * ScanResult for Bluetooth LE scan. */ @SuppressWarnings({"WeakerAccess", "unused"}) -public final class ScanResult implements Parcelable { - - /** - * For chained advertisements, indicates that the data contained in this - * scan result is complete. - */ - public static final int DATA_COMPLETE = 0x00; - - /** - * For chained advertisements, indicates that the controller was - * unable to receive all chained packets and the scan result contains - * incomplete truncated data. - */ - public static final int DATA_TRUNCATED = 0x02; - - /** - * Indicates that the secondary physical layer was not used. - */ - public static final int PHY_UNUSED = 0x00; - - /** - * Advertising Set ID is not present in the packet. - */ - public static final int SID_NOT_PRESENT = 0xFF; - - /** - * TX power is not present in the packet. - */ - public static final int TX_POWER_NOT_PRESENT = 0x7F; - - /** - * Periodic advertising interval is not present in the packet. - */ - public static final int PERIODIC_INTERVAL_NOT_PRESENT = 0x00; - - /** - * Mask for checking whether event type represents legacy advertisement. - */ - static final int ET_LEGACY_MASK = 0x10; - - /** - * Mask for checking whether event type represents connectable advertisement. - */ - static final int ET_CONNECTABLE_MASK = 0x01; - - // Remote Bluetooth device. - @SuppressWarnings("NullableProblems") - @NonNull - private BluetoothDevice device; - - // Scan record, including advertising data and scan response data. - @Nullable - private ScanRecord scanRecord; - - // Received signal strength. - private int rssi; - - // Device timestamp when the result was last seen. - private long timestampNanos; - - private int eventType; - private int primaryPhy; - private int secondaryPhy; - private int advertisingSid; - private int txPower; - private int periodicAdvertisingInterval; - - /** - * Constructs a new ScanResult. - * - * @param device Remote Bluetooth device found. - * @param scanRecord Scan record including both advertising data and scan response data. - * @param rssi Received signal strength. - * @param timestampNanos Timestamp at which the scan result was observed. - * @deprecated use {@link #ScanResult(BluetoothDevice, int, int, int, int, int, int, int, ScanRecord, long)} - */ - public ScanResult(@NonNull final BluetoothDevice device, @Nullable final ScanRecord scanRecord, - int rssi, long timestampNanos) { - this.device = device; - this.scanRecord = scanRecord; - this.rssi = rssi; - this.timestampNanos = timestampNanos; - this.eventType = (DATA_COMPLETE << 5) | ET_LEGACY_MASK | ET_CONNECTABLE_MASK; - this.primaryPhy = 1; // BluetoothDevice.PHY_LE_1M; - this.secondaryPhy = PHY_UNUSED; - this.advertisingSid = SID_NOT_PRESENT; - this.txPower = 127; - this.periodicAdvertisingInterval = 0; - } - - /** - * Constructs a new ScanResult. - * - * @param device Remote Bluetooth device found. - * @param eventType Event type. - * @param primaryPhy Primary advertising phy. - * @param secondaryPhy Secondary advertising phy. - * @param advertisingSid Advertising set ID. - * @param txPower Transmit power. - * @param rssi Received signal strength. - * @param periodicAdvertisingInterval Periodic advertising interval. - * @param scanRecord Scan record including both advertising data and scan response data. - * @param timestampNanos Timestamp at which the scan result was observed. - */ - public ScanResult(@NonNull final BluetoothDevice device, final int eventType, - final int primaryPhy, final int secondaryPhy, - final int advertisingSid, final int txPower, final int rssi, - final int periodicAdvertisingInterval, - @Nullable final ScanRecord scanRecord, final long timestampNanos) { - this.device = device; - this.eventType = eventType; - this.primaryPhy = primaryPhy; - this.secondaryPhy = secondaryPhy; - this.advertisingSid = advertisingSid; - this.txPower = txPower; - this.rssi = rssi; - this.periodicAdvertisingInterval = periodicAdvertisingInterval; - this.scanRecord = scanRecord; - this.timestampNanos = timestampNanos; - } - - private ScanResult(final Parcel in) { - readFromParcel(in); - } - - @Override - public void writeToParcel(final Parcel dest, final int flags) { - device.writeToParcel(dest, flags); - if (scanRecord != null) { - dest.writeInt(1); - dest.writeByteArray(scanRecord.getBytes()); - } else { - dest.writeInt(0); - } - dest.writeInt(rssi); - dest.writeLong(timestampNanos); - dest.writeInt(eventType); - dest.writeInt(primaryPhy); - dest.writeInt(secondaryPhy); - dest.writeInt(advertisingSid); - dest.writeInt(txPower); - dest.writeInt(periodicAdvertisingInterval); - } - - private void readFromParcel(final Parcel in) { - device = BluetoothDevice.CREATOR.createFromParcel(in); - if (in.readInt() == 1) { - scanRecord = ScanRecord.parseFromBytes(in.createByteArray()); - } - rssi = in.readInt(); - timestampNanos = in.readLong(); - eventType = in.readInt(); - primaryPhy = in.readInt(); - secondaryPhy = in.readInt(); - advertisingSid = in.readInt(); - txPower = in.readInt(); - periodicAdvertisingInterval = in.readInt(); - } - - @Override - public int describeContents() { - return 0; - } - - /** - * Returns the remote Bluetooth device identified by the Bluetooth device address. - */ - @NonNull - public BluetoothDevice getDevice() { - return device; - } - - /** - * Returns the scan record, which is a combination of advertisement and scan response. - */ - @Nullable - public ScanRecord getScanRecord() { - return scanRecord; - } - - /** - * Returns the received signal strength in dBm. The valid range is [-127, 126]. - */ - public int getRssi() { - return rssi; - } - - /** - * Returns timestamp since boot when the scan record was observed. - */ - public long getTimestampNanos() { - return timestampNanos; - } - - /** - * Returns true if this object represents legacy scan result. - * Legacy scan results do not contain advanced advertising information - * as specified in the Bluetooth Core Specification v5. - */ - public boolean isLegacy() { - return (eventType & ET_LEGACY_MASK) != 0; - } - - /** - * Returns true if this object represents connectable scan result. - */ - public boolean isConnectable() { - return (eventType & ET_CONNECTABLE_MASK) != 0; - } - - /** - * Returns the data status. - * Can be one of {@link ScanResult#DATA_COMPLETE} or - * {@link ScanResult#DATA_TRUNCATED}. - */ - public int getDataStatus() { - // return bit 5 and 6 - return (eventType >> 5) & 0x03; - } - - /** - * Returns the primary Physical Layer - * on which this advertisement was received. - * Can be one of {@link BluetoothDevice#PHY_LE_1M} or - * {@link BluetoothDevice#PHY_LE_CODED}. - */ - public int getPrimaryPhy() { return primaryPhy; } - - /** - * Returns the secondary Physical Layer - * on which this advertisement was received. - * Can be one of {@link BluetoothDevice#PHY_LE_1M}, - * {@link BluetoothDevice#PHY_LE_2M}, {@link BluetoothDevice#PHY_LE_CODED} - * or {@link ScanResult#PHY_UNUSED} - if the advertisement - * was not received on a secondary physical channel. - */ - public int getSecondaryPhy() { return secondaryPhy; } - - /** - * Returns the advertising set id. - * May return {@link ScanResult#SID_NOT_PRESENT} if - * no set id was is present. - */ - public int getAdvertisingSid() { return advertisingSid; } - - /** - * Returns the transmit power in dBm. - * Valid range is [-127, 126]. A value of {@link ScanResult#TX_POWER_NOT_PRESENT} - * indicates that the TX power is not present. - */ - public int getTxPower() { return txPower; } - - /** - * Returns the periodic advertising interval in units of 1.25ms. - * Valid range is 6 (7.5ms) to 65536 (81918.75ms). A value of - * {@link ScanResult#PERIODIC_INTERVAL_NOT_PRESENT} means periodic - * advertising interval is not present. - */ - public int getPeriodicAdvertisingInterval() { - return periodicAdvertisingInterval; - } - - @Override - public int hashCode() { - return Objects.hash(device, rssi, scanRecord, timestampNanos, - eventType, primaryPhy, secondaryPhy, - advertisingSid, txPower, - periodicAdvertisingInterval); - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - final ScanResult other = (ScanResult) obj; - return Objects.equals(device, other.device) && (rssi == other.rssi) && - Objects.equals(scanRecord, other.scanRecord) && - (timestampNanos == other.timestampNanos) && - eventType == other.eventType && - primaryPhy == other.primaryPhy && - secondaryPhy == other.secondaryPhy && - advertisingSid == other.advertisingSid && - txPower == other.txPower && - periodicAdvertisingInterval == other.periodicAdvertisingInterval; - } - - @Override - public String toString() { - return "ScanResult{" + "device=" + device + ", scanRecord=" + - Objects.toString(scanRecord) + ", rssi=" + rssi + - ", timestampNanos=" + timestampNanos + ", eventType=" + eventType + - ", primaryPhy=" + primaryPhy + ", secondaryPhy=" + secondaryPhy + - ", advertisingSid=" + advertisingSid + ", txPower=" + txPower + - ", periodicAdvertisingInterval=" + periodicAdvertisingInterval + '}'; - } - - public static final Parcelable.Creator CREATOR = new Creator() { - @Override - public ScanResult createFromParcel(final Parcel source) { - return new ScanResult(source); - } - - @Override - public ScanResult[] newArray(final int size) { - return new ScanResult[size]; - } - }; - +public final class ScanResult implements Sequenceable { + + /** + * For chained advertisements, indicates that the data contained in this + * scan result is complete. + */ + public static final int DATA_COMPLETE = 0x00; + + /** + * For chained advertisements, indicates that the controller was + * unable to receive all chained packets and the scan result contains + * incomplete truncated data. + */ + public static final int DATA_TRUNCATED = 0x02; + + /** + * Indicates that the secondary physical layer was not used. + */ + public static final int PHY_UNUSED = 0x00; + + /** + * Advertising Set ID is not present in the packet. + */ + public static final int SID_NOT_PRESENT = 0xFF; + + /** + * TX power is not present in the packet. + */ + public static final int TX_POWER_NOT_PRESENT = 0x7F; + + /** + * Periodic advertising interval is not present in the packet. + */ + public static final int PERIODIC_INTERVAL_NOT_PRESENT = 0x00; + + /** + * Mask for checking whether event type represents legacy advertisement. + */ + static final int ET_LEGACY_MASK = 0x10; + + /** + * Mask for checking whether event type represents connectable advertisement. + */ + static final int ET_CONNECTABLE_MASK = 0x01; + + // Remote Bluetooth device. + @SuppressWarnings("NullableProblems") + + private Object device; + + // Scan record, including advertising data and scan response data. + + private ScanRecord scanRecord; + + // Received signal strength. + private int rssi; + + // Device timestamp when the result was last seen. + private long timestampNanos; + + private int eventType; + private int primaryPhy; + private int secondaryPhy; + private int advertisingSid; + private int txPower; + private int periodicAdvertisingInterval; + + /** + * Constructs a new ScanResult. + * + * @param device Remote Bluetooth device found. + * @param scanRecord Scan record including both advertising data and scan response data. + * @param rssi Received signal strength. + * @param timestampNanos Timestamp at which the scan result was observed. + * @deprecated use {@link #ScanResult(Object, int, int, int, int, int, int, int, ScanRecord, long)} + */ + public ScanResult(final Object device, @Nullable final ScanRecord scanRecord, + int rssi, long timestampNanos) { + this.device = device; + this.scanRecord = scanRecord; + this.rssi = rssi; + this.timestampNanos = timestampNanos; + this.eventType = (DATA_COMPLETE << 5) | ET_LEGACY_MASK | ET_CONNECTABLE_MASK; + this.primaryPhy = 1; // BluetoothDevice.PHY_LE_1M; + this.secondaryPhy = PHY_UNUSED; + this.advertisingSid = SID_NOT_PRESENT; + this.txPower = 127; + this.periodicAdvertisingInterval = 0; + } + + /** + * Constructs a new ScanResult. + * + * @param device Remote Bluetooth device found. + * @param eventType Event type. + * @param primaryPhy Primary advertising phy. + * @param secondaryPhy Secondary advertising phy. + * @param advertisingSid Advertising set ID. + * @param txPower Transmit power. + * @param rssi Received signal strength. + * @param periodicAdvertisingInterval Periodic advertising interval. + * @param scanRecord Scan record including both advertising data and scan response data. + * @param timestampNanos Timestamp at which the scan result was observed. + */ + public ScanResult(final Object device, final int eventType, + final int primaryPhy, final int secondaryPhy, + final int advertisingSid, final int txPower, final int rssi, + final int periodicAdvertisingInterval, + @Nullable final ScanRecord scanRecord, final long timestampNanos) { + this.device = device; + this.eventType = eventType; + this.primaryPhy = primaryPhy; + this.secondaryPhy = secondaryPhy; + this.advertisingSid = advertisingSid; + this.txPower = txPower; + this.rssi = rssi; + this.periodicAdvertisingInterval = periodicAdvertisingInterval; + this.scanRecord = scanRecord; + this.timestampNanos = timestampNanos; + } + + private ScanResult(final Parcel in) { + readFromParcel(in); + } + +// @Override + public void writeToParcel(final Parcel dest, final int flags) { +// device.writeToParcel(dest, flags); + if (scanRecord != null) { + dest.writeInt(1); + dest.writeByteArray(scanRecord.getBytes()); + } else { + dest.writeInt(0); + } + dest.writeInt(rssi); + dest.writeLong(timestampNanos); + dest.writeInt(eventType); + dest.writeInt(primaryPhy); + dest.writeInt(secondaryPhy); + dest.writeInt(advertisingSid); + dest.writeInt(txPower); + dest.writeInt(periodicAdvertisingInterval); + } + + private void readFromParcel(final Parcel in) { +// device = BluetoothDevice.CREATOR.createFromParcel(in); + if (in.readInt() == 1) { +// scanRecord = ScanRecord.parseFromBytes(in.createByteArray()); + scanRecord = ScanRecord.parseFromBytes(in.readByteArray()); + } + rssi = in.readInt(); + timestampNanos = in.readLong(); + eventType = in.readInt(); + primaryPhy = in.readInt(); + secondaryPhy = in.readInt(); + advertisingSid = in.readInt(); + txPower = in.readInt(); + periodicAdvertisingInterval = in.readInt(); + } + +// @Override + public int describeContents() { + return 0; + } + + /** + * Returns the remote Bluetooth device identified by the Bluetooth device address. + */ + + public Object getDevice() { + return device; + } + + /** + * Returns the scan record, which is a combination of advertisement and scan response. + */ + @Nullable + public ScanRecord getScanRecord() { + return scanRecord; + } + + /** + * Returns the received signal strength in dBm. The valid range is [-127, 126]. + */ + public int getRssi() { + return rssi; + } + + /** + * Returns timestamp since boot when the scan record was observed. + */ + public long getTimestampNanos() { + return timestampNanos; + } + + /** + * Returns true if this object represents legacy scan result. + * Legacy scan results do not contain advanced advertising information + * as specified in the Bluetooth Core Specification v5. + */ + public boolean isLegacy() { + return (eventType & ET_LEGACY_MASK) != 0; + } + + /** + * Returns true if this object represents connectable scan result. + */ + public boolean isConnectable() { + return (eventType & ET_CONNECTABLE_MASK) != 0; + } + + /** + * Returns the data status. + * Can be one of {@link ScanResult#DATA_COMPLETE} or + * {@link ScanResult#DATA_TRUNCATED}. + */ + public int getDataStatus() { + // return bit 5 and 6 + return (eventType >> 5) & 0x03; + } + + /** + * Returns the primary Physical Layer + * on which this advertisement was received. + */ + public int getPrimaryPhy() { + return primaryPhy; + } + + /** + * Returns the secondary Physical Layer + * or {@link ScanResult#PHY_UNUSED} - if the advertisement + * was not received on a secondary physical channel. + */ + public int getSecondaryPhy() { + return secondaryPhy; + } + + /** + * Returns the advertising set id. + * May return {@link ScanResult#SID_NOT_PRESENT} if + * no set id was is present. + */ + public int getAdvertisingSid() { + return advertisingSid; + } + + /** + * Returns the transmit power in dBm. + * Valid range is [-127, 126]. A value of {@link ScanResult#TX_POWER_NOT_PRESENT} + * indicates that the TX power is not present. + */ + public int getTxPower() { + return txPower; + } + + /** + * Returns the periodic advertising interval in units of 1.25ms. + * Valid range is 6 (7.5ms) to 65536 (81918.75ms). A value of + * {@link ScanResult#PERIODIC_INTERVAL_NOT_PRESENT} means periodic + * advertising interval is not present. + */ + public int getPeriodicAdvertisingInterval() { + return periodicAdvertisingInterval; + } + + @Override + public int hashCode() { + return Objects.hash(device, rssi, scanRecord, timestampNanos, + eventType, primaryPhy, secondaryPhy, + advertisingSid, txPower, + periodicAdvertisingInterval); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final ScanResult other = (ScanResult) obj; + return Objects.equals(device, other.device) && (rssi == other.rssi) && + Objects.equals(scanRecord, other.scanRecord) && + (timestampNanos == other.timestampNanos) && + eventType == other.eventType && + primaryPhy == other.primaryPhy && + secondaryPhy == other.secondaryPhy && + advertisingSid == other.advertisingSid && + txPower == other.txPower && + periodicAdvertisingInterval == other.periodicAdvertisingInterval; + } + + @Override + public String toString() { + return "ScanResult{" + "device=" + device + ", scanRecord=" + + Objects.toString(scanRecord) + ", rssi=" + rssi + + ", timestampNanos=" + timestampNanos + ", eventType=" + eventType + + ", primaryPhy=" + primaryPhy + ", secondaryPhy=" + secondaryPhy + + ", advertisingSid=" + advertisingSid + ", txPower=" + txPower + + ", periodicAdvertisingInterval=" + periodicAdvertisingInterval + '}'; + } + +// public static final Parcelable.Creator CREATOR = new Creator() { + public static final Sequenceable.Producer CREATOR = new Producer() { + @Override + public ScanResult createFromParcel(final Parcel source) { + return new ScanResult(source); + } + +// @Override + public BleScanResult[] newArray(final int size) { + return new BleScanResult[size]; + } + }; + + @Override + public boolean hasFileDescriptor() { + return false; + } + + @Override + public boolean marshalling(Parcel parcel) { + return false; + } + + @Override + public boolean unmarshalling(Parcel parcel) { + return false; + } } diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanSettings.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanSettings.java index fbb1572..04fd733 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanSettings.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScanSettings.java @@ -22,621 +22,623 @@ package no.nordicsemi.android.support.v18.scanner; -import android.bluetooth.BluetoothDevice; -import android.os.Parcel; -import android.os.Parcelable; +import ohos.utils.Parcel; +import ohos.utils.Sequenceable; import java.util.List; -import androidx.annotation.NonNull; - /** * Bluetooth LE scan settings are passed to {@link BluetoothLeScannerCompat#startScan} to define the * parameters for the scan. */ @SuppressWarnings({"WeakerAccess", "unused"}) -public final class ScanSettings implements Parcelable { - - /** - * The default value of the maximum time for the device not to be discoverable before it will be - * assumed lost. - */ - public static final long MATCH_LOST_DEVICE_TIMEOUT_DEFAULT = 10000L; // [ms] - - /** - * The default interval of the task that calls match lost events. - */ - public static final long MATCH_LOST_TASK_INTERVAL_DEFAULT = 10000L; // [ms] - - /** - * A special Bluetooth LE scan mode. Applications using this scan mode will passively listen for - * other scan results without starting BLE scans themselves. - *

- * On Android Lollipop {@link #SCAN_MODE_LOW_POWER} will be used instead, as opportunistic - * mode was not yet supported. - *

- * On pre-Lollipop devices it is possible to override the default intervals - * using {@link Builder#setPowerSave(long, long)}. - */ - public static final int SCAN_MODE_OPPORTUNISTIC = -1; - - /** - * Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes the - * least power. This mode is enforced if the scanning application is not in foreground. - *

- * On pre-Lollipop devices this mode will be emulated by scanning for 0.5 second followed - * by 4.5 second of idle, which corresponds to the low power intervals on Lollipop or newer. - */ - public static final int SCAN_MODE_LOW_POWER = 0; - - /** - * Perform Bluetooth LE scan in balanced power mode. Scan results are returned at a rate that - * provides a good trade-off between scan frequency and power consumption. - *

- * On pre-Lollipop devices this mode will be emulated by scanning for 2 second followed - * by 3 seconds of idle, which corresponds to the low power intervals on Lollipop or newer. - */ - public static final int SCAN_MODE_BALANCED = 1; - - /** - * Scan using highest duty cycle. It's recommended to only use this mode when the application is - * running in the foreground. - */ - public static final int SCAN_MODE_LOW_LATENCY = 2; - - /** - * Trigger a callback for every Bluetooth advertisement found that matches the filter criteria. - * If no filter is active, all advertisement packets are reported. - */ - public static final int CALLBACK_TYPE_ALL_MATCHES = 1; - - /** - * A result callback is only triggered for the first advertisement packet received that matches - * the filter criteria. - */ - public static final int CALLBACK_TYPE_FIRST_MATCH = 2; - - /** - * Receive a callback when advertisements are no longer received from a device that has been - * previously reported by a first match callback. - */ - public static final int CALLBACK_TYPE_MATCH_LOST = 4; - - - /* - * Determines how many advertisements to match per filter, as this is scarce hw resource - */ - /** - * Match one advertisement per filter - */ - public static final int MATCH_NUM_ONE_ADVERTISEMENT = 1; - - /** - * Match few advertisement per filter, depends on current capability and availability of - * the resources in hw - */ - public static final int MATCH_NUM_FEW_ADVERTISEMENT = 2; - - /** - * Match as many advertisement per filter as hw could allow, depends on current - * capability and availability of the resources in hw - */ - public static final int MATCH_NUM_MAX_ADVERTISEMENT = 3; - - /** - * In Aggressive mode, hw will determine a match sooner even with feeble signal strength - * and few number of sightings/match in a duration. - */ - public static final int MATCH_MODE_AGGRESSIVE = 1; - - /** - * For sticky mode, higher threshold of signal strength and sightings is required - * before reporting by hw - */ - public static final int MATCH_MODE_STICKY = 2; - - /** - * Use all supported PHYs for scanning. - * This will check the controller capabilities, and start - * the scan on 1Mbit and LE Coded PHYs if supported, or on - * the 1Mbit PHY only. - */ - public static final int PHY_LE_ALL_SUPPORTED = 255; - - /** - * Pre-Lollipop scanning requires a wakelock and the CPU cannot go to sleep. - * To conserve power we can optionally scan for a certain duration (scan interval) - * and then rest for a time before starting scanning again. - */ - private final long powerSaveScanInterval; - private final long powerSaveRestInterval; - - // Bluetooth LE scan mode. - private int scanMode; - - // Bluetooth LE scan callback type - private int callbackType; - - // Time of delay for reporting the scan result - private long reportDelayMillis; - - private int matchMode; - - private int numOfMatchesPerFilter; - - private boolean useHardwareFilteringIfSupported; - - private boolean useHardwareBatchingIfSupported; - - private boolean useHardwareCallbackTypesIfSupported; - - private long matchLostDeviceTimeout; - - private long matchLostTaskInterval; - - // Include only legacy advertising results - private boolean legacy; - - private int phy; - - public int getScanMode() { - return scanMode; - } - - public int getCallbackType() { - return callbackType; - } - - public int getMatchMode() { - return matchMode; - } - - public int getNumOfMatches() { - return numOfMatchesPerFilter; - } - - public boolean getUseHardwareFilteringIfSupported() { - return useHardwareFilteringIfSupported; - } - - public boolean getUseHardwareBatchingIfSupported() { - return useHardwareBatchingIfSupported; - } - - public boolean getUseHardwareCallbackTypesIfSupported() { - return useHardwareCallbackTypesIfSupported; - } - - /** - * Some devices with Android Marshmallow (Nexus 6) theoretically support other callback types, - * but call {@link android.bluetooth.le.ScanCallback#onScanFailed(int)} with error = 5. - * In that case the Scanner Compat will disable the hardware support and start using compat - * mechanism. - */ - /* package */ void disableUseHardwareCallbackTypes() { - useHardwareCallbackTypesIfSupported = false; - } - - public long getMatchLostDeviceTimeout() { - return matchLostDeviceTimeout; - } - - public long getMatchLostTaskInterval() { - return matchLostTaskInterval; - } - - /** - * Returns whether only legacy advertisements will be returned. - * Legacy advertisements include advertisements as specified - * by the Bluetooth core specification 4.2 and below. - */ - public boolean getLegacy() { - return legacy; - } - - /** - * Returns the physical layer used during a scan. - */ - public int getPhy() { - return phy; - } - - /** - * Returns report delay timestamp based on the device clock. - */ - public long getReportDelayMillis() { - return reportDelayMillis; - } - - private ScanSettings(final int scanMode, final int callbackType, - final long reportDelayMillis, final int matchMode, - final int numOfMatchesPerFilter, final boolean legacy, final int phy, - final boolean hardwareFiltering, final boolean hardwareBatching, - final boolean hardwareCallbackTypes, final long matchTimeout, - final long taskInterval, - final long powerSaveScanInterval, final long powerSaveRestInterval) { - this.scanMode = scanMode; - this.callbackType = callbackType; - this.reportDelayMillis = reportDelayMillis; - this.numOfMatchesPerFilter = numOfMatchesPerFilter; - this.matchMode = matchMode; - this.legacy = legacy; - this.phy = phy; - this.useHardwareFilteringIfSupported = hardwareFiltering; - this.useHardwareBatchingIfSupported = hardwareBatching; - this.useHardwareCallbackTypesIfSupported = hardwareCallbackTypes; - this.matchLostDeviceTimeout = matchTimeout * 1000000L; // convert to nanos - this.matchLostTaskInterval = taskInterval; - this.powerSaveScanInterval = powerSaveScanInterval; - this.powerSaveRestInterval = powerSaveRestInterval; - } - - private ScanSettings(final Parcel in) { - scanMode = in.readInt(); - callbackType = in.readInt(); - reportDelayMillis = in.readLong(); - matchMode = in.readInt(); - numOfMatchesPerFilter = in.readInt(); - legacy = in.readInt() != 0; - phy = in.readInt(); - useHardwareFilteringIfSupported = in.readInt() == 1; - useHardwareBatchingIfSupported = in.readInt() == 1; - powerSaveScanInterval = in.readLong(); - powerSaveRestInterval = in.readLong(); - } - - @Override - public void writeToParcel(final Parcel dest, final int flags) { - dest.writeInt(scanMode); - dest.writeInt(callbackType); - dest.writeLong(reportDelayMillis); - dest.writeInt(matchMode); - dest.writeInt(numOfMatchesPerFilter); - dest.writeInt(legacy ? 1 : 0); - dest.writeInt(phy); - dest.writeInt(useHardwareFilteringIfSupported ? 1 : 0); - dest.writeInt(useHardwareBatchingIfSupported ? 1 : 0); - dest.writeLong(powerSaveScanInterval); - dest.writeLong(powerSaveRestInterval); - } - - @Override - public int describeContents() { - return 0; - } - - public static final Parcelable.Creator CREATOR = new Creator() { - @Override - public ScanSettings[] newArray(final int size) { - return new ScanSettings[size]; - } - - @Override - public ScanSettings createFromParcel(final Parcel in) { - return new ScanSettings(in); - } - }; - - /** - * Determine if we should do power-saving sleep on pre-Lollipop - */ - public boolean hasPowerSaveMode() { - return powerSaveRestInterval > 0 && powerSaveScanInterval > 0; - } - - public long getPowerSaveRest() { - return powerSaveRestInterval; - } - - public long getPowerSaveScan() { - return powerSaveScanInterval; - } - - /** - * Builder for {@link ScanSettings}. - */ - @SuppressWarnings({"UnusedReturnValue", "unused"}) - public static final class Builder { - private int scanMode = SCAN_MODE_LOW_POWER; - private int callbackType = CALLBACK_TYPE_ALL_MATCHES; - private long reportDelayMillis = 0; - private int matchMode = MATCH_MODE_AGGRESSIVE; - private int numOfMatchesPerFilter = MATCH_NUM_MAX_ADVERTISEMENT; - private boolean legacy = true; - private int phy = PHY_LE_ALL_SUPPORTED; - private boolean useHardwareFilteringIfSupported = true; - private boolean useHardwareBatchingIfSupported = true; - private boolean useHardwareCallbackTypesIfSupported = true; - private long matchLostDeviceTimeout = MATCH_LOST_DEVICE_TIMEOUT_DEFAULT; - private long matchLostTaskInterval = MATCH_LOST_TASK_INTERVAL_DEFAULT; - private long powerSaveRestInterval = 0; - private long powerSaveScanInterval = 0; - - /** - * Set scan mode for Bluetooth LE scan. - *

- * {@link #SCAN_MODE_OPPORTUNISTIC} is supported on Android Marshmallow onwards. - * On Lollipop this mode will fall back {@link #SCAN_MODE_LOW_POWER}, which actually means - * that the library will start its own scan instead of relying on scans from other apps. - * This may have significant impact on battery usage. - *

- * On pre-Lollipop devices, the settings set by {@link #setPowerSave(long, long)} - * will be used. By default, the intervals are the same as for {@link #SCAN_MODE_LOW_POWER}. - * - * @param scanMode The scan mode can be one of {@link ScanSettings#SCAN_MODE_LOW_POWER}, - * {@link #SCAN_MODE_BALANCED}, - * {@link #SCAN_MODE_LOW_LATENCY} or - * {@link #SCAN_MODE_OPPORTUNISTIC}. - * @throws IllegalArgumentException If the {@code scanMode} is invalid. - */ - @NonNull - public Builder setScanMode(final int scanMode) { - if (scanMode < SCAN_MODE_OPPORTUNISTIC || scanMode > SCAN_MODE_LOW_LATENCY) { - throw new IllegalArgumentException("invalid scan mode " + scanMode); - } - this.scanMode = scanMode; - return this; - } - - /** - * Set callback type for Bluetooth LE scan. - * - * @param callbackType The callback type flags for the scan. - * @throws IllegalArgumentException If the {@code callbackType} is invalid. - */ - @NonNull - public Builder setCallbackType(final int callbackType) { - if (!isValidCallbackType(callbackType)) { - throw new IllegalArgumentException("invalid callback type - " + callbackType); - } - this.callbackType = callbackType; - return this; - } - - // Returns true if the callbackType is valid. - private boolean isValidCallbackType(final int callbackType) { - if (callbackType == CALLBACK_TYPE_ALL_MATCHES || - callbackType == CALLBACK_TYPE_FIRST_MATCH || - callbackType == CALLBACK_TYPE_MATCH_LOST) { - return true; - } - return callbackType == (CALLBACK_TYPE_FIRST_MATCH | CALLBACK_TYPE_MATCH_LOST); - } - - /** - * Set report delay timestamp for Bluetooth LE scan. - * - * @param reportDelayMillis Delay of report in milliseconds. Set to 0 to be notified of - * results immediately. Values > 0 causes the scan results - * to be queued up and delivered after the requested delay or - * when the internal buffers fill up.

- * For delays below 5000 ms (5 sec) the - * {@link ScanCallback#onBatchScanResults(List)} - * will be called in unreliable intervals, but starting from - * around 5000 the intervals get even. - * @throws IllegalArgumentException If {@code reportDelayMillis} < 0. - */ - @NonNull - public Builder setReportDelay(final long reportDelayMillis) { - if (reportDelayMillis < 0) { - throw new IllegalArgumentException("reportDelay must be > 0"); - } - this.reportDelayMillis = reportDelayMillis; - return this; - } - - /** - * Set the number of matches for Bluetooth LE scan filters hardware match. - * - * @param numOfMatches The num of matches can be one of - * {@link ScanSettings#MATCH_NUM_ONE_ADVERTISEMENT} or - * {@link ScanSettings#MATCH_NUM_FEW_ADVERTISEMENT} or - * {@link ScanSettings#MATCH_NUM_MAX_ADVERTISEMENT} - * @throws IllegalArgumentException If the {@code matchMode} is invalid. - */ - @NonNull - public Builder setNumOfMatches(final int numOfMatches) { - if (numOfMatches < MATCH_NUM_ONE_ADVERTISEMENT - || numOfMatches > MATCH_NUM_MAX_ADVERTISEMENT) { - throw new IllegalArgumentException("invalid numOfMatches " + numOfMatches); - } - numOfMatchesPerFilter = numOfMatches; - return this; - } - - /** - * Set match mode for Bluetooth LE scan filters hardware match - * - * @param matchMode The match mode can be one of - * {@link ScanSettings#MATCH_MODE_AGGRESSIVE} or - * {@link ScanSettings#MATCH_MODE_STICKY} - * @throws IllegalArgumentException If the {@code matchMode} is invalid. - */ - @NonNull - public Builder setMatchMode(final int matchMode) { - if (matchMode < MATCH_MODE_AGGRESSIVE - || matchMode > MATCH_MODE_STICKY) { - throw new IllegalArgumentException("invalid matchMode " + matchMode); - } - this.matchMode = matchMode; - return this; - } - - /** - * Set whether only legacy advertisements should be returned in scan results. - * Legacy advertisements include advertisements as specified by the - * Bluetooth core specification 4.2 and below. This is true by default - * for compatibility with older apps. - * - * @param legacy true if only legacy advertisements will be returned - */ - @NonNull - public Builder setLegacy(final boolean legacy) { - this.legacy = legacy; - return this; - } - - /** - * Set the Physical Layer to use during this scan. - * This is used only if {@link ScanSettings.Builder#setLegacy} - * is set to false and only on Android 0reo or newer. - * {@link android.bluetooth.BluetoothAdapter#isLeCodedPhySupported} - * may be used to check whether LE Coded phy is supported by calling - * {@link android.bluetooth.BluetoothAdapter#isLeCodedPhySupported}. - * Selecting an unsupported phy will result in failure to start scan. - * - * @param phy Can be one of - * {@link BluetoothDevice#PHY_LE_1M}, - * {@link BluetoothDevice#PHY_LE_CODED} or - * {@link ScanSettings#PHY_LE_ALL_SUPPORTED} - */ - @NonNull - public Builder setPhy(final int phy) { - this.phy = phy; - return this; - } - - /** - * Several phones may have some issues when it comes to offloaded filtering. - * Even if it should be supported, it may not work as expected. - * It has been observed for example, that setting 2 filters with different devices - * addresses on Nexus 6 with Lollipop gives no callbacks if one or both devices advertise. - * See https://code.google.com/p/android/issues/detail?id=181561. - * - * @param use true to enable (default) hardware offload filtering. - * If false a compat software filtering will be used - * (uses much more resources). - */ - @NonNull - public Builder setUseHardwareFilteringIfSupported(final boolean use) { - useHardwareFilteringIfSupported = use; - return this; - } - - /** - * Some devices, for example Samsung S6 and S6 Edge with Lollipop, return always - * the same RSSI value for all devices if offloaded batching is used. - * Batching may also be emulated using a compat mechanism - a periodically called timer. - * Timer approach requires more resources but reports devices in constant delays - * and works on devices that does not support offloaded batching. - * In comparison, when setReportDelay(..) is called with parameter 1000 the standard, - * hardware triggered callback will be called every 1500ms +-200ms. - * - * @param use true to enable (default) hardware offloaded batching if they are supported. - * False to always use compat mechanism. - */ - @NonNull - public Builder setUseHardwareBatchingIfSupported(final boolean use) { - useHardwareBatchingIfSupported = use; - return this; - } - - /** - * This method may be used when callback type is set to a value different than - * {@link #CALLBACK_TYPE_ALL_MATCHES}. When disabled, the Scanner Compat itself will - * take care of reporting first match and match lost. The compat behaviour may differ - * from the one natively supported on Android Marshmallow or newer. - *

- * Also, in compat mode values set by {@link #setMatchMode(int)} and - * {@link #setNumOfMatches(int)} are ignored. - * Instead use {@link #setMatchOptions(long, long)} to set timer options. - * - * @param use true to enable (default) the offloaded match reporting if hardware supports it, - * false to enable compat implementation. - */ - @NonNull - public Builder setUseHardwareCallbackTypesIfSupported(final boolean use) { - useHardwareCallbackTypesIfSupported = use; - return this; - } - - /** - * The match options are used when the callback type has been set to - * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} or - * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} and hardware does not support those types. - * In that case {@link BluetoothLeScannerCompat} starts a task that runs periodically - * and calls {@link ScanCallback#onScanResult(int, ScanResult)} with type - * {@link #CALLBACK_TYPE_MATCH_LOST} if a device has not been seen for at least given time. - * - * @param deviceTimeoutMillis the time required for the device to be recognized as lost - * (default {@link #MATCH_LOST_DEVICE_TIMEOUT_DEFAULT}). - * @param taskIntervalMillis the task interval (default {@link #MATCH_LOST_TASK_INTERVAL_DEFAULT}). - */ - @NonNull - public Builder setMatchOptions(final long deviceTimeoutMillis, final long taskIntervalMillis) { - if (deviceTimeoutMillis <= 0 || taskIntervalMillis <= 0) { - throw new IllegalArgumentException("maxDeviceAgeMillis and taskIntervalMillis must be > 0"); - } - matchLostDeviceTimeout = deviceTimeoutMillis; - matchLostTaskInterval = taskIntervalMillis; - return this; - } - - /** - * Pre-Lollipop scanning requires a wakelock and the CPU cannot go to sleep. - * To conserve power we can optionally scan for a certain duration (scan interval) - * and then rest for a time before starting scanning again. Won't affect Lollipop - * or later devices. - * - * @param scanInterval interval in ms to scan at a time. - * @param restInterval interval to sleep for without scanning before scanning again for - * scanInterval. - */ - @NonNull - public Builder setPowerSave(final long scanInterval, final long restInterval) { - if (scanInterval <= 0 || restInterval <= 0) { - throw new IllegalArgumentException("scanInterval and restInterval must be > 0"); - } - powerSaveScanInterval = scanInterval; - powerSaveRestInterval = restInterval; - return this; - } - - /** - * Build {@link ScanSettings}. - */ - @NonNull - public ScanSettings build() { - if (powerSaveRestInterval == 0 && powerSaveScanInterval == 0) - updatePowerSaveSettings(); - - return new ScanSettings(scanMode, callbackType, reportDelayMillis, matchMode, - numOfMatchesPerFilter, legacy, phy, useHardwareFilteringIfSupported, - useHardwareBatchingIfSupported, useHardwareCallbackTypesIfSupported, - matchLostDeviceTimeout, matchLostTaskInterval, - powerSaveScanInterval, powerSaveRestInterval); - } - - /** - * Sets power save settings based on the scan mode selected. - */ - private void updatePowerSaveSettings() { - switch (scanMode) { - case SCAN_MODE_LOW_LATENCY: - // Disable power save mode - powerSaveScanInterval = 0; - powerSaveRestInterval = 0; - break; - case SCAN_MODE_BALANCED: - // Scan for 2 seconds every 5 seconds - powerSaveScanInterval = 2000; - powerSaveRestInterval = 3000; - break; - case SCAN_MODE_OPPORTUNISTIC: - // It is not possible to emulate OPPORTUNISTIC scanning, but in theory - // that should be even less battery consuming than LOW_POWER. - // For pre-Lollipop devices intervals can be overwritten by - // setPowerSave(long, long) if needed. - - // On Android Lollipop the native SCAN_MODE_LOW_POWER will be used instead - // of power save values. - case SCAN_MODE_LOW_POWER: - default: - // Scan for 0.5 second every 5 seconds - powerSaveScanInterval = 500; - powerSaveRestInterval = 4500; - break; - } - } - } +public final class ScanSettings implements Sequenceable { + + /** + * The default value of the maximum time for the device not to be discoverable before it will be + * assumed lost. + */ + public static final long MATCH_LOST_DEVICE_TIMEOUT_DEFAULT = 10000L; // [ms] + + /**0 + * The default interval of the task that calls match lost events. + */ + public static final long MATCH_LOST_TASK_INTERVAL_DEFAULT = 10000L; // [ms] + /** + * A special Bluetooth LE scan mode. Applications using this scan mode will passively listen for + * other scan results without starting BLE scans themselves. + *

+ * On Android Lollipop {@link #SCAN_MODE_LOW_POWER} will be used instead, as opportunistic + * mode was not yet supported. + *

+ * On pre-Lollipop devices it is possible to override the default intervals + * using {@link Builder#setPowerSave(long, long)}. + */ + public static final int SCAN_MODE_OPPORTUNISTIC = -1; + + /** + * Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes the + * least power. This mode is enforced if the scanning application is not in foreground. + *

+ * On pre-Lollipop devices this mode will be emulated by scanning for 0.5 second followed + * by 4.5 second of idle, which corresponds to the low power intervals on Lollipop or newer. + */ + public static final int SCAN_MODE_LOW_POWER = 0; + + /** + * Perform Bluetooth LE scan in balanced power mode. Scan results are returned at a rate that + * provides a good trade-off between scan frequency and power consumption. + *

+ * On pre-Lollipop devices this mode will be emulated by scanning for 2 second followed + * by 3 seconds of idle, which corresponds to the low power intervals on Lollipop or newer. + */ + public static final int SCAN_MODE_BALANCED = 1; + + /** + * Scan using highest duty cycle. It's recommended to only use this mode when the application is + * running in the foreground. + */ + public static final int SCAN_MODE_LOW_LATENCY = 2; + + /** + * Trigger a callback for every Bluetooth advertisement found that matches the filter criteria. + * If no filter is active, all advertisement packets are reported. + */ + public static final int CALLBACK_TYPE_ALL_MATCHES = 1; + + /** + * A result callback is only triggered for the first advertisement packet received that matches + * the filter criteria. + */ + public static final int CALLBACK_TYPE_FIRST_MATCH = 2; + + /** + * Receive a callback when advertisements are no longer received from a device that has been + * previously reported by a first match callback. + */ + public static final int CALLBACK_TYPE_MATCH_LOST = 4; + + + /* + * Determines how many advertisements to match per filter, as this is scarce hw resource + */ + /** + * Match one advertisement per filter + */ + public static final int MATCH_NUM_ONE_ADVERTISEMENT = 1; + + /** + * Match few advertisement per filter, depends on current capability and availability of + * the resources in hw + */ + public static final int MATCH_NUM_FEW_ADVERTISEMENT = 2; + + /** + * Match as many advertisement per filter as hw could allow, depends on current + * capability and availability of the resources in hw + */ + public static final int MATCH_NUM_MAX_ADVERTISEMENT = 3; + + /** + * In Aggressive mode, hw will determine a match sooner even with feeble signal strength + * and few number of sightings/match in a duration. + */ + public static final int MATCH_MODE_AGGRESSIVE = 1; + + /** + * For sticky mode, higher threshold of signal strength and sightings is required + * before reporting by hw + */ + public static final int MATCH_MODE_STICKY = 2; + + /** + * Use all supported PHYs for scanning. + * This will check the controller capabilities, and start + * the scan on 1Mbit and LE Coded PHYs if supported, or on + * the 1Mbit PHY only. + */ + public static final int PHY_LE_ALL_SUPPORTED = 255; + + /** + * Pre-Lollipop scanning requires a wakelock and the CPU cannot go to sleep. + * To conserve power we can optionally scan for a certain duration (scan interval) + * and then rest for a time before starting scanning again. + */ + private final long powerSaveScanInterval; + private final long powerSaveRestInterval; + + // Bluetooth LE scan mode. + private int scanMode; + + // Bluetooth LE scan callback type + private int callbackType; + + // Time of delay for reporting the scan result + private long reportDelayMillis; + + private int matchMode; + + private int numOfMatchesPerFilter; + + private boolean useHardwareFilteringIfSupported; + + private boolean useHardwareBatchingIfSupported; + + private boolean useHardwareCallbackTypesIfSupported; + + private long matchLostDeviceTimeout; + + private long matchLostTaskInterval; + + // Include only legacy advertising results + private boolean legacy; + + private int phy; + + public int getScanMode() { + return scanMode; + } + + public int getCallbackType() { + return callbackType; + } + + public int getMatchMode() { + return matchMode; + } + + public int getNumOfMatches() { + return numOfMatchesPerFilter; + } + + public boolean getUseHardwareFilteringIfSupported() { + return useHardwareFilteringIfSupported; + } + + public boolean getUseHardwareBatchingIfSupported() { + return useHardwareBatchingIfSupported; + } + + public boolean getUseHardwareCallbackTypesIfSupported() { + return useHardwareCallbackTypesIfSupported; + } + + /** + * Some devices with Android Marshmallow (Nexus 6) theoretically support other callback types, + * but call {@link ScanCallback#onScanFailed(int)} with error = 5. + * In that case the Scanner Compat will disable the hardware support and start using compat + * mechanism. + */ + /* package */ void disableUseHardwareCallbackTypes() { + useHardwareCallbackTypesIfSupported = false; + } + + public long getMatchLostDeviceTimeout() { + return matchLostDeviceTimeout; + } + + public long getMatchLostTaskInterval() { + return matchLostTaskInterval; + } + + /** + * Returns whether only legacy advertisements will be returned. + * Legacy advertisements include advertisements as specified + * by the Bluetooth core specification 4.2 and below. + */ + public boolean getLegacy() { + return legacy; + } + + /** + * Returns the physical layer used during a scan. + */ + public int getPhy() { + return phy; + } + + /** + * Returns report delay timestamp based on the device clock. + */ + public long getReportDelayMillis() { + return reportDelayMillis; + } + + private ScanSettings(final int scanMode, final int callbackType, + final long reportDelayMillis, final int matchMode, + final int numOfMatchesPerFilter, final boolean legacy, final int phy, + final boolean hardwareFiltering, final boolean hardwareBatching, + final boolean hardwareCallbackTypes, final long matchTimeout, + final long taskInterval, + final long powerSaveScanInterval, final long powerSaveRestInterval) { + this.scanMode = scanMode; + this.callbackType = callbackType; + this.reportDelayMillis = reportDelayMillis; + this.numOfMatchesPerFilter = numOfMatchesPerFilter; + this.matchMode = matchMode; + this.legacy = legacy; + this.phy = phy; + this.useHardwareFilteringIfSupported = hardwareFiltering; + this.useHardwareBatchingIfSupported = hardwareBatching; + this.useHardwareCallbackTypesIfSupported = hardwareCallbackTypes; + this.matchLostDeviceTimeout = matchTimeout * 1000000L; // convert to nanos + this.matchLostTaskInterval = taskInterval; + this.powerSaveScanInterval = powerSaveScanInterval; + this.powerSaveRestInterval = powerSaveRestInterval; + } + + private ScanSettings(final Parcel in) { + scanMode = in.readInt(); + callbackType = in.readInt(); + reportDelayMillis = in.readLong(); + matchMode = in.readInt(); + numOfMatchesPerFilter = in.readInt(); + legacy = in.readInt() != 0; + phy = in.readInt(); + useHardwareFilteringIfSupported = in.readInt() == 1; + useHardwareBatchingIfSupported = in.readInt() == 1; + powerSaveScanInterval = in.readLong(); + powerSaveRestInterval = in.readLong(); + } + + + public void writeToParcel(final Parcel dest, final int flags) { + + dest.writeInt(scanMode); + dest.writeInt(callbackType); + dest.writeLong(reportDelayMillis); + dest.writeInt(matchMode); + dest.writeInt(numOfMatchesPerFilter); + dest.writeInt(legacy ? 1 : 0); + dest.writeInt(phy); + dest.writeInt(useHardwareFilteringIfSupported ? 1 : 0); + dest.writeInt(useHardwareBatchingIfSupported ? 1 : 0); + dest.writeLong(powerSaveScanInterval); + dest.writeLong(powerSaveRestInterval); + } + + + public int describeContents() { + return 0; + } + + public static final Sequenceable.Producer CREATOR = new Producer() { + public ScanSettings[] newArray(final int size) { + return new ScanSettings[size]; + } + + @Override + public ScanSettings createFromParcel(final Parcel in) { + return new ScanSettings(in); + } + }; + + /** + * Determine if we should do power-saving sleep on pre-Lollipop + */ + public boolean hasPowerSaveMode() { + return powerSaveRestInterval > 0 && powerSaveScanInterval > 0; + } + + public long getPowerSaveRest() { + return powerSaveRestInterval; + } + + public long getPowerSaveScan() { + return powerSaveScanInterval; + } + + @Override + public boolean hasFileDescriptor() { + return false; + } + + @Override + public boolean marshalling(Parcel parcel) { + return false; + } + + @Override + public boolean unmarshalling(Parcel parcel) { + return false; + } + + /** + * Builder for {@link ScanSettings}. + */ + @SuppressWarnings({"UnusedReturnValue", "unused"}) + public static final class Builder { + private int scanMode = SCAN_MODE_LOW_POWER; + private int callbackType = CALLBACK_TYPE_ALL_MATCHES; + private long reportDelayMillis = 0; + private int matchMode = MATCH_MODE_AGGRESSIVE; + private int numOfMatchesPerFilter = MATCH_NUM_MAX_ADVERTISEMENT; + private boolean legacy = true; + private int phy = PHY_LE_ALL_SUPPORTED; + private boolean useHardwareFilteringIfSupported = true; + private boolean useHardwareBatchingIfSupported = true; + private boolean useHardwareCallbackTypesIfSupported = true; + private long matchLostDeviceTimeout = MATCH_LOST_DEVICE_TIMEOUT_DEFAULT; + private long matchLostTaskInterval = MATCH_LOST_TASK_INTERVAL_DEFAULT; + private long powerSaveRestInterval = 0; + private long powerSaveScanInterval = 0; + + /** + * Set scan mode for Bluetooth LE scan. + *

+ * {@link #SCAN_MODE_OPPORTUNISTIC} is supported on Android Marshmallow onwards. + * On Lollipop this mode will fall back {@link #SCAN_MODE_LOW_POWER}, which actually means + * that the library will start its own scan instead of relying on scans from other apps. + * This may have significant impact on battery usage. + *

+ * On pre-Lollipop devices, the settings set by {@link #setPowerSave(long, long)} + * will be used. By default, the intervals are the same as for {@link #SCAN_MODE_LOW_POWER}. + * + * @param scanMode The scan mode can be one of {@link ScanSettings#SCAN_MODE_LOW_POWER}, + * {@link #SCAN_MODE_BALANCED}, + * {@link #SCAN_MODE_LOW_LATENCY} or + * {@link #SCAN_MODE_OPPORTUNISTIC}. + * @throws IllegalArgumentException If the {@code scanMode} is invalid. + */ + + public Builder setScanMode(final int scanMode) { + if (scanMode < SCAN_MODE_OPPORTUNISTIC || scanMode > SCAN_MODE_LOW_LATENCY) { + throw new IllegalArgumentException("invalid scan mode " + scanMode); + } + this.scanMode = scanMode; + return this; + } + + /** + * Set callback type for Bluetooth LE scan. + * + * @param callbackType The callback type flags for the scan. + * @throws IllegalArgumentException If the {@code callbackType} is invalid. + */ + + public Builder setCallbackType(final int callbackType) { + if (!isValidCallbackType(callbackType)) { + throw new IllegalArgumentException("invalid callback type - " + callbackType); + } + this.callbackType = callbackType; + return this; + } + + // Returns true if the callbackType is valid. + private boolean isValidCallbackType(final int callbackType) { + if (callbackType == CALLBACK_TYPE_ALL_MATCHES || + callbackType == CALLBACK_TYPE_FIRST_MATCH || + callbackType == CALLBACK_TYPE_MATCH_LOST) { + return true; + } + return callbackType == (CALLBACK_TYPE_FIRST_MATCH | CALLBACK_TYPE_MATCH_LOST); + } + + /** + * Set report delay timestamp for Bluetooth LE scan. + * + * @param reportDelayMillis Delay of report in milliseconds. Set to 0 to be notified of + * results immediately. Values > 0 causes the scan results + * to be queued up and delivered after the requested delay or + * when the internal buffers fill up.

+ * For delays below 5000 ms (5 sec) the + * {@link ScanCallback#onBatchScanResults(List)} + * will be called in unreliable intervals, but starting from + * around 5000 the intervals get even. + * @throws IllegalArgumentException If {@code reportDelayMillis} < 0. + */ + + public Builder setReportDelay(final long reportDelayMillis) { + if (reportDelayMillis < 0) { + throw new IllegalArgumentException("reportDelay must be > 0"); + } + this.reportDelayMillis = reportDelayMillis; + return this; + } + + /** + * Set the number of matches for Bluetooth LE scan filters hardware match. + * + * @param numOfMatches The num of matches can be one of + * {@link ScanSettings#MATCH_NUM_ONE_ADVERTISEMENT} or + * {@link ScanSettings#MATCH_NUM_FEW_ADVERTISEMENT} or + * {@link ScanSettings#MATCH_NUM_MAX_ADVERTISEMENT} + * @throws IllegalArgumentException If the {@code matchMode} is invalid. + */ + + public Builder setNumOfMatches(final int numOfMatches) { + if (numOfMatches < MATCH_NUM_ONE_ADVERTISEMENT + || numOfMatches > MATCH_NUM_MAX_ADVERTISEMENT) { + throw new IllegalArgumentException("invalid numOfMatches " + numOfMatches); + } + numOfMatchesPerFilter = numOfMatches; + return this; + } + + /** + * Set match mode for Bluetooth LE scan filters hardware match + * + * @param matchMode The match mode can be one of + * {@link ScanSettings#MATCH_MODE_AGGRESSIVE} or + * {@link ScanSettings#MATCH_MODE_STICKY} + * @throws IllegalArgumentException If the {@code matchMode} is invalid. + */ + + public Builder setMatchMode(final int matchMode) { + if (matchMode < MATCH_MODE_AGGRESSIVE + || matchMode > MATCH_MODE_STICKY) { + throw new IllegalArgumentException("invalid matchMode " + matchMode); + } + this.matchMode = matchMode; + return this; + } + + /** + * Set whether only legacy advertisements should be returned in scan results. + * Legacy advertisements include advertisements as specified by the + * Bluetooth core specification 4.2 and below. This is true by default + * for compatibility with older apps. + * + * @param legacy true if only legacy advertisements will be returned + */ + + public Builder setLegacy(final boolean legacy) { + this.legacy = legacy; + return this; + } + + /** + * Set the Physical Layer to use during this scan. + * This is used only if {@link Builder#setLegacy} + * is set to false and only on Android 0reo or newer. + * may be used to check whether LE Coded phy is supported by calling + * Selecting an unsupported phy will result in failure to start scan. + */ + + public Builder setPhy(final int phy) { + this.phy = phy; + return this; + } + + /** + * Several phones may have some issues when it comes to offloaded filtering. + * Even if it should be supported, it may not work as expected. + * It has been observed for example, that setting 2 filters with different devices + * addresses on Nexus 6 with Lollipop gives no callbacks if one or both devices advertise. + * See https://code.google.com/p/android/issues/detail?id=181561. + * + * @param use true to enable (default) hardware offload filtering. + * If false a compat software filtering will be used + * (uses much more resources). + */ + + public Builder setUseHardwareFilteringIfSupported(final boolean use) { + useHardwareFilteringIfSupported = use; + return this; + } + + /** + * Some devices, for example Samsung S6 and S6 Edge with Lollipop, return always + * the same RSSI value for all devices if offloaded batching is used. + * Batching may also be emulated using a compat mechanism - a periodically called timer. + * Timer approach requires more resources but reports devices in constant delays + * and works on devices that does not support offloaded batching. + * In comparison, when setReportDelay(..) is called with parameter 1000 the standard, + * hardware triggered callback will be called every 1500ms +-200ms. + * + * @param use true to enable (default) hardware offloaded batching if they are supported. + * False to always use compat mechanism. + */ + + public Builder setUseHardwareBatchingIfSupported(final boolean use) { + useHardwareBatchingIfSupported = use; + return this; + } + + /** + * This method may be used when callback type is set to a value different than + * {@link #CALLBACK_TYPE_ALL_MATCHES}. When disabled, the Scanner Compat itself will + * take care of reporting first match and match lost. The compat behaviour may differ + * from the one natively supported on Android Marshmallow or newer. + *

+ * Also, in compat mode values set by {@link #setMatchMode(int)} and + * {@link #setNumOfMatches(int)} are ignored. + * Instead use {@link #setMatchOptions(long, long)} to set timer options. + * + * @param use true to enable (default) the offloaded match reporting if hardware supports it, + * false to enable compat implementation. + */ + + public Builder setUseHardwareCallbackTypesIfSupported(final boolean use) { + useHardwareCallbackTypesIfSupported = use; + return this; + } + + /** + * The match options are used when the callback type has been set to + * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} or + * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} and hardware does not support those types. + * In that case {@link BluetoothLeScannerCompat} starts a task that runs periodically + * {@link #CALLBACK_TYPE_MATCH_LOST} if a device has not been seen for at least given time. + * + * @param deviceTimeoutMillis the time required for the device to be recognized as lost + * (default {@link #MATCH_LOST_DEVICE_TIMEOUT_DEFAULT}). + * @param taskIntervalMillis the task interval (default {@link #MATCH_LOST_TASK_INTERVAL_DEFAULT}). + */ + + public Builder setMatchOptions(final long deviceTimeoutMillis, final long taskIntervalMillis) { + if (deviceTimeoutMillis <= 0 || taskIntervalMillis <= 0) { + throw new IllegalArgumentException("maxDeviceAgeMillis and taskIntervalMillis must be > 0"); + } + matchLostDeviceTimeout = deviceTimeoutMillis; + matchLostTaskInterval = taskIntervalMillis; + return this; + } + + /** + * Pre-Lollipop scanning requires a wakelock and the CPU cannot go to sleep. + * To conserve power we can optionally scan for a certain duration (scan interval) + * and then rest for a time before starting scanning again. Won't affect Lollipop + * or later devices. + * + * @param scanInterval interval in ms to scan at a time. + * @param restInterval interval to sleep for without scanning before scanning again for + * scanInterval. + */ + + public Builder setPowerSave(final long scanInterval, final long restInterval) { + if (scanInterval <= 0 || restInterval <= 0) { + throw new IllegalArgumentException("scanInterval and restInterval must be > 0"); + } + powerSaveScanInterval = scanInterval; + powerSaveRestInterval = restInterval; + return this; + } + + /** + * Build {@link ScanSettings}. + */ + + public ScanSettings build() { + if (powerSaveRestInterval == 0 && powerSaveScanInterval == 0) + updatePowerSaveSettings(); + + return new ScanSettings(scanMode, callbackType, reportDelayMillis, matchMode, + numOfMatchesPerFilter, legacy, phy, useHardwareFilteringIfSupported, + useHardwareBatchingIfSupported, useHardwareCallbackTypesIfSupported, + matchLostDeviceTimeout, matchLostTaskInterval, + powerSaveScanInterval, powerSaveRestInterval); + } + + /** + * Sets power save settings based on the scan mode selected. + */ + private void updatePowerSaveSettings() { + switch (scanMode) { + case SCAN_MODE_LOW_LATENCY: + // Disable power save mode + powerSaveScanInterval = 0; + powerSaveRestInterval = 0; + break; + case SCAN_MODE_BALANCED: + // Scan for 2 seconds every 5 seconds + powerSaveScanInterval = 2000; + powerSaveRestInterval = 3000; + break; + case SCAN_MODE_OPPORTUNISTIC: + // It is not possible to emulate OPPORTUNISTIC scanning, but in theory + // that should be even less battery consuming than LOW_POWER. + // For pre-Lollipop devices intervals can be overwritten by + // setPowerSave(long, long) if needed. + // On Android Lollipop the native SCAN_MODE_LOW_POWER will be used instead + // of power save values. + case SCAN_MODE_LOW_POWER: + default: + // Scan for 0.5 second every 5 seconds + powerSaveScanInterval = 500; + powerSaveRestInterval = 4500; + break; + } + } + } } diff --git a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScannerService.java b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScannerService.java index d33034f..c23fc31 100644 --- a/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScannerService.java +++ b/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/ScannerService.java @@ -1,61 +1,76 @@ package no.nordicsemi.android.support.v18.scanner; -import android.Manifest; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Handler; -import android.os.IBinder; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresPermission; + +import java.security.Provider; +import java.util.*; +import java.util.jar.Manifest; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + + +import ohos.aafwk.content.Intent; +import ohos.app.Context; +import ohos.bluetooth.ble.BleScanFilter; +import ohos.event.intentagent.IntentAgent; +import ohos.eventhandler.EventHandler; +import ohos.eventhandler.EventRunner; +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; +import ohos.rpc.IRemoteObject; /** - * A service that will emulate - * {@link android.bluetooth.le.BluetoothLeScanner#startScan(List, android.bluetooth.le.ScanSettings, PendingIntent)} - * on Android versions before Oreo. - *

- * To start the service call - * {@link BluetoothLeScannerCompat#startScan(List, ScanSettings, Context, PendingIntent)}. - * It will be stopped automatically when the last scan has been stopped using - * {@link BluetoothLeScannerCompat#stopScan(Context, PendingIntent)}. + * A service that will emulate* on Android versions before Oreo. *

+ * To start the service call* It will be stopped automatically when the last scan has been stopped using*

* As this service will run and scan in background it is recommended to use * {@link ScanSettings#SCAN_MODE_LOW_POWER} mode and set filter to lower power consumption. */ -public class ScannerService extends Service { - private static final String TAG = "ScannerService"; +public class ScannerService extends Provider.Service { +// private static final String TAG = "ScannerService"; /* package */ static final String EXTRA_PENDING_INTENT = "no.nordicsemi.android.support.v18.EXTRA_PENDING_INTENT"; /* package */ static final String EXTRA_FILTERS = "no.nordicsemi.android.support.v18.EXTRA_FILTERS"; /* package */ static final String EXTRA_SETTINGS = "no.nordicsemi.android.support.v18.EXTRA_SETTINGS"; /* package */ static final String EXTRA_START = "no.nordicsemi.android.support.v18.EXTRA_START"; +// private static final String TAG = "ScannerService"; +// private static final HiLogLabel LABEL_LOG = ; + + private final Object LOCK = new Object(); - @NonNull private final Object LOCK = new Object(); + private HashMap callbacks; + private EventHandler handler; - private HashMap callbacks; - private Handler handler; + public ScannerService(Provider provider, String type, String algorithm, String className, List aliases, Map attributes) { + super(provider, type, algorithm, className, aliases, attributes); + } - @Override + // @Override public void onCreate() { - super.onCreate(); callbacks = new HashMap<>(); - handler = new Handler(); + handler = new EventHandler(EventRunner.create()) { +// @Override + public void publish(LogRecord logRecord) { + + } + +// @Override + public void flush() { + + } + +// @Override + public void close() throws SecurityException { + + } + }; } - @Override - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + // @Override +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) public int onStartCommand(final Intent intent, final int flags, final int startId) { - final PendingIntent callbackIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); - final boolean start = intent.getBooleanExtra(EXTRA_START, false); +// final IntentAgent callbackIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); + final IntentAgent callbackIntent = intent.getSequenceableParam(EXTRA_PENDING_INTENT); + final boolean start = intent.getBooleanParam(EXTRA_START, false); final boolean stop = !start; if (callbackIntent == null) { @@ -63,9 +78,10 @@ public class ScannerService extends Service { synchronized (LOCK) { shouldStop = callbacks.isEmpty(); } + if (shouldStop) - stopSelf(); - return START_NOT_STICKY; + stopScan(callbackIntent); +// return START_NOT_STICKY; } boolean knownCallback; @@ -74,41 +90,41 @@ public class ScannerService extends Service { } if (start && !knownCallback) { - final ArrayList filters = intent.getParcelableArrayListExtra(EXTRA_FILTERS); - final ScanSettings settings = intent.getParcelableExtra(EXTRA_SETTINGS); +// final ArrayList filters = intent.getParcelableArrayListExtra(EXTRA_FILTERS); + final ArrayList filters = intent.getSequenceableArrayListParam(EXTRA_FILTERS); +// final ScanSettings settings = intent.getParcelableExtra(EXTRA_SETTINGS); + final ScanSettings settings = intent.getSequenceableParam(EXTRA_SETTINGS); startScan(filters != null ? filters : Collections.emptyList(), - settings != null ? settings : new ScanSettings.Builder().build(), + settings != null ? settings : new ScanSettings.Builder().build(), callbackIntent); } else if (stop && knownCallback) { stopScan(callbackIntent); } - - return START_NOT_STICKY; + return 2; } - @Nullable - @Override - public IBinder onBind(final Intent intent) { + // @Override + public IRemoteObject onBind(final Intent intent) { // Forbid binding return null; } - - @Override - public void onTaskRemoved(final Intent rootIntent) { - super.onTaskRemoved(rootIntent); - // Stopping self here would cause the service to be killed when user removes the task - // from Recents. This is not the behavior found in Oreo+. - // Related issue: https://github.com/NordicSemiconductor/Android-Scanner-Compat-Library/issues/45 - - // Even with this line removed, the service will stop receiving devices when the phone - // enters Doze mode. - // Find out more here: https://developer.android.com/training/monitoring-device-state/doze-standby - - // stopSelf(); - } - - @Override - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) +// +// @Override +// public void onTaskRemoved(final Intent rootIntent) { +// super.onTaskRemoved(rootIntent); +// // Stopping self here would cause the service to be killed when user removes the task +// // from Recents. This is not the behavior found in Oreo+. +// // Related issue: https://github.com/NordicSemiconductor/Android-Scanner-Compat-Library/issues/45 +// +// // Even with this line removed, the service will stop receiving devices when the phone +// // enters Doze mode. +// // Find out more here: https://developer.android.com/training/monitoring-device-state/doze-standby +// +// // stopSelf(); +// } + + // @Override +// @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) public void onDestroy() { final BluetoothLeScannerCompat scannerCompat = BluetoothLeScannerCompat.getScanner(); for (final ScanCallback callback : callbacks.values()) { @@ -121,13 +137,13 @@ public class ScannerService extends Service { callbacks.clear(); callbacks = null; handler = null; - super.onDestroy(); + } - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - private void startScan(@NonNull final List filters, - @NonNull final ScanSettings settings, - @NonNull final PendingIntent callbackIntent) { + // @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + private void startScan(final List filters, + final ScanSettings settings, + final IntentAgent callbackIntent) { final PendingIntentExecutor executor = new PendingIntentExecutor(callbackIntent, settings, this); synchronized (LOCK) { @@ -136,14 +152,16 @@ public class ScannerService extends Service { try { final BluetoothLeScannerCompat scannerCompat = BluetoothLeScannerCompat.getScanner(); - scannerCompat.startScanInternal(filters, settings, executor, handler); +// scannerCompat.startScanInternal(filters, settings, executor, handler); + scannerCompat.startScanInternal(filters,settings,executor,handler); } catch (final Exception e) { - Log.w(TAG, "Starting scanning failed", e); +// Log.w(TAG, "Starting scanning failed", e); +// HiLog.warn(LABEL_LOG, "Starting scanning failed", e); } } - @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) - private void stopScan(@NonNull final PendingIntent callbackIntent) { + // @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH}) + private void stopScan(final IntentAgent callbackIntent) { ScanCallback callback; boolean shouldStop; synchronized (LOCK) { @@ -157,10 +175,11 @@ public class ScannerService extends Service { final BluetoothLeScannerCompat scannerCompat = BluetoothLeScannerCompat.getScanner(); scannerCompat.stopScan(callback); } catch (final Exception e) { - Log.w(TAG, "Stopping scanning failed", e); +// Log.w(TAG, "Stopping scanning failed", e); +// HiLog.warn(LABEL_LOG, "Starting scanning failed", e); } - if (shouldStop) - stopSelf(); +// stopSelf(); + stopScan(callbackIntent); } } diff --git a/settings.gradle b/settings.gradle index ad5fbba..fa0fffb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':scanner' +include ':entry', ':scanner' -- Gitee From 79c52819593aa19213ed8cd34f818c93ce42e7af Mon Sep 17 00:00:00 2001 From: gu_tian_le Date: Sat, 8 May 2021 17:37:49 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E3=80=91#AR000000=EF=BC=9Axml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/$PROJECT_FILE$ | 11 ++ .idea/.gitignore | 3 + .idea/checkstyle-idea.xml | 16 ++ .idea/code-check/java/codemars.log | 30 +++ .idea/code-check/java/detect.txt | 1 + .idea/code-check/java/output.xml | 9 + .idea/code-check/java/ruleclasspath.txt | 1 + .idea/compiler.xml | 6 + .idea/gradle.xml | 23 +++ .idea/jarRepositories.xml | 30 +++ .idea/markdown-navigator-enh.xml | 10 + .idea/markdown-navigator.xml | 62 ++++++ .idea/misc.xml | 28 +++ .idea/qaplug_profiles.xml | 183 ++++++++++++++++++ .idea/vcs.xml | 6 + entry/.gitignore | 1 + entry/build.gradle | 25 +++ entry/src/main/config.json | 57 ++++++ .../example/myapplication/MainAbility.java | 13 ++ .../example/myapplication/MyApplication.java | 10 + .../myapplication/slice/MainAbilitySlice.java | 100 ++++++++++ .../main/resources/base/element/string.json | 16 ++ .../base/graphic/background_ability_main.xml | 6 + .../resources/base/layout/ability_main.xml | 18 ++ entry/src/main/resources/base/media/icon.png | Bin 0 -> 6790 bytes .../example/myapplication/ExampleTest.java | 9 + scanner/src/main/config.json | 27 +++ .../v18/scanner/BluetoothScannerImplV5.java | 137 +++++++++++++ .../main/resources/base/element/string.json | 8 + 29 files changed, 846 insertions(+) create mode 100644 .idea/$PROJECT_FILE$ create mode 100644 .idea/.gitignore create mode 100644 .idea/checkstyle-idea.xml create mode 100644 .idea/code-check/java/codemars.log create mode 100644 .idea/code-check/java/detect.txt create mode 100644 .idea/code-check/java/output.xml create mode 100644 .idea/code-check/java/ruleclasspath.txt create mode 100644 .idea/compiler.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/markdown-navigator-enh.xml create mode 100644 .idea/markdown-navigator.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/qaplug_profiles.xml create mode 100644 .idea/vcs.xml create mode 100644 entry/.gitignore create mode 100644 entry/build.gradle create mode 100644 entry/src/main/config.json create mode 100644 entry/src/main/java/com/example/myapplication/MainAbility.java create mode 100644 entry/src/main/java/com/example/myapplication/MyApplication.java create mode 100644 entry/src/main/java/com/example/myapplication/slice/MainAbilitySlice.java create mode 100644 entry/src/main/resources/base/element/string.json create mode 100644 entry/src/main/resources/base/graphic/background_ability_main.xml create mode 100644 entry/src/main/resources/base/layout/ability_main.xml create mode 100644 entry/src/main/resources/base/media/icon.png create mode 100644 entry/src/test/java/com/example/myapplication/ExampleTest.java create mode 100644 scanner/src/main/config.json create mode 100644 scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothScannerImplV5.java create mode 100644 scanner/src/main/resources/base/element/string.json diff --git a/.idea/$PROJECT_FILE$ b/.idea/$PROJECT_FILE$ new file mode 100644 index 0000000..58b7e3e --- /dev/null +++ b/.idea/$PROJECT_FILE$ @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..6eefe0e --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/.idea/code-check/java/codemars.log b/.idea/code-check/java/codemars.log new file mode 100644 index 0000000..d9ff793 --- /dev/null +++ b/.idea/code-check/java/codemars.log @@ -0,0 +1,30 @@ +2021-04-28 14:30:56.015 [main] INFO . - user input: D:\soft\deveco\DevEco Studio 2.1.0.301\tools\openjdk\bin\java,-j,-source,@D:/project3-17/Android-Scanner-Compat-Library-master/ohos-Scanner-Compat-Library-master/.idea/code-check/java/detect.txt,-output,D:/project3-17/Android-Scanner-Compat-Library-master/ohos-Scanner-Compat-Library-master/.idea/code-check/java/output.xml +2021-04-28 14:30:56.017 [main] INFO . - CodeMars Version:2.1.2.sp4 +2021-04-28 14:30:56.067 [main] INFO . - starting analyzing. +2021-04-28 14:30:56.076 [main] INFO . - start collecting report. +2021-04-28 14:30:56.079 [CodeMars1] INFO . - Command: "D:\soft\deveco\DevEco Studio 2.1.0.301\plugins\codecheck\lib\CodeMars\engines\SecFinder-J\bin\run_SecFinder-J.bat",-filelist,D:\project3-17\Android-Scanner-Compat-Library-master\ohos-Scanner-Compat-Library-master\.idea\code-check\java\filelist_2021_04_28_14_30_56_077_74.txt,-f,xml,-default,-progress,-r,D:\project3-17\Android-Scanner-Compat-Library-master\ohos-Scanner-Compat-Library-master\.idea\code-check\java\\errorreport_2021_04_28_14_30_55_967_31.xml,-ruleclasspath,file:///D:\project3-17\Android-Scanner-Compat-Library-master\ohos-Scanner-Compat-Library-master\.idea\code-check\java\ruleclasspath.txt +2021-04-28 14:30:56.216 [Thread-2] INFO . - ËÄÔ 28, 2021 2:30:56 ÏÂÎç com.huawei.secfinderj.SecFinderJ needScan +2021-04-28 14:30:56.216 [Thread-2] INFO . - ÐÅÏ¢: SecFinder-J Version: 2.1.3 +2021-04-28 14:30:56.293 [Thread-1] INFO . - 2021-04-28 14:30:56.254: SecFinder-J Output: Inspect start... +2021-04-28 14:30:56.360 [Thread-1] INFO . - 2021-04-28 14:30:56.359: SecFinder-J Output: Load checkers... +2021-04-28 14:30:56.502 [Thread-1] INFO . - 2021-04-28 14:30:56.502: SecFinder-J Output: Load config... +2021-04-28 14:30:56.528 [Thread-1] INFO . - 2021-04-28 14:30:56.528: SecFinder-J Output: step 1/4: Find files +2021-04-28 14:30:56.552 [Thread-1] INFO . - 2021-04-28 14:30:56.552: SecFinder-J Output: step 2/4: Process files +2021-04-28 14:30:56.589 [Thread-1] INFO . - 2021-04-28 14:30:56.589: SecFinder-J Output: step 3/4: Run analysis... +2021-04-28 14:30:56.590 [Thread-1] INFO . - 2021-04-28 14:30:56.590: SecFinder-J Output: [SecFinder-J--Thread--1] - during processing of [PendingIntentExecutor.java] +2021-04-28 14:30:56.985 [Thread-1] INFO . - 2021-04-28 14:30:56.985: SecFinder-J Output: step 4/4: Result output... +2021-04-28 14:30:56.985 [Thread-1] INFO . - 2021-04-28 14:30:56.985: SecFinder-J Output: Inspect finish... +2021-04-28 14:30:56.985 [Thread-1] INFO . - Analysis result: +2021-04-28 14:30:56.985 [Thread-1] INFO . - files analyzed : 1 +2021-04-28 14:30:56.985 [Thread-1] INFO . - lines analyzed : 128 +2021-04-28 14:30:56.985 [Thread-1] INFO . - rules used : 59 +2021-04-28 14:30:56.985 [Thread-1] INFO . - issues detected : 0 +2021-04-28 14:30:56.985 [Thread-1] INFO . - time cost(sec) : 0 +2021-04-28 14:30:56.985 [Thread-1] INFO . - +2021-04-28 14:30:56.985 [Thread-2] INFO . - ËÄÔ 28, 2021 2:30:56 ÏÂÎç com.huawei.secfinderj.override.HwPmd end +2021-04-28 14:30:56.985 [Thread-2] INFO . - ÐÅÏ¢: SecFinder-J run successed! +2021-04-28 14:30:57.023 [CodeMars1] INFO . - start parse errorreport xml +2021-04-28 14:30:57.024 [CodeMars1] INFO . - parse xml time : 4 +2021-04-28 14:30:57.024 [CodeMars1] INFO . - end parse errorreport xml +2021-04-28 14:30:57.025 [main] INFO . - end collecting report. +2021-04-28 14:30:57.025 [main] INFO . - end analyzing. diff --git a/.idea/code-check/java/detect.txt b/.idea/code-check/java/detect.txt new file mode 100644 index 0000000..d040002 --- /dev/null +++ b/.idea/code-check/java/detect.txt @@ -0,0 +1 @@ +D:/project3-17/Android-Scanner-Compat-Library-master/ohos-Scanner-Compat-Library-master/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentExecutor.java diff --git a/.idea/code-check/java/output.xml b/.idea/code-check/java/output.xml new file mode 100644 index 0000000..7f044eb --- /dev/null +++ b/.idea/code-check/java/output.xml @@ -0,0 +1,9 @@ + + + + +1 +128 +0 +0 + diff --git a/.idea/code-check/java/ruleclasspath.txt b/.idea/code-check/java/ruleclasspath.txt new file mode 100644 index 0000000..02ab439 --- /dev/null +++ b/.idea/code-check/java/ruleclasspath.txt @@ -0,0 +1 @@ +D:\soft\deveco\DevEco Studio 2.1.0.301\plugins\codecheck\lib\CodeMars\engines\SecFinder-J\rule\ diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..d964c31 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..ba2e744 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator-enh.xml b/.idea/markdown-navigator-enh.xml new file mode 100644 index 0000000..a8fcc84 --- /dev/null +++ b/.idea/markdown-navigator-enh.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml new file mode 100644 index 0000000..a2fc086 --- /dev/null +++ b/.idea/markdown-navigator.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f07eb59 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/qaplug_profiles.xml b/.idea/qaplug_profiles.xml new file mode 100644 index 0000000..6c583df --- /dev/null +++ b/.idea/qaplug_profiles.xml @@ -0,0 +1,183 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/entry/.gitignore b/entry/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/entry/.gitignore @@ -0,0 +1 @@ +/build diff --git a/entry/build.gradle b/entry/build.gradle new file mode 100644 index 0000000..e89df81 --- /dev/null +++ b/entry/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.huawei.ohos.hap' +ohos { + signingConfigs { + debug { + storeFile file('D:\\ohos\\trunk\\keyStore\\BGABanner\\root_1.p12') + storePassword '0000002037A1D11B07FA5BFB33CF64DAB632CD228E6570F7C36EE345D67936E5CE7A745A48E0FAA9E488D8C6BC105E44' + keyAlias = 'hos_platform_os' + keyPassword '00000020642523EDD154D8B9437AB57FB400F49176C068D2E13E547F21D70153995483EE68FCF3B78C0301241AB629F1' + signAlg = 'SHA256withECDSA' + profile file('D:\\ohos\\trunk\\ç­¾åæ–‡ä»¶\\BGABannerDebug.p7b') + certpath file('D:\\ohos\\trunk\\keyStore\\BGABanner\\root.cer') + } + } + compileSdkVersion 5 + defaultConfig { + compatibleSdkVersion 5 + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.har']) + testCompile 'junit:junit:4.12' + implementation project(':scanner') +} diff --git a/entry/src/main/config.json b/entry/src/main/config.json new file mode 100644 index 0000000..7c78bf8 --- /dev/null +++ b/entry/src/main/config.json @@ -0,0 +1,57 @@ +{ + "app": { + "bundleName": "cn.bingoogolapple.bgabanner.demo", + "vendor": "example", + "version": { + "code": 1, + "name": "1.0" + }, + "apiVersion": { + "compatible": 5, + "target": 5, + "releaseType": "Release" + } + }, + "deviceConfig": {}, + "module": { + "package": "com.example.myapplication", + "name": ".MyApplication", + "deviceType": [ + "phone" + ], + "distro": { + "deliveryWithInstall": true, + "moduleName": "entry", + "moduleType": "entry" + }, + "abilities": [ + { + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ], + "orientation": "unspecified", + "name": "com.example.myapplication.MainAbility", + "icon": "$media:icon", + "description": "$string:mainability_description", + "label": "MyApplication", + "type": "page", + "launchType": "standard" + } + ], + "reqPermissions": [ + { + "name": "ohos.permission.USE_BLUETOOTH" + }, + { + "name": "ohos.permission.DISCOVER_BLUETOOTH" + } + ] + } +} \ No newline at end of file diff --git a/entry/src/main/java/com/example/myapplication/MainAbility.java b/entry/src/main/java/com/example/myapplication/MainAbility.java new file mode 100644 index 0000000..813ed4e --- /dev/null +++ b/entry/src/main/java/com/example/myapplication/MainAbility.java @@ -0,0 +1,13 @@ +package com.example.myapplication; + +import com.example.myapplication.slice.MainAbilitySlice; +import ohos.aafwk.ability.Ability; +import ohos.aafwk.content.Intent; + +public class MainAbility extends Ability { + @Override + public void onStart(Intent intent) { + super.onStart(intent); + super.setMainRoute(MainAbilitySlice.class.getName()); + } +} diff --git a/entry/src/main/java/com/example/myapplication/MyApplication.java b/entry/src/main/java/com/example/myapplication/MyApplication.java new file mode 100644 index 0000000..33915e2 --- /dev/null +++ b/entry/src/main/java/com/example/myapplication/MyApplication.java @@ -0,0 +1,10 @@ +package com.example.myapplication; + +import ohos.aafwk.ability.AbilityPackage; + +public class MyApplication extends AbilityPackage { + @Override + public void onInitialize() { + super.onInitialize(); + } +} diff --git a/entry/src/main/java/com/example/myapplication/slice/MainAbilitySlice.java b/entry/src/main/java/com/example/myapplication/slice/MainAbilitySlice.java new file mode 100644 index 0000000..e1a2f73 --- /dev/null +++ b/entry/src/main/java/com/example/myapplication/slice/MainAbilitySlice.java @@ -0,0 +1,100 @@ +package com.example.myapplication.slice; + +import com.example.myapplication.ResourceTable; +import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat; +import no.nordicsemi.android.support.v18.scanner.ScanCallback; +import no.nordicsemi.android.support.v18.scanner.ScanFilter; +import no.nordicsemi.android.support.v18.scanner.ScanSettings; +import ohos.aafwk.ability.AbilitySlice; +import ohos.aafwk.content.Intent; +import ohos.bluetooth.ble.BleScanResult; +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; +import ohos.utils.SequenceUuid; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class MainAbilitySlice extends AbilitySlice { + + private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00201, "MY_TAG"); + //设置è“牙扫æè¿‡æ»¤å™¨é›†åˆ + private List scanFilterList; + //设置è“牙扫æè¿‡æ»¤å™¨ + private ScanFilter.Builder scanFilterBuilder; + //设置è“牙扫æè®¾ç½® + private ScanSettings.Builder scanSettingBuilder; + @Override + public void onStart(Intent intent) { + super.onStart(intent); + super.setUIContent(ResourceTable.Layout_ability_main); + onView(); + onData(); + } + + private void onData() { + HiLog.info(LABEL, ""); + } + + private void onView() { + BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); + ScanSettings settings = new ScanSettings.Builder() + .setLegacy(false) + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + .setReportDelay(10000) + .setUseHardwareBatchingIfSupported(false) + .build(); + List filters = new ArrayList<>(); + UUID uuid = UUID.randomUUID(); + HiLog.info(LABEL, "uuid"+uuid.toString()); + filters.add(new ScanFilter.Builder().setServiceUuid(new SequenceUuid(UUID.fromString(uuid.toString()))).build()); + scanner.startScan(null, buildScanSettings(), new ScanCallback() { + @Override + public void onScanResult(int callbackType, BleScanResult result) { + super.onScanResult(callbackType, result); + HiLog.info(LABEL, "ScanResult22"+result.getPeripheralDevice()); + } + + @Override + public void onBatchScanResults(List results) { + super.onBatchScanResults(results); + HiLog.info(LABEL, "onBatchScanResults"+results.size()); + } + + @Override + public void onScanFailed(int errorCode) { + super.onScanFailed(errorCode); + HiLog.info(LABEL, "onScanFailed"); + } + }); + } + + @Override + public void onActive() { + super.onActive(); + } + + @Override + public void onForeground(Intent intent) { + super.onForeground(intent); + } + + private ScanSettings buildScanSettings() { + scanSettingBuilder = new ScanSettings.Builder(); + scanSettingBuilder.setUseHardwareBatchingIfSupported(false); + //设置è“牙LE扫æçš„æ‰«ææ¨¡å¼ã€‚ + //使用最高å ç©ºæ¯”进行扫æã€‚建议åªåœ¨åº”用程åºå¤„äºŽæ­¤æ¨¡å¼æ—¶ä½¿ç”¨æ­¤æ¨¡å¼åœ¨å‰å°è¿è¡Œ + scanSettingBuilder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY); + //设置è“牙LEæ‰«ææ»¤æ³¢å™¨ç¡¬ä»¶åŒ¹é…çš„åŒ¹é…æ¨¡å¼ + //在主动模å¼ä¸‹ï¼Œå³ä½¿ä¿¡å·å¼ºåº¦è¾ƒå¼±ï¼Œhw也会更快地确定匹é….在一段时间内很少有目击/匹é…。 + scanSettingBuilder.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE); + //设置è“牙LE扫æçš„回调类型 + //为æ¯ä¸€ä¸ªåŒ¹é…过滤æ¡ä»¶çš„è“牙广告触å‘一个回调。如果没有过滤器是活动的,所有的广告包被报告 + scanSettingBuilder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES); + scanSettingBuilder.setReportDelay(10000); + scanSettingBuilder.setUseHardwareBatchingIfSupported(false); + return scanSettingBuilder.build(); + } + +} diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000..6becfe4 --- /dev/null +++ b/entry/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "app_name", + "value": "MyApplication" + }, + { + "name": "mainability_description", + "value": "Java_Phone_Empty Feature Ability" + }, + { + "name": "HelloWorld", + "value": "Hello World" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/graphic/background_ability_main.xml b/entry/src/main/resources/base/graphic/background_ability_main.xml new file mode 100644 index 0000000..c0c0a3d --- /dev/null +++ b/entry/src/main/resources/base/graphic/background_ability_main.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/entry/src/main/resources/base/layout/ability_main.xml b/entry/src/main/resources/base/layout/ability_main.xml new file mode 100644 index 0000000..c99c277 --- /dev/null +++ b/entry/src/main/resources/base/layout/ability_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/icon.png b/entry/src/main/resources/base/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce307a8827bd75456441ceb57d530e4c8d45d36c GIT binary patch literal 6790 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 wrappers = new HashMap<>(); + private long powerSaveRestInterval; + private long powerSaveScanInterval; + @Nullable + private EventHandler handlerThread; + @Nullable + private Handler powerSaveHandler; + + @Override + protected void startScanInternal(List scanFilters, ScanSettings scanSettings, ScanCallback callback, EventHandler handler) { + + boolean shouldStart; + + synchronized (wrappers) { + if (wrappers.containsKey(callback)) { + throw new IllegalArgumentException("scanner already started with given scanCallback"); + } + final ScanCallbackWrapper wrapper = new ScanCallbackWrapper( + false, false, + scanFilters, scanSettings, callback, handler); + shouldStart = wrappers.isEmpty(); + wrappers.put(callback, wrapper); + } + +// if (handlerThread == null) { +// handlerThread = new EventHandler(BluetoothScannerImplV5.class.getName()); +// handlerThread.start(); +// powerSaveHandler = new Handler(handlerThread.getLooper()); +// } + + setPowerSaveSettings(); + } + + @Override + void stopScanInternal(ScanCallback callback) { + boolean shouldStop; + ScanCallbackWrapper wrapper; + synchronized (wrappers) { + wrapper = wrappers.remove(callback); + shouldStop = wrappers.isEmpty(); + } + if (wrapper == null) + return; + + wrapper.close(); + + setPowerSaveSettings(); + + if (shouldStop) { + + + if (powerSaveHandler != null) { +// powerSaveHandler.removeCallbacksAndMessages(null); + } + + if (handlerThread != null) { +// handlerThread.quitSafely(); + handlerThread = null; + } + } + } + + @Override + void startScanInternal(List filters, ScanSettings settings, Context context, IntentAgent callbackIntent) { +// final Intent service = new Intent(context, ScannerService.class); +// service.putParcelableArrayListExtra(ScannerService.EXTRA_FILTERS, new ArrayList<>(filters)); +// service.putExtra(ScannerService.EXTRA_SETTINGS, settings); +// service.putExtra(ScannerService.EXTRA_PENDING_INTENT, callbackIntent); +// service.putExtra(ScannerService.EXTRA_START, true); +// context.startService(service); + } + + @Override + void stopScanInternal(Context context, IntentAgent callbackIntent) { + final Intent service = new Intent(); +// service.putExtra(ScannerService.EXTRA_PENDING_INTENT, callbackIntent); +// service.putExtra(ScannerService.EXTRA_START, false); +// context.startService(service); + } + + @Override + public void flushPendingScanResults(ScanCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null!"); + } + + ScanCallbackWrapper wrapper; + synchronized (wrappers) { + wrapper = wrappers.get(callback); + } + + if (wrapper == null) { + throw new IllegalArgumentException("callback not registered!"); + } + + wrapper.flushPendingScanResults(); + } + + private void setPowerSaveSettings() { + long minRest = Long.MAX_VALUE, minScan = Long.MAX_VALUE; + synchronized (wrappers) { + for (final ScanCallbackWrapper wrapper : wrappers.values()) { + final ScanSettings settings = wrapper.scanSettings; + if (settings.hasPowerSaveMode()) { + if (minRest > settings.getPowerSaveRest()) { + minRest = settings.getPowerSaveRest(); + } + if (minScan > settings.getPowerSaveScan()) { + minScan = settings.getPowerSaveScan(); + } + } + } + } + if (minRest < Long.MAX_VALUE && minScan < Long.MAX_VALUE) { + powerSaveRestInterval = minRest; + powerSaveScanInterval = minScan; + } else { + powerSaveRestInterval = powerSaveScanInterval = 0; + } + } + +} diff --git a/scanner/src/main/resources/base/element/string.json b/scanner/src/main/resources/base/element/string.json new file mode 100644 index 0000000..e7b2790 --- /dev/null +++ b/scanner/src/main/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "scanner" + } + ] +} -- Gitee From 387c9af6fb3d30040cb08d9a61f8621ee6875d17 Mon Sep 17 00:00:00 2001 From: gu_tian_le Date: Sat, 8 May 2021 17:38:40 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E3=80=91#AR000000=EF=BC=9Axml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 210 ++++++++++++------------------------------------------ 1 file changed, 45 insertions(+), 165 deletions(-) diff --git a/README.md b/README.md index ef3839a..d55af9c 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,50 @@ -# Android BLE Scanner Compat library +# Scanner-Compat-Library -[ ![Download](https://api.bintray.com/packages/nordic/android/no.nordicsemi.android.support.v18%3Ascanner/images/download.svg) ](https://bintray.com/nordic/android/no.nordicsemi.android.support.v18%3Ascanner/_latestVersion) +**本项目基于开æºé¡¹ç›®android-Scanner-Compat-Library 进行鸿蒙化的移æ¤å’Œå¼€å‘,å¯ä»¥é€šè¿‡é¡¹ç›®æ ‡ç­¾ä»¥åŠgithub地å€ï¼ˆï¼‰è¿½è¸ªåˆ°åŽŸå®‰å“项目版本** -The Scanner Compat library solves the problem with scanning for Bluetooth Low Energy devices on Android. -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. -This library allows to use modern API even on older phones, emulating not supported features. If a feature -(for example offloaded filtering or batching) is not available natively, it will be emulated by -the compat library. Also, native filtering, batching and reporting first match or match lost may -be disabled if you find them not working on some devices. Advertising Extension (`ScanSetting#setLegacy(boolean)` -or `setPhy(int)`) is available only on Android Oreo or newer and such calls will be ignored on -older platforms where only legacy advertising packets on PHY LE 1M will be reported, -due to the Bluetooth chipset capabilities. +#### é¡¹ç›®ä»‹ç» +- 项目å称:è“牙库 +- 所属系列:鸿蒙的第三方组件适é…ç§»æ¤ +- 功能:关于è“牙的æ“作 +- 项目移æ¤çжæ€ï¼šä¸»åŠŸèƒ½å®Œæˆ +- 调用差异:无 +- å¼€å‘版本:sdk5,DevEco Studio2.1 beta3 +- 项目作者和维护人:赵旭东 +- è”系方å¼ï¼šzhaoxudong073@chinasoftinc.com +- 原项目Doc地å€ï¼š -### Background scanning -`SCAN_MODE_LOW_POWER` or `SCAN_MODE_OPPORTUNISTIC` should be used when scanning in background. -Note, that newer Android versions will enforce using low power mode in background, even if another one has been set. -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)) -on pre-Oreo devices by starting a background service that will scan with requested scan mode. -This is much less battery friendly than when the original method is used, but works and saves -a lot of development time if such feature should be implemented anyway. Please read below -for more details. +#### 效果演示 +![效果演示](https://gitee.com/chinasoft_ohos/upload/raw/master/img/demo.gif "效果演示.gif") -## Usage - -The compat library may be found on jcenter repository. Add it to your project by adding the -following dependency: - -```Groovy -implementation 'no.nordicsemi.android.support.v18:scanner:1.4.2' +#### 安装教程 +1.在项目根目录下的build.gradle文件中添加 ``` - -Projects not migrated to Android Jetpack should use version 1.3.1, which is feature-equal to 1.4.0. - -## API - -The Scanner Compat API is very similar to the original one, known from Android Oreo. - -Instead of getting it from the **BluetoothAdapter**, acquire the scanner instance using: - -```java -BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); +allprojects { + repositories { + maven { + url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' + } + } +} ``` - -You also need to change the packets for **ScanSettings**, **ScanFilter** and **ScanCallback** -classes to: - -```java -no.nordicsemi.android.support.v18.scanner +2.在entry模å—下的build.gradle文件中添加ä¾èµ–。 +``` +dependencies { + implementation('com.gitee.chinasoft_ohos:Scanner-Compat-Library:0.0.1-SNAPSHOT') + ...... +} ``` +在sdk4,DevEco Studio2.1 beta2下项目å¯ç›´æŽ¥è¿è¡Œ +如无法è¿è¡Œï¼Œåˆ é™¤é¡¹ç›®.gradle,.idea,build,gradle,build.gradle文件, +并便®è‡ªå·±çš„版本创建新项目,将新项目的对应文件å¤åˆ¶åˆ°æ ¹ç›®å½•下 -## Sample +#### 使用说明 -To start scanning use (example): +使用该库éžå¸¸ç®€å•,åªéœ€æŸ¥çœ‹æä¾›çš„示例的æºä»£ç ã€‚ ```java - BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); + BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); ScanSettings settings = new ScanSettings.Builder() .setLegacy(false) .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) @@ -67,133 +56,24 @@ To start scanning use (example): scanner.startScan(filters, settings, scanCallback); ``` -to stop scanning use: -```java - BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); - scanner.stopScan(scanCallback); -``` +#### æµ‹è¯•ä¿¡æ¯ -### Scanning modes - -There are 4 scanning modes available in native [ScanSettings](https://developer.android.com/reference/android/bluetooth/le/ScanSettings). -3 of them are available since Android Lollipop while the opportunistic scan mode has been added in Marshmallow. -This library tries to emulate them on platforms where they are not supported natively. -1. [SCAN_MODE_LOW_POWER](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_LOW_POWER) - -Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes the least power. -The scanner will scan for 0.5 second and rest for 4.5 seconds. A Bluetooth LE device should advertise -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 -advertising events. This mode may be enforced if the scanning application is not in foreground. -2. [SCAN_MODE_BALANCED](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_BALANCED) - -Perform Bluetooth LE scan in balanced power mode. Scan results are returned at a rate that provides a -good trade-off between scan frequency and power consumption. The scanner will scan for 2 seconds followed -by 3 seconds of idle. -3. [SCAN_MODE_LOW_LATENCY](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_LOW_LATENCY) - -Scan using highest duty cycle. It's recommended to only use this mode when the application is running in the foreground. -4. [SCAN_MODE_OPPORTUNISTIC](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_OPPORTUNISTIC) - -A special Bluetooth LE scan mode. Applications using this scan mode will passively listen for other scan results -without starting BLE scans themselves. - -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 -and rests in between. To set scanning and rest intervals use `Builder#setPowerSave(long,long)`. - -Opportunistic scanning is not possible to emulate and will fallback to `SCAN_MODE_LOW_POWER` on Lollipop and -power save settings on pre-Lollipop devices. That means that this library actually will initiate scanning -on its own. This may have impact on battery consumption and should be used with care. - -### Scan filters and batching - -Offloaded filtering is available on Lollipop or newer devices where -[BluetoothAdapter#isOffloadedFilteringSupported()](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#isOffloadedFilteringSupported()) -returns *true* (when Bluetooth is enabled). If it is not supported, this library will scan without a filter and -apply the filter to the results. If you find offloaded filtering unreliable you may force using compat filtering by calling -`Builder#useHardwareFilteringIfSupported(false)`. Keep in mind that, newer Android versions may prohibit -background scanning without native filters to save battery, so this method should be used with care. - -Android Scanner Compat Library may also emulate batching. To enable scan batching call `Builder#setScanDelay(interval)` -with an interval greater than 0. For intervals less 5 seconds the actual interval may vary. -If you want to get results in lower intervals, call `Builder#useHardwareBatchingIfSupported(false)`, which will -start a normal scan and report results in given interval. Emulated batching uses significantly more battery -than offloaded as it wakes CPU with every device found. - -### Scanning with Pending Intent - -Android 8.0 Oreo introduced [Background Execution Limits](https://developer.android.com/about/versions/oreo/background) -which made background running services short-lived. At the same time, to make background scanning possible, a new -[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)) -was added to [BluetoothLeScanner](https://developer.android.com/reference/android/bluetooth/le/BluetoothLeScanner.html) -which allows registering a [PendingIntent](https://developer.android.com/reference/android/app/PendingIntent) -that will be sent whenever a device matching filter criteria is found. This will also work after -your application has been killed (the receiver must be added in *AndroidManifest* and the -`PendingIntent` must be created with an explicit Intent). - -Starting from version 1.3.0, this library may emulate such feature on older Android versions. -In order to do that, a background service will be started after calling -`scanner.startScan(filters, settings, context, pendingIntent)`, which will be scanning in -background with given settings and will send the given `PendingIntent` when a device -matching filter is found. To lower battery consumption it is recommended to set -`ScanSettings.SCAN_MODE_LOW_POWER` scanning mode and use filter, but even with those conditions fulfilled -**the battery consumption will be significantly higher than on Oreo+**. To stop scanning call -`scanner.stopScan(context, pendingIntent)` with -[the same](https://developer.android.com/reference/android/app/PendingIntent) intent in parameter. -The service will be stopped when the last scan was stopped. - -On Android Oreo or newer this library will use the native scanning mechanism. However, as it may also -emulate batching or apply filtering (when `useHardwareBatchingIfSupported` or `useHardwareFilteringIfSupported` -were called with parameter *false*) the library will register its own broadcast -receiver that will translate results from native to compat classes. - -The receiver and service will be added automatically to the manifest even if they are not used by -the application. No changes are required to make it work. - -To use this feature: +CodeCheckä»£ç æµ‹è¯•无异常 -```java - Intent intent = new Intent(context, MyReceiver.class); // explicite intent - intent.setAction("com.example.ACTION_FOUND"); - intent.putExtra("some.extra", value); // optional - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, id, intent, PendingIntent.FLAG_UPDATE_CURRENT); - - BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); - ScanSettings settings = new ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) - .setReportDelay(10000) - .build(); - List filters = new ArrayList<>(); - filters.add(new ScanFilter.Builder().setServiceUuid(mUuid).build()); - scanner.startScan(filters, settings, context, pendingIntent); -``` - -Add your `MyRecever` to *AndroidManifest*, as the application context might have been released -and all broadcast receivers registered to it together with it. +CloudTestä»£ç æµ‹è¯•无异常 -To stop scanning call: +ç«ç»’安全病毒安全检测通过 -```java - // To stop scanning use the same or an equal PendingIntent (check PendingIntent documentation) - Intent intent = new Intent(context, MyReceiver.class); - intent.setAction("com.example.ACTION_FOUND"); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, id, intent, PendingIntent.FLAG_CANCEL_CURRENT); - - BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); - scanner.stopScan(context, pendingIntent); -``` +当å‰ç‰ˆæœ¬demo功能与安å“原组件基本无差异 -**Note:** Android versions 6 and 7 will not report any advertising packets when in Doze mode. -Read more about it here: https://developer.android.com/training/monitoring-device-state/doze-standby +测试员:余丽飞 -## Background scanning guidelines +#### 版本迭代 -To save power it is recommended to use as low power settings as possible and and use filters. -However, the more battery friendly settings are used, the longest time to finding a device. -In general, scanning with `PendingIntent` and `SCAN_MODE_LOW_POWER` or `SCAN_MODE_OPPORTUNISTIC` -should be used, together with report delay set and filters used. -`useHardwareFilteringIfSupported` and `useHardwareBatchingIfSupported` should be set to *true* (default). +- v0.0.1_SNAPSHOT -Background scanning on Android 4.3 and 4.4.x will use a lot of power, as all those properties -will have to be emulated. It is recommended to scan in background only on Lollipop or newer, or -even Oreo or newer devices and giving the user an option to disable this feature. +#### 版æƒå’Œè®¸å¯ä¿¡æ¯ -## License + The Scanner Compat library is available under BSD 3-Clause license. See the LICENSE file for more info. -The Scanner Compat library is available under BSD 3-Clause license. See the LICENSE file for more info. \ No newline at end of file -- Gitee From 7b3fe9ad55816a0bab128bd56d1ec296fca04cf8 Mon Sep 17 00:00:00 2001 From: gu_tian_le Date: Sat, 8 May 2021 17:39:18 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E3=80=91#AR000000=EF=BC=9Axml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/markdown-navigator.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml index a2fc086..2aba662 100644 --- a/.idea/markdown-navigator.xml +++ b/.idea/markdown-navigator.xml @@ -3,7 +3,7 @@ - + @@ -26,7 +26,7 @@ - + @@ -52,9 +52,11 @@ - + - + + + -- Gitee From ad1c7583600e06a06a1ee62dfe3543ae5a2024f7 Mon Sep 17 00:00:00 2001 From: gu_tian_le Date: Sat, 8 May 2021 17:40:27 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E3=80=91#AR000000=EF=BC=9Areadme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d55af9c..a141eeb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Scanner-Compat-Library -**本项目基于开æºé¡¹ç›®android-Scanner-Compat-Library 进行鸿蒙化的移æ¤å’Œå¼€å‘,å¯ä»¥é€šè¿‡é¡¹ç›®æ ‡ç­¾ä»¥åŠgithub地å€ï¼ˆï¼‰è¿½è¸ªåˆ°åŽŸå®‰å“项目版本** +**本项目基于开æºé¡¹ç›®android-Scanner-Compat-Library进行鸿蒙化的移æ¤å’Œå¼€å‘,å¯ä»¥é€šè¿‡é¡¹ç›®æ ‡ç­¾ä»¥åŠgithub地å€ï¼ˆï¼‰è¿½è¸ªåˆ°åŽŸå®‰å“项目版本** #### é¡¹ç›®ä»‹ç» - 项目å称:è“牙库 -- Gitee From 0738245a40c52d5fc14ae1a8e78ed20a7488774b Mon Sep 17 00:00:00 2001 From: Gutl <5739850+gu_tian_le@user.noreply.gitee.com> Date: Fri, 14 May 2021 17:04:14 +0800 Subject: [PATCH 6/8] update README.md. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a141eeb..32d1c2f 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,13 @@ - 项目作者和维护人:赵旭东 - è”系方å¼ï¼šzhaoxudong073@chinasoftinc.com - 原项目Doc地å€ï¼š +- 基线版本:Tag v1.4.3 #### 效果演示 -![效果演示](https://gitee.com/chinasoft_ohos/upload/raw/master/img/demo.gif "效果演示.gif") +![演示说明](https://images.gitee.com/uploads/images/2021/0514/170336_0b17951c_5739850.jpeg "A1.jpg") +![演示说明](https://images.gitee.com/uploads/images/2021/0514/170357_2f9e3002_5739850.jpeg "A2.jpg") + #### 安装教程 1.在项目根目录下的build.gradle文件中添加 -- Gitee From 2ff7bd67310ef8754f1f105089e8a83041d32c55 Mon Sep 17 00:00:00 2001 From: Gutl <5739850+gu_tian_le@user.noreply.gitee.com> Date: Mon, 17 May 2021 10:08:42 +0800 Subject: [PATCH 7/8] update README.md. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32d1c2f..26442af 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - 功能:关于è“牙的æ“作 - 项目移æ¤çжæ€ï¼šä¸»åŠŸèƒ½å®Œæˆ - 调用差异:无 -- å¼€å‘版本:sdk5,DevEco Studio2.1 beta3 +- å¼€å‘版本:sdk5,DevEco Studio2.1 beta4 - 项目作者和维护人:赵旭东 - è”系方å¼ï¼šzhaoxudong073@chinasoftinc.com - 原项目Doc地å€ï¼š -- Gitee From 5785c7b284a29994f3c0125454f68686914859f9 Mon Sep 17 00:00:00 2001 From: gu_tian_le Date: Thu, 20 May 2021 17:17:09 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E3=80=90=E4=BF=AE=E6=94=B9=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E3=80=91=EF=BC=9A#AR000000=EF=BC=9Acreate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/checkstyle-idea.xml | 6 +- .idea/checkstyleidea-libs/readme.txt | 6 + .idea/code-check/java/codemars.log | 60 ++-- .idea/code-check/java/detect.txt | 2 +- .idea/code-check/java/output.xml | 2 +- .idea/misc.xml | 4 +- .../phone/phoneSettingConfig_1341911773.json | 25 ++ .idea/previewer/previewConfig.json | 9 + ChangeLog.md | 33 +++ README.md | 6 +- entry/build.gradle | 11 - entry/src/main/config.json | 65 ++++- .../example/myapplication/HistoryBean.java | 16 ++ .../myapplication/HistoryItemProvider.java | 71 +++++ .../example/myapplication/MainAbility.java | 2 + .../example/myapplication/ScannerService.java | 129 +++++++++ .../myapplication/slice/MainAbilitySlice.java | 262 +++++++++++++++--- .../base/graphic/background_list.xml | 11 + .../resources/base/layout/ability_main.xml | 87 +++++- .../resources/base/layout/item_layout.xml | 19 ++ scanner/src/main/config.json | 10 +- .../v18/scanner/BluetoothLeScannerCompat.java | 188 +++---------- .../support/v18/scanner/BluetoothLeUtils.java | 47 +--- .../v18/scanner/BluetoothScannerImplV5.java | 155 ++++++++--- .../support/v18/scanner/BluetoothUuid.java | 32 +-- .../android/support/v18/scanner/Objects.java | 43 --- .../v18/scanner/PendingIntentExecutor.java | 68 +---- .../v18/scanner/PendingIntentReceiver.java | 157 +++-------- .../support/v18/scanner/ScanCallback.java | 15 +- .../support/v18/scanner/ScanFilter.java | 241 ++-------------- .../support/v18/scanner/ScanRecord.java | 124 +-------- .../support/v18/scanner/ScanResult.java | 68 +++-- .../support/v18/scanner/ScanSettings.java | 242 ++-------------- .../support/v18/scanner/ScannerService.java | 156 ++++------- 34 files changed, 1074 insertions(+), 1298 deletions(-) create mode 100644 .idea/checkstyleidea-libs/readme.txt create mode 100644 .idea/previewer/phone/phoneSettingConfig_1341911773.json create mode 100644 .idea/previewer/previewConfig.json create mode 100644 ChangeLog.md create mode 100644 entry/src/main/java/com/example/myapplication/HistoryBean.java create mode 100644 entry/src/main/java/com/example/myapplication/HistoryItemProvider.java create mode 100644 entry/src/main/java/com/example/myapplication/ScannerService.java create mode 100644 entry/src/main/resources/base/graphic/background_list.xml create mode 100644 entry/src/main/resources/base/layout/item_layout.xml diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml index 6eefe0e..04546e0 100644 --- a/.idea/checkstyle-idea.xml +++ b/.idea/checkstyle-idea.xml @@ -3,13 +3,15 @@ diff --git a/.idea/checkstyleidea-libs/readme.txt b/.idea/checkstyleidea-libs/readme.txt new file mode 100644 index 0000000..3bb2010 --- /dev/null +++ b/.idea/checkstyleidea-libs/readme.txt @@ -0,0 +1,6 @@ +This folder contains libraries copied from the "ohos-Scanner-Compat-Library-master" project. +It is managed by the CheckStyle-IDEA IDE plugin. +Do not modify this folder while the IDE is running. +When the IDE is stopped, you may delete this folder at any time. It will be recreated as needed. +In order to prevent the CheckStyle-IDEA IDE plugin from creating this folder, +uncheck the "Copy libraries from project directory" option in the CheckStyle-IDEA settings dialog. diff --git a/.idea/code-check/java/codemars.log b/.idea/code-check/java/codemars.log index d9ff793..03b4dec 100644 --- a/.idea/code-check/java/codemars.log +++ b/.idea/code-check/java/codemars.log @@ -1,30 +1,30 @@ -2021-04-28 14:30:56.015 [main] INFO . - user input: D:\soft\deveco\DevEco Studio 2.1.0.301\tools\openjdk\bin\java,-j,-source,@D:/project3-17/Android-Scanner-Compat-Library-master/ohos-Scanner-Compat-Library-master/.idea/code-check/java/detect.txt,-output,D:/project3-17/Android-Scanner-Compat-Library-master/ohos-Scanner-Compat-Library-master/.idea/code-check/java/output.xml -2021-04-28 14:30:56.017 [main] INFO . - CodeMars Version:2.1.2.sp4 -2021-04-28 14:30:56.067 [main] INFO . - starting analyzing. -2021-04-28 14:30:56.076 [main] INFO . - start collecting report. -2021-04-28 14:30:56.079 [CodeMars1] INFO . - Command: "D:\soft\deveco\DevEco Studio 2.1.0.301\plugins\codecheck\lib\CodeMars\engines\SecFinder-J\bin\run_SecFinder-J.bat",-filelist,D:\project3-17\Android-Scanner-Compat-Library-master\ohos-Scanner-Compat-Library-master\.idea\code-check\java\filelist_2021_04_28_14_30_56_077_74.txt,-f,xml,-default,-progress,-r,D:\project3-17\Android-Scanner-Compat-Library-master\ohos-Scanner-Compat-Library-master\.idea\code-check\java\\errorreport_2021_04_28_14_30_55_967_31.xml,-ruleclasspath,file:///D:\project3-17\Android-Scanner-Compat-Library-master\ohos-Scanner-Compat-Library-master\.idea\code-check\java\ruleclasspath.txt -2021-04-28 14:30:56.216 [Thread-2] INFO . - ËÄÔ 28, 2021 2:30:56 ÏÂÎç com.huawei.secfinderj.SecFinderJ needScan -2021-04-28 14:30:56.216 [Thread-2] INFO . - ÐÅÏ¢: SecFinder-J Version: 2.1.3 -2021-04-28 14:30:56.293 [Thread-1] INFO . - 2021-04-28 14:30:56.254: SecFinder-J Output: Inspect start... -2021-04-28 14:30:56.360 [Thread-1] INFO . - 2021-04-28 14:30:56.359: SecFinder-J Output: Load checkers... -2021-04-28 14:30:56.502 [Thread-1] INFO . - 2021-04-28 14:30:56.502: SecFinder-J Output: Load config... -2021-04-28 14:30:56.528 [Thread-1] INFO . - 2021-04-28 14:30:56.528: SecFinder-J Output: step 1/4: Find files -2021-04-28 14:30:56.552 [Thread-1] INFO . - 2021-04-28 14:30:56.552: SecFinder-J Output: step 2/4: Process files -2021-04-28 14:30:56.589 [Thread-1] INFO . - 2021-04-28 14:30:56.589: SecFinder-J Output: step 3/4: Run analysis... -2021-04-28 14:30:56.590 [Thread-1] INFO . - 2021-04-28 14:30:56.590: SecFinder-J Output: [SecFinder-J--Thread--1] - during processing of [PendingIntentExecutor.java] -2021-04-28 14:30:56.985 [Thread-1] INFO . - 2021-04-28 14:30:56.985: SecFinder-J Output: step 4/4: Result output... -2021-04-28 14:30:56.985 [Thread-1] INFO . - 2021-04-28 14:30:56.985: SecFinder-J Output: Inspect finish... -2021-04-28 14:30:56.985 [Thread-1] INFO . - Analysis result: -2021-04-28 14:30:56.985 [Thread-1] INFO . - files analyzed : 1 -2021-04-28 14:30:56.985 [Thread-1] INFO . - lines analyzed : 128 -2021-04-28 14:30:56.985 [Thread-1] INFO . - rules used : 59 -2021-04-28 14:30:56.985 [Thread-1] INFO . - issues detected : 0 -2021-04-28 14:30:56.985 [Thread-1] INFO . - time cost(sec) : 0 -2021-04-28 14:30:56.985 [Thread-1] INFO . - -2021-04-28 14:30:56.985 [Thread-2] INFO . - ËÄÔ 28, 2021 2:30:56 ÏÂÎç com.huawei.secfinderj.override.HwPmd end -2021-04-28 14:30:56.985 [Thread-2] INFO . - ÐÅÏ¢: SecFinder-J run successed! -2021-04-28 14:30:57.023 [CodeMars1] INFO . - start parse errorreport xml -2021-04-28 14:30:57.024 [CodeMars1] INFO . - parse xml time : 4 -2021-04-28 14:30:57.024 [CodeMars1] INFO . - end parse errorreport xml -2021-04-28 14:30:57.025 [main] INFO . - end collecting report. -2021-04-28 14:30:57.025 [main] INFO . - end analyzing. +2021-05-11 19:29:31.986 [main] INFO . - user input: D:\soft\deveco\DevEco Studio 2.1.0.301\tools\openjdk\bin\java,-j,-source,@D:/ohos/trunk/SOW-24/ohos-Scanner-Compat-Library-master/.idea/code-check/java/detect.txt,-output,D:/ohos/trunk/SOW-24/ohos-Scanner-Compat-Library-master/.idea/code-check/java/output.xml +2021-05-11 19:29:31.989 [main] INFO . - CodeMars Version:2.1.2.sp4 +2021-05-11 19:29:32.033 [main] INFO . - starting analyzing. +2021-05-11 19:29:32.040 [main] INFO . - start collecting report. +2021-05-11 19:29:32.044 [CodeMars1] INFO . - Command: "D:\soft\deveco\DevEco Studio 2.1.0.301\plugins\codecheck\lib\CodeMars\engines\SecFinder-J\bin\run_SecFinder-J.bat",-filelist,D:\ohos\trunk\SOW-24\ohos-Scanner-Compat-Library-master\.idea\code-check\java\filelist_2021_05_11_19_29_32_040_5.txt,-f,xml,-default,-progress,-r,D:\ohos\trunk\SOW-24\ohos-Scanner-Compat-Library-master\.idea\code-check\java\\errorreport_2021_05_11_19_29_31_910_33.xml,-ruleclasspath,file:///D:\ohos\trunk\SOW-24\ohos-Scanner-Compat-Library-master\.idea\code-check\java\ruleclasspath.txt +2021-05-11 19:29:32.164 [Thread-2] INFO . - ÎåÔ 11, 2021 7:29:32 ÏÂÎç com.huawei.secfinderj.SecFinderJ needScan +2021-05-11 19:29:32.164 [Thread-2] INFO . - ÐÅÏ¢: SecFinder-J Version: 2.1.3 +2021-05-11 19:29:32.233 [Thread-1] INFO . - 2021-05-11 19:29:32.197: SecFinder-J Output: Inspect start... +2021-05-11 19:29:32.285 [Thread-1] INFO . - 2021-05-11 19:29:32.284: SecFinder-J Output: Load checkers... +2021-05-11 19:29:32.409 [Thread-1] INFO . - 2021-05-11 19:29:32.409: SecFinder-J Output: Load config... +2021-05-11 19:29:32.436 [Thread-1] INFO . - 2021-05-11 19:29:32.436: SecFinder-J Output: step 1/4: Find files +2021-05-11 19:29:32.459 [Thread-1] INFO . - 2021-05-11 19:29:32.459: SecFinder-J Output: step 2/4: Process files +2021-05-11 19:29:32.498 [Thread-1] INFO . - 2021-05-11 19:29:32.498: SecFinder-J Output: step 3/4: Run analysis... +2021-05-11 19:29:32.499 [Thread-1] INFO . - 2021-05-11 19:29:32.499: SecFinder-J Output: [SecFinder-J--Thread--1] - during processing of [BluetoothScannerImplV5.java] +2021-05-11 19:29:32.954 [Thread-1] INFO . - 2021-05-11 19:29:32.954: SecFinder-J Output: step 4/4: Result output... +2021-05-11 19:29:32.955 [Thread-1] INFO . - 2021-05-11 19:29:32.954: SecFinder-J Output: Inspect finish... +2021-05-11 19:29:32.955 [Thread-1] INFO . - Analysis result: +2021-05-11 19:29:32.955 [Thread-1] INFO . - files analyzed : 1 +2021-05-11 19:29:32.955 [Thread-1] INFO . - lines analyzed : 266 +2021-05-11 19:29:32.955 [Thread-1] INFO . - rules used : 59 +2021-05-11 19:29:32.955 [Thread-1] INFO . - issues detected : 0 +2021-05-11 19:29:32.955 [Thread-1] INFO . - time cost(sec) : 0 +2021-05-11 19:29:32.955 [Thread-1] INFO . - +2021-05-11 19:29:32.955 [Thread-2] INFO . - ÎåÔ 11, 2021 7:29:32 ÏÂÎç com.huawei.secfinderj.override.HwPmd end +2021-05-11 19:29:32.955 [Thread-2] INFO . - ÐÅÏ¢: SecFinder-J run successed! +2021-05-11 19:29:33.025 [CodeMars1] INFO . - start parse errorreport xml +2021-05-11 19:29:33.026 [CodeMars1] INFO . - parse xml time : 2 +2021-05-11 19:29:33.026 [CodeMars1] INFO . - end parse errorreport xml +2021-05-11 19:29:33.027 [main] INFO . - end collecting report. +2021-05-11 19:29:33.027 [main] INFO . - end analyzing. diff --git a/.idea/code-check/java/detect.txt b/.idea/code-check/java/detect.txt index d040002..0229ee1 100644 --- a/.idea/code-check/java/detect.txt +++ b/.idea/code-check/java/detect.txt @@ -1 +1 @@ -D:/project3-17/Android-Scanner-Compat-Library-master/ohos-Scanner-Compat-Library-master/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/PendingIntentExecutor.java +D:/ohos/trunk/SOW-24/ohos-Scanner-Compat-Library-master/scanner/src/main/java/no/nordicsemi/android/support/v18/scanner/BluetoothScannerImplV5.java diff --git a/.idea/code-check/java/output.xml b/.idea/code-check/java/output.xml index 7f044eb..8cf7ac8 100644 --- a/.idea/code-check/java/output.xml +++ b/.idea/code-check/java/output.xml @@ -3,7 +3,7 @@ 1 -128 +266 0 0 diff --git a/.idea/misc.xml b/.idea/misc.xml index f07eb59..53f05c5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -19,9 +19,7 @@ - - - + diff --git a/.idea/previewer/phone/phoneSettingConfig_1341911773.json b/.idea/previewer/phone/phoneSettingConfig_1341911773.json new file mode 100644 index 0000000..f505e6b --- /dev/null +++ b/.idea/previewer/phone/phoneSettingConfig_1341911773.json @@ -0,0 +1,25 @@ +{ + "setting": { + "1.0.1": { + "Language": { + "args": { + "Language": "zh-CN" + } + } + } + }, + "frontend": { + "1.0.0": { + "Resolution": { + "args": { + "Resolution": "360*780" + } + }, + "DeviceType": { + "args": { + "DeviceType": "phone" + } + } + } + } +} \ No newline at end of file diff --git a/.idea/previewer/previewConfig.json b/.idea/previewer/previewConfig.json new file mode 100644 index 0000000..29f1efd --- /dev/null +++ b/.idea/previewer/previewConfig.json @@ -0,0 +1,9 @@ +{ + "1.0.0": { + "LastPreviewDevice": { + "D:\\ohos\\trunk\\SOW-24\\ohos-Scanner-Compat-Library-master\\entry": [ + "phone" + ] + } + } +} \ No newline at end of file diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..e2e988d --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,33 @@ +## 0.0.1-SNAPSHOT +* å‘布beta版本 + +Version 1.4.3 +Filtering based on service data + +Version 1.4.2 +Crashing when disabling scanning after Bluetooth has been turned OFF. + +Version 1.4.1 +#58 - memory leak when using PendingIntent fixed. +Migrated to Android Studio 3.4.2, Gradle wrapper 5.1.1. +Improved publication script. + +Version 1.4.0 +Feature equal to 1.3.1, but migrated to Android Jetpack (androidX). + +Version 1.3.1 +#45 - removing a task from Recents is killing the background service. Not any more. Watch out for Doze mode! + +Version 1.3.0 +#33 - Scanning with PendingIntent supported (and emulated on older platforms using a background service). +#44 - SCAN_MODE_OPPORTUNISTIC falls back to SCAN_MODE_LOW_POWER on Lollipop and to power save settings on older devices. +#39 - fixed infinite scanning with #40. + +Version 1.2.0 +#32, #34, #36 - Fixed race conditions, supported worker thread, simplified code + +Version 1.1.0 +Support for Android Oreo added + +Version 1.0.0 +JUnit tests added \ No newline at end of file diff --git a/README.md b/README.md index 26442af..3ad04bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Scanner-Compat-Library -**本项目基于开æºé¡¹ç›®android-Scanner-Compat-Library进行鸿蒙化的移æ¤å’Œå¼€å‘,å¯ä»¥é€šè¿‡é¡¹ç›®æ ‡ç­¾ä»¥åŠgithub地å€ï¼ˆï¼‰è¿½è¸ªåˆ°åŽŸå®‰å“项目版本** +**本项目基于开æºé¡¹ç›®android-Scanner-Compat-Library 进行鸿蒙化的移æ¤å’Œå¼€å‘,å¯ä»¥é€šè¿‡é¡¹ç›®æ ‡ç­¾ä»¥åŠgithub地å€ï¼ˆï¼‰è¿½è¸ªåˆ°åŽŸå®‰å“项目版本** #### é¡¹ç›®ä»‹ç» - 项目å称:è“牙库 @@ -16,9 +16,7 @@ #### 效果演示 -![演示说明](https://images.gitee.com/uploads/images/2021/0514/170336_0b17951c_5739850.jpeg "A1.jpg") -![演示说明](https://images.gitee.com/uploads/images/2021/0514/170357_2f9e3002_5739850.jpeg "A2.jpg") - +![效果演示] #### 安装教程 1.在项目根目录下的build.gradle文件中添加 diff --git a/entry/build.gradle b/entry/build.gradle index e89df81..18fad72 100644 --- a/entry/build.gradle +++ b/entry/build.gradle @@ -1,16 +1,5 @@ apply plugin: 'com.huawei.ohos.hap' ohos { - signingConfigs { - debug { - storeFile file('D:\\ohos\\trunk\\keyStore\\BGABanner\\root_1.p12') - storePassword '0000002037A1D11B07FA5BFB33CF64DAB632CD228E6570F7C36EE345D67936E5CE7A745A48E0FAA9E488D8C6BC105E44' - keyAlias = 'hos_platform_os' - keyPassword '00000020642523EDD154D8B9437AB57FB400F49176C068D2E13E547F21D70153995483EE68FCF3B78C0301241AB629F1' - signAlg = 'SHA256withECDSA' - profile file('D:\\ohos\\trunk\\ç­¾åæ–‡ä»¶\\BGABannerDebug.p7b') - certpath file('D:\\ohos\\trunk\\keyStore\\BGABanner\\root.cer') - } - } compileSdkVersion 5 defaultConfig { compatibleSdkVersion 5 diff --git a/entry/src/main/config.json b/entry/src/main/config.json index 7c78bf8..838029d 100644 --- a/entry/src/main/config.json +++ b/entry/src/main/config.json @@ -1,6 +1,6 @@ { "app": { - "bundleName": "cn.bingoogolapple.bgabanner.demo", + "bundleName": "com.example.myapplication", "vendor": "example", "version": { "code": 1, @@ -8,8 +8,7 @@ }, "apiVersion": { "compatible": 5, - "target": 5, - "releaseType": "Release" + "target": 5 } }, "deviceConfig": {}, @@ -17,7 +16,7 @@ "package": "com.example.myapplication", "name": ".MyApplication", "deviceType": [ - "phone" + "tv" ], "distro": { "deliveryWithInstall": true, @@ -43,15 +42,69 @@ "label": "MyApplication", "type": "page", "launchType": "standard" + }, + { + "name": "no.nordicsemi.android.support.v18.scanner.ScannerService", + "type": "service", + "visible": true } ], "reqPermissions": [ { - "name": "ohos.permission.USE_BLUETOOTH" + "name": "ohos.permission.USE_BLUETOOTH", + "reason": "使用è“牙æƒé™", + "usedScene": { + "ability": [ + "com.example.myapplication.slice.MainAbility", + "no.nordicsemi.android.support.v18.scanner.ScannerService" + ], + "when": "always" + } }, { - "name": "ohos.permission.DISCOVER_BLUETOOTH" + "name": "ohos.permission.DISCOVER_BLUETOOTH", + "reason": "æœç´¢è“牙æƒé™", + "usedScene": { + "ability": [ + "com.example.myapplication.slice.MainAbility", + "no.nordicsemi.android.support.v18.scanner.ScannerService" + ], + "when": "always" } + }, + { + "name": "ohos.permission.LOCATION", + "reason": "å…许å‰å°èŽ·å–æƒé™", + "usedScene": { + "ability": [ + "com.example.myapplication.slice.MainAbility", + "no.nordicsemi.android.support.v18.scanner.ScannerService" + ], + "when": "always" + } + }, + { + "name": "ohos.permission.LOCATION_IN_BACKGROUND", + "reason": "å…许åŽå°èŽ·å–æƒé™", + "usedScene": { + "ability": [ + "com.example.myapplication.slice.MainAbility", + "no.nordicsemi.android.support.v18.scanner.ScannerService" + ], + "when": "always" + } + }, + { + "name": "ohos.permission.GET_NETWORK_INFO", + "reason": "å…è®¸åº”ç”¨èŽ·å–æ•°æ®ç½‘络信æ¯", + "usedScene": { + "ability": [ + "com.example.myapplication.slice.MainAbility", + "no.nordicsemi.android.support.v18.scanner.ScannerService" + ], + "when": "always" + } + } ] } } \ No newline at end of file diff --git a/entry/src/main/java/com/example/myapplication/HistoryBean.java b/entry/src/main/java/com/example/myapplication/HistoryBean.java new file mode 100644 index 0000000..1c58943 --- /dev/null +++ b/entry/src/main/java/com/example/myapplication/HistoryBean.java @@ -0,0 +1,16 @@ +package com.example.myapplication; + +public class HistoryBean { + + private String name; + public HistoryBean(String name) { + this.name = name; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + +} diff --git a/entry/src/main/java/com/example/myapplication/HistoryItemProvider.java b/entry/src/main/java/com/example/myapplication/HistoryItemProvider.java new file mode 100644 index 0000000..f573c85 --- /dev/null +++ b/entry/src/main/java/com/example/myapplication/HistoryItemProvider.java @@ -0,0 +1,71 @@ +package com.example.myapplication; + +import no.nordicsemi.android.support.v18.scanner.ScanResult; +import ohos.aafwk.ability.AbilitySlice; +import ohos.agp.components.*; +import ohos.bluetooth.ble.BleScanResult; + +import java.util.ArrayList; +import java.util.List; + +public class HistoryItemProvider extends BaseItemProvider { + private List list; + private AbilitySlice slice; + private int layout; + + public HistoryItemProvider(List list, AbilitySlice slice, int layout) { + this.list = list; + this.slice = slice; + this.layout = layout; + } + + @Override + public int getCount() { + return list == null ? 0 : list.size(); + } + + public List getList() { + if (list == null) { + return new ArrayList<>(); + } + return list; + } + + @Override + public Object getItem(int i) { + if (list != null && i >= 0 && i < list.size()) { + return list.get(i); + } + return null; + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public Component getComponent(int i, Component component, ComponentContainer componentContainer) { + final Component comp; + if (component == null) { + comp = LayoutScatter.getInstance(slice).parse(layout, null, false); + } else { + comp = component; + } + List list = getList(); + ScanResult historyBean = list.get(i); + BleScanResult bleScanResult = (BleScanResult) historyBean.getDevice(); + String str = bleScanResult.getPeripheralDevice().getDeviceName().toString(); + str = str.replace("Optional[", ""); + str = str.replace("]", ""); + Text text = (Text) comp.findComponentById(ResourceTable.Id_text); + if (str.equals("Optional.empty")) { + String strs = bleScanResult.getPeripheralDevice().getDeviceAddr().toString(); + text.setText(strs); + } else { + text.setText(str); + } + + return comp; + } +} \ No newline at end of file diff --git a/entry/src/main/java/com/example/myapplication/MainAbility.java b/entry/src/main/java/com/example/myapplication/MainAbility.java index 813ed4e..d184331 100644 --- a/entry/src/main/java/com/example/myapplication/MainAbility.java +++ b/entry/src/main/java/com/example/myapplication/MainAbility.java @@ -3,6 +3,7 @@ package com.example.myapplication; import com.example.myapplication.slice.MainAbilitySlice; import ohos.aafwk.ability.Ability; import ohos.aafwk.content.Intent; +import ohos.aafwk.content.Operation; public class MainAbility extends Ability { @Override @@ -10,4 +11,5 @@ public class MainAbility extends Ability { super.onStart(intent); super.setMainRoute(MainAbilitySlice.class.getName()); } + } diff --git a/entry/src/main/java/com/example/myapplication/ScannerService.java b/entry/src/main/java/com/example/myapplication/ScannerService.java new file mode 100644 index 0000000..62890b6 --- /dev/null +++ b/entry/src/main/java/com/example/myapplication/ScannerService.java @@ -0,0 +1,129 @@ +package com.example.myapplication; + +import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat; +import no.nordicsemi.android.support.v18.scanner.ScanCallback; +import no.nordicsemi.android.support.v18.scanner.ScanSettings; +import ohos.aafwk.ability.Ability; +import ohos.aafwk.content.Intent; +import ohos.app.Context; +import ohos.bluetooth.ble.BleScanFilter; +import ohos.event.intentagent.IntentAgent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +public class ScannerService extends Ability { + + /* package */ static final String EXTRA_PENDING_INTENT = "ohos.permission.GET_NETWORK_INFO"; + /* package */ static final String EXTRA_FILTERS = "no.nordicsemi.android.support.v18.EXTRA_FILTERS"; + /* package */ static final String EXTRA_SETTINGS = "no.nordicsemi.android.support.v18.EXTRA_SETTINGS"; + /* package */ static final String EXTRA_START = "no.nordicsemi.android.support.v18.EXTRA_START"; + + private HashMap callbacks; + Handler handler; + private final Object LOCK = new Object(); + Context context = getContext(); + + @Override + protected void onStart(Intent intent) { + super.onStart(intent); + callbacks = new HashMap<>(); + handler = new Handler() { + @Override + public void publish(LogRecord logRecord) { + } + @Override + public void flush() { + } + @Override + public void close() throws SecurityException { + } + }; + + } + + @Override + public void onCommand(final Intent intent,final boolean restart,final int startId) { + super.onCommand(intent, restart, startId); +// final IntentAgent callbackIntent = intent.getSequenceableParam(EXTRA_PENDING_INTENT); + final boolean start = intent.getBooleanParam(EXTRA_START, false); + final boolean stop = !start; +// if (callbackIntent == null) { +// boolean shouldStop; +// synchronized (LOCK) { +// shouldStop = callbacks.isEmpty(); +// } +// +// if (shouldStop) +// stopScan(callbackIntent); +//// return 2; +// } + boolean knownCallback; + synchronized (LOCK) { +// knownCallback = callbacks.containsKey(callbackIntent); + } + +// if (start && !knownCallback) { +// final ArrayList filters = intent.getSequenceableArrayListParam(EXTRA_FILTERS); +// final ScanSettings settings = intent.getSequenceableParam(EXTRA_SETTINGS); +// startScan(filters != null ? filters : Collections.emptyList(), +// settings != null ? settings : new ScanSettings.Builder().build(), +// callbackIntent); +// } else if (stop && knownCallback) { +// stopScan(callbackIntent); +// } +// return 2; + } + + private void startScan(List bleScanFilters, ScanSettings scanSettings, IntentAgent callbackIntent) { +// final PendingIntentExecutor executor = +// new PendingIntentExecutor(callbackIntent, scanSettings,this); + synchronized (LOCK) { +// callbacks.put(callbackIntent, executor); + } + + try { + final BluetoothLeScannerCompat scannerCompat = BluetoothLeScannerCompat.getScanner(); + } catch (final Exception e) { + } + } + + private void stopScan(final IntentAgent callbackIntent) { + ScanCallback callback; + boolean shouldStop; + synchronized (LOCK) { + callback = callbacks.remove(callbackIntent); + shouldStop = callbacks.isEmpty(); + } + if (callback == null) + return; + try { + final BluetoothLeScannerCompat scannerCompat = BluetoothLeScannerCompat.getScanner(); + scannerCompat.stopScan(callback); + } catch (final Exception e) { + } + if (shouldStop) + stopScan(callbackIntent); + } + + @Override + protected void onStop() { + final BluetoothLeScannerCompat scannerCompat = BluetoothLeScannerCompat.getScanner(); + for (final ScanCallback callback : callbacks.values()) { + try { + scannerCompat.stopScan(callback); + } catch (final Exception e) { + // Ignore + } + } + callbacks.clear(); + callbacks = null; + handler = null; + super.onStop(); + super.onStop(); + } +} diff --git a/entry/src/main/java/com/example/myapplication/slice/MainAbilitySlice.java b/entry/src/main/java/com/example/myapplication/slice/MainAbilitySlice.java index e1a2f73..42e5d26 100644 --- a/entry/src/main/java/com/example/myapplication/slice/MainAbilitySlice.java +++ b/entry/src/main/java/com/example/myapplication/slice/MainAbilitySlice.java @@ -1,75 +1,142 @@ package com.example.myapplication.slice; +import com.example.myapplication.HistoryItemProvider; import com.example.myapplication.ResourceTable; -import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat; -import no.nordicsemi.android.support.v18.scanner.ScanCallback; -import no.nordicsemi.android.support.v18.scanner.ScanFilter; -import no.nordicsemi.android.support.v18.scanner.ScanSettings; +import no.nordicsemi.android.support.v18.scanner.*; import ohos.aafwk.ability.AbilitySlice; import ohos.aafwk.content.Intent; +import ohos.agp.components.*; +import ohos.agp.window.dialog.ToastDialog; +import ohos.app.Context; +import ohos.bluetooth.ble.BleScanFilter; import ohos.bluetooth.ble.BleScanResult; +import ohos.bundle.IBundleManager; import ohos.hiviewdfx.HiLog; import ohos.hiviewdfx.HiLogLabel; -import ohos.utils.SequenceUuid; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; +import java.util.*; -public class MainAbilitySlice extends AbilitySlice { +public class MainAbilitySlice extends AbilitySlice implements Component.ClickedListener { + private Button btnStartScan, btnStopScan; + private RadioButton one, two, three, four; + private RadioContainer rabut; + private BluetoothLeScannerCompat scanner; + + private List beans = new ArrayList<>(); + Context context; private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00201, "MY_TAG"); //设置è“牙扫æè¿‡æ»¤å™¨é›†åˆ private List scanFilterList; //设置è“牙扫æè¿‡æ»¤å™¨ private ScanFilter.Builder scanFilterBuilder; //设置è“牙扫æè®¾ç½® - private ScanSettings.Builder scanSettingBuilder; + private ScanSettings.Builder scanSettingBuilder; + private ListContainer dialog; + private HistoryItemProvider historyItemProvider; + private boolean isBut = false; + private boolean isPD = false; +// private ToastDialog toastDialog; + + @Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_ability_main); - onView(); - onData(); - } + initView(); + initClick(); + String[] permission = {"ohos.permission.LOCATION", "ohos.permission.USE_BLUETOOTH" + , "ohos.permission.DISCOVER_BLUETOOTH", "ohos.permission.LOCATION_IN_BACKGROUND" + , "ohos.permission.GET_NETWORK_INFO"}; + requestPermissionsFromUser(permission, 0); + if (verifySelfPermission("ohos.permission.LOCATION_IN_BACKGROUND") != IBundleManager.PERMISSION_GRANTED) { + return; + } else { + return; + } - private void onData() { - HiLog.info(LABEL, ""); } - private void onView() { - BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); - ScanSettings settings = new ScanSettings.Builder() - .setLegacy(false) - .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) - .setReportDelay(10000) - .setUseHardwareBatchingIfSupported(false) - .build(); - List filters = new ArrayList<>(); - UUID uuid = UUID.randomUUID(); - HiLog.info(LABEL, "uuid"+uuid.toString()); - filters.add(new ScanFilter.Builder().setServiceUuid(new SequenceUuid(UUID.fromString(uuid.toString()))).build()); - scanner.startScan(null, buildScanSettings(), new ScanCallback() { - @Override - public void onScanResult(int callbackType, BleScanResult result) { - super.onScanResult(callbackType, result); - HiLog.info(LABEL, "ScanResult22"+result.getPeripheralDevice()); - } + private void initView() { + dialog = (ListContainer) findComponentById(ResourceTable.Id_dialog); + btnStartScan = (Button) findComponentById(ResourceTable.Id_btn_start_scan); + btnStopScan = (Button) findComponentById(ResourceTable.Id_btn_stop_scan); + one = (RadioButton) findComponentById(ResourceTable.Id_one); + two = (RadioButton) findComponentById(ResourceTable.Id_two); + three = (RadioButton) findComponentById(ResourceTable.Id_three); + four = (RadioButton) findComponentById(ResourceTable.Id_four); + rabut = (RadioContainer) findComponentById(ResourceTable.Id_rabut); + scanner = BluetoothLeScannerCompat.getScanner(); + historyItemProvider = new HistoryItemProvider(beans, MainAbilitySlice.this, ResourceTable.Layout_item_layout); + dialog.setItemProvider(historyItemProvider); + rabut.mark(0); + } - @Override - public void onBatchScanResults(List results) { - super.onBatchScanResults(results); - HiLog.info(LABEL, "onBatchScanResults"+results.size()); - } + private void initClick() { + btnStartScan.setClickedListener(this); + btnStopScan.setClickedListener(this); + rabut.setMarkChangedListener(new RadioContainer.CheckedStateChangedListener() { @Override - public void onScanFailed(int errorCode) { - super.onScanFailed(errorCode); - HiLog.info(LABEL, "onScanFailed"); + public void onCheckedChanged(RadioContainer radioContainer, int i) { + if (i == 0) { + if (isBut == false) { + ScanSettings settings = new ScanSettings.Builder() + .setLegacy(false) + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + .setReportDelay(10000) + .setUseHardwareBatchingIfSupported(false) + .build(); + } else { + new ToastDialog(getContext()) + .setText("æœç´¢ä¸­,请结æŸå½“剿œç´¢") + .show(); + } + } else if (i == 1) { + if (isBut == false) { + ScanSettings settings = new ScanSettings.Builder() + .setLegacy(false) + .setScanMode(ScanSettings.SCAN_MODE_BALANCED) + .setReportDelay(10000) + .setUseHardwareBatchingIfSupported(false) + .build(); + } else { + new ToastDialog(getContext()) + .setText("æœç´¢ä¸­,请结æŸå½“剿œç´¢") + .show(); + } + } else if (i == 2) { + if (isBut == false) { + ScanSettings settings = new ScanSettings.Builder() + .setLegacy(false) + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setReportDelay(10000) + .setUseHardwareBatchingIfSupported(false) + .build(); + } else { + new ToastDialog(getContext()) + .setText("æœç´¢ä¸­,请结æŸå½“剿œç´¢") + .show(); + } + } else { + if (isBut == false) { + ScanSettings settings = new ScanSettings.Builder() + .setLegacy(false) + .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) + .setReportDelay(10000) + .setUseHardwareBatchingIfSupported(false) + .build(); + } else { + new ToastDialog(getContext()) + .setText("æœç´¢ä¸­,请结æŸå½“剿œç´¢") + .show(); + } + } } }); } + @Override public void onActive() { super.onActive(); @@ -97,4 +164,115 @@ public class MainAbilitySlice extends AbilitySlice { return scanSettingBuilder.build(); } -} + @Override + public void onClick(Component component) { + switch (component.getId()) { + case ResourceTable.Id_btn_start_scan: + if (isBut == false) { + start(); + new ToastDialog(getContext()) + .setText("开始") + .show(); + } else { + new ToastDialog(getContext()) + .setText("æœç´¢ä¸­") + .show(); + } + break; + case ResourceTable.Id_btn_stop_scan: + if (isBut == true) { + stop(); + new ToastDialog(getContext()) + .setText("结æŸ") + .show(); + } else { + new ToastDialog(getContext()) + .setText("已结æŸ") + .show(); + } + break; + } + } + + private void start() { + isBut = true; + List filters = new ArrayList<>(); + UUID uuid = UUID.randomUUID(); + scanner.startScan(this, null, buildScanSettings(), scanCallback); + scanner.startScan(filters, buildScanSettings(), this, null); + } + + private void stop() { + isBut = false; + scanner.stopScan(scanCallback); + beans.clear(); + getUITaskDispatcher().asyncDispatch(new Runnable() { + @Override + public void run() { + historyItemProvider.notifyDataChanged(); + } + }); + } + + private ScanCallback scanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + super.onScanResult(callbackType, result); + System.out.println("LOGLIST" + "/**/" + "RESULTDEVICE-------------------------------" + result.getDevice()); + } + + + @Override + public void onBatchScanResults(List results) { + super.onBatchScanResults(results); + if (results.size() > 0) { + beans.clear(); + beans.addAll(results); + } + + getUITaskDispatcher().asyncDispatch(new Runnable() { + @Override + public void run() { + if (results.size() > 0) { + int size = results.size(); + } + } + }); + + for (ScanResult scanResult : results) { + + String deviceName = scanResult.getScanRecord().getDeviceName(); + if (deviceName != null && !deviceName.isEmpty()) { +// System.out.println("LOGLISTALL" + "/--/--/ " +// + "DEVICE =" + ((BleScanResult)scanResult.getDevice()).getPeripheralDevice().getDeviceAddr()+ ", " +// +"DEVICENAME ="+scanResult.getScanRecord().getDeviceName()+", " +// +"DATASTATUS ="+scanResult.getDataStatus()); + } + int size = results.size(); + System.out.println("LOGLISTALL" + "/**/" + "ALLSIZE = " + size); + } + getUITaskDispatcher().asyncDispatch(new Runnable() { + @Override + public void run() { + historyItemProvider.notifyDataChanged(); + } + }); + } + + @Override + public void onScanFailed(int errorCode) { + super.onScanFailed(errorCode); + System.out.println("onScanFailed"); + } + }; + + @Override + protected void onBackground() { + super.onBackground(); + } + + @Override + protected void onStop() { + super.onStop(); + } +} \ No newline at end of file diff --git a/entry/src/main/resources/base/graphic/background_list.xml b/entry/src/main/resources/base/graphic/background_list.xml new file mode 100644 index 0000000..1629bd1 --- /dev/null +++ b/entry/src/main/resources/base/graphic/background_list.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/entry/src/main/resources/base/layout/ability_main.xml b/entry/src/main/resources/base/layout/ability_main.xml index c99c277..84da3f7 100644 --- a/entry/src/main/resources/base/layout/ability_main.xml +++ b/entry/src/main/resources/base/layout/ability_main.xml @@ -1,18 +1,83 @@ - - - - \ No newline at end of file + ohos:width="match_parent" + ohos:orientation="horizontal"> + +