Android 12 蓝牙适配 Java版(下)

简介: Android 12 蓝牙适配 Java版(下)

Android 12 蓝牙适配 Java版(上)https://developer.aliyun.com/article/1407507


四、蓝牙扫描

  在Android6.0 - Android11.0之间,扫描蓝牙都是需要打开定位权限的,而在Android12中则不需要了,换成了BLUETOOTH_SCAN权限,那么我们下面来看看,怎么操作的。


这里扫描蓝牙就以低功耗蓝牙为例子。


① 扫描者

在MainActivity中定义如下变量

    private final String TAG = MainActivity.class.getSimpleName();
    //获取系统蓝牙适配器
    private BluetoothAdapter mBluetoothAdapter;
    //扫描者
    private BluetoothLeScanner scanner;
    //是否正在扫描
    boolean isScanning = false;


获取系统蓝牙适配器,要在initView方法中增加代码,如下图所示:

53b14bc476d44262a70c53ba9f29b8a7.png


② 扫描回调

扫描设备时会有扫描的结果,在MainActivity中增加如下代码:

  //扫描结果回调
    private final ScanCallback scanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            BluetoothDevice device = result.getDevice();
            Log.d(TAG, "name: " + device.getName() + ", address: " + device.getAddress());
        }
    };


这里可能你的device.name下面会有一个红线,这是因为AS会检查你这里需要一个BLUETOOTH_CONNECT权限,而这个权限我们在打开蓝牙时已经请求过了,那么为了避免麻烦,我们在当前MainActivity上面增加如下注解。

@SuppressLint("MissingPermission")


如下图所示:

bd72b86377ed4acc833a079f7862d912.png

这个注解加上去之后你需要小心蓝牙权限的问题。


③ 扫描方法

下面我们写一个开始扫描和停止扫描的方法,首先有一个地方触发扫描和停止扫描,修改activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <Button
        android:id="@+id/btn_open_bluetooth"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="10dp"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="打开蓝牙"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/btn_scan_bluetooth"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="10dp"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="扫描蓝牙"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_open_bluetooth" />
</androidx.constraintlayout.widget.ConstraintLayout>


然后回到MainActivity中,写扫描和停止扫描的方法。

    private void startScan() {
        if (!isScanning) {
            scanner.startScan(scanCallback);
            isScanning = true;
            binding.btnScanBluetooth.setText("停止扫描");
        }
    }
    private void stopScan() {
        if (isScanning) {
            scanner.stopScan(scanCallback);
            isScanning = false;
            binding.btnScanBluetooth.setText("扫描蓝牙");
        }
    }


扫描和停止扫描时修改一下变量值并且改动按钮的文字以表示当前是否正在扫描中。


④ 执行扫描

执行扫描就很简单了,首先我们需要在MainActivity中声明变量:

private ActivityResultLauncher<String> requestBluetoothScan;    //请求蓝牙扫描权限意图


然后在registerIntent()方法中进行赋值,代码如下所示:

        //请求BLUETOOTH_SCAN权限意图
        requestBluetoothScan = registerForActivityResult(new ActivityResultContracts.RequestPermission(), result -> {
            if (result) {
                //进行扫描
                startScan();
            } else {
                showMsg("Android12中未获取此权限,则无法扫描蓝牙。");
            }
        });


这个意图我们将在点击扫描按钮的时候会用到,下面我们在initView()中增加扫描按钮点击的代码:

        //扫描蓝牙按钮点击事件
        binding.btnScanBluetooth.setOnClickListener(v -> {
            if (isAndroid12()) {
          if (!hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) {
                    requestBluetoothConnect.launch(Manifest.permission.BLUETOOTH_CONNECT);
                    return;
                }
                if (hasPermission(Manifest.permission.BLUETOOTH_SCAN)) {
                    //扫描或者停止扫描
                    if (isScanning) stopScan();
                    else startScan();
                } else {
                    //请求权限
                    requestBluetoothScan.launch(Manifest.permission.BLUETOOTH_SCAN);
                }
            }
        });


相对来说还是比较的简洁,其实还可以更简洁。我在扫描回调中打印了日志,如果有扫描到设备的话,就会有日志,下面我们扫描一下看看:

c7f3d400203f47df94f060494ce1eb3a.png

扫描启动了,但是没有设备被扫描到,可我附近明明有蓝牙设备正在广播,这是为什么呢?


⑤ 应用不推导物理位置

