Android车载应用开发与分析(13)- 系统设置-蓝牙设置

1. 前言

Android 车载应用开发与分析是一个系列性的文章,这个是第13篇分析系统设置,该系列文章旨在分析原生车载Android系统中核心应用的实现方式,帮助初次从事车载应用开发的同学,更好地理解车载应用开发的方式,积累android系统应用的开发经验。

2. 系统设置概述

系统设置是车载Android系统中非常重要的一个系统级应用,是整个车载IVI系统的控制中心,整车的音效、无线通信、状态信息、安全信息等等都是需要通过系统设置来查看和控制。例如,开启/关闭 wifi 和蓝牙,查看每个应用的网络流量,开启调试信息等。

有车载经验的同学,应该都见过下面这种字体颜色怪异的系统设置,这其实是手机的系统设置移植到车载系统中的样子。一个车载 Android 项目启动时,大都会选择保留功能更全的手机原生系统设置,而不是使用车载版本的系统设置。


车载原生的系统设置是长这样的



鉴于系统设置的功能非常多,由于系统设置的源码也比较复杂,而且一般我们编写车载系统设置也不会沿用原生的代码架构,所以本篇不再介绍系统设置源码架构和初始化流程,主要聚焦于系统 API 的运用。

本次就先从蓝牙模块开始入手。

3. 蓝牙简介

蓝牙Bluetooth),是一种无线通讯技术标准,用来让固定与移动设备,在短距离间交换资料,以形成个人局域网(PAN)。其使用短波特高频(UHF)无线电波,经由2.4至2.485 GHz的ISM频段来进行通信。1994年由电信商爱立信(Ericsson)发展出这个技术。它最初的设计,是希望创建一个RS-232数据线的无线通信替代版本。它能够连接多个设备,克服同步的问题。

蓝牙技术目前由蓝牙技术联盟(SIG)来负责维护其技术标准,其成员已超过三万,分布在电信、电脑、网络与消费性电子产品等领域。IEEE曾经将蓝牙技术标准化为IEEE 802.15.1,但是这个标准已经不再继续使用。

3.1. 蓝牙分类

2010年7月7日,蓝牙技术联盟推出了蓝牙4.0规范,蓝牙4.0包括3个子规范,即“低功耗蓝牙”、“传统蓝牙”和“高速蓝牙”。

  • 低功耗蓝牙

蓝牙低功耗Bluetooth Low Energy,或称Bluetooth LEBLE,旧商标Bluetooth Smart)也称蓝牙低能耗低功耗蓝牙,是蓝牙技术联盟设计和销售的一种个人局域网技术,旨在用于医疗保健、运动健身、信标、安防、家庭娱乐等领域的新兴应用。相较经典蓝牙,低功耗蓝牙旨在保持同等通信范围的同时显著降低功耗和成本。

  • 经典蓝牙

经典蓝牙模块,一般用于数量比较大的传输:如语音、音乐等较高数据量传输

  • 高速蓝牙

高速蓝牙主攻数据交换与传输

3.2. 蓝牙规范

蓝牙规范(Bluetooth profile),蓝牙技术联盟定义了许多Profile。Profile目的是要确保Bluetooth设备间的互通性(interoperability)。但Bluetooth产品无须实现所有的Bluetooth规范Profile。Bluetooth 版本 1.1 定义了13个Profiles。下面几个是Android中常用的:

PBAP 协议,电话本访问协议(Phone Book Access Profile),是一种基于OBEX的上层协议,该协议可以同步手机这些具有电话本功能设备上的通讯录和通话记录等信息。

HFP 协议,免手持设备规范(Hands-Free Profile),移动电话和免提装置之间的远程无线控制和语音连接就是通过 HFP 协议。

A2DP 协议, 蓝牙立体声音频传输规范(Advance Audio Distribution Profile),规定了使用蓝牙异步传输信道方式,传输高质量音乐文件数据的协议堆栈软件和使用方法,基于该协议就能通过以蓝牙方式传输高质量的立体声音乐。分为1.1版和1.2版,只要连接双方支持A2DP协议都能以16 bits,44.1 kHz的质量传输声音信号。假如有一方没有支持A2DP的话,只能以8 bits,8 kHz的质量的免手持设备规范(Handsfree Profile)传输模式,声音质量会大打折扣。

3. 蓝牙设置关键API

3.1. BluetoothAdapter

API 文档地址:https://developer.android.google.cn/reference/kotlin/android/bluetooth/BluetoothAdapter

