Android 蓝牙开发——HFP协议(九)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

SDK路径frameworks/base/core/java/android/bluetooth/

服务路径packages/apps/Bluetooth/src/com/android/bluetooth/

        在使用协议类的时候无法找到该类由于安卓源码中关于蓝牙协议的 Client 部分或相关接口都被 @hide 给隐藏掉了这样 android.jar 满足不了安卓源码 framework 层开发人员的需求可以使用反射机制或者引用 framework.jar 代替 android.jar。

位置out\target\common\obj\JAVA_LIBRARIES\framework_intermediates\classes.jar

        HSP 和 HFP都是为了实现蓝牙通话而制定所实现的功能都和蓝牙通话相关。基本上所有的蓝牙耳机、车载蓝牙都会支持这两个协议。

        HSP仅实现了最基本的通话操作接听电话、挂断电话、调节音量、声音在手机与蓝牙耳机之间切换。HFP在功能上是对HSP的扩展除了上述功能以外还包括来电拒接、耳机端来电显示等高级功能。

        在Android设计上并没有将上述两个协议分开显示而是均表述为“手机音频”。在使用的时候优先连接HFP只有在对方仅支持HSP或者HFP连接失败的时候才会尝试HSP。 

一、SDK 接口

        BluetoothHeadsetClient.java蓝牙apk中的HFP协议的代理类应用通过此代理类访问HFP协议的方法。

接口名描述
connect连接指定设备
disconnect断开指定设备
getConnectedDevices获取已连接设备列表
getDevicesMatchingConnectionStates获取指定状态的设备列表
getConnectionState获得指定设备的状态
setPriority设置协议的优先级
getPriority获取协议的优先级
startVoiceRecognition开始语音识别
stopVoiceRecognition停止语音识别
sendVendorAtCommand发送供应商特定的AT命令
getCurrentCalls获取任意状态下所有调用的列表
getCurrentAgEvents获取AG指标的当前值列表
acceptCall接听电话
holdCall保持通话
rejectCall拒绝来电
terminateCall终止指定的调用
enterPrivateMode使用指定的呼叫进入私有模式
explicitCallTransfer执行显式呼叫转移
dial使用指定号码进行呼叫
sendDTMF发送DTMF代码
getLastVoiceTagNumber获取AG上记录的最后一个语音标签对应的数字
getAudioState获取音频网关的当前音频状态
setAudioRouteAllowed设置是否允许音频路由
getAudioRouteAllowed获取是否允许音频路由
connectAudio发起音频通道的连接
disconnectAudio断开音频通道
getCurrentAgFeatures获取音频网关功能

重点关注的接口

/**
 * 连接HFP协议对应的为disconnect
 *
 * @param device 要连接的远程设备
 * @return 连接成功返回true失败则返回false
 */
public boolean connect(BluetoothDevice device) { }

/**
 * 拨打电话
 *
 * @param device 远程设备
 * @param number 有效电话号码
 * @return 返回值不为null命令成功发出
 */
public BluetoothHeadsetClientCall dial(BluetoothDevice device, String number) { }

/**
 * 接听电话
 *
 * @param device 远程设备
 * @param flag 在接受呼叫时标记操作策略
 * @return 命令成功发出返回true失败则返回false
 */
public boolean acceptCall(BluetoothDevice device, int flag) { }

/**
 * 拒接电话
 *
 * @param device 远程设备
 * @return 命令成功发出返回true失败则返回false
 */
public boolean rejectCall(BluetoothDevice device) { }

/**
 * 免提模式
 *
 * @param device 远程设备
 * @return 命令成功发出返回true失败则返回false
 */
public boolean connectAudio(BluetoothDevice device) { }

/**
 * 听筒模式
 *
 * @param device 远程设备
 * @return 命令成功发出返回true失败则返回false
 */
public boolean disconnectAudio(BluetoothDevice device) { }

二、APP调用SDK

1、获取 BluetoothHeadsetClient

BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
btAdapter.getProfileProxy(mService, new HeadsetServiceListener(), BluetoothProfile.HEADSET_CLIENT);

2、HeadsetServiceListener()

private BluetoothHeadsetClient headsetClient;

class HeadsetServiceListener implements BluetoothProfile.ServiceListener {
    @Override
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
        synchronized (this) {
            headsetClient = (BluetoothHeadsetClient) proxy;
        }
    }

    @Override
    public void onServiceDisconnected(int profile) {
        synchronized (this) {
            headsetClient = null;
        }
    }
}

        这里通过回调方法拿到 BluetoothHeadsetClient 类。通过该类可以调用 SDK 中的各种方法。

