1. 前言
Android 車載應用開發(fā)與分析是一個系列性的文章,這個是第13篇分析系統(tǒng)設置,該系列文章旨在分析原生車載Android系統(tǒng)中核心應用的實現(xiàn)方式,幫助初次從事車載應用開發(fā)的同學,更好地理解車載應用開發(fā)的方式,積累android系統(tǒng)應用的開發(fā)經驗。
2. 系統(tǒng)設置概述
系統(tǒng)設置是車載Android系統(tǒng)中非常重要的一個系統(tǒng)級應用,是整個車載IVI系統(tǒng)的控制中心,整車的音效、無線通信、狀態(tài)信息、安全信息等等都是需要通過系統(tǒng)設置來查看和控制。例如,開啟/關閉 wifi 和藍牙,查看每個應用的網絡流量,開啟調試信息等。
有車載經驗的同學,應該都見過下面這種字體顏色怪異的系統(tǒng)設置,這其實是手機的系統(tǒng)設置移植到車載系統(tǒng)中的樣子。一個車載 Android 項目啟動時,大都會選擇保留功能更全的手機原生系統(tǒng)設置,而不是使用車載版本的系統(tǒng)設置。

車載原生的系統(tǒng)設置是長這樣的

鑒于系統(tǒng)設置的功能非常多,由于系統(tǒng)設置的源碼也比較復雜,而且一般我們編寫車載系統(tǒng)設置也不會沿用原生的代碼架構,所以本篇不再介紹系統(tǒng)設置源碼架構和初始化流程,主要聚焦于系統(tǒng) API 的運用。
本次就先從藍牙模塊開始入手。
3. 藍牙簡介
藍牙(Bluetooth),是一種無線通訊技術標準,用來讓固定與移動設備,在短距離間交換資料,以形成個人局域網(PAN)。其使用短波特高頻(UHF)無線電波,經由2.4至2.485 GHz的ISM頻段來進行通信。1994年由電信商愛立信(Ericsson)發(fā)展出這個技術。它最初的設計,是希望創(chuàng)建一個RS-232數(shù)據(jù)線的無線通信替代版本。它能夠連接多個設備,克服同步的問題。
藍牙技術目前由藍牙技術聯(lián)盟(SIG)來負責維護其技術標準,其成員已超過三萬,分布在電信、電腦、網絡與消費性電子產品等領域。IEEE曾經將藍牙技術標準化為IEEE 802.15.1,但是這個標準已經不再繼續(xù)使用。
3.1. 藍牙分類
2010年7月7日,藍牙技術聯(lián)盟推出了藍牙4.0規(guī)范,藍牙4.0包括3個子規(guī)范,即“低功耗藍牙”、“傳統(tǒng)藍牙”和“高速藍牙”。
- 低功耗藍牙
藍牙低功耗(Bluetooth Low Energy,或稱Bluetooth LE、BLE,舊商標Bluetooth Smart)也稱藍牙低能耗、低功耗藍牙,是藍牙技術聯(lián)盟設計和銷售的一種個人局域網技術,旨在用于醫(yī)療保健、運動健身、信標、安防、家庭娛樂等領域的新興應用。相較經典藍牙,低功耗藍牙旨在保持同等通信范圍的同時顯著降低功耗和成本。
- 經典藍牙
經典藍牙模塊,一般用于數(shù)量比較大的傳輸:如語音、音樂等較高數(shù)據(jù)量傳輸
- 高速藍牙
高速藍牙主攻數(shù)據(jù)交換與傳輸
3.2. 藍牙規(guī)范
藍牙規(guī)范(Bluetooth profile),藍牙技術聯(lián)盟定義了許多Profile。Profile目的是要確保Bluetooth設備間的互通性(interoperability)。但Bluetooth產品無須實現(xiàn)所有的Bluetooth規(guī)范Profile。Bluetooth 版本 1.1 定義了13個Profiles。下面幾個是Android中常用的:
PBAP 協(xié)議,電話本訪問協(xié)議(Phone Book Access Profile),是一種基于OBEX的上層協(xié)議,該協(xié)議可以同步手機這些具有電話本功能設備上的通訊錄和通話記錄等信息。
HFP 協(xié)議,免手持設備規(guī)范(Hands-Free Profile),移動電話和免提裝置之間的遠程無線控制和語音連接就是通過 HFP 協(xié)議。
A2DP 協(xié)議, 藍牙立體聲音頻傳輸規(guī)范(Advance Audio Distribution Profile),規(guī)定了使用藍牙異步傳輸信道方式,傳輸高質量音樂文件數(shù)據(jù)的協(xié)議堆棧軟件和使用方法,基于該協(xié)議就能通過以藍牙方式傳輸高質量的立體聲音樂。分為1.1版和1.2版,只要連接雙方支持A2DP協(xié)議都能以16 bits,44.1 kHz的質量傳輸聲音信號。假如有一方沒有支持A2DP的話,只能以8 bits,8 kHz的質量的免手持設備規(guī)范(Handsfree Profile)傳輸模式,聲音質量會大打折扣。
3. 藍牙設置關鍵API
3.1. BluetoothAdapter
API 文檔地址:https://developer.android.google.cn/reference/kotlin/android/bluetooth/BluetoothAdapter
BluetoothAdapter表示本地設備藍牙適配器(此類中的操作是線程安全的)。必須通過BluetoothAdapter才能執(zhí)行基本的藍牙任務,例如啟動設備發(fā)現(xiàn)、查詢綁定(配對)設備列表、使用已知MAC地址實例化Bluetooth device、創(chuàng)建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()啟動設備發(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)啟動藍牙LE設備掃描。
3.2. BluetoothDevice
API 文檔地址:https://developer.android.google.cn/reference/android/bluetooth/BluetoothDevice
BluetoothDevice是遠程藍牙設備的實體類。 通過BluetoothDevice可以創(chuàng)建與相應設備的連接或查詢有關藍牙設備的信息,例如名稱、地址、類和綁定狀態(tài)。
要獲取BluetoothDevice有多種方式:
如果已經知道藍牙的mac地址,可以使用
BluetoothAdapter.getRemoteDevice(String mac)創(chuàng)建一個藍牙設備。從
BluetoothAdapter.getBondedDevices()返回的一組綁定設備中獲取一個。然后,可以通過藍牙 BR/EDR 使用createRfcommSocketToServiceRecord(java.util.UUID)或通過藍牙LE使用createL2capChannel(int),打開一個BluetoothSocket與遠程設備通信。使用
BluetoothAdapter.startDiscovery()開啟藍牙搜索,然后監(jiān)聽BluetoothDevice.ACTION_FOUND廣播也可以獲取到BluetoothDevice。
3.3. 其它關鍵類
在 Android 的 framework 目錄下封裝了很多實用的藍牙組件,不過這些類是 framework 的私有類,并不能通過應用層的Android API直接調用,實際項目中根據(jù)需要將這些類移植到應用中再做修改。不建議直接修改 framework 層的代碼!這樣可能會導致一些原生應用無法正常運行。
源碼位置:/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/