BluetoothAdapter表示本地设备蓝牙适配器(此类中的操作是线程安全的)。必须通过BluetoothAdapter才能执行基本的蓝牙任务,例如启动设备发现、查询绑定(配对)设备列表、使用已知MAC地址实例化Bluetooth device、创建BluetootServerSocket以侦听来自其他设备的连接请求,以及开始扫描Bluetooch LE设备。

BluetoothAdapter的初始化方式有两种:

  • JELLY_BEAN_MR1(API 17)及以下,使用getDefaultAdapter()
 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
  • JELLY_BEAN_MR1(API 17)以上,使用 BluetoothManager.getAdapter()
BluetoothManager btManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (btManager != null) {
    BluetoothAdapter btAdapter = btManager.getAdapter();
}

从根本上来说,BluetoothAdapter是所有蓝牙操作的起点。

拥有BluetoothAdapter后,可以使用getBondedDevices()获取一组BluetoothDevice对象,表示所有配对过的设备;使用startDiscovery()启动设备发现;或创建BluetoothServerSocket以监听传入的RFComm连接请求,并使用listenUsingRfcommWithServiceRecord(java.lang.String,java.util.UUID);使用listenUsingL2capChannel()监听传入的L2CAP面向连接的通道(CoC)连接请求;或使用startLeScan(android.Bluetooth.BluetoothAdapter.LeScanCallback)启动蓝牙LE设备扫描。

3.2. BluetoothDevice

API 文档地址:https://developer.android.google.cn/reference/android/bluetooth/BluetoothDevice

BluetoothDevice是远程蓝牙设备的实体类。 通过BluetoothDevice可以创建与相应设备的连接或查询有关蓝牙设备的信息,例如名称、地址、类和绑定状态。

要获取BluetoothDevice有多种方式:

  • 如果已经知道蓝牙的mac地址,可以使用BluetoothAdapter.getRemoteDevice(String mac)创建一个蓝牙设备。

  • BluetoothAdapter.getBondedDevices()返回的一组绑定设备中获取一个。然后,可以通过蓝牙 BR/EDR 使用 createRfcommSocketToServiceRecord(java.util.UUID) 或通过蓝牙LE使用 createL2capChannel(int),打开一个BluetoothSocket与远程设备通信。

  • 使用BluetoothAdapter.startDiscovery()开启蓝牙搜索,然后监听BluetoothDevice.ACTION_FOUND广播也可以获取到BluetoothDevice

3.3. 其它关键类

在 Android 的 framework 目录下封装了很多实用的蓝牙组件,不过这些类是 framework 的私有类,并不能通过应用层的Android API直接调用,实际项目中根据需要将这些类移植到应用中再做修改。不建议直接修改 framework 层的代码!这样可能会导致一些原生应用无法正常运行。

源码位置:/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/

4. 蓝牙设置关键功能实现

系统设置作为系统级应用,在使用蓝牙设置功能时,需要添加以下权限。

1)基本蓝牙权限,需要此权限才能执行任何蓝牙通信,例如请求连接、接受连接和传输数据等

<uses-permission android:name="android.permission.BLUETOOTH" />

2)蓝牙设置“超级管理员”权限,需要此权限才能启动设备发现或操纵蓝牙设置

<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

3)允许应用程序在无需用户交互的情况下配对蓝牙设备,并允许或禁止电话簿访问或消息访问

<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />

4)位置权限,因为蓝牙扫描可用于收集用户的位置信息。此类信息可能来自用户自己的设备,以及在商店和交通设施等位置使用的蓝牙信标

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

4.1. 开启/关闭 蓝牙

车载蓝牙设置的主页是BluetoothSettingsFragment,它管理蓝牙适配器的开关, 它还显示已配对的设备和设备配对功能的入口点。

源码位置:/packages/apps/Car/Settings/src/com/android/car/settings/bluetooth/BluetoothSettingsFragment.java

设定蓝牙开启或关闭的方法如下所示,BluetoothAdapter的初始化以及各个 API 的含义在上面已经介绍过了,这里就不再赘述。

private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

private final MenuItem.OnClickListener mBluetoothSwitchListener = item -> {
        item.setEnabled(false);
        if (item.isChecked()) {
            // 开启蓝牙
            mBluetoothAdapter.enable();
        } else {
            // 关闭蓝牙
            mBluetoothAdapter.disable();
        }
    };

此外,我们必须要监听BluetoothAdapter.ACTION_STATE_CHANGED广播,该广播表示蓝牙状态发生变化,此时我们需要同步一下蓝牙的状态,来保证内部的状态机或 UI 一直是正确的。

private  final IntentFilter mIntentFilter  = new  IntentFilter(
        BluetoothAdapter.ACTION_STATE_CHANGED)  ;