三、源码解析

        getProfileProxy() 在上一篇MAP协议中已经分析这里不再重复分析。

1、BluetoothHeadsetClient

private final BluetoothProfileConnector<IBluetoothHeadsetClient> mProfileConnector = new BluetoothProfileConnector(this, BluetoothProfile.HEADSET_CLIENT, "BluetoothHeadsetClient", IBluetoothHeadsetClient.class.getName()) {
    @Override
    public IBluetoothHeadsetClient getServiceInterface(IBinder service) {
        return IBluetoothHeadsetClient.Stub.asInterface(Binder.allowBlocking(service));
    }
};

/* package */ BluetoothHeadsetClient(Context context, ServiceListener listener, BluetoothAdapter adapter) {
    mAdapter = adapter;
    mAttributionSource = adapter.getAttributionSource();
    mProfileConnector.connect(context, listener);
}

        这里同样调用 BluetoothProfileConnector 的 connect()方法只是传入参数不同最后绑定的是 HeadsetClientService/packages/apps/Bluetooth/src/com/android/bluetooth/hfpclient/。同样 BluetoothHeadsetClient 的 connect() 与上面 BluetoothMapClient 中的 connect() 差不多最后都是调用各自服务的 connect() 方法。

2、HeadsetClientService.connect()

public boolean connect(BluetoothDevice device) {
    HeadsetClientStateMachine sm = getStateMachine(device);
    if (sm == null) {
        Log.e(TAG, "Cannot allocate SM for device " + device);
        return false;
    }

    if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
        Log.w(TAG, "Connection not allowed: <" + device.getAddress() + "> is CONNECTION_POLICY_FORBIDDEN");
        return false;
    }

    sm.sendMessage(HeadsetClientStateMachine.CONNECT, device);
    return true;
}

        这里通过 HeadsetClientStateMachine 调用 StateMachine 的 sendMessage() 方法。然后回到 processMessage() 函数。

3、HeadsetClientStateMachine.processMessage()

@Override
public synchronized boolean processMessage(Message message) {
    switch (message.what) {
        case CONNECT:
            BluetoothDevice device = (BluetoothDevice) message.obj;
            Attributable.setAttributionSource(device, ActivityThread.currentAttributionSource());
            mNativeInterface.connect(getByteAddress(device));
            break;
    }
}

         接下来就调用到 Native 层的 connect() 方法这里不再深入研究。

四、拨打电话解析

HeadsetClientService.dial()

BluetoothHeadsetClientCall dial(BluetoothDevice device, String number) {
    HeadsetClientStateMachine sm = getStateMachine(device);
    ......
    BluetoothHeadsetClientCall call = new BluetoothHeadsetClientCall(device,
    HeadsetClientStateMachine.HF_ORIGINATED_CALL_ID,
    BluetoothHeadsetClientCall.CALL_STATE_DIALING, number, false  /* multiparty */, true  /* outgoing */, sm.getInBandRing());
    Message msg = sm.obtainMessage(HeadsetClientStateMachine.DIAL_NUMBER);
    msg.obj = call;
    sm.sendMessage(msg);
    return call;
}

         可以看到最终都是调用状态机的 sendMessage 方法只是消息状态有所区别同样也会在 HeadsetClientStateMachine 的 processMessage() 方法里收到对应的消息然后调用 Native 层的对应方法。

五、其他

1、通话状态变更是按照下面顺序传递

        Bluedroid协议栈 -> Bluetooth FW -> 广播传递 -> Telecom -> App

2、手机侧如果不支持In-band ring来电时需要车机端播放本地铃音。

3、HeadsetClientStateMachine 蓝牙电话业务逻辑类主要核心函数

        queryCallsStart  两秒查询一次电话状态

        queryCallsUpdate  跟新本地电话状态

        queryCallsDone  根据电话状态处理相应通知

        通知App HFP连接状态函数broadcastConnectionState

4、FW蓝牙电话不与App直接交互需要通过 Telecom 的 updateCall来传递电话状态。

5、连接不上时多注意状态机中的 connect 变化。

6、打电话时经常收不到协议栈回调导致画面显示异常。

7、测试经常使用微信通话、钉钉通话来模拟手机打电话实际上手机一些软件的语音通话无法开启车机录音会导致对方听不到属于手机软件问题。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: android