这个说起来就和之前的Android 6.0 至 Android 11.0中需要定位权限才能扫描有关系了,就是因为这个推导物理位置,手机是可以通过扫描到的设备知道设备的具体位置的,所以之前需要定位权限,那么现在我没有定位权限了,你扫不到设备就很离谱,怎么解决呢?


如果您的应用不推导物理位置,那么您可以坚定地断言您的应用绝不会使用蓝牙权限来推导物理位置。为此,请完成以下步骤:


将 android:usesPermissionFlags 属性添加到 BLUETOOTH_SCAN 权限声明,并将此属性的值设为 neverForLocation。


注意:如果 android:usesPermissionFlags 中包含 neverForLocation,则会从扫描结果中过滤出某些 BLE 信标。

这是官方的说明,操作起来很简单,如下图所示:

2cbad969bebb41d9a422236b7f87561f.png

意思很明显,就是说你如果不需要推导物理地址,那么就设置一下这个权限的标识即可。下面我们再来运行一下:

12f4fab5f3ec44c18411ee1a1ed3b1f4.png

设备就扫描到了,可以看到这里有设备的Mac地址,再点一下就可以停止扫描了。

不过我们这里是控制台显示了设备,并没有在页面显示设备,下面我们完成这一步。


五、页面显示扫描设备


  显示蓝牙设备首先我们需要修改一下activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <Button
        android:id="@+id/btn_open_bluetooth"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="10dp"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="打开蓝牙"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/btn_scan_bluetooth"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="10dp"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="扫描蓝牙"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_open_bluetooth" />
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_device"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="20dp"
        android:overScrollMode="never"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_scan_bluetooth" />
</androidx.constraintlayout.widget.ConstraintLayout>


① 蓝牙设备适配器

这个里的适配器使我们自己去写的,需要显示数据的,首先我们需要创建一个蓝牙图标,在drawable包下新建一个icon_bluetooth.xml,里面的代码如下:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="48dp"
    android:height="48dp"
    android:autoMirrored="true"
    android:tint="#000000"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M14.24,12.01l2.32,2.32c0.28,-0.72 0.44,-1.51 0.44,-2.33 0,-0.82 -0.16,-1.59 -0.43,-2.31l-2.33,2.32zM19.53,6.71l-1.26,1.26c0.63,1.21 0.98,2.57 0.98,4.02s-0.36,2.82 -0.98,4.02l1.2,1.2c0.97,-1.54 1.54,-3.36 1.54,-5.31 -0.01,-1.89 -0.55,-3.67 -1.48,-5.19zM15.71,7.71L10,2L9,2v7.59L4.41,5 3,6.41 8.59,12 3,17.59 4.41,19 9,14.41L9,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM11,5.83l1.88,1.88L11,9.59L11,5.83zM12.88,16.29L11,18.17v-3.76l1.88,1.88z" />
</vector>


因为我们的设备需要显示信号强度,那么我们创建一个数据类,在com.llw.bluetooth包下新建一个MyDevice类,代码如下:

public class MyDevice {
    private BluetoothDevice device;
    private int rssi;
    public BluetoothDevice getDevice() {
        return device;
    }
    public void setDevice(BluetoothDevice device) {
        this.device = device;
    }
    public int getRssi() {
        return rssi;
    }
    public void setRssi(int rssi) {
        this.rssi = rssi;
    }
    public MyDevice(BluetoothDevice device, int rssi) {
        this.device = device;
        this.rssi = rssi;
    }
}


然后我们构建适配器的item布局,在layout包下新建一个item_device.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="device"
            type="com.llw.bluetooth.MyDevice" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="70dp">
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginStart="10dp"
            android:layout_marginTop="10dp"
            android:padding="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/icon_bluetooth" />
        <TextView
            android:id="@+id/tv_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:layout_marginTop="4dp"
            android:layout_marginEnd="10dp"
            android:text="@{device.device.name ?? `Unknown` }"
            android:textColor="@color/black"
            android:textSize="14sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toStartOf="@+id/tv_rssi"
            app:layout_constraintStart_toEndOf="@+id/imageView"
            app:layout_constraintTop_toTopOf="@+id/imageView" />
        <TextView
            android:id="@+id/tv_address"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="10dp"
            android:layout_marginBottom="4dp"
            android:text="@{device.device.address}"
            android:textSize="12sp"
            app:layout_constraintBottom_toBottomOf="@+id/imageView"
            app:layout_constraintEnd_toStartOf="@+id/tv_rssi"
            app:layout_constraintStart_toEndOf="@+id/imageView" />
        <TextView
            android:id="@+id/tv_rssi"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="10dp"
            android:text="@{device.rssi+`dBm`}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>