@Override
public void onStart() {
    super.onStart();
    // 注册蓝牙状态的广播  
    requireContext().registerReceiver(mReceiver, mIntentFilter) ;
    mLocalBluetoothManager.setForegroundActivity(requireActivity());
    // 页面初始化后,要同步一次蓝牙开关的状态
    handleStateChanged(mBluetoothAdapter.getState());
}

private  final BroadcastReceiver mReceiver  = new BroadcastReceiver() {
    @Override
    public  void onReceive(Context context, Intent intent) {
        int  state = intent.getIntExtra(BluetoothAdapter.E XTRA_STATE,  BluetoothAdapter.ERROR)  ;
        handleStateChanged(state);
    }
};

private  void handleStateChanged(int state) {
    // 暂时清除监听器,以便我们在尝试反映适配器状态时不会更新适配器。  mBluetoothSwitch.setOnClickListener(null ) ;
    switch ( state) {
        case BluetoothAdapter.S TATE_TURNING_ON: 
            mBluetoothSwitch.setEnabled(false ) ;
            mBluetoothSwitch.setChecked(true ) ;
            break ;
        case BluetoothAdapter.S TATE_ON: 
            mBluetoothSwitch.setEnabled(!isUserRestricted());
            mBluetoothSwitch.setChecked(true ) ;
            break ;
        case  BluetoothAdapter.S TATE_TURNING_OFF: 
            mBluetoothSwitch.setEnabled(false ) ;
            mBluetoothSwitch.setChecked(false ) ;
            break ;
        case BluetoothAdapter.S TATE_OFF: 
        default :
            mBluetoothSwitch.setEnabled(!isUserRestricted());
            mBluetoothSwitch.setChecked(false ) ;
    }
    mBluetoothSwitch.setOnClickListener(mBluetoothSwitchListener);
}

有的博客中可能会看到使用的是LocalBluetoothAdapter,它的源码位置是/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java ,不过根据官方的注释,该类已经过时,现在更推荐使用BluetoothAdapter

4.2. 查找已连接、已配对的蓝牙设备

开启蓝牙后,紧接着我们就需要开始搜索蓝牙设备,但是在执行搜索之前,应该先查询配对设备集,以查看所需的设备是否已知。已连接已配对的蓝牙还是显示这个页面中

需要注意文字上的描述差异:已配对的设备和已连接的设备之间是有区别的:

  • 已配对(paired 或 bonded)意味着两个设备知道彼此的存在,具有可用于身份验证的共享链接密钥,并且能够彼此建立加密连接。
  • 已连接(connected)意味着设备当前共享RFCOMM信道,并且能够相互传输数据。当前的蓝牙 API 要求在建立 RFCOMM 连接之前配对设备。当启动与蓝牙 API 的加密连接时,将自动执行配对。

借用手机的蓝牙设置界面举个例子,红框内的是已连接的设备,绿框内的是已配对的设备,如下图所示

获取连接蓝牙设备有以下几步:

1)注册广播BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED监听蓝牙的连接状态

该广播的intent中有三个extras,分别是

  • BluetoothAdapter.EXTRA_CONNECTION_STATE:当前连接状态
  • BluetoothAdapter . EXTRA_PREVIOUS_CONNECTION_STATE:之前的连接状态
  • BluetoothDevice.EXTRA_DEVICE:蓝牙设备

注册此广播需要蓝牙权限android.Manifest.permission.BLUETOOTH。

2)判断连接状态,如果已连接状态,则通过EXTRA_DEVICE获取已连接的蓝牙设备


获取配对蓝牙设备有以下几步:

1)注册广播BluetoothDevice . ACTION_BOND_STATE_CHANGED监听蓝牙的配对状态

该广播的intent中有四个extras,分别是

  • BluetoothDevice.EXTRA_BOND_STATE:当前配对状态
  • BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE:之前的配对状态
  • BluetoothDevice.EXTRA_DEVICE:蓝牙设备
  • BluetoothDevice.EXTRA_REASON: 当 EXTRA_BOND_STATE 为 BOND_NONE 时,可以通过EXTRA_REASON 获取一个结果代码。

2)判断配对状态,如果已配对状态,则通过EXTRA_DEVICE获取已连接的蓝牙设备


了解步骤之后,我们来看在车载Settings的源码中是如何处理的。

BluetoothSettingsFragment的布局文件bluetooth_settings_fragment.xml中,使用了一个BluetoothBondedDevicesPreferenceController的类,这个类的上一层继承自BluetoothPreferenceController,通过在BluetoothPreferenceController中向LocalBluetoothManager.BluetoothEventManager注册了一个BluetoothCallback来监听蓝牙设备的状态回调。

