Ble蓝牙App(七)扫描过滤
- 前言
- 目录
- 正文
- 一、增加菜单
- 二、使用MMKV
- ① 添加依赖
- ② 封装MMKV
- ③ 使用MMKV
- 三、过滤空设备名
- 四、过滤Mac地址
- 五、过滤RSSI
- 六、源码
前言
在上一篇文章中了解了MTU的相关知识以及对于设备操作信息的展示,本篇文章中将增加扫描设备的过滤功能让你更方便的扫描想要找的低功耗蓝牙设备。
目录
- Ble蓝牙App(一)扫描
- Ble蓝牙App(二)连接与发现服务
- Ble蓝牙App(三)特性和属性
- Ble蓝牙App(四)UI优化和描述符
- Ble蓝牙App(五)数据操作
- Ble蓝牙App(六)请求MTU与显示设备信息
- Ble蓝牙App(七)扫描过滤
正文
增加扫描过滤主要就是让扫描设备的时候更方便找到想要的设备,下面我们来看有哪些功能的增加。
一、增加菜单
为了不占用扫描页面的空间,我打算通过添加菜单来进行扫描的过滤操作,那么首先我们在menu下增加一个menu_scan.xml文件,代码如下所示:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/item_filter_null"
android:checkable="true"
android:title="过滤空设备名" />
<item
android:id="@+id/item_filter_mac"
android:checkable="true"
android:title="过滤Mac地址" />
<item
android:id="@+id/item_filter_rssi"
android:checkable="true"
android:title="过滤RSSI" />
</menu>
菜单中有三个Item,看一下预览效果图:
三个Item都是选中Item,选中表示这个过滤功能项启用,可以全部都选中,也可以任意选择,之后我们进入到ScanActivity,首先是创建菜单和菜单选中,修改地方有三处:
第一处:在onCreate()
函数中增加支持ActionBar,代码如下所示:
override fun onCreate(savedInstanceState: Bundle?) {
...
setSupportActionBar(binding.toolbar)
...
}
第二处:在ScanActivity中重写onCreateOptionsMenu()
函数,代码如下所示:
private lateinit var mMenu: Menu
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
mMenu = menu
return true
}
创建选项菜单,再创建一个mMenu变量,在后面会用到的这个变量。
第三处:在ScanActivity中重写onOptionsItemSelected()
函数,代码如下所示:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
R.id.item_filter_null -> { // 过滤空设备名称
}
R.id.item_filter_mac -> { // 过滤Mac地址
}
R.id.item_filter_rssi -> { // 过滤RSSI
}
}
return true
}
现在三个Item的点击事件中什么都不做,我们一步一步给它加上,现在菜单就创建好了。
二、使用MMKV
因为我们修改的菜单项会涉及到保存过滤设置的功能,所以需要将一些参数报错到手机中,那么我们可以使用SP、DataStore等方式,但是这里我是用MMKV,主要是因为用起来比较的方便,下面我们来使用MMKV。
① 添加依赖
MMKV是腾讯的一个开源项目,已经发布在mavenCentral()
仓库中了,我们在App中使用只需要在app模块下的build.gradle
中的dependencies{}
闭包中添加如下依赖代码即可:
dependencies {
...
//mmkv
implementation 'com.tencent:mmkv:1.2.14'
}
然后点击Sync Now
,同步一下,添加依赖就完成了。
② 封装MMKV
针对于MMKV的使用其实非常简单,就是两步,先初始化,然后使用就好了,那么为了使用的更方便,我们可以简单封装一下MMKV,做成一个工具类,下面我们在com.llw.goodble
包下新建一个utils
包,utils
包下新建一个MVUtils
类,代码如下所示:
object MVUtils {
val mmkv = MMKV.defaultMMKV()
fun put(key: String, value: Any): Boolean {
return when (value) {
is String -> mmkv.encode(key, value)
is Float -> mmkv.encode(key, value)
is Boolean -> mmkv.encode(key, value)
is Int -> mmkv.encode(key, value)
is Long -> mmkv.encode(key, value)
is Double -> mmkv.encode(key, value)
is ByteArray -> mmkv.encode(key, value)
is Parcelable -> mmkv.encode(key, value)
else -> false
}
}
fun put(key: String, sets: Set<String>?): Boolean {
if (sets == null) {
return false
}
return mmkv.encode(key, sets)
}
fun getInt(key: String, defaultValue: Int = 0) = mmkv.decodeInt(key, defaultValue)
fun getDouble(key: String, defaultValue: Double = 0.00) = mmkv.decodeDouble(key, defaultValue)
fun getLong(key: String, defaultValue: Long = 0L) = mmkv.decodeLong(key, defaultValue)
fun getBoolean(key: String, defaultValue: Boolean = false) = mmkv.decodeBool(key, defaultValue)
fun getFloat(key: String, defaultValue: Float = 0F) = mmkv.decodeFloat(key, defaultValue)
fun getByteArray(key: String) = mmkv.decodeBytes(key)
fun getString(key: String, defaultValue: String = "") = mmkv.decodeString(key, defaultValue)
inline fun <reified T : Parcelable> getParcelable(key: String) =
mmkv.decodeParcelable(key, T::class.java)
fun getStringSet(key: String) = mmkv.decodeStringSet(key, Collections.emptySet())
fun removeKey(key: String) = mmkv.removeValueForKey(key)
fun clearAll() = mmkv.clearAll()
}
这里实际上大体就分为三个部分,首先是初始化,然后是数据的存和取,最后是清除数据,是不是很简单呢?
③ 使用MMKV
使用MMKV,首先需要做的就是初始化,我们需要在BleApp
的onCreate()
函数中进行初始化,代码如下所示:
override fun onCreate() {
...
//mmkv初始化
MMKV.initialize(this)
}
使用MMKV同样是采用键值对的形式,那么基于我们的菜单功能,我们需要增加一些键,在BleConstant
中增加如下常量,代码如下所示:
//过滤RSSI
const val FILTER_RSSI_FLAG = "filterRssiFlag"
//RSSI 值
const val FILTER_RSSI_VALUE = "filterRssiValue"
//过滤空设备名
const val FILTER_NULL_FLAG = "filterNullFlag"
//是否过滤Mac地址
const val FILTER_MAC_FLAG = "filterMacFlag"
//需要过滤的Mac地址
const val FILTER_MAC_VALUE = "filterMacValue"
下面我们修改ScanActivity
中的onCreateOptionsMenu()
函数,代码如下所示:
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_scan, menu)
mMenu = menu
mMenu.findItem(R.id.item_filter_rssi).isChecked = MVUtils.getBoolean(FILTER_RSSI_FLAG)
if (MVUtils.getBoolean(FILTER_RSSI_FLAG)) {
mMenu.findItem(R.id.item_filter_rssi).title =
"过滤RSSI:-" + MVUtils.getInt(FILTER_RSSI_VALUE, 100)
}
mMenu.findItem(R.id.item_filter_null).isChecked = MVUtils.getBoolean(FILTER_NULL_FLAG)
mMenu.findItem(R.id.item_filter_mac).isChecked = MVUtils.getBoolean(FILTER_MAC_FLAG)
return true
}
在这里的代码就是在创建菜单的时候,判断一下保存的参数,是否需要选中Item,可以修改Item的选中状态和标题内容,这里就是获取参数。
三、过滤空设备名
下面我们来保存参数,修改onOptionsItemSelected()
函数中的代码:
R.id.item_filter_null -> { // 过滤空设备名称
if (bleCore.isScanning()) stopScan()
val filterNull = MVUtils.getBoolean(FILTER_NULL_FLAG)
MVUtils.put(FILTER_NULL_FLAG, !filterNull)
mMenu.findItem(R.id.item_filter_null).isChecked = MVUtils.getBoolean(FILTER_NULL_FLAG)
showMsg(if (MVUtils.getBoolean(FILTER_NULL_FLAG)) "过滤空设备名称的设备" else "保留空设备名称的设备")
if (!bleCore.isScanning()) startScan()
}
这里看到就是在点击过滤空设备Item时,首先停止扫描,然后获取参数值,再保存,根据值设置Item是否选中,最后开始扫描,那么我们怎么过滤这个空设备名称的设备呢?还需要修改扫描回调中的代码:
override fun onScanResult(result: ScanResult) {
//过滤空设备名
if (MVUtils.getBoolean(FILTER_NULL_FLAG)) {
if (result.scanRecord!!.deviceName == null) {
return
}
if (result.scanRecord!!.deviceName!!.isEmpty()) {
return
}
}
...
}
这里我们只需要在原有的条件上再增加一个判断即可,因为缺省值是false,所以如果是不过滤空设备名就不会执行判断里面空处理和空设备名处理,看一下运行的效果。
我们看到默认是不过滤空设备名称的,当选中过滤空设备名后就会过滤设备名称为空的设备,只不过我们这里对于空设备名称的设备显示的UI还没有处理的很好,下面我们简单改一下,将onScanResult()
函数中的这一行代码:
val bleDevice = BleDevice(result.scanRecord!!.deviceName, result.device.address, result.rssi, result.device)
改成
val realName = result.scanRecord?.deviceName?.let { it.ifEmpty { BleConstant.UNKNOWN_DEVICE } } ?: BleConstant.UNKNOWN_DEVICE
val bleDevice = BleDevice(realName, result.device.address, result.rssi, result.device)
这里改的目的就是首先判断获取的设备名是否为空,如果为空则返回一个Unknown device
作为设备名称,不为空则检查是否为空字符串,是的话也返回Unknown device
,不是则返回本身设备名称,再运行一下就可以了。
四、过滤Mac地址
下面我们要做过滤Mac地址,那么要过滤Mac地址,首先要输入Mac地址,那么我们可以写一个弹窗来进行输入的工作,在layout下创建一个dialog_settings_mac.xml
作为弹窗布局,代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="过滤Mac地址" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/data_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:boxStrokeColor="@color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Mac Address"
android:lines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<CheckBox
android:id="@+id/cb_format_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Mac地址格式检查"
app:layout_constraintStart_toStartOf="@+id/data_layout"
app:layout_constraintTop_toBottomOf="@+id/data_layout" />
<Button
android:id="@+id/btn_negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="18dp"
android:layout_weight="1"
android:text="取消"
app:layout_constraintEnd_toStartOf="@+id/btn_positive"
app:layout_constraintTop_toTopOf="@+id/btn_positive" />
<Button
android:id="@+id/btn_positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_weight="1"
android:text="确定"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/data_layout"
app:layout_constraintTop_toBottomOf="@+id/cb_format_check" />
</androidx.constraintlayout.widget.ConstraintLayout>
这个布局中有一个检查Mac地址正确性的复选框,同样我们需要在BleUtils中增加一个函数,代码如下所示:
fun isValidMac(macStr: String) = Regex("([A-Fa-f0-9]{2}[:]){5}[A-Fa-f0-9]{2}").matches(macStr)
下面我们回到ScanActivity中,写一个showSettingMacDialog()
函数,代码如下所示:
private fun showSettingMacDialog() {
val dialog = BottomSheetDialog(this, R.style.BottomSheetDialogStyle)
val macBinding = DialogSettingMacBinding.inflate(layoutInflater)
macBinding.btnPositive.setOnClickListener {
val inputData = macBinding.etData.text.toString()
if (inputData.isEmpty()) {
macBinding.dataLayout.error = "请输入Mac地址"
return@setOnClickListener
}
if (macBinding.cbFormatCheck.isChecked) {
if (!BleUtils.isValidMac(inputData)) {
macBinding.dataLayout.error = "请输入正确的Mac地址"
return@setOnClickListener
}
}
if (bleCore.isScanning()) stopScan()
MVUtils.put(FILTER_MAC_VALUE, inputData)
MVUtils.put(FILTER_MAC_FLAG, true)
mMenu.findItem(R.id.item_filter_mac).isChecked = true
showMsg("过滤Mac地址")
if (!bleCore.isScanning()) startScan()
dialog.dismiss()
}
macBinding.btnNegative.setOnClickListener {
dialog.dismiss()
}
dialog.setContentView(macBinding.root)
dialog.show()
}
弹窗中点击确定按钮就会先检查一遍,然后就会保存Mac地址,再保存过滤标识,然后我们修改一下过滤Mac地址Item的点击事件,代码如下所示:
R.id.item_filter_mac -> { // 过滤Mac地址
if (MVUtils.getBoolean(FILTER_MAC_FLAG)) {
mMenu.findItem(R.id.item_filter_mac).isChecked = false
MVUtils.put(FILTER_MAC_FLAG, false)
MVUtils.put(FILTER_MAC_VALUE, "")
showMsg("不过滤设备地址")
} else {
showSettingMacDialog()
}
}
首先判断是否过滤,有的话就不再过滤,没有的话就显示输入Mac地址弹窗,如果过滤了,我们就需要在扫描回调函数中增加一个过滤的选项。
override fun onScanResult(result: ScanResult) {
//过滤空设备名
...
//过滤Mac地址
if (MVUtils.getBoolean(FILTER_MAC_FLAG)) {
val filterMac: String? = MVUtils.getString(FILTER_MAC_VALUE, "")
if (filterMac!!.isNotEmpty()) {
if (!result.device.address.contains(filterMac)) return
}
}
...
}
过滤的位置可以放在过滤空设备名称之后或者之前都可以,最后还需要修改onCreateOptionsMenu()
函数中的代码如下所示:
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_scan, menu)
mMenu = menu
mMenu.findItem(R.id.item_filter_null).isChecked = MVUtils.getBoolean(FILTER_NULL_FLAG)
mMenu.findItem(R.id.item_filter_mac).isChecked = MVUtils.getBoolean(FILTER_MAC_FLAG)
return true
}
增加了一个对于Mac地址Item项是否选中的判断,下面我们可以运行看看,我们的过滤是否有效果。
这样过滤Mac地址就做好了,下面过滤RSSI信号强度。
五、过滤RSSI
与过滤Mac地址一样,过滤RSSI首先要做的就是设置RSSI,对此,我们同样在layout下创建一个dialog_settings_rssi.xml
作为弹窗的布局文件,代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="过滤RSSI" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="RSSI:"
android:textColor="@color/black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/sb_rssi"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="100"
android:min="35"
android:progress="100"
android:progressTint="@color/orange"
android:thumbTint="@color/dark_orange"
app:layout_constraintBottom_toBottomOf="@+id/textView"
app:layout_constraintEnd_toStartOf="@+id/tv_rssi"
app:layout_constraintStart_toEndOf="@+id/textView"
app:layout_constraintTop_toTopOf="@+id/textView" />
<TextView
android:id="@+id/tv_rssi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="-100 dBm"
android:textColor="@color/black"
app:layout_constraintBottom_toBottomOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btn_negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="18dp"
android:layout_weight="1"
android:text="取消"
app:layout_constraintEnd_toStartOf="@+id/btn_positive"
app:layout_constraintTop_toTopOf="@+id/btn_positive" />
<Button
android:id="@+id/btn_positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_weight="1"
android:text="确定"
app:layout_constraintEnd_toEndOf="@+id/tv_rssi"
app:layout_constraintTop_toBottomOf="@+id/tv_rssi" />
</androidx.constraintlayout.widget.ConstraintLayout>
然后就是在ScanActivity中增加一个showSettingRssi()
函数,代码如下所示:
private fun showSettingRssi() {
val dialog = BottomSheetDialog(this, R.style.BottomSheetDialogStyle)
val rssiBinding = DialogSettingRssiBinding.inflate(layoutInflater)
var progress = 100
rssiBinding.sbRssi.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
@SuppressLint("SetTextI18n")
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
rssiBinding.tvRssi.text = "-$progress dBm"
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {
progress = seekBar.progress
}
})
val rssi: Int = MVUtils.getInt(FILTER_RSSI_VALUE, 100)
rssiBinding.sbRssi.progress = rssi
rssiBinding.tvRssi.text = String.format("-%s dBm", rssi)
rssiBinding.btnPositive.setOnClickListener {
//保存
if (bleCore.isScanning()) stopScan()
MVUtils.put(FILTER_RSSI_FLAG, true)
//保存设置的RSSI值
MVUtils.put(FILTER_RSSI_VALUE, progress)
mMenu.findItem(R.id.item_filter_rssi).isChecked = true
mMenu.findItem(R.id.item_filter_rssi).title = "过滤RSSI:-$progress"
showMsg("过滤RSSI:-" + progress + "dBm")
if (!bleCore.isScanning()) startScan()
dialog.dismiss()
}
rssiBinding.btnNegative.setOnClickListener { dialog.dismiss() }
dialog.setContentView(rssiBinding.root)
dialog.show()
}
在点击确定按钮的时候,保存设置的RSSI信号强度值,如果没有设置就是默认的值,然后我们修改一下过滤RSSI Item的点击事件,代码如下所示:
R.id.item_filter_rssi -> { // 过滤RSSI
if (MVUtils.getBoolean(FILTER_RSSI_FLAG)) {
if (bleCore.isScanning()) stopScan()
//关闭过滤RSSI
MVUtils.put(FILTER_RSSI_FLAG, false)
mMenu.findItem(R.id.item_filter_rssi).isChecked = false
MVUtils.put(FILTER_RSSI_VALUE, 100)
showMsg("取消过滤RSSI")
if (!bleCore.isScanning()) startScan()
} else {
showSettingRssi()
}
}
当前已有过滤RSSI,再次点击时就会取消过滤的信息,知道你再次设置RSSI过滤值,接下来就是扫描回调中,根据这个设置项进行一次过滤:
override fun onScanResult(result: ScanResult) {
//过滤Mac地址
...
//过滤RSSI
if (MVUtils.getBoolean(FILTER_RSSI_FLAG)) {
val rssi: Int = -MVUtils.getInt(FILTER_RSSI_VALUE, 100)
if (result.rssi < rssi) {
return
}
}
...
}
最后为了保存设置项,是我们再次打开App时,UI上是正确的,我们修改onCreateOptionsMenu()函数,代码如下所示:
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_scan, menu)
mMenu = menu
mMenu.findItem(R.id.item_filter_null).isChecked = MVUtils.getBoolean(FILTER_NULL_FLAG)
mMenu.findItem(R.id.item_filter_mac).isChecked = MVUtils.getBoolean(FILTER_MAC_FLAG)
mMenu.findItem(R.id.item_filter_rssi).isChecked = MVUtils.getBoolean(FILTER_RSSI_FLAG)
if (MVUtils.getBoolean(FILTER_RSSI_FLAG)) {
mMenu.findItem(R.id.item_filter_rssi).title =
"过滤RSSI:-" + MVUtils.getInt(FILTER_RSSI_VALUE, 100)
}
return true
}
运行一下,看看效果:
关于扫描过滤的功能就写好了,本文内容介绍。
六、源码
如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~
源码地址:GoodBle