这里的布局数据采用了DataBinding。下面我们去写适配器,在com.llw.bluetooth包新建一个MyDeviceAdapter类,里面的代码如下:

public class MyDeviceAdapter extends RecyclerView.Adapter<MyDeviceAdapter.ViewHolder> {
    private final List<MyDevice> lists;
    public MyDeviceAdapter(List<MyDevice> lists) {
        this.lists = lists;
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ItemDeviceBinding binding = ItemDeviceBinding.inflate(LayoutInflater.from(parent.getContext()),parent, false);
        return new ViewHolder(binding);
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        ItemDeviceBinding binding = DataBindingUtil.getBinding(holder.binding.getRoot());
        if (binding != null) {
            binding.setDevice(lists.get(position));
            binding.executePendingBindings();
        }
    }
    @Override
    public int getItemCount() {
        return lists.size();
    }
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public ItemDeviceBinding binding;
        public ViewHolder(@NonNull ItemDeviceBinding itemTextDataRvBinding) {
            super(itemTextDataRvBinding.getRoot());
            binding = itemTextDataRvBinding;
        }
    }
}


这里使用原生的方式,下面我们回到MainActivity中。


② 显示列表设备

在MainActivity中创建两个变量:

    //设备列表
    private final List<MyDevice> deviceList = new ArrayList<>();
    //适配器
    private MyDeviceAdapter myDeviceAdapter;


这里我们需要思考一个问题,那就是列表设备的唯一性,因为蓝牙设备是一直广播的,所以我们扫描到的结果会有重复的设备,重复的设备有信号强度上的差异,这个地方我们要做的就是判断当前列表中是否有此设备,有就更新rssi,没有就添加,我们新增一个findDeviceIndex()函数,代码如下:

    private int findDeviceIndex(MyDevice scanDevice, List<MyDevice> deviceList) {
        int index = 0;
        for (MyDevice myDevice : deviceList) {
            if (myDevice.getDevice().getAddress().equals(scanDevice.getDevice().getAddress())) return index;
            index += 1;
        }
        return -1;
    }


下面我们再新增一个addDeviceList()函数,代码如下:

    private void addDeviceList(MyDevice device) {
        int index = findDeviceIndex(device, deviceList);
        if (index == -1) {
            deviceList.add(device);
            myDeviceAdapter.notifyDataSetChanged();
        } else {
            deviceList.get(index).setRssi(device.getRssi());
            myDeviceAdapter.notifyItemChanged(index);
        }
    }


最后我们在扫描回调中调用此方法:

700025d7687b42598307aa48305d76bf.png

最后别忘记了我们的适配器和列表都需要初始化的,我写在initView()函数中,如下图所示:

f5f926ba9110409dbb2fd999b377f78c.png

现在就可以运行了。

aa134fdb28934464ac23d8d9ef96a9bf.gif


六、适配Android12.0以下设备


当前的代码我们在Android12上是没有问题了,但是Android12以下 Android6.0以上 还是扫描不到设备,然后我们回到MainActivity中,增加一个变量:

    private ActivityResultLauncher<String> requestLocation;         //请求定位权限


然后在registerIntent()方法中进行赋值,代码如下所示:

        //请求定位权限
        requestLocation = registerForActivityResult(new ActivityResultContracts.RequestPermission(), result -> {
            if (result) {
                //扫描蓝牙
                startScan();
            } else {
                showMsg("Android12以下,6及以上需要定位权限才能扫描设备");
            }
        });


然后我们回到扫描按钮的点击事件,修改代码如下所示:

        //扫描蓝牙按钮点击事件
        binding.btnScanBluetooth.setOnClickListener(v -> {
            if (isAndroid12()) {
                if (!hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) {
                    requestBluetoothConnect.launch(Manifest.permission.BLUETOOTH_CONNECT);
                    return;
                }
                if (hasPermission(Manifest.permission.BLUETOOTH_SCAN)) {
                    //扫描或者停止扫描
                    if (isScanning) stopScan();
                    else startScan();
                } else {
                    //请求权限
                    requestBluetoothScan.launch(Manifest.permission.BLUETOOTH_SCAN);
                }
            } else {
                if (hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
                    //扫描或者停止扫描
                    if (isScanning) stopScan();
                    else startScan();
                } else {
                    requestLocation.launch(Manifest.permission.ACCESS_FINE_LOCATION);
                }
            }
        });