private final LocalBluetoothManager mBluetoothManager;
   
protected void onStartInternal() {
    mBluetoothManager.getEventManager().registerCallback(this);
}

LocalBluetoothManager.BluetoothEventManager是 framework 的私有类,蓝牙所有广播事件都是在这里完成注册和分发的,是我们需要重点关注的类。

// 蓝牙开关的广播 
addHandler(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedHandler());

// 蓝牙连接状态的广播 
addHandler(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED,new ConnectionStateChangedHandler());

// 蓝牙扫描的广播 
addHandler(BluetoothAdapter.ACTION_DISCOVERY_STARTED,new ScanningStateChangedHandler(true));
addHandler(BluetoothAdapter.ACTION_DISCOVERY_FINISHED,new ScanningStateChangedHandler(false));
addHandler(BluetoothDevice.ACTION_FOUND, new DeviceFoundHandler());
addHandler(BluetoothDevice.ACTION_NAME_CHANGED, new NameChangedHandler());
addHandler(BluetoothDevice.ACTION_ALIAS_CHANGED, new NameChangedHandler());

// 蓝牙配对状态的广播 
addHandler(BluetoothDevice.ACTION_BOND_STATE_CHANGED, new BondStateChangedHandler());

// Fine-grained state broadcasts a
ddHandler(BluetoothDevice.ACTION_CLASS_CHANGED, new ClassChangedHandler());
addHandler(BluetoothDevice.ACTION_UUID, new UuidChangedHandler());
addHandler(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED, new BatteryLevelChangedHandler());

// 活跃设备的广播 
addHandler(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,new ActiveDeviceChangedHandler());

// 耳机状态改变广播 
addHandler(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,new AudioModeChangedHandler());
addHandler(TelephonyManager.ACTION_PHONE_STATE_CHANGED,new AudioModeChangedHandler());

// ACL 连接更改的广播 
addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler());
addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler());

源码位置:/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java

以下是处理连接状态的蓝牙设备

 // Generic connected/not broadcast
addHandler(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED, new ConnectionStateChangedHandler());

// 这个 Handler 不是Android.OS中的handler,它只是一个接口
private  class ConnectionStateChangedHandler implements Handler {
    @Override
    public  void onReceive(Context context, Intent intent, BluetoothDevice device) {
        // 更新本地缓存,并返回一个二次封装类
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
        int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, BluetoothAdapter.ERROR);
        // 分发 连接 状态
dispatchConnectionStateChanged(cachedDevice, state);
    }
}

以下是处理配对状态的蓝牙设备

 // Pairing broadcasts
addHandler(BluetoothDevice.ACTION_BOND_STATE_CHANGED, new BondStateChangedHandler());

public  void onReceive (Context context, Intent intent, BluetoothDevice device){
    if (device == null) {
        Log.e(TAG, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE" );
        return;
    }
    int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR);
    // 更新本地缓存,并返回一个二次封装的蓝牙实体类
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
    if (cachedDevice == null) {
        Log.w(TAG, "Got bonding state changed for " + device + ", but we have no record of that device." );
        cachedDevice = mDeviceManager.addDevice(device);
    }
    // 分发 配对 状态
 for (BluetoothCallback callback : mCallbacks) {
        callback.onDeviceBondStateChanged(cachedDevice, bondState);
    }
    cachedDevice.onBondingStateChanged(bondState);

    if (bondState == BluetoothDevice.BOND_NONE) {
        /* 检查我们是否需要移除其他hearing aid设备 */
 if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
            mDeviceManager.onDeviceUnpaired(cachedDevice);
        }
        int reason = intent.getIntExtra(BluetoothDevice.EXTRA_REASON,
                BluetoothDevice.ERROR);
        // 显示错误信息
        showUnbondMessage(context, cachedDevice.getName(), reason);
    }
}

4.3. 扫描蓝牙设备

开启蓝牙后,紧接着我们就需要开始对蓝牙设备的扫描,检索外部蓝牙设备有如下几个步骤:

1)注册 BluetoothAdapter.ACTION_DISCOVERY_STARTED、BluetoothAdapter.ACTION_DISCOVERY_FINISHED 监听蓝牙扫描状态

2)注册 BluetoothDevice.ACTION_FOUND 监听扫描期间是否发现蓝牙设备

