android车载蓝牙开发,车载蓝牙开发二
本篇主要實現藍牙電話,藍牙音樂,同步通訊錄通話記錄。藍牙的查找,連接可以看上一篇。
一:藍牙電話
藍牙電話主要用到BluetoothHeadsetClient這個類,目錄地址為frameworks\base\core\java\android\bluetooth\BluetoothHeadsetClient.java。
里面定義了很多廣播意圖,最有用的是這個action
/**
* Intent sent whenever state of a call changes.
*
*
It includes:
* {@link #EXTRA_CALL},
* with value of {@link BluetoothHeadsetClientCall} instance,
* representing actual call state.
*/
public static final String ACTION_CALL_CHANGED =
"android.bluetooth.headsetclient.profile.action.AG_CALL_CHANGED";
它監聽來電,接聽來電,去電,通話中等狀態,要想在車載設備中操作電話需要知道這些狀態。
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (null != intent) {
String action = intent.getAction();
Log.i(TAG, "BTService receiver action == "+action);
//監聽來電
if (BluetoothHeadsetClient.ACTION_CALL_CHANGED.equals(action)) {
BluetoothHeadsetClientCall mCall = (BluetoothHeadsetClientCall) intent.getExtra(BluetoothHeadsetClient.EXTRA_CALL, null);
if (mCall != null) {
int callState = mCall.getState();
Log.d(TAG, "when call status changes: mConnStat is " + mConnStat+" number == "+mCall.getNumber());
if (callState == BluetoothHeadsetClientCall.CALL_STATE_INCOMING) {
//來電
} else if (callState == BluetoothHeadsetClientCall.CALL_STATE_DIALING) {
//去電
} else if (callState == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) {
//接聽中
} else if (callState == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
//結束
}
}
}
看下它的構造方法
BluetoothHeadsetClient(Context context, ServiceListener l) {
mContext = context;
mServiceListener = l;
mAdapter = BluetoothAdapter.getDefaultAdapter();
IBluetoothManager mgr = mAdapter.getBluetoothManager();
if (mgr != null) {
try {
mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
} catch (RemoteException e) {
Log.e(TAG,"",e);
}
}
doBind();
}
mBluetoothStateChangeCallback監聽藍牙打開或關閉狀態,重點看下doBind方法
boolean doBind() {
Intent intent = new Intent(IBluetoothHeadsetClient.class.getName());
ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0);
intent.setComponent(comp);
if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0,
android.os.Process.myUserHandle())) {
Log.e(TAG, "Could not bind to Bluetooth Headset Client Service with " + intent);
return false;
}
return true;
}
其實就是去綁定一個service
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
if (DBG) Log.d(TAG, "Proxy object connected");
mService = IBluetoothHeadsetClient.Stub.asInterface(Binder.allowBlocking(service));
if (mServiceListener != null) {
mServiceListener.onServiceConnected(BluetoothProfile.HEADSET_CLIENT,
BluetoothHeadsetClient.this);
}
}
@Override
public void onServiceDisconnected(ComponentName className) {
if (DBG) Log.d(TAG, "Proxy object disconnected");
mService = null;
if (mServiceListener != null) {
mServiceListener.onServiceDisconnected(BluetoothProfile.HEADSET_CLIENT);
}
}
};
當服務連接時返回IBluetoothHeadsetClient并通知協議請求成功,這是aidl的客戶端,不了解aidl的去查看一下android跨進程通信相關知識。
/**
* Connects to remote device.
*
* Currently, the system supports only 1 connection. So, in case of the
* second connection, this implementation will disconnect already connected
* device automatically and will process the new one.
*
* @param device a remote device we want connect to
* @return true if command has been issued successfully;
* false otherwise;
* upon completion HFP sends {@link #ACTION_CONNECTION_STATE_CHANGED}
* intent.
*/
public boolean connect(BluetoothDevice device) {
if (DBG) log("connect(" + device + ")");
final IBluetoothHeadsetClient service = mService;
if (service != null && isEnabled() && isValidDevice(device)) {
try {
return service.connect(device);
} catch (RemoteException e) {
Log.e(TAG, Log.getStackTraceString(new Throwable()));
return false;
}
}
if (service == null) Log.w(TAG, "Proxy not attached to service");
return false;
}
連接設備就是調用服務端的connect方法。服務端的實現在packages\apps\Bluetooth\src\com\android\bluetooth\hfpclient\HeadsetClientService.java,有興趣的可以自己去看看服務端是如何實現的。
接聽電話
/**
* Accepts a call
*
* @param device remote device
* @param flag action policy while accepting a call. Possible values
* {@link #CALL_ACCEPT_NONE}, {@link #CALL_ACCEPT_HOLD},
* {@link #CALL_ACCEPT_TERMINATE}
* @return true if command has been issued successfully;
* false otherwise;
* upon completion HFP sends {@link #ACTION_CALL_CHANGED}
* intent.
*/
public boolean acceptCall(BluetoothDevice device, int flag)
撥打電話
/**
* Places a call with specified number.
*
* @param device remote device
* @param number valid phone number
* @return {@link BluetoothHeadsetClientCall} call if command has been
* issued successfully;
* {@link null} otherwise;
* upon completion HFP sends {@link #ACTION_CALL_CHANGED}
* intent in case of success; {@link #ACTION_RESULT} is sent
* otherwise;
*/
public BluetoothHeadsetClientCall dial(BluetoothDevice device, String number)
拒接接聽
/**
* Rejects a call.
*
* @param device remote device
* @return true if command has been issued successfully;
* false otherwise;
* upon completion HFP sends {@link #ACTION_CALL_CHANGED}
* intent.
*
*
Feature required for successful execution is being reported by:
* {@link #EXTRA_AG_FEATURE_REJECT_CALL}.
* This method invocation will fail silently when feature is not supported.
*/
public boolean rejectCall(BluetoothDevice device)
掛斷電話
/**
* Rejects a call.
*
* @param device remote device
* @return true if command has been issued successfully;
* false otherwise;
* upon completion HFP sends {@link #ACTION_CALL_CHANGED}
* intent.
*
*
Feature required for successful execution is being reported by:
* {@link #EXTRA_AG_FEATURE_REJECT_CALL}.
* This method invocation will fail silently when feature is not supported.
*/
public boolean rejectCall(BluetoothDevice device)
發送DTMF編碼
/**
* Sends DTMF code.
*
* Possible code values : 0,1,2,3,4,5,6,7,8,9,A,B,C,D,*,#
*
* @param device remote device
* @param code ASCII code
* @return true if command has been issued successfully;
* false otherwise;
* upon completion HFP sends {@link #ACTION_RESULT} intent;
*/
public boolean sendDTMF(BluetoothDevice device, byte code)
根據自己的需求直接調用對應的方法,這里只列舉了常用的,更多api 自己可以去frameworks\base\core\java\android\bluetooth\BluetoothHeadsetClient.java 中查看
二.藍牙音樂
A2DP和AVRCP協議的請求和連接與上面完全一樣,實現方式也一模一樣。只要BluetoothProfile.A2DP_SINK連接成功,聲音就能傳輸的車載藍牙設備上。8.1源碼音樂控制有一個坑,客戶端去掉了藍牙指令發送的方法sendPassThroughCmd(BluetoothDevice device,int keyCode,int keyState),但是服務端還保留了這方法的實現,不知道是這邊移植代碼時漏掉了還是google去掉的,如果碰巧你開發的版本也沒有這個方法可以加上。
先在frameworks\base\core\java\android\bluetooth\BluetoothAvrcpController.java中添加
public void sendPassThroughCmd(BluetoothDevice device, int keyCode, int keyState) {
Log.d(TAG, "sendPassThroughCmd dev = " + device + " key " + keyCode + " State = "
+ keyState);
final IBluetoothAvrcpController service = mService;
if (mService != null && isEnabled()) {
try {
mService.sendPassThroughCmd(device, keyCode, keyState);
return;
} catch (RemoteException e) {
Log.e(TAG, "Error talking to BT service in sendPassThroughCmd()", e);
return;
}
}
if (mService == null) Log.w(TAG, "Proxy not attached to service");
}
在frameworks\base\core\java\android\bluetooth\IBluetoothAvrcpController.aidl 中添加方法void sendPassThroughCmd (in BluetoothDevice device, int keyCode, int keyState);
藍牙指令都放在frameworks\base\core\java\android\bluetooth\BluetoothAvrcp.java 中
暫停音樂sendPassThroughCmd(BluetoothDevice,BluetoothAvrcp.PASSTHROUGH_ID_PAUSE,BluetoothAvrcp.PASSTHROUGH_STATE_PRESS);
sendPassThroughCmd(BluetoothDevice,BluetoothAvrcp.PASSTHROUGH_ID_PAUSE,PASSTHROUGH_STATE_RELEASE);
注意要發送兩次一次press狀態,一次relsease狀態。
播放音樂指令BluetoothAvrcp.PASSTHROUGH_ID_PLAY
下一首指令BluetoothAvrcp.PASSTHROUGH_ID_FORWARD
上一首指令BluetoothAvrcp.PASSTHROUGH_ID_BACKWARD
這幾種協議所對應的功能實現方式都差不多,例如這個AvrcpControllerService
//Binder object: Must be static class or memory leak may occur
private static class BluetoothAvrcpControllerBinder extends IBluetoothAvrcpController.Stub
implements IProfileServiceBinder {
private AvrcpControllerService mService;
private AvrcpControllerService getService() {
if (!Utils.checkCaller()) {
Log.w(TAG, "AVRCP call not allowed for non-active user");
return null;
}
if (mService != null && mService.isAvailable()) {
return mService;//得到自己本身
}
return null;
}
BluetoothAvrcpControllerBinder(AvrcpControllerService svc) {
mService = svc;
}
@Override
public void sendGroupNavigationCmd(BluetoothDevice device, int keyCode, int keyState) {
Log.v(TAG, "Binder Call: sendGroupNavigationCmd");
AvrcpControllerService service = getService();
if (service == null) {
return;
}
if (device == null) {
throw new IllegalStateException("Device cannot be null!");
}
調用自身的sendGroupNavigationCmd方法
service.sendGroupNavigationCmd(device, keyCode, keyState);
}
......
首先實現了aidl方法,自己可以去看看,都是調用自身的方法。啟動狀態機StateMachine,然后與狀態機進行交互,狀態機的作用是藍牙不同狀態下處理不同的邏輯。藍牙,wifi等都用到了狀態機模式,所以要看懂代碼,先要把狀態機模式搞懂。
protected boolean start() {
HandlerThread thread = new HandlerThread("BluetoothAvrcpHandler");
thread.start();
mAvrcpCtSm = new AvrcpControllerStateMachine(this);
mAvrcpCtSm.start();//開啟狀態機
setAvrcpControllerService(this);
return true;
}
public synchronized void sendGroupNavigationCmd(BluetoothDevice device, int keyCode, int keyState) {
Log.v(TAG, "sendGroupNavigationCmd keyCode: " + keyCode + " keyState: " + keyState);
if (device == null) {
Log.e(TAG, "sendGroupNavigationCmd device is null");
}
if (!(device.equals(mConnectedDevice))) {
Log.e(TAG, " Device does not match " + device + " connected " + mConnectedDevice);
return;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");//判斷權限
Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
MESSAGE_SEND_GROUP_NAVIGATION_CMD, keyCode, keyState, device);
mAvrcpCtSm.sendMessage(msg);//發送消息給狀態機處理
}
class Connected extends State {//在連接狀態下處理邏輯
@Override
public boolean processMessage(Message msg){
.....
case MESSAGE_SEND_PASS_THROUGH_CMD:
BluetoothDevice device = (BluetoothDevice) msg.obj;
AvrcpControllerService 又回去調用native方法
.sendPassThroughCommandNative(Utils.getByteAddress(device), msg.arg1,
msg.arg2);
if (a2dpSinkService != null) {
Log.d(TAG, " inform AVRCP Commands to A2DP Sink ");
a2dpSinkService.informAvrcpPassThroughCmd(device, msg.arg1, msg.arg2);
}
break;
.....
}
基本上服務端的實現都是這種模式,哪個api調不通或有問題都能找到實現從而分析出原因,可以追蹤到jni層。
監聽音樂暫停與播放
我是監聽BluetoothAvrcpController.ACTION_TRACK_EVENT這個廣播,在AvrcpControllerStateMachine中會回調播放消息。
class Connected extends State {
@Override
public boolean processMessage(Message msg) {
Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
synchronized (mLock) {
switch (msg.what) {
case MESSAGE_STOP_METADATA_BROADCASTS:
mBroadcastMetadata = false;
broadcastPlayBackStateChanged(new PlaybackState.Builder().setState(
PlaybackState.STATE_PAUSED, mAddressedPlayer.getPlayTime(),
0).build());
break;
case MESSAGE_START_METADATA_BROADCASTS:
mBroadcastMetadata = true;
broadcastPlayBackStateChanged(mAddressedPlayer.getPlaybackState());
if (mAddressedPlayer.getCurrentTrack() != null) {
broadcastMetaDataChanged(
mAddressedPlayer.getCurrentTrack().getMediaMetaData());
}
break;
連接狀態下,會接收聲音信息并發出一個廣播
private void broadcastPlayBackStateChanged(PlaybackState state) {
Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
intent.putExtra(AvrcpControllerService.EXTRA_PLAYBACK, state);
if (DBG) {
Log.d(TAG, " broadcastPlayBackStateChanged = " + state.toString());
}
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
三、同步通訊錄和通話記錄
在7.0之前可以參考https://blog.csdn.net/bingsiju123123/article/details/53065108,8.0源碼中這部分代碼改了,改成pbap profile一連接上馬上去同步通訊錄與通話記錄,然后批處理寫入到ContentProvider中。
class Connected extends State {
@Override
public void enter() {
Log.d(TAG, "Enter Connected: " + getCurrentMessage().what);
onConnectionStateChanged(mCurrentDevice, mMostRecentState,
BluetoothProfile.STATE_CONNECTED);
mMostRecentState = BluetoothProfile.STATE_CONNECTED;
if (mUserManager.isUserUnlocked()) {
mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
.sendToTarget();
}
}
@Override
public boolean processMessage(Message message) {
if (DBG) Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
switch (message.what) {
case MSG_DISCONNECT:
if ((message.obj instanceof BluetoothDevice) &&
((BluetoothDevice) message.obj).equals(mCurrentDevice)) {
transitionTo(mDisconnecting);
}
break;
default:
Log.w(TAG, "Received unexpected message while Connected");
return NOT_HANDLED;
}
return HANDLED;
}
}
當profile連接成功之后會PbapClientConnectionHandler發出MSG_DOWNLOAD消息
try {
//創建賬號
mAccountCreated = addAccount(mAccount);
if (mAccountCreated == false) {
Log.e(TAG, "Account creation failed.");
return;
}
// Start at contact 1 to exclued Owner Card PBAP 1.1 sec 3.1.5.2
//下載通訊錄
BluetoothPbapRequestPullPhoneBook request =
new BluetoothPbapRequestPullPhoneBook(
PB_PATH, mAccount, PBAP_REQUESTED_FIELDS, VCARD_TYPE_30, 0, 1);
request.execute(mObexSession);
PhonebookPullRequest processor =
new PhonebookPullRequest(mPbapClientStateMachine.getContext(),
mAccount);
Log.d(TAG, "request.getList().size() == "+request.getList().size());
processor.setResults(request.getList());
processor.onPullComplete();
//下載來電、去電、未接來電
downloadCallLog(MCH_PATH);
downloadCallLog(ICH_PATH);
downloadCallLog(OCH_PATH);
//因為下載完沒有通知或回調,所以這里我自己做是發送一個廣播給應用去contentproview中取數據
mContext.sendBroadcast(new Intent(ACTION_DOWNLOAD_COMPLETE));
} catch (IOException e) {
mContext.sendBroadcast(new Intent(ACTION_DOWNLOAD_EXCEPTION));
Log.w(TAG, "DOWNLOAD_CONTACTS Failure" + e.toString());
}
這里還有一個坑,源碼中下載通話記錄沒有把名稱存進去,有需要的可以在onPullComplete()時for循環中加上
if (TextUtils.isEmpty(vcard.getDisplayName())) {
values.put(CallLog.Calls.CACHED_NAME, "");
} else {
values.put(CallLog.Calls.CACHED_NAME, vcard.getDisplayName());
}
總結
以上是生活随笔為你收集整理的android车载蓝牙开发,车载蓝牙开发二的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 1506G. Maximize the
- 下一篇: 陇东学院c语言程序设计,C语言程序设计