Android藍牙短信功能開發
由于我司是做車機中控的,目前需要在車機上實現與藍牙手機相連,并通過藍牙進行短信發送和接收的功能,針對這一功能的實現方式做個簡單的記錄。
文檔目錄說明
以下開發基于Android 9.0 車機版本,其他版本或存在不同
一、藍牙短信協議規范
相關藍牙協議:MAP(MESSAGE ACCESS PROFILE):藍牙短信訪問協議規范。
借助MAP協議規范,可在車機上通過連接的遠程設備收發短信。目前不會將短信內容存儲在 IVI 本地存儲空間,而是每當連接的遠程設備收到短信時,IVI 會接收相應短信并對其進行解析,然后在 intent 中廣播消息內容,應用便可收到相應內容。
要連接到移動設備以收發短信,IVI 必須啟動 MAP 連接。 MapClientService 中的 MAXIMUM_CONNECTED_DEVICES 指定了 IVI 允許同時連接的 MAP 設備數量上限。同時獲得 IVI 和移動設備的授權時每個連接才能傳輸消息。
協議SDK代碼在源碼內的路徑:frameworks/base/core/java/android/bluetooth/BluetoothMapClient.java
協議服務代碼在源碼內的路徑:packages/apps/Bluetooth/src/com/android/bluetooth/mapclient/MapClientService.java
二、協議SDK文件接口說明
接口名 | 描述 |
---|---|
connect | 連接指定設備 |
disconnect | 斷開指定設備 |
isConnected | 判斷指定設備是否連接,連接則返回true,否則false |
getConnectedDevices | 獲有已連接設備列表 |
getDevicesMatchingConnectionStates | 獲得與指定狀態匹配的設備 |
getConnectionState | 獲得指定設備的連接狀態 |
setPriority | 設置設備MAP協議的優先級 |
getPriority | 獲得設備MAP協議的優先級 |
sendMessage | 使用指定設備發送消息至指定的聯系人 |
getUnreadMessages | 獲得未讀消息 |
其中我們需要重點關注的接口如下:
/**
* 向指定的電話號碼發送SMS消息
*
* @param device 藍牙設備
* @param contacts 聯系人的Uri[]列表
* @param message y要發送的消息
* @param sentIntent 發送消息時發出的意圖 SMS消息發送成功將發送{@link #ACTION_MESSAGE_SENT_SUCCESSFULLY} 廣播
* @param deliveredIntent 消息傳遞時發出的意圖 SMS消息傳遞成功將發送{@link #ACTION_MESSAGE_DELIVERED_SUCCESSFULLY} 廣播
* @return 如果消息入隊則返回 true,錯誤則返回 false
*/
public boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message,
PendingIntent sentIntent, PendingIntent deliveredIntent)
/**
* 獲取未讀消息. 未讀消息將發送 {@link #ACTION_MESSAGE_RECEIVED} 廣播。
*
* @param device 藍牙設備
* @return 如果消息入隊則返回 true,錯誤則返回 false
*/
public boolean getUnreadMessages(BluetoothDevice device)
三、藍牙MapClient協議支持
若需要車機設備與支持MAP的藍牙設備連接后,可進行短信收發,那么我們車機系統就需要在藍牙配對時支持MapClient協議規范,那么我們需要修改或overlay frameworks/base/core/res/res/values/config.xml內的enable_pbap_pce_profile配置值為true(AutoMotive內該值默認overlay為true),這樣我們在進行藍牙設備連接時才會將MapClientProfile協議加入到可連接的藍牙協議規范列表內,具體的代碼如下:
framework/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
public class LocalBluetoothProfileManager {
....
private MapClientProfile mMapClientProfile;
....
LocalBluetoothProfileManager(Context context,
LocalBluetoothAdapter adapter,
CachedBluetoothDeviceManager deviceManager,
BluetoothEventManager eventManager) {
....
// pbap為電話簿訪問協議規范
mUsePbapPce = mContext.getResources().getBoolean(R.bool.enable_pbap_pce_profile);
// MAP Client 通常用于與 PBAP Client 相同的情況
mUseMapClient = mContext.getResources().getBoolean(R.bool.enable_pbap_pce_profile);
....
if (mUseMapClient) {
if(mMapClientProfile==null){
mMapClientProfile = new MapClientProfile(mContext, mLocalAdapter,
mDeviceManager, this);
addProfile(mMapClientProfile, MapClientProfile.NAME,
BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
}
} else {
if(mMapProfile==null){
mMapProfile = new MapProfile(mContext, mLocalAdapter, mDeviceManager, this);
addProfile(mMapProfile, MapProfile.NAME,
BluetoothMap.ACTION_CONNECTION_STATE_CHANGED);
}
}
....
}
....
}
之后進行連接就可以在車機的藍牙設備詳情頁面看到對應的協議開關選項,如一個Text Messages選項:
勾選之后就會進行藍牙短信協議的連接。
MapClient連接設備狀態監聽
當MapClient設備連接狀態變化時,會發送BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED廣播,我們需要監聽該廣播,并記錄當前連接的藍牙設備,以備后續的未讀短信獲取和短信發送功能的開發,具體的代碼如下:
// 記錄當前連接的藍牙設備
private BluetoothDevice mBluetoothDevice;
// MapClient設備連接狀態變化廣播注冊
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
BtServiceApplication.getApplication().registerReceiver(mBroadcastReceiver, filter);
// MapClient設備連接狀態變化廣播監聽
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED)) {
if (intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0) == BluetoothProfile.STATE_CONNECTED) {
// 設備連接
mBluetoothDevice = (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
} else if(intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0) == BluetoothProfile.STATE_DISCONNECTED){
// 設備斷開
BluetoothDevice bluetoothDevice = (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if(bluetoothDevice!=null && mBluetoothDevice.getAddress().equals(bluetoothDevice.getAddress())){
mBluetoothDevice = null;
}
}
}
}
};
四、藍牙短信接收功能開發
根據官方協議來看,目前可支持的藍牙短信接收功能有如下兩個:
- 獲取所有未讀短信;
- 實時短信接收;
針對已讀短信僅通過現有的協議還無法實現,后續有機會再繼續研究。
根據協議規范說明可知,藍牙短信收實際上是通過廣播的形式進行接收的,所以我們只需要在代碼監聽對應的廣播信息,解析內部的短信內容即可,實現代碼如下:
// 短信接收廣播注冊
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
BtServiceApplication.getApplication().registerReceiver(mBroadcastReceiver, filter);
// 短信接收廣播監聽
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BluetoothMapClient.ACTION_MESSAGE_RECEIVED)) {
// 收到短信
// 獲得發送者的Uri
String senderUri = intent.getStringExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_URI);
if (senderUri == null) {
senderUri = "<null>";
}
// 獲得發送者名稱
String senderName = intent.getStringExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME);
if (senderName == null) {
senderName = "<null>";
}
// 獲得短息內容
String message = intent.getStringExtra(android.content.Intent.EXTRA_TEXT);
// 獲得短信接受的藍牙設備
String bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
}
}
};
通過以上代碼即可實現實時短信的接收,若要獲得未讀短信,則需要調用BluetoothMapClient的getUnreadMessages接口觸發對應的藍牙設備將未讀短信發送至我們的車機上,接收處理的上面一致,都是通過廣播來進行接收,就不再闡述。
針對BluetoothMapClient的對象創建和getUnreadMessages的調用可參考如下代碼:
public class Test {
private BluetoothMapClient mMapProfile;
private BluetoothAdapter mAdapter;
private final int MAP_CLIENT = 18;
class MapServiceListener implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
synchronized (mLock) {
mMapProfile = (BluetoothMapClient) proxy;
}
}
@Override
public void onServiceDisconnected(int profile) {
synchronized (mLock) {
mMapProfile = null;
}
}
}
public Test(Contexst context){
mAdapter = BluetoothAdapter.getDefaultAdapter();
mAdapter.getProfileProxy(mContext, new MapServiceListener(), MAP_CLIENT);
}
// 獲得未讀短信
public void syncUnreadMessages(String address) {
synchronized (mLock) {
// 觸發同步
BluetoothDevice remoteDevice;
try {
// 獲得對應Address的藍牙遠程設備
remoteDevice = mAdapter.getRemoteDevice(address);
} catch (java.lang.IllegalArgumentException e) {
return;
}
if (mMapProfile != null) {
// 觸發未讀短信獲取
boolean isSuccess = mMapProfile.getUnreadMessages(remoteDevice);
}
}
}
}
五、藍牙短信發送功能開發
藍牙短信發送可參考代碼如下(BluetoothMapClient對象創建過程省略):
// 藍牙短信發送成功廣播注冊
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY);
filter.addAction(BluetoothMapClient.ACTION_MESSAGE_DELIVERED_SUCCESSFULLY);
BtServiceApplication.getApplication().registerReceiver(mBroadcastReceiver, filter);
// 藍牙短信發送成功廣播監聽
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY)) {
Logcat.d("Message sent successfully");
} else if (action.equals(BluetoothMapClient.ACTION_MESSAGE_DELIVERED_SUCCESSFULLY)) {
Logcat.d("Message delivered successfully");
}
}
};
/**
* 藍牙短信發送
*
* @param context
* @param address 藍牙設備對應的Address
* @param recipients 收件人號碼
* @param message 短信內容
*/
private void sendMessage(Context context, String address, Uri[] recipients, String message) {
BluetoothDevice remoteDevice;
try {
remoteDevice = mAdapter.getRemoteDevice(address);
} catch (java.lang.IllegalArgumentException e) {
Logcat.d(e.toString());
return;
}
if (mMapProfile != null) {
Logcat.d("Sending reply");
if (recipients == null) {
Logcat.d("Recipients is null");
return;
}
if (address == null) {
Logcat.d("BluetoothDevice is null");
return;
}
// 以下兩個Intent設置后,短信發送成就會有對應的廣播發送回來,若只需要一個,另一個可直接設置為null,簡單看了下源碼,目前未看到兩個Intent的明顯差異,后續再細究
mSentIntent = PendingIntent.getBroadcast(context, 0, mSendIntent, PendingIntent.FLAG_ONE_SHOT);
mDeliveredIntent = PendingIntent.getBroadcast(context, 0, mDeliveryIntent, PendingIntent.FLAG_ONE_SHOT);
mMapProfile.sendMessage(remoteDevice, recipients, message, mSentIntent, mDeliveredIntent);
}
}