该广播的 intent 包含以下 extras

  • BluetoothDevice.EXTRA_DEVICE:蓝牙设备
  • BluetoothDevice.EXTRA_CLASS:BluetoothClass,它表示蓝牙类,它描述了设备的一般特性和功能。 例如,蓝牙类将指定通用设备类型,如电话、计算机或耳机,以及它是否能够提供音频或电话等服务。每个蓝牙类都由零个或多个服务类和一个设备类组成。 设备类进一步分为主要和次要设备类组件。

下面这些 extras 不一定总是可用的,而且也不常用,要注意

  • BluetoothDevice.EXTRA_NAME:蓝牙设备的名称
  • BluetoothDevice.EXTRA_RSSI:蓝牙设备的信号强度
  • BluetoothDevice.EXTRA_IS_COORDINATED_SET_MEMBER:它包含设备是否被发现为协调集成员的信息。 与属于集合的设备配对将触发与其余集合成员的配对。 有关详细信息,请参阅蓝牙 CSIP 规范。

3)调用BluetoothAdapter.startDiscovery()开启蓝牙扫描

4)从 intent 中获取扫描到的蓝牙设备

以上的步骤需要android.permission.BLUETOOTH权限,对于API 31以上的Android系统需要 android.permission.BLUETOOTH_SCAN权限。


ok,继续来看车载Settings的源码中是如何处理扫描的。
在车载Settings中BluetoothPairingSelectionFragment显示蓝牙设备列表。 当此fragment可见时,会有一个进度条以指示发现或配对进度。

UI的源码位置:/packages/apps/Car/Settings/src/com/android/car/settings/bluetooth/BluetoothPairingSelectionFragment.java

<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto"
    android:title="@string/bluetooth_pair_new_device"
    android:key="@string/psk_bluetooth_pairing_selection">
    <!-- 本机蓝牙的名称 -->
    <Preference
        android:key="@string/pk_bluetooth_name"
        android:title="@string/bluetooth_name"
        settings:controller="com.android.car.settings.bluetooth.BluetoothNamePreferenceController"/>
    <!-- 未配对的蓝牙设备 -->
    <PreferenceCategory
        android:key="@string/pk_bluetooth_available_devices"
        android:title="@string/bluetooth_available_devices"
        settings:controller="com.android.car.settings.bluetooth.BluetoothUnbondedDevicesPreferenceController"/>
    <!-- 本机蓝牙设备的地址 -->
    <Preference
        android:icon="@drawable/ic_settings_about"
        android:key="@string/pk_bluetooth_address"
        android:selectable="false"
        settings:controller="com.android.car.settings.bluetooth.BluetoothAddressPreferenceController"/>
</PreferenceScreen>

页面逻辑源码位置:/packages/apps/Car/Settings/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceController.java

开始或停止蓝牙搜索的源码如下所示

private void enableScanning() {
    mIsScanningEnabled = true;
    if (!mBluetoothAdapter.isDiscovering()) {
        // 开启扫描
        mBluetoothAdapter.startDiscovery();
    }
    // 开启蓝牙可见
    mAlwaysDiscoverable.start();
    getPreference().setEnabled(true);
}

private  void disableScanning() {
    mIsScanningEnabled = false;
    getPreference().setEnabled(false);
    // 关闭蓝牙可见
    mAlwaysDiscoverable.stop();
    if (mBluetoothAdapter.isDiscovering()) {
        // 取消扫描
        mBluetoothAdapter.cancelDiscovery();
    }
}

在界面主动开启蓝牙搜索后,对于ACTION_DISCOVERY_STARTEDACTION_DISCOVERY_FINISHEDACTION_FOUND*这三个广播的监听都是在 framework 层私有代码中完成的。就像之前说的,蓝牙的广播时间基本都是在这个类中完成监听和事件分发的。

源码位置:/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java

addHandler(BluetoothAdapter.ACTION_DISCOVERY_STARTED, new ScanningStateChangedHandler(true));
addHandler(BluetoothAdapter.ACTION_DISCOVERY_FINISHED, new ScanningStateChangedHandler(false));

private  class ScanningStateChangedHandler implements Handler {
    private  final  boolean mStarted;

    ScanningStateChangedHandler(boolean started) {
        mStarted = started;
    }

    public  void onReceive(Context context, Intent intent, BluetoothDevice device) {
        for (BluetoothCallback callback : mCallbacks) {
            callback.onScanningStateChanged(mStarted);
        }
        mDeviceManager.onScanningStateChanged(mStarted);
    }
}   

最后在 UI 界面收到的回调时,条件允许则开启搜索。

