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车载蓝牙开发)