日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

android车载蓝牙开发,车载蓝牙开发二

發布時間:2023/12/10 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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车载蓝牙开发,车载蓝牙开发二的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。