4. 藍牙設置關鍵功能實現(xiàn)
系統(tǒng)設置作為系統(tǒng)級應用,在使用藍牙設置功能時,需要添加以下權限。
1)基本藍牙權限,需要此權限才能執(zhí)行任何藍牙通信,例如請求連接、接受連接和傳輸數(shù)據(jù)等
<uses-permission android:name="android.permission.BLUETOOTH" />
2)藍牙設置“超級管理員”權限,需要此權限才能啟動設備發(fā)現(xiàn)或操縱藍牙設置
<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();
}
};
此外,我們必須要監(jiān)聽BluetoothAdapter.ACTION_STATE_CHANGED廣播,該廣播表示藍牙狀態(tài)發(fā)生變化,此時我們需要同步一下藍牙的狀態(tài),來保證內部的狀態(tài)機或 UI 一直是正確的。
private final IntentFilter mIntentFilter = new IntentFilter(
BluetoothAdapter.ACTION_STATE_CHANGED) ;
@Override
public void onStart() {
super.onStart();
// 注冊藍牙狀態(tài)的廣播
requireContext().registerReceiver(mReceiver, mIntentFilter) ;
mLocalBluetoothManager.setForegroundActivity(requireActivity());
// 頁面初始化后,要同步一次藍牙開關的狀態(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ù)官方的注釋,該類已經過時,現(xiàn)在更推薦使用BluetoothAdapter。
4.2. 查找已連接、已配對的藍牙設備
開啟藍牙后,緊接著我們就需要開始搜索藍牙設備,但是在執(zhí)行搜索之前,應該先查詢配對設備集,以查看所需的設備是否已知。已連接、已配對的藍牙還是顯示這個頁面中

需要注意文字上的描述差異:已配對的設備和已連接的設備之間是有區(qū)別的:
-
已配對(paired 或 bonded)意味著兩個設備知道彼此的存在,具有可用于身份驗證的共享鏈接密鑰,并且能夠彼此建立加密連接。
-
已連接(connected)意味著設備當前共享RFCOMM信道,并且能夠相互傳輸數(shù)據(jù)。當前的藍牙 API 要求在建立 RFCOMM 連接之前配對設備。當啟動與藍牙 API 的加密連接時,將自動執(zhí)行配對。
借用手機的藍牙設置界面舉個例子,紅框內的是已連接的設備,綠框內的是已配對的設備,如下圖所示

獲取連接藍牙設備有以下幾步:
1)注冊廣播BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED監(jiān)聽藍牙的連接狀態(tài)
該廣播的intent中有三個extras,分別是
- BluetoothAdapter.EXTRA_CONNECTION_STATE:當前連接狀態(tài)
- BluetoothAdapter . EXTRA_PREVIOUS_CONNECTION_STATE:之前的連接狀態(tài)
- BluetoothDevice.EXTRA_DEVICE:藍牙設備
注冊此廣播需要藍牙權限android.Manifest.permission.BLUETOOTH。
2)判斷連接狀態(tài),如果已連接狀態(tài),則通過EXTRA_DEVICE獲取已連接的藍牙設備
獲取配對藍牙設備有以下幾步:
1)注冊廣播BluetoothDevice . ACTION_BOND_STATE_CHANGED監(jiān)聽藍牙的配對狀態(tài)
該廣播的intent中有四個extras,分別是
- BluetoothDevice.EXTRA_BOND_STATE:當前配對狀態(tài)
- BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE:之前的配對狀態(tài)
- BluetoothDevice.EXTRA_DEVICE:藍牙設備
- BluetoothDevice.EXTRA_REASON: 當 EXTRA_BOND_STATE 為 BOND_NONE 時,可以通過EXTRA_REASON 獲取一個結果代碼。
2)判斷配對狀態(tài),如果已配對狀態(tài),則通過EXTRA_DEVICE獲取已連接的藍牙設備
了解步驟之后,我們來看在車載Settings的源碼中是如何處理的。
在BluetoothSettingsFragment的布局文件bluetooth_settings_fragment.xml中,使用了一個BluetoothBondedDevicesPreferenceController的類,這個類的上一層繼承自BluetoothPreferenceController,通過在BluetoothPreferenceController中向LocalBluetoothManager.BluetoothEventManager注冊了一個BluetoothCallback來監(jiān)聽藍牙設備的狀態(tài)回調。
private final LocalBluetoothManager mBluetoothManager;
protected void onStartInternal() {
mBluetoothManager.getEventManager().registerCallback(this);
}
LocalBluetoothManager.BluetoothEventManager是 framework 的私有類,藍牙所有廣播事件都是在這里完成注冊和分發(fā)的,是我們需要重點關注的類。
// 藍牙開關的廣播
addHandler(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedHandler());
// 藍牙連接狀態(tài)的廣播
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());
// 藍牙配對狀態(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());
// 活躍設備的廣播
addHandler(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,new ActiveDeviceChangedHandler());
// 耳機狀態(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)的藍牙設備
// 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)的藍牙設備
// 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);
}
// 分發(fā) 配對 狀態(tài)
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 監(jiān)聽藍牙掃描狀態(tài)
2)注冊 BluetoothDevice.ACTION_FOUND 監(jiān)聽掃描期間是否發(fā)現(xiàn)藍牙設備
該廣播的 intent 包含以下 extras
- BluetoothDevice.EXTRA_DEVICE:藍牙設備
- BluetoothDevice.EXTRA_CLASS:BluetoothClass,它表示藍牙類,它描述了設備的一般特性和功能。 例如,藍牙類將指定通用設備類型,如電話、計算機或耳機,以及它是否能夠提供音頻或電話等服務。每個藍牙類都由零個或多個服務類和一個設備類組成。 設備類進一步分為主要和次要設備類組件。
下面這些 extras 不一定總是可用的,而且也不常用,要注意
- BluetoothDevice.EXTRA_NAME:藍牙設備的名稱
- BluetoothDevice.EXTRA_RSSI:藍牙設備的信號強度
- BluetoothDevice.EXTRA_IS_COORDINATED_SET_MEMBER:它包含設備是否被發(fā)現(xiàn)為協(xié)調集成員的信息。 與屬于集合的設備配對將觸發(fā)與其余集合成員的配對。 有關詳細信息,請參閱藍牙 CSIP 規(guī)范。
3)調用BluetoothAdapter.startDiscovery()開啟藍牙掃描
4)從 intent 中獲取掃描到的藍牙設備
以上的步驟需要android.permission.BLUETOOTH權限,對于API 31以上的Android系統(tǒng)需要 android.permission.BLUETOOTH_SCAN權限。
ok,繼續(xù)來看車載Settings的源碼中是如何處理掃描的。
在車載Settings中BluetoothPairingSelectionFragment顯示藍牙設備列表。 當此fragment可見時,會有一個進度條以指示發(fā)現(xià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">
<!-- 本機藍牙的名稱 -->
<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>
開始或停止藍牙搜索的源碼如下所示
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*這三個廣播的監(jiān)聽都是在 framework 層私有代碼中完成的。就像之前說的,藍牙的廣播時間基本都是在這個類中完成監(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 界面收到的回調時,條件允許則開啟搜索。
@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的進一步封裝,其內部實現(xià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。它們應適用于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()) {
// 調度設備添加回調以在發(fā)現(xiàn)模式下顯示綁定但未連接的設備
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)取消掃描過程
在執(zhí)行配對之前, 務必停止藍牙搜索,因為搜索過程會顯著減少可用于連接的帶寬,導致連接操作失敗。
3)執(zhí)行BluetoothDevice.createBond()進行配對
執(zhí)行配對后,根據(jù)需要開啟藍牙設備的以下權限
BluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED)
BluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED)
4)處理PAIRING_REQUEST廣播消息,顯示對應的UI
繼續(xù)看源碼中是如何處理的,在藍牙設備列表中點擊未配對的藍牙設備
@Override
protected void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice) {
if (cachedDevice.startPairing()) {
LOG.d( "startPairing" );
// 如果有服務端允許(通常是電話),則表明該客戶端(車輛)希望訪問聯(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;
}
藍牙的配對過程會有一個 dialog 的提示給到用戶,這個dialog 也需要通過監(jiān)聽廣播實現(xiàn)。
<receiver android:name=".bluetooth.BluetoothPairingRequest">
<intent-filter>
<action android:name="android.bluetooth.device.action.PAIRING_REQUEST" />
</intent-filter>
</receiver>
BluetoothPairingRequest是任何藍牙配對請求的接收器。它會檢查藍牙設置當前是否可見,并顯示 PIN、密碼或確認輸入對話框。 否則,它會啟動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;
}
// 將廣播意圖轉換為活動意圖
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 {
// 發(fā)布一個通知,用于觸發(fā) dialog
intent.setClass(context, BluetoothPairingService.class);
context.startServiceAsUser(intent, UserHandle.CURRENT);
}
}
}
BluetoothPairingService核心代碼如下,在BluetoothPairingService中還需要監(jiān)聽ACTION_BOND_STATE_CHANGED廣播,如果配對完成了需要取消狀態(tài)欄的消息。
// 轉換 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);
}
}
/**
* 處理與藍牙設備的必要通信以建立成功配對
* 參數(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");
}
}
以上就是一個藍牙配對的全部流程。如果是已配對的藍牙設備,則直接連接即可
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 尚未發(fā)送。
// 最終將收到 ACTION.uuid,這將觸發(fā)各種配置文件的連接如果 UUID 尚不可用,則連接將在 ACTION_UUID 意圖到達時發(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;
}
}
-
設置藍牙可見性
默認情況下,其它藍牙設備是無法搜索到當前的藍牙設備的,必須使用下面的代碼將藍牙設備設定為可見狀態(tài),timeout 為藍牙可見時間,超過這個時間,藍牙就會恢復到默認狀態(tài),最長可以設定為1個小時。
BluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout);
在原生系統(tǒng)設置中由AlwaysDiscoverable管理藍牙可見性的類。
該類注冊了BluetoothAdapter.ACTION_SCAN_MODE_CHANGED,并在SCAN_MODE發(fā)生變化時,再次設定藍牙是可見的,這樣就可以無限期地保持 BluetoothAdapter 處于可發(fā)現(xiàn)模式。默認情況下,將掃描模式設置為 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE 將超時,但對于配對,我們希望在頁面正在掃描時始終保持設備可發(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;
}
/**
* 將適配器掃描模式設置為 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE。
* 當不再需要發(fā)現(xiàn)模式時,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. 總結
以上就是原生系統(tǒng)設置中藍牙設置關鍵部分的解析,讀完本篇博客其實,并不能讓你立即精通藍牙設置的開發(fā),因為設置功能中還有許多的細節(jié)沒有面面俱到,例如:監(jiān)聽活躍設備等,所以開發(fā)系統(tǒng)應用時我們閱讀原生的代碼才是最好的辦法。
本篇博客的目的就像前言說的那樣,是為了讓開發(fā)者對車載系統(tǒng)應用本身有一個大致的了解。我個人從移動互聯(lián)網轉行做車載的第一個應用就是寫系統(tǒng)設置,由于當時對系統(tǒng)設置完全不了解,一直在使用Android應用層API進行開發(fā),也沒有想到去移植framework的代碼,結果就是成噸的BUG,相信讀完本篇或許可以少走一些彎路了。