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 LE、BLE,旧商标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() {@Overridepublic 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。
- 判断连接状态,如果已连接状态,则通过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 {@Overridepublic 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
<PreferenceScreenxmlns: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"><!-- 本机蓝牙的名称 --><Preferenceandroid:key="@string/pk_bluetooth_name"android:title="@string/bluetooth_name"settings:controller="com.android.car.settings.bluetooth.BluetoothNamePreferenceController"/><!-- 未配对的蓝牙设备 --><PreferenceCategoryandroid:key="@string/pk_bluetooth_available_devices"android:title="@string/bluetooth_available_devices"settings:controller="com.android.car.settings.bluetooth.BluetoothUnbondedDevicesPreferenceController"/><!-- 本机蓝牙设备的地址 --><Preferenceandroid: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_STARTED、*ACTION_DISCOVERY_FINISHED、ACTION_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 {@Overridepublic 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) {// 刷新 UIrefreshUi();
}
最后一步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 {@Overridepublic 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;}
}
设置蓝牙可见性
默认情况下,其它蓝牙设备是无法搜索到当前的蓝牙设备的,必须使用下面的代码将蓝牙设备设定为可见状态,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);}@Overridepublic 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,相信读完本篇或许可以少走一些弯路了。
Android车载应用开发与分析(13)- 系统设置-蓝牙设置相关推荐
- Android车载应用开发与分析(6)- 车载多媒体(一)- 音视频基础知识与MediaPlayer
多媒体应用是车载信息娱乐系统的一个重要组成部分,一般包含音视频播放.收音机.相册等.车载应用多媒体系列初步计划分为六篇,这是第一篇. 参考资料 视频和视频帧:视频和帧基础知识整理 百度百科 - 声道 ...
- Android车载应用开发与分析 - 车载Android应用开发入门指南
1. 前言 - 移动互联网退潮下的汽车大战 2019年中国首个外商独资的整车制造项目,"上海特斯拉超级工厂"开工了. 特斯拉在智能化和电子化上的巨大优势将智能汽车推向了一个全新的高 ...
- Android车载应用开发与分析(10)- 车载空调系统(HVAC)
1. HVAC 功能介绍 HVAC 全称:供暖通风与空气调节(Heating Ventilation and Air Conditioning).用户可以通过他来控制整个汽车的空调系统,是汽车中非常重 ...
- Android 车载应用开发与分析 - CarLauncher
本篇文章中的功能以及源码分析基于android-11.0.0_r43,CarLauncher源码位于 packages/apps/Car/Launcher. 1.Launcher 与 CarLaunc ...
- 一份全面详细的Android 车载系统开发入门指南与面试题
前言 众所周知,近两年互联网行业的就业形势不太理想,三点原因 技术原因: Android经过一代又一代的发展,早已经过了张口一句组件化就能月薪过万的时代,现如今不会Framework底层/性能优化 很 ...
- “2023热门岗位”Android车载系统开发工程师入门指南
众所周知,近两年互联网行业的就业形势不太理想,许多Android开发因此萌生了转行做车载的想法.这篇文章就是从一个车载应用工程师的角度,探讨一下Android车载究竟值不值得学,以及怎样去学习? 首先 ...
- 车载兴起已成必然,最新《Android车载操作系统开发指南》开源分享
目前,国内厂家在车载信息娱乐应用中主要采用Android系统,尤其是各大互联网巨头.自主品牌和造车新势力纷纷基于Android进行定制化改造,推出自己的汽车操作系统,例如,阿里AliOS.百度小度车载 ...
- 2023最新整理,Android车载操作系统开发揭秘,无偿分享!
临近年末大关,猛烈的疫情冲击打乱了原本的工作节奏,让互联网行业的发展形势愈发严峻.最近,我的身边也有不少Android开发程序员萌生了转行做车载的想法. 从中国车联网发布的市场规模预计来看,汽车市场将 ...
- 这是一份全面详细的Android 车载系统开发入门指南
目前的就业形势越来越严峻,很多大中小厂因为业务停滞不前都选择"精简人员",节约成本.对于Android开发来说,市场的冷静,明显可以感知到企业招聘门槛的提高.就未来发展来说,选择一 ...
最新文章
- MATLAB机器学习系列-4函数篇
- boost::hana::repeat用法的测试程序
- python建立虚拟环境不成功_virtualenv 创建虚拟环境不成功
- 2014年,马上要上班啦,希望一切顺利
- 将读写锁放到共享内存,实现进程之间对于同一文件的读写操作
- iPhone XS Max第三方售价低于7千元 引用户疯抢
- 华为云回应“关闭私有云”传闻 将进行业务调整形成混合云产品部
- 1146 Topological Order(25 分)
- JS错误 theForm.submit();SCRIPT3: 找不到成员。
- Windows11在Edge浏览器中打开IE浏览器兼容的页面,在Edge浏览器打开加载ActiveX控件的页面
- Typhoon-v1.02 靶机入侵
- 马尔可夫毯/马尔科夫链/因果图/贝叶斯网络
- 内存换CPU CPU换内存例子
- 清晰明了有趣味的数字加密讲解
- 电脑之间快速传输超大文件(100GB以上)的方法
- iOS开发关于“发送原图”功能问题的记录
- 更透明的隐私设置和全新的APP资源库:iOS 14测试版深度体验~~~
- 【极简教程】Linux Shell 脚本编程
- Cesium 车辆跟踪事件气泡弹窗追随
- zero eclipse_全球首款真无孔机!魅族zero亮相:支持屏幕发声和18W无线快充