下面我们在Android10.0上运行一下:

855b5d9110c74ca0a7c74ed6ad4d7be7.gif


七、源码


如果你觉得代码对你有帮助的话,不妨Fork或者Star一下~

GitHub:Android12Bluetooth-Java

相关文章
|
2月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
121 1
|
2月前
|
调度 Android开发 UED
Android经典实战之Android 14前台服务适配
本文介绍了在Android 14中适配前台服务的关键步骤与最佳实践,包括指定服务类型、请求权限、优化用户体验及使用WorkManager等。通过遵循这些指南,确保应用在新系统上顺畅运行并提升用户体验。
183 6
|
3月前
|
存储 搜索推荐 Java
探索安卓开发中的自定义视图:打造个性化UI组件Java中的异常处理:从基础到高级
【8月更文挑战第29天】在安卓应用的海洋中,一个独特的用户界面(UI)能让应用脱颖而出。自定义视图是实现这一目标的强大工具。本文将通过一个简单的自定义计数器视图示例,展示如何从零开始创建一个具有独特风格和功能的安卓UI组件,并讨论在此过程中涉及的设计原则、性能优化和兼容性问题。准备好让你的应用与众不同了吗?让我们开始吧!
|
3月前
|
Android开发
Android 配置蓝牙遥控器键值
本文详细介绍了Android系统中配置蓝牙遥控器键值的步骤,包括查看设备号、配置键位映射文件(kl文件)、部署kl文件以及调试过程,确保蓝牙遥控器的按键能正确映射到Android系统对应的按键功能。
131 1
|
3月前
|
Java 调度 Android开发
Android经典实战之Kotlin的delay函数和Java中的Thread.sleep有什么不同?
本文介绍了 Kotlin 中的 `delay` 函数与 Java 中 `Thread.sleep` 方法的区别。两者均可暂停代码执行,但 `delay` 适用于协程,非阻塞且高效;`Thread.sleep` 则阻塞当前线程。理解这些差异有助于提高程序效率与可读性。
71 1
|
3月前
|
Java Android开发
解决Android编译报错:Unable to make field private final java.lang.String java.io.File.path accessible
解决Android编译报错:Unable to make field private final java.lang.String java.io.File.path accessible
455 1
|
3月前
|
Android开发
Cannot create android app from an archive...containing both DEX and Java-bytecode content
Cannot create android app from an archive...containing both DEX and Java-bytecode content
35 2
|
3月前
|
IDE Java Linux
探索安卓开发:从基础到进阶的旅程Java中的异常处理:从基础到高级
【8月更文挑战第30天】在这个数字时代,移动应用已经成为我们日常生活中不可或缺的一部分。安卓系统由于其开放性和灵活性,成为了开发者的首选平台之一。本文将带领读者踏上一段从零开始的安卓开发之旅,通过深入浅出的方式介绍安卓开发的基础知识、核心概念以及进阶技巧。我们将一起构建一个简单的安卓应用,并探讨如何优化代码以提高性能和应用的用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供宝贵的知识和启发。
|
4月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
【7月更文挑战第28天】在 Android 开发中, NDK 让 Java 与 C++ 混合编程成为可能, 从而提升应用性能。**为何选 NDK?** C++ 在执行效率与内存管理上优于 Java, 特别适合高性能需求场景。**环境搭建** 需 Android Studio 和 NDK, 工具如 CMake。**JNI** 构建 Java-C++ 交互, 通过声明 `native` 方法并在 C++ 中实现。**实战** 示例: 使用 C++ 计算斐波那契数列以提高效率。**总结** 混合编程增强性能, 但增加复杂性, 使用前需谨慎评估。
140 4
|
4月前
|
IDE API Android开发
安卓与iOS开发环境的差异及适配策略
在移动应用开发的广阔舞台上,Android和iOS两大操作系统各据一方,各自拥有独特的开发环境和工具集。本文旨在深入探讨这两个平台在开发环境上的关键差异,并提供有效的适配策略,帮助开发者优化跨平台开发流程。通过比较Android的Java/Kotlin和iOS的Swift/Objective-C语言特性、IDE的选择、以及API和系统服务的访问方式,本文揭示了两个操作系统在开发实践中的主要分歧点,并提出了一套实用的适配方法,以期为移动开发者提供指导和启示。