@Override
public  void onScanningStateChanged(boolean started) {
    LOG.d( "onScanningStateChanged started: " + started + " mIsScanningEnabled: " + mIsScanningEnabled);
    if (!started && mIsScanningEnabled) {
        enableScanning();
    }
}

开启搜索后,就需要处理搜索到的蓝牙设备。

addHandler(BluetoothDevice.ACTION_FOUND, new DeviceFoundHandler());

private  class BluetoothBroadcastReceiver extends BroadcastReceiver {

    @Override
    public  void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        Handler handler = mHandlerMap.get(action);
        if (handler != null) {
            handler.onReceive(context, intent, device);
        }
    }
}

在获取到BluetoothDevice后,还需要对其进行过滤,只保留未配对、未连接的实体,最后把BluetoothDevice封装成CachedBluetoothDevice回调给显示UI的类,将搜索到蓝牙设备显示在 UI 上。

CachedBluetoothDevice是对BluetoothDevice的进一步封装,其内部实现了蓝牙的连接、配对、状态获取等功能。它是 framework 层的一个私有类,源码位置:/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java

// BluetoothEventManager.java
private  class DeviceFoundHandler implements Handler {

    public  void onReceive(Context context, Intent intent, BluetoothDevice device) {
        short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
        String name = intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
        // TODO 获取UUID。它们应适用于2.1版本。 
 // 现在跳过,有一个bluez问题,即使是2.1版本,也无法获得uuid。
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
        if (cachedDevice == null) {
            cachedDevice = mDeviceManager.addDevice(device);
            Log.d(TAG, "DeviceFoundHandler created new CachedBluetoothDevice: " + cachedDevice);
        } else  if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED
&& !cachedDevice.getDevice().isConnected()) {
            // 调度设备添加回调以在发现模式下显示绑定但未连接的设备
dispatchDeviceAdded(cachedDevice);
            Log.d(TAG, "DeviceFoundHandler found bonded and not connected device:" + cachedDevice);
        } else {
            Log.d(TAG, "DeviceFoundHandler found existing CachedBluetoothDevice:" + cachedDevice);
        }
        cachedDevice.setRssi(rssi);
        cachedDevice.setJustDiscovered(true);
    }
}

void dispatchDeviceAdded(CachedBluetoothDevice cachedDevice){
    for (BluetoothCallback callback : mCallbacks) {
        callback.onDeviceAdded(cachedDevice);
    }
}
// BluetoothDevicesGroupPreferenceController.java
@Override
public  final  void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
    // 刷新 UI
    refreshUi();
}

最后一步refreshUi可以看出,并没有用到cachedDevice来更新 UI,是因为LocalBluetoothManager中已经缓存了所有的扫描到的蓝牙设备,只需要将从LocalBluetoothManager中把 list 取出更新UI 界面即可。

4.4 蓝牙配对

蓝牙的配对有如下几步:

1)注册android.bluetooth.device.action.PAIRING_REQUEST广播

2)取消扫描过程

在执行配对之前, 务必停止蓝牙搜索,因为搜索过程会显著减少可用于连接的带宽,导致连接操作失败。

3)执行BluetoothDevice.createBond()进行配对

执行配对后,根据需要开启蓝牙设备的以下权限

BluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED)
BluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED)

4)处理PAIRING_REQUEST广播消息,显示对应的UI


继续看源码中是如何处理的,在蓝牙设备列表中点击未配对的蓝牙设备

源码位置:/packages/apps/Car/Settings/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceController.java

@Override
protected  void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice) {
    if (cachedDevice.startPairing()) {
        LOG.d( "startPairing" );
        // 如果有服务端允许(通常是电话),则表明该客户端(车辆)希望访问联系人(PBAP)和消息(MAP)。
cachedDevice.getDevice().setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
        cachedDevice.getDevice().setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
    } else {
        BluetoothUtils.showError(getContext(), cachedDevice.getName(),
                R.string.bluetooth_pairing_error_message);
        refreshUi();
    }
}

源码位置:/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java

public boolean startPairing() {
    // 扫描时配对是不可靠的,因此取消扫描
if (mLocalAdapter.isDiscovering()) {
        mLocalAdapter.cancelDiscovery();
    }
    if (!mDevice.createBond()) {
        return false;
    }
    return true;
}

蓝牙的配对过程会有一个 dialog 的提示给到用户,这个dialog 也需要通过监听广播实现。

<receiver android:name=".bluetooth.BluetoothPairingRequest">
    <intent-filter>
        <action android:name="android.bluetooth.device.action.PAIRING_REQUEST" />
    </intent-filter>
</receiver>

