本篇主要实现蓝牙电话,蓝牙音乐,同步通讯录通话记录。蓝牙的查找,连接可以看上一篇。
一:蓝牙电话
蓝牙电话主要用到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());
}