车载蓝牙开发二

本篇主要实现蓝牙电话,蓝牙音乐,同步通讯录通话记录。蓝牙的查找,连接可以看上一篇

一:蓝牙电话

蓝牙电话主要用到BluetoothHeadsetClient这个类,目录地址为frameworks\base\core\java\android\bluetooth\BluetoothHeadsetClient.java。

里面定义了很多广播意图,最有用的是这个action

/**
     * Intent sent whenever state of a call changes.
     *
     * <p>It includes:
     * {@link #EXTRA_CALL},
     * with value of {@link BluetoothHeadsetClientCall} instance,
     * representing actual call state.</p>
     */
    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 <code>true</code> if command has been issued successfully;
     *          <code>false</code> 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          <code>true</code> if command has been issued successfully;
     *                   <code>false</code> 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          <code>{@link BluetoothHeadsetClientCall} call</code> if command has been
     *                  issued successfully;
     *                  <code>{@link null}</code> 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          <code>true</code> if command has been issued successfully;
     *                   <code>false</code> otherwise;
     *                   upon completion HFP sends {@link #ACTION_CALL_CHANGED}
     *                   intent.
     *
     * <p>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.</p>
     */
    public boolean rejectCall(BluetoothDevice device)

挂断电话

/**
     * Rejects a call.
     *
     * @param device    remote device
     * @return          <code>true</code> if command has been issued successfully;
     *                   <code>false</code> otherwise;
     *                   upon completion HFP sends {@link #ACTION_CALL_CHANGED}
     *                   intent.
     *
     * <p>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.</p>
     */
    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          <code>true</code> if command has been issued successfully;
     *                   <code>false</code> 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());
                }

 

 

 

  • 8
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值