BluetoothPairingRequest是任何蓝牙配对请求的接收器。它会检查蓝牙设置当前是否可见,并显示 PIN、密码或确认输入对话框。 否则,它会启动BluetoothPairingService,它会在状态栏中启动一个通知,单击该通知会显示相同的对话框。

public final class BluetoothPairingRequest extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (!action.equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) {
            return;
        }
        // 将广播意图转换为活动意图
Intent pairingIntent = BluetoothPairingService.getPairingDialogIntent(context, intent);

        PowerManager powerManager =
                (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        BluetoothDevice device =
                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        String deviceAddress = device != null ? device.getAddress() : null;
        String deviceName = device != null ? device.getName() : null;
        // 判断dialog 是否已经显示
boolean shouldShowDialog = BluetoothUtils.shouldShowDialogInForeground(
                context, deviceAddress, deviceName);
        // 判断屏幕是否开启
if (powerManager.isInteractive() && shouldShowDialog) {
            // 由于屏幕已打开且BT相关的活动在前台,因此只需打开对话框
context.startActivityAsUser(pairingIntent, UserHandle.CURRENT);
        } else {
            // 发布一个通知,用于触发 dialog
intent.setClass(context, BluetoothPairingService.class);
            context.startServiceAsUser(intent, UserHandle.CURRENT);
        }
    }
}
 

BluetoothPairingService核心代码如下,在BluetoothPairingService中还需要监听ACTION_BOND_STATE_CHANGED广播,如果配对完成了需要取消状态栏的消息。

 // 转换 intent 的方法。
public static Intent getPairingDialogIntent(Context context, Intent intent) {
    BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    // 获取配对类型
int type = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT,
            BluetoothDevice.ERROR);
    Intent pairingIntent = new Intent();
    pairingIntent.setClass(context, BluetoothPairingDialog.class);
    pairingIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
    pairingIntent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, type);
    // 获取配对的key
if (type == BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION ||
            type == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY ||
            type == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) {
        int pairingKey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY,
                BluetoothDevice.ERROR);
        pairingIntent.putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, pairingKey);
    }
    pairingIntent.setAction(BluetoothDevice.ACTION_PAIRING_REQUEST);
    pairingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    return pairingIntent;
}

蓝牙进行配对时会显示 PIN,以及是否同意读取电话本等信息,这些内容都包含在ACTION_PAIRING_REQUEST广播的intent中,具体获取方式在上述代码已经添加注释。其中需要注意配对时的不同的type需要显示不同的界面。

提示用于需要输入密钥/PIN:

BluetoothDevice.PAIRING_VARIANT_PIN
BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS
BluetoothDevice.PAIRING_VARIANT_PASSKEY

提示用户是否同意配对请求:

BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
BluetoothDevice.PAIRING_VARIANT_CONSENT:
BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:

通知用户配对请求并向他们显示设备的 PIN/ 密钥:

BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN

接下就是由用户确认,是否同意配对请求:

  • 用户拒绝配对请求的处理流程:
private BluetoothDevice mDevice;

@Override
public void onDialogNegativeClick(BluetoothPairingDialogFragment dialog) {
    onCancel();
}

/**
* 一种正确结束与蓝牙设备通信的方法。
* BluetoothPairingDialogFragment 关闭时将调用它。
*/
public void onCancel() {
    LOG.d("Pairing dialog canceled");
    mDevice.cancelPairing();
}
  • 用户同意配对请求的处理流程:
@Override
public void onDialogPositiveClick(BluetoothPairingDialogFragment dialog) {
    if (getDialogType() == USER_ENTRY_DIALOG) {
        onPair(mUserInput);
    } else {
        onPair(null);
    }
}

/**
* 处理与蓝牙设备的必要通信以建立成功配对
* 参数:密码 - - 我们将尝试与设备配对的密码。
*/
private void onPair(String passkey) {
    LOG.d("Pairing dialog accepted");
    switch (mType) {
        case BluetoothDevice.PAIRING_VARIANT_PIN:
        case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
            mDevice.setPin(passkey);
            break;
        case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
            int pass = Integer.parseInt(passkey);
            break;
        
        case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
        case BluetoothDevice.PAIRING_VARIANT_CONSENT:
            mDevice.setPairingConfirmation(true);
            break;

        case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
        case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
        case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
            // Do nothing.
break;
        default:
            LOG.e("Incorrect pairing type received");
    }
}

以上就是一个蓝牙配对的全部流程。如果是已配对的蓝牙设备,则直接连接即可

public  void connect() {
    if (!ensurePaired()) {
        return;
    }
    mConnectAttempted = SystemClock.elapsedRealtime();
    connectAllEnabledProfiles();
}

private  void connectAllEnabledProfiles() {
    synchronized (mProfileLock) {
        // 如果没有,请尝试初始化配置文件。
 if (mProfiles.isEmpty()) {
            // 如果 mProfiles 为空,则不要调用 updateProfiles。
 // 这会在配对期间导致与 carkits 的竞争条件,其中 RemoteDevice.UUIDs 已从蓝牙堆栈更新,但 ACTION.uuid 尚未发送。
 // 最终将收到 ACTION.uuid,这将触发各种配置文件的连接如果 UUID 尚不可用,则连接将在 ACTION_UUID 意图到达时发生。
Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice);
            return;
        }
        mLocalAdapter.connectAllEnabledProfiles(mDevice);
    }
}

private  boolean ensurePaired() {
    if (getBondState() == BluetoothDevice.BOND_NONE) {
        startPairing();
        return  false;
    } else {
        return  true;
    }
}
  1. 设置蓝牙可见性

默认情况下,其它蓝牙设备是无法搜索到当前的蓝牙设备的,必须使用下面的代码将蓝牙设备设定为可见状态,timeout 为蓝牙可见时间,超过这个时间,蓝牙就会恢复到默认状态,最长可以设定为1个小时。

BluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout);

在原生系统设置中由AlwaysDiscoverable管理蓝牙可见性的类。

源码位置:/packages/apps/Car/Settings/src/com/android/car/settings/bluetooth/BluetoothDevicePreferenceController.java

该类注册了BluetoothAdapter.ACTION_SCAN_MODE_CHANGED,并在SCAN_MODE发生变化时,再次设定蓝牙是可见的,这样就可以无限期地保持 BluetoothAdapter 处于可发现模式。默认情况下,将扫描模式设置为 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE 将超时,但对于配对,我们希望在页面正在扫描时始终保持设备可发现。

private static final class AlwaysDiscoverable extends BroadcastReceiver {

    private final Context mContext;
    private final BluetoothAdapter mAdapter;
    private final IntentFilter mIntentFilter = new IntentFilter(
            BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);

    private boolean mStarted;

    AlwaysDiscoverable(Context context, BluetoothAdapter adapter) {
        mContext = context;
        mAdapter = adapter;
    }

    /**
     * 将适配器扫描模式设置为 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE。 
     * 当不再需要发现模式时,start() 调用应该有对 stop() 的匹配调用。
     */
    void start() {
        if (mStarted) {
            return;
        }
        mContext.registerReceiver(this, mIntentFilter);
        mStarted = true;
        setDiscoverable();
    }

    void stop() {
        if (!mStarted) {
            return;
        }
        mContext.unregisterReceiver(this);
        mStarted = false;
        mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        setDiscoverable();
    }

    private void setDiscoverable() {
        if (mAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
            mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
        }
    }
}

5. 总结

以上就是原生系统设置中蓝牙设置关键部分的解析,读完本篇博客其实,并不能让你立即精通蓝牙设置的开发,因为设置功能中还有许多的细节没有面面俱到,例如:监听活跃设备等,所以开发系统应用时我们阅读原生的代码才是最好的办法。

本篇博客的目的就像前言说的那样,是为了让开发者对车载系统应用本身有一个大致的了解。我个人从移动互联网转行做车载的第一个应用就是写系统设置,由于当时对系统设置完全不了解,一直在使用Android应用层API进行开发,也没有想到去移植framework的代码,结果就是成吨的BUG,相信读完本篇或许可以少走一些弯路了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 前言 最近在做Android蓝牙这部分内容,所以查阅了很多相关资料,在此总结一下。 基本概念 Bluetooth是...
    猫疏阅读 14,476评论 7 113
  • 一. 蓝牙权限 二.配对 代码走起~~ 会顺带加些常用的知识点。简书这个贴代码,格式都没了,将就的看吧,需...
    未丑阅读 4,774评论 0 1
  • 最近项目使用蓝牙,之前并没有接触,还是发现了很多坑,查阅了很多资料,说的迷迷糊糊,今天特查看官方文档。 说下遇到的...
    King9527阅读 1,780评论 0 1
  • 最近项目需求要做蓝牙自动配对,也就是在首次配对的时候跳过用户输入PIN码。网上有很多分享的如何实现自动配对。以下做...
    Mr_Tu阅读 12,428评论 2 7
  • 话不多说我们直接步入正题,下面是一个思维导图: 首先我们先与另外两种通信方案进行一下对比: 配对流程: 1. ...
    自己_d7eb阅读 7,862评论 0 1