1. 前言
Android 車載應(yīng)用開發(fā)與分析是一個系列性的文章钉嘹,這個是第13篇分析系統(tǒng)設(shè)置,該系列文章旨在分析原生車載Android系統(tǒng)中核心應(yīng)用的實(shí)現(xiàn)方式烛芬,幫助初次從事車載應(yīng)用開發(fā)的同學(xué)隧期,更好地理解車載應(yīng)用開發(fā)的方式飒责,積累android系統(tǒng)應(yīng)用的開發(fā)經(jīng)驗(yàn)。
2. 系統(tǒng)設(shè)置概述
系統(tǒng)設(shè)置是車載Android系統(tǒng)中非常重要的一個系統(tǒng)級應(yīng)用仆潮,是整個車載IVI系統(tǒng)的控制中心宏蛉,整車的音效、無線通信性置、狀態(tài)信息拾并、安全信息等等都是需要通過系統(tǒng)設(shè)置來查看和控制。例如鹏浅,開啟/關(guān)閉 wifi 和藍(lán)牙嗅义,查看每個應(yīng)用的網(wǎng)絡(luò)流量,開啟調(diào)試信息等隐砸。
有車載經(jīng)驗(yàn)的同學(xué)之碗,應(yīng)該都見過下面這種字體顏色怪異的系統(tǒng)設(shè)置,這其實(shí)是手機(jī)的系統(tǒng)設(shè)置移植到車載系統(tǒng)中的樣子季希。一個車載 Android 項目啟動時褪那,大都會選擇保留功能更全的手機(jī)原生系統(tǒng)設(shè)置,而不是使用車載版本的系統(tǒng)設(shè)置式塌。
車載原生的系統(tǒng)設(shè)置是長這樣的
鑒于系統(tǒng)設(shè)置的功能非常多博敬,由于系統(tǒng)設(shè)置的源碼也比較復(fù)雜,而且一般我們編寫車載系統(tǒng)設(shè)置也不會沿用原生的代碼架構(gòu)峰尝,所以本篇不再介紹系統(tǒng)設(shè)置源碼架構(gòu)和初始化流程偏窝,主要聚焦于系統(tǒng) API 的運(yùn)用。
本次就先從藍(lán)牙模塊開始入手武学。
3. 藍(lán)牙簡介
藍(lán)牙(Bluetooth)祭往,是一種無線通訊技術(shù)標(biāo)準(zhǔn),用來讓固定與移動設(shè)備劳淆,在短距離間交換資料链沼,以形成個人局域網(wǎng)(PAN)。其使用短波特高頻(UHF)無線電波沛鸵,經(jīng)由2.4至2.485 GHz的ISM頻段來進(jìn)行通信括勺。1994年由電信商愛立信(Ericsson)發(fā)展出這個技術(shù)。它最初的設(shè)計曲掰,是希望創(chuàng)建一個RS-232數(shù)據(jù)線的無線通信替代版本疾捍。它能夠連接多個設(shè)備,克服同步的問題栏妖。
藍(lán)牙技術(shù)目前由藍(lán)牙技術(shù)聯(lián)盟(SIG)來負(fù)責(zé)維護(hù)其技術(shù)標(biāo)準(zhǔn)乱豆,其成員已超過三萬,分布在電信吊趾、電腦宛裕、網(wǎng)絡(luò)與消費(fèi)性電子產(chǎn)品等領(lǐng)域瑟啃。IEEE曾經(jīng)將藍(lán)牙技術(shù)標(biāo)準(zhǔn)化為IEEE 802.15.1,但是這個標(biāo)準(zhǔn)已經(jīng)不再繼續(xù)使用揩尸。
3.1. 藍(lán)牙分類
2010年7月7日蛹屿,藍(lán)牙技術(shù)聯(lián)盟推出了藍(lán)牙4.0規(guī)范,藍(lán)牙4.0包括3個子規(guī)范岩榆,即“低功耗藍(lán)牙”错负、“傳統(tǒng)藍(lán)牙”和“高速藍(lán)牙”。
- 低功耗藍(lán)牙
藍(lán)牙低功耗(Bluetooth Low Energy勇边,或稱Bluetooth LE犹撒、BLE,舊商標(biāo)Bluetooth Smart)也稱藍(lán)牙低能耗粒褒、低功耗藍(lán)牙识颊,是藍(lán)牙技術(shù)聯(lián)盟設(shè)計和銷售的一種個人局域網(wǎng)技術(shù),旨在用于醫(yī)療保健怀浆、運(yùn)動健身谊囚、信標(biāo)怕享、安防执赡、家庭娛樂等領(lǐng)域的新興應(yīng)用。相較經(jīng)典藍(lán)牙函筋,低功耗藍(lán)牙旨在保持同等通信范圍的同時顯著降低功耗和成本沙合。
- 經(jīng)典藍(lán)牙
經(jīng)典藍(lán)牙模塊,一般用于數(shù)量比較大的傳輸:如語音跌帐、音樂等較高數(shù)據(jù)量傳輸
- 高速藍(lán)牙
高速藍(lán)牙主攻數(shù)據(jù)交換與傳輸
3.2. 藍(lán)牙規(guī)范
藍(lán)牙規(guī)范(Bluetooth profile)首懈,藍(lán)牙技術(shù)聯(lián)盟定義了許多Profile。Profile目的是要確保Bluetooth設(shè)備間的互通性(interoperability)谨敛。但Bluetooth產(chǎn)品無須實(shí)現(xiàn)所有的Bluetooth規(guī)范Profile究履。Bluetooth 版本 1.1 定義了13個Profiles。下面幾個是Android中常用的:
PBAP 協(xié)議脸狸,電話本訪問協(xié)議(Phone Book Access Profile)最仑,是一種基于OBEX的上層協(xié)議,該協(xié)議可以同步手機(jī)這些具有電話本功能設(shè)備上的通訊錄和通話記錄等信息炊甲。
HFP 協(xié)議泥彤,免手持設(shè)備規(guī)范(Hands-Free Profile),移動電話和免提裝置之間的遠(yuǎn)程無線控制和語音連接就是通過 HFP 協(xié)議卿啡。
A2DP 協(xié)議吟吝, 藍(lán)牙立體聲音頻傳輸規(guī)范(Advance Audio Distribution Profile),規(guī)定了使用藍(lán)牙異步傳輸信道方式颈娜,傳輸高質(zhì)量音樂文件數(shù)據(jù)的協(xié)議堆棧軟件和使用方法剑逃,基于該協(xié)議就能通過以藍(lán)牙方式傳輸高質(zhì)量的立體聲音樂浙宜。分為1.1版和1.2版,只要連接雙方支持A2DP協(xié)議都能以16 bits蛹磺,44.1 kHz的質(zhì)量傳輸聲音信號梆奈。假如有一方?jīng)]有支持A2DP的話,只能以8 bits称开,8 kHz的質(zhì)量的免手持設(shè)備規(guī)范(Handsfree Profile)傳輸模式亩钟,聲音質(zhì)量會大打折扣。
3. 藍(lán)牙設(shè)置關(guān)鍵API
3.1. BluetoothAdapter
API 文檔地址:https://developer.android.google.cn/reference/kotlin/android/bluetooth/BluetoothAdapter
BluetoothAdapter
表示本地設(shè)備藍(lán)牙適配器(此類中的操作是線程安全的)鳖轰。必須通過BluetoothAdapter
才能執(zhí)行基本的藍(lán)牙任務(wù)清酥,例如啟動設(shè)備發(fā)現(xiàn)、查詢綁定(配對)設(shè)備列表蕴侣、使用已知MAC地址實(shí)例化Bluetooth device焰轻、創(chuàng)建BluetootServerSocket以偵聽來自其他設(shè)備的連接請求,以及開始掃描Bluetooch LE設(shè)備昆雀。
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
是所有藍(lán)牙操作的起點(diǎn)揩懒。
擁有BluetoothAdapter
后,可以使用getBondedDevices()
獲取一組BluetoothDevice
對象挽封,表示所有配對過的設(shè)備已球;使用startDiscovery()
啟動設(shè)備發(fā)現(xiàn);或創(chuàng)建BluetoothServerSocket
以監(jiān)聽傳入的RFComm
連接請求辅愿,并使用listenUsingRfcommWithServiceRecord(java.lang.String智亮,java.util.UUID)
;使用listenUsingL2capChannel()
監(jiān)聽傳入的L2CAP面向連接的通道(CoC)連接請求;或使用startLeScan(android.Bluetooth.BluetoothAdapter.LeScanCallback)
啟動藍(lán)牙LE設(shè)備掃描点待。
3.2. BluetoothDevice
API 文檔地址:https://developer.android.google.cn/reference/android/bluetooth/BluetoothDevice
BluetoothDevice
是遠(yuǎn)程藍(lán)牙設(shè)備的實(shí)體類阔蛉。 通過BluetoothDevice
可以創(chuàng)建與相應(yīng)設(shè)備的連接或查詢有關(guān)藍(lán)牙設(shè)備的信息,例如名稱癞埠、地址状原、類和綁定狀態(tài)。
要獲取BluetoothDevice
有多種方式:
如果已經(jīng)知道藍(lán)牙的mac地址燕差,可以使用
BluetoothAdapter.getRemoteDevice(String mac)
創(chuàng)建一個藍(lán)牙設(shè)備遭笋。從
BluetoothAdapter.getBondedDevices()
返回的一組綁定設(shè)備中獲取一個。然后徒探,可以通過藍(lán)牙 BR/EDR 使用createRfcommSocketToServiceRecord(java.util.UUID)
或通過藍(lán)牙LE使用createL2capChannel(int)
瓦呼,打開一個BluetoothSocket
與遠(yuǎn)程設(shè)備通信。使用
BluetoothAdapter.startDiscovery()
開啟藍(lán)牙搜索,然后監(jiān)聽BluetoothDevice.ACTION_FOUND
廣播也可以獲取到BluetoothDevice
央串。
3.3. 其它關(guān)鍵類
在 Android 的 framework 目錄下封裝了很多實(shí)用的藍(lán)牙組件磨澡,不過這些類是 framework 的私有類,并不能通過應(yīng)用層的Android API直接調(diào)用质和,實(shí)際項目中根據(jù)需要將這些類移植到應(yīng)用中再做修改稳摄。不建議直接修改 framework 層的代碼!這樣可能會導(dǎo)致一些原生應(yīng)用無法正常運(yùn)行饲宿。
源碼位置:/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/
4. 藍(lán)牙設(shè)置關(guān)鍵功能實(shí)現(xiàn)
系統(tǒng)設(shè)置作為系統(tǒng)級應(yīng)用厦酬,在使用藍(lán)牙設(shè)置功能時,需要添加以下權(quán)限瘫想。
1)基本藍(lán)牙權(quán)限仗阅,需要此權(quán)限才能執(zhí)行任何藍(lán)牙通信,例如請求連接国夜、接受連接和傳輸數(shù)據(jù)等
<uses-permission android:name="android.permission.BLUETOOTH" />
2)藍(lán)牙設(shè)置“超級管理員”權(quán)限减噪,需要此權(quán)限才能啟動設(shè)備發(fā)現(xiàn)或操縱藍(lán)牙設(shè)置
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
3)允許應(yīng)用程序在無需用戶交互的情況下配對藍(lán)牙設(shè)備,并允許或禁止電話簿訪問或消息訪問
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
4)位置權(quán)限车吹,因?yàn)樗{(lán)牙掃描可用于收集用戶的位置信息筹裕。此類信息可能來自用戶自己的設(shè)備,以及在商店和交通設(shè)施等位置使用的藍(lán)牙信標(biāo)
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
4.1. 開啟/關(guān)閉 藍(lán)牙
車載藍(lán)牙設(shè)置的主頁是BluetoothSettingsFragment
窄驹,它管理藍(lán)牙適配器的開關(guān)朝卒, 它還顯示已配對的設(shè)備和設(shè)備配對功能的入口點(diǎn)。
源碼位置:/packages/apps/Car/Settings/src/com/android/car/settings/bluetooth/BluetoothSettingsFragment.java
設(shè)定藍(lán)牙開啟或關(guān)閉的方法如下所示馒吴,BluetoothAdapter
的初始化以及各個 API 的含義在上面已經(jīng)介紹過了扎运,這里就不再贅述。
private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
private final MenuItem.OnClickListener mBluetoothSwitchListener = item -> {
item.setEnabled(false);
if (item.isChecked()) {
// 開啟藍(lán)牙
mBluetoothAdapter.enable();
} else {
// 關(guān)閉藍(lán)牙
mBluetoothAdapter.disable();
}
};
此外饮戳,我們必須要監(jiān)聽BluetoothAdapter.ACTION_STATE_CHANGED廣播,該廣播表示藍(lán)牙狀態(tài)發(fā)生變化洞拨,此時我們需要同步一下藍(lán)牙的狀態(tài)扯罐,來保證內(nèi)部的狀態(tài)機(jī)或 UI 一直是正確的。
private final IntentFilter mIntentFilter = new IntentFilter(
BluetoothAdapter.ACTION_STATE_CHANGED) ;
@Override
public void onStart() {
super.onStart();
// 注冊藍(lán)牙狀態(tài)的廣播
requireContext().registerReceiver(mReceiver, mIntentFilter) ;
mLocalBluetoothManager.setForegroundActivity(requireActivity());
// 頁面初始化后烦衣,要同步一次藍(lán)牙開關(guān)的狀態(tài)
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) {
// 暫時清除監(jiān)聽器歹河,以便我們在嘗試反映適配器狀態(tài)時不會更新適配器。 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 秸歧,不過根據(jù)官方的注釋,該類已經(jīng)過時衅澈,現(xiàn)在更推薦使用BluetoothAdapter
键菱。
4.2. 查找已連接、已配對的藍(lán)牙設(shè)備
開啟藍(lán)牙后今布,緊接著我們就需要開始搜索藍(lán)牙設(shè)備经备,但是在執(zhí)行搜索之前拭抬,應(yīng)該先查詢配對設(shè)備集,以查看所需的設(shè)備是否已知侵蒙。已連接
造虎、已配對
的藍(lán)牙還是顯示這個頁面中
需要注意文字上的描述差異:已配對
的設(shè)備和已連接
的設(shè)備之間是有區(qū)別的:
-
已配對
(paired 或 bonded)意味著兩個設(shè)備知道彼此的存在,具有可用于身份驗(yàn)證的共享鏈接密鑰纷闺,并且能夠彼此建立加密連接算凿。
-
已連接
(connected)意味著設(shè)備當(dāng)前共享RFCOMM信道,并且能夠相互傳輸數(shù)據(jù)犁功。當(dāng)前的藍(lán)牙 API 要求在建立 RFCOMM 連接之前配對設(shè)備澎媒。當(dāng)啟動與藍(lán)牙 API 的加密連接時,將自動執(zhí)行配對波桩。
借用手機(jī)的藍(lán)牙設(shè)置界面舉個例子戒努,紅框內(nèi)的是已連接
的設(shè)備,綠框內(nèi)的是已配對
的設(shè)備镐躲,如下圖所示
獲取連接藍(lán)牙設(shè)備有以下幾步:
1)注冊廣播BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED監(jiān)聽藍(lán)牙的連接狀態(tài)
該廣播的intent中有三個extras储玫,分別是
- BluetoothAdapter.EXTRA_CONNECTION_STATE:當(dāng)前連接狀態(tài)
- BluetoothAdapter . EXTRA_PREVIOUS_CONNECTION_STATE:之前的連接狀態(tài)
- BluetoothDevice.EXTRA_DEVICE:藍(lán)牙設(shè)備
注冊此廣播需要藍(lán)牙權(quán)限android.Manifest.permission.BLUETOOTH。
2)判斷連接狀態(tài)萤皂,如果已連接狀態(tài)撒穷,則通過EXTRA_DEVICE獲取已連接的藍(lán)牙設(shè)備
獲取配對藍(lán)牙設(shè)備有以下幾步:
1)注冊廣播BluetoothDevice . ACTION_BOND_STATE_CHANGED監(jiān)聽藍(lán)牙的配對狀態(tài)
該廣播的intent中有四個extras,分別是
- BluetoothDevice.EXTRA_BOND_STATE:當(dāng)前配對狀態(tài)
- BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE:之前的配對狀態(tài)
- BluetoothDevice.EXTRA_DEVICE:藍(lán)牙設(shè)備
- BluetoothDevice.EXTRA_REASON: 當(dāng) EXTRA_BOND_STATE 為 BOND_NONE 時裆熙,可以通過EXTRA_REASON 獲取一個結(jié)果代碼端礼。
2)判斷配對狀態(tài),如果已配對狀態(tài),則通過EXTRA_DEVICE獲取已連接的藍(lán)牙設(shè)備
了解步驟之后,我們來看在車載Settings的源碼中是如何處理的寻咒。
在BluetoothSettingsFragment
的布局文件bluetooth_settings_fragment.xml中偶宫,使用了一個BluetoothBondedDevicesPreferenceController的類,這個類的上一層繼承自BluetoothPreferenceController,通過在BluetoothPreferenceController
中向LocalBluetoothManager.BluetoothEventManager
注冊了一個BluetoothCallback
來監(jiān)聽藍(lán)牙設(shè)備的狀態(tài)回調(diào)。
private final LocalBluetoothManager mBluetoothManager;
protected void onStartInternal() {
mBluetoothManager.getEventManager().registerCallback(this);
}
LocalBluetoothManager.BluetoothEventManager
是 framework 的私有類,藍(lán)牙所有廣播事件都是在這里完成注冊和分發(fā)的缅刽,是我們需要重點(diǎn)關(guān)注的類。
// 藍(lán)牙開關(guān)的廣播
addHandler(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedHandler());
// 藍(lán)牙連接狀態(tài)的廣播
addHandler(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED,new ConnectionStateChangedHandler());
// 藍(lán)牙掃描的廣播
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());
// 藍(lán)牙配對狀態(tài)的廣播
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());
// 活躍設(shè)備的廣播
addHandler(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,new ActiveDeviceChangedHandler());
// 耳機(jī)狀態(tài)改變廣播
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());
以下是處理連接狀態(tài)的藍(lán)牙設(shè)備
// 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);
// 分發(fā) 連接 狀態(tài)
dispatchConnectionStateChanged(cachedDevice, state);
}
}
以下是處理配對狀態(tài)的藍(lán)牙設(shè)備
// 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);
// 更新本地緩存衰猛,并返回一個二次封裝的藍(lán)牙實(shí)體類
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);
}
// 分發(fā) 配對 狀態(tài)
for (BluetoothCallback callback : mCallbacks) {
callback.onDeviceBondStateChanged(cachedDevice, bondState);
}
cachedDevice.onBondingStateChanged(bondState);
if (bondState == BluetoothDevice.BOND_NONE) {
/* 檢查我們是否需要移除其他hearing aid設(shè)備 */
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. 掃描藍(lán)牙設(shè)備
開啟藍(lán)牙后,緊接著我們就需要開始對藍(lán)牙設(shè)備的掃描刹孔,檢索外部藍(lán)牙設(shè)備有如下幾個步驟:
1)注冊 BluetoothAdapter.ACTION_DISCOVERY_STARTED啡省、BluetoothAdapter.ACTION_DISCOVERY_FINISHED 監(jiān)聽藍(lán)牙掃描狀態(tài)
2)注冊 BluetoothDevice.ACTION_FOUND 監(jiān)聽掃描期間是否發(fā)現(xiàn)藍(lán)牙設(shè)備
該廣播的 intent 包含以下 extras
- BluetoothDevice.EXTRA_DEVICE:藍(lán)牙設(shè)備
- BluetoothDevice.EXTRA_CLASS:BluetoothClass,它表示藍(lán)牙類,它描述了設(shè)備的一般特性和功能冕杠。 例如微姊,藍(lán)牙類將指定通用設(shè)備類型,如電話分预、計算機(jī)或耳機(jī)兢交,以及它是否能夠提供音頻或電話等服務(wù)。每個藍(lán)牙類都由零個或多個服務(wù)類和一個設(shè)備類組成笼痹。 設(shè)備類進(jìn)一步分為主要和次要設(shè)備類組件配喳。
下面這些 extras 不一定總是可用的,而且也不常用凳干,要注意
- BluetoothDevice.EXTRA_NAME:藍(lán)牙設(shè)備的名稱
- BluetoothDevice.EXTRA_RSSI:藍(lán)牙設(shè)備的信號強(qiáng)度
- BluetoothDevice.EXTRA_IS_COORDINATED_SET_MEMBER:它包含設(shè)備是否被發(fā)現(xiàn)為協(xié)調(diào)集成員的信息晴裹。 與屬于集合的設(shè)備配對將觸發(fā)與其余集合成員的配對。 有關(guān)詳細(xì)信息救赐,請參閱藍(lán)牙 CSIP 規(guī)范涧团。
3)調(diào)用BluetoothAdapter.startDiscovery()
開啟藍(lán)牙掃描
4)從 intent 中獲取掃描到的藍(lán)牙設(shè)備
以上的步驟需要android.permission.BLUETOOTH權(quán)限,對于API 31以上的Android系統(tǒng)需要 android.permission.BLUETOOTH_SCAN權(quán)限经磅。
ok泌绣,繼續(xù)來看車載Settings的源碼中是如何處理掃描的。
在車載Settings中BluetoothPairingSelectionFragment
顯示藍(lán)牙設(shè)備列表预厌。 當(dāng)此fragment可見時阿迈,會有一個進(jìn)度條以指示發(fā)現(xiàn)或配對進(jìn)度。
<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">
<!-- 本機(jī)藍(lán)牙的名稱 -->
<Preference
android:key="@string/pk_bluetooth_name"
android:title="@string/bluetooth_name"
settings:controller="com.android.car.settings.bluetooth.BluetoothNamePreferenceController"/>
<!-- 未配對的藍(lán)牙設(shè)備 -->
<PreferenceCategory
android:key="@string/pk_bluetooth_available_devices"
android:title="@string/bluetooth_available_devices"
settings:controller="com.android.car.settings.bluetooth.BluetoothUnbondedDevicesPreferenceController"/>
<!-- 本機(jī)藍(lán)牙設(shè)備的地址 -->
<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>
開始或停止藍(lán)牙搜索的源碼如下所示
private void enableScanning() {
mIsScanningEnabled = true;
if (!mBluetoothAdapter.isDiscovering()) {
// 開啟掃描
mBluetoothAdapter.startDiscovery();
}
// 開啟藍(lán)牙可見
mAlwaysDiscoverable.start();
getPreference().setEnabled(true);
}
private void disableScanning() {
mIsScanningEnabled = false;
getPreference().setEnabled(false);
// 關(guān)閉藍(lán)牙可見
mAlwaysDiscoverable.stop();
if (mBluetoothAdapter.isDiscovering()) {
// 取消掃描
mBluetoothAdapter.cancelDiscovery();
}
}
在界面主動開啟藍(lán)牙搜索后轧叽,對于ACTION_DISCOVERY_STARTED苗沧、ACTION_DISCOVERY_FINISHED、ACTION_FOUND*這三個廣播的監(jiān)聽都是在 framework 層私有代碼中完成的炭晒。就像之前說的待逞,藍(lán)牙的廣播時間基本都是在這個類中完成監(jiān)聽和事件分發(fā)的。
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 界面收到的回調(diào)時腰埂,條件允許則開啟搜索飒焦。
@Override
public void onScanningStateChanged(boolean started) {
LOG.d( "onScanningStateChanged started: " + started + " mIsScanningEnabled: " + mIsScanningEnabled);
if (!started && mIsScanningEnabled) {
enableScanning();
}
}
開啟搜索后,就需要處理搜索到的藍(lán)牙設(shè)備屿笼。
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
后,還需要對其進(jìn)行過濾翁巍,只保留未配對驴一、未連接的實(shí)體,最后把BluetoothDevice
封裝成CachedBluetoothDevice
回調(diào)給顯示UI的類灶壶,將搜索到藍(lán)牙設(shè)備顯示在 UI 上肝断。
CachedBluetoothDevice
是對BluetoothDevice
的進(jìn)一步封裝,其內(nèi)部實(shí)現(xiàn)了藍(lán)牙的連接、配對胸懈、狀態(tài)獲取等功能担扑。它是 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趣钱。它們應(yīng)適用于2.1版本涌献。
// 現(xiàn)在跳過,有一個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()) {
// 調(diào)度設(shè)備添加回調(diào)以在發(fā)現(xiàn)模式下顯示綁定但未連接的設(shè)備
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卜壕,是因?yàn)?code>LocalBluetoothManager中已經(jīng)緩存了所有的掃描到的藍(lán)牙設(shè)備,只需要將從LocalBluetoothManager
中把 list 取出更新UI 界面即可烙常。
4.4 藍(lán)牙配對
藍(lán)牙的配對有如下幾步:
1)注冊android.bluetooth.device.action.PAIRING_REQUEST廣播
2)取消掃描過程
在執(zhí)行配對之前轴捎, 務(wù)必停止藍(lán)牙搜索,因?yàn)樗阉鬟^程會顯著減少可用于連接的帶寬蚕脏,導(dǎo)致連接操作失敗侦副。
3)執(zhí)行BluetoothDevice.createBond()
進(jìn)行配對
執(zhí)行配對后,根據(jù)需要開啟藍(lán)牙設(shè)備的以下權(quán)限
BluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED)
BluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED)
4)處理PAIRING_REQUEST廣播消息蝗锥,顯示對應(yīng)的UI
繼續(xù)看源碼中是如何處理的跃洛,在藍(lán)牙設(shè)備列表中點(diǎn)擊未配對的藍(lán)牙設(shè)備
@Override
protected void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice) {
if (cachedDevice.startPairing()) {
LOG.d( "startPairing" );
// 如果有服務(wù)端允許(通常是電話),則表明該客戶端(車輛)希望訪問聯(lián)系人(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();
}
}
public boolean startPairing() {
// 掃描時配對是不可靠的汇竭,因此取消掃描
if (mLocalAdapter.isDiscovering()) {
mLocalAdapter.cancelDiscovery();
}
if (!mDevice.createBond()) {
return false;
}
return true;
}
藍(lán)牙的配對過程會有一個 dialog 的提示給到用戶,這個dialog 也需要通過監(jiān)聽廣播實(shí)現(xiàn)穴张。
<receiver android:name=".bluetooth.BluetoothPairingRequest">
<intent-filter>
<action android:name="android.bluetooth.device.action.PAIRING_REQUEST" />
</intent-filter>
</receiver>
BluetoothPairingRequest
是任何藍(lán)牙配對請求的接收器细燎。它會檢查藍(lán)牙設(shè)置當(dāng)前是否可見,并顯示 PIN皂甘、密碼或確認(rèn)輸入對話框玻驻。 否則,它會啟動BluetoothPairingService
偿枕,它會在狀態(tài)欄中啟動一個通知璧瞬,單擊該通知會顯示相同的對話框。
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;
}
// 將廣播意圖轉(zhuǎn)換為活動意圖
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 是否已經(jīng)顯示
boolean shouldShowDialog = BluetoothUtils.shouldShowDialogInForeground(
context, deviceAddress, deviceName);
// 判斷屏幕是否開啟
if (powerManager.isInteractive() && shouldShowDialog) {
// 由于屏幕已打開且BT相關(guān)的活動在前臺渐夸,因此只需打開對話框
context.startActivityAsUser(pairingIntent, UserHandle.CURRENT);
} else {
// 發(fā)布一個通知嗤锉,用于觸發(fā) dialog
intent.setClass(context, BluetoothPairingService.class);
context.startServiceAsUser(intent, UserHandle.CURRENT);
}
}
}
BluetoothPairingService
核心代碼如下,在BluetoothPairingService
中還需要監(jiān)聽ACTION_BOND_STATE_CHANGED廣播墓塌,如果配對完成了需要取消狀態(tài)欄的消息瘟忱。
// 轉(zhuǎn)換 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;
}
藍(lán)牙進(jìn)行配對時會顯示 PIN,以及是否同意讀取電話本等信息访诱,這些內(nèi)容都包含在ACTION_PAIRING_REQUEST廣播的intent中垫挨,具體獲取方式在上述代碼已經(jīng)添加注釋。其中需要注意配對時的不同的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:
通知用戶配對請求并向他們顯示設(shè)備的 PIN/ 密鑰:
BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN
接下就是由用戶確認(rèn)九榔,是否同意配對請求:
- 用戶拒絕配對請求的處理流程:
private BluetoothDevice mDevice;
@Override
public void onDialogNegativeClick(BluetoothPairingDialogFragment dialog) {
onCancel();
}
/**
* 一種正確結(jié)束與藍(lán)牙設(shè)備通信的方法。
* BluetoothPairingDialogFragment 關(guān)閉時將調(diào)用它玫氢。
*/
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);
}
}
/**
* 處理與藍(lán)牙設(shè)備的必要通信以建立成功配對
* 參數(shù):密碼 - - 我們將嘗試與設(shè)備配對的密碼帚屉。
*/
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");
}
}
以上就是一個藍(lán)牙配對的全部流程。如果是已配對的藍(lán)牙設(shè)備漾峡,則直接連接即可
public void connect() {
if (!ensurePaired()) {
return;
}
mConnectAttempted = SystemClock.elapsedRealtime();
connectAllEnabledProfiles();
}
private void connectAllEnabledProfiles() {
synchronized (mProfileLock) {
// 如果沒有攻旦,請嘗試初始化配置文件。
if (mProfiles.isEmpty()) {
// 如果 mProfiles 為空生逸,則不要調(diào)用 updateProfiles牢屋。
// 這會在配對期間導(dǎo)致與 carkits 的競爭條件,其中 RemoteDevice.UUIDs 已從藍(lán)牙堆棧更新槽袄,但 ACTION.uuid 尚未發(fā)送烙无。
// 最終將收到 ACTION.uuid,這將觸發(fā)各種配置文件的連接如果 UUID 尚不可用遍尺,則連接將在 ACTION_UUID 意圖到達(dá)時發(fā)生截酷。
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;
}
}
-
設(shè)置藍(lán)牙可見性
默認(rèn)情況下,其它藍(lán)牙設(shè)備是無法搜索到當(dāng)前的藍(lán)牙設(shè)備的乾戏,必須使用下面的代碼將藍(lán)牙設(shè)備設(shè)定為可見狀態(tài)迂苛,timeout 為藍(lán)牙可見時間,超過這個時間鼓择,藍(lán)牙就會恢復(fù)到默認(rèn)狀態(tài)三幻,最長可以設(shè)定為1個小時。
BluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout);
在原生系統(tǒng)設(shè)置中由AlwaysDiscoverable
管理藍(lán)牙可見性的類呐能。
該類注冊了BluetoothAdapter.ACTION_SCAN_MODE_CHANGED念搬,并在SCAN_MODE發(fā)生變化時,再次設(shè)定藍(lán)牙是可見的摆出,這樣就可以無限期地保持 BluetoothAdapter 處于可發(fā)現(xiàn)模式朗徊。默認(rèn)情況下,將掃描模式設(shè)置為 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE 將超時偎漫,但對于配對荣倾,我們希望在頁面正在掃描時始終保持設(shè)備可發(fā)現(xiàn)。
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;
}
/**
* 將適配器掃描模式設(shè)置為 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE骑丸。
* 當(dāng)不再需要發(fā)現(xiàn)模式時舌仍,start() 調(diào)用應(yīng)該有對 stop() 的匹配調(diào)用。
*/
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. 總結(jié)
以上就是原生系統(tǒng)設(shè)置中藍(lán)牙設(shè)置關(guān)鍵部分的解析通危,讀完本篇博客其實(shí)铸豁,并不能讓你立即精通藍(lán)牙設(shè)置的開發(fā),因?yàn)樵O(shè)置功能中還有許多的細(xì)節(jié)沒有面面俱到菊碟,例如:監(jiān)聽活躍設(shè)備等节芥,所以開發(fā)系統(tǒng)應(yīng)用時我們閱讀原生的代碼才是最好的辦法。
本篇博客的目的就像前言說的那樣逆害,是為了讓開發(fā)者對車載系統(tǒng)應(yīng)用本身有一個大致的了解头镊。我個人從移動互聯(lián)網(wǎng)轉(zhuǎn)行做車載的第一個應(yīng)用就是寫系統(tǒng)設(shè)置,由于當(dāng)時對系統(tǒng)設(shè)置完全不了解魄幕,一直在使用Android應(yīng)用層API進(jìn)行開發(fā)相艇,也沒有想到去移植framework的代碼,結(jié)果就是成噸的BUG纯陨,相信讀完本篇或許可以少走一些彎路了坛芽。