[关闭]
@JudyYe 2016-01-15T07:29:31.000000Z 字数 8930 阅读 507

软工文档 JudyYe

QueenBlue


软件实现

在本节将介绍如何将移植好的蓝牙协议应用到Android APP,以实现车载蓝牙的相应功能,包括:同步通讯录(pbap client),接打电话(hfp client),流同步播放音频(a2dp)。在介绍使用这三个协议之前,还将说明都要用到的监听机制是如何实现的并通过状态机简要分析三个协议的基本流程。

监听机制

三个协议都涉及了监听机制,并且像hfp client需要全局监听来电,所以将监听机制单独列写一节。
每一个界面都有需要监听的内容,在每个Activity中,注册与注销监听者的函数都分别命名为register() unregister()。每个Activity负责自己销毁自己,所以每一个因为某个事件而弹出的Activity,都会注册状态变化事件的监听者,比如

/src/com/example/myapp/OkActivity.java

中检测getCurrentCall有无正在拨通的电话来决定是否关闭自己。其它的Activity,如CalledActivity, CallActivity中都使用相似的方法。

  1. void register() {
  2. IntentFilter filter = new IntentFilter(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
  3. registerReceiver(new BroadcastReceiver() {
  4. @Override
  5. public void onReceive(Context context, Intent intent) {
  6. String action = intent.getAction();
  7. if (action.equals(BluetoothHeadsetClient.ACTION_CALL_CHANGED)) {
  8. List<BluetoothHeadsetClientCall> calls
  9. = Global.mBluetoothHeadsetClient.getCurrentCalls(Global.device);
  10. if (calls.size() == 0) {
  11. finish();
  12. } else {
  13. for (BluetoothHeadsetClientCall any : calls) {
  14. if (any.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
  15. finish();
  16. }
  17. }
  18. }
  19. }
  20. }
  21. }, filter);
  22. }

除了hfp的全局监听,在pbap中还会通过回调函数来实现监听,实现Handler中的handleMessage()

状态机

BluetoothAdapter,BluetoothHeadsetClient,都有各自的状态。为了加强鲁棒性,我们使用状态机的方法处理每个状态,如hfp client,还有大状态机套小状态机,比较麻烦。基本的流程如下图所示。在之后的三个协议独自的部分,我们将分别讨论如何应对各种不同状态。

建议在开发过程中,要管理号状态机的状态,便于了解状态机何时转移状态,比如Global.java中,我们是如下代码所示,管理不同的状态的.

  1. if (action.equals(BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED)) { // 大状态机里面的AudioConnection小状态机
  2. switch (mBluetoothHeadsetClient.getAudioState(device)) {
  3. case (BluetoothHeadsetClient.STATE_AUDIO_CONNECTED):
  4. makeText(getApplicationContext(), "APP: audio connected", LENGTH_SHORT).show();
  5. break;
  6. case (BluetoothHeadsetClient.STATE_AUDIO_CONNECTING):
  7. makeText(getApplicationContext(), "APP: audio connecting", LENGTH_SHORT).show();
  8. break;
  9. case (BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED):
  10. makeText(getApplicationContext(), "APP: audio disconnected", LENGTH_SHORT).show();
  11. break;
  12. }
  13. }

特别需要注意的是,主动连接Audio Connection的对象,销毁前该对象有责任断开连接,但幸运的是,其中APIterminateCall()会做好“善后工作”。

插入状态机图??

pbap client

连接

前提是蓝牙已经连接,得到了一个远端的BluetoothDevice device,连接过程需要创建一个Pbap Client,并连接。

/src/com/example/myapp/MyActivity.java的onCreate中,调用createPbapClientAndConnect()

  1. Global.client = new BluetoothPbapClient(Global.device, new Handler());
  2. Global.client.connect();

通信

当建立好pbap连接后,App与远端设备通过Message的传递机制通信。当App发出一个请求后,远端会发回一条响应Message,以回应请求者该条请求是被如何响应的。

/src/com/example/myapp/MyActivity.java的onCreate中,重写Handler.handleMessage(Message msg)

  1. Global.client = new BluetoothPbapClient(Global.device, new Handler() {
  2. @Override
  3. public void handleMessage(Message msg) {
  4. super.handleMessage(msg);
  5. switch (msg.what) {
  6. case BluetoothPbapClient.EVENT_SESSION_CONNECTED:
  7. Global.client.pullPhoneBook(BluetoothPbapClient.PB_PATH);
  8. makeText(MyActivity.this, "connected\npulling phone book", LENGTH_SHORT).show();
  9. break;
  10. case BluetoothPbapClient.EVENT_SESSION_DISCONNECTED:
  11. makeText(MyActivity.this, "disconnected pbap", LENGTH_SHORT).show();
  12. break;
  13. case BluetoothPbapClient.EVENT_SESSION_AUTH_REQUESTED:
  14. makeText(MyActivity.this, "authority requested", LENGTH_SHORT).show();
  15. break;
  16. case BluetoothPbapClient.EVENT_SESSION_AUTH_TIMEOUT:
  17. makeText(MyActivity.this, "authority timeout", LENGTH_SHORT).show();
  18. break;
  19. case BluetoothPbapClient.EVENT_PULL_PHONE_BOOK_DONE: // 成功拉取远端设备的联系人列表
  20. Global.pbList = (List<VCardEntry>)msg.obj;
  21. makeText(MyActivity.this, "pull pb successfully", LENGTH_SHORT).show();
  22. break;
  23. case BluetoothPbapClient.EVENT_PULL_PHONE_BOOK_ERROR:
  24. makeText(MyActivity.this, "pull pb error!", LENGTH_SHORT).show();
  25. break;
  26. }

具体的请求内容,请参阅PbapClient 的API 插入链接,这里,我们举例师范请求一个拉取远端设备的通讯录的动作。其实也非常简单:

  1. if (!Global.client.pullPhoneBook(BluetoothPbapClient.PB_PATH)) {
  2. Toast.makeText(this, "pull failed",
  3. Toast.LENGTH_SHORT).show();
  4. } else {
  5. Toast.makeText(this, "pull successfully",
  6. Toast.LENGTH_SHORT).show();
  7. }

只需要注意的是,调用函数返回值true 或false,并不代表本次请求响应的成功或失败,只是表示能否向远端发出请求。至于远端是否响应成功,是发出请求后的一段时间,远端发出Message,app在回调函数handleMessage里面的msg.what查看。

hfp client

接听电话,接通电话都是事件触发的,并且以上事件无论在何界面,都需要立刻执行(比如,无论在哪个界面,有电话拨进,都需要立刻停止当前音频流,并弹出来电显示对话框),所以,需要将hfp client的监听者注册在全局Application中。

/src/com/example/myapp/Global.java

有注册监听者以下代码。
注意,Global继承了Application,所以是全局的。

  1. IntentFilter intentFilter = new IntentFilter();
  2. intentFilter.addAction(BluetoothDevice.ACTION_FOUND); // 寻找蓝牙设备
  3. intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); // 配对蓝牙列表发生变化时被触发
  4. intentFilter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); // 寻找蓝牙模式变化时被触发
  5. intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); // 连接状态变化时被触发
  6. intentFilter.addAction(BluetoothHeadsetClient.ACTION_CALL_CHANGED); // 电话连接状态变化时被触发
  7. intentFilter.addAction(BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED); // 音频状态变化被触发
  8. intentFilter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); // 连接层变化时被触发

去电

需要支持直接拨号,从通讯录拨号,并且如果拨出的号码在联系人列表中,应该直接显示联系人信息。
电话拨打时,应该弹出拨号界面,并且允许停止拨打的操作。

/src/com/example/myapp/CallActivity.java中

  1. 拨打电话

    1. dial.setOnClickListener(new View.OnClickListener(){
    2. public void onClick(View v) {
    3. if (Global.mBluetoothHeadsetClient.getConnectionState(Global.device) == BluetoothHeadsetClient.STATE_CONNECTED) {
    4. Global.mBluetoothHeadsetClient.dial(Global.device, number_view.getText().toString());
    5. Toast.makeText(CallActivity.this, number_view.getText().toString(),LENGTH_SHORT).show();
    6. if (Global.mBluetoothHeadsetClient.connectAudio()) {
    7. Toast.makeText(CallActivity.this, "Audio connected successfully!",
    8. LENGTH_SHORT).show();
    9. Log.d("DEBUG", "" + Global.mBluetoothHeadsetClient.getAudioState(Global.device));
    10. } else {
    11. Toast.makeText(CallActivity.this, "Audio connection failed!",
    12. LENGTH_SHORT).show();
    13. }
    14. }
    15. }
    16. });
  2. 挂断电话

    1. putdown.setOnClickListener(new View.OnClickListener() {
    2. public void onClick(View v) {
    3. if (Global.mBluetoothHeadsetClient.getAudioState(Global.device) == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) {
    4. Global.mBluetoothHeadsetClient.terminateCall(Global.device, 0);
    5. }
    6. }
    7. });
  3. 管理拨打电话过程中的状态。
    需要做的:由于我们没有弄懂getCurrentCalls中返回的call list是以何种机制维护的,有时挂断后,call list不会立刻消失,并且顺序也不定,所以用了很多的逻辑判断找到我们当前最想要的那个call。
  1. void register() {
  2. IntentFilter intentFilter = new IntentFilter();
  3. intentFilter.addAction(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
  4. CallActivity.this.registerReceiver(receiver = new BroadcastReceiver() {
  5. @Override
  6. public void onReceive(Context context, Intent intent) {
  7. if (intent.getAction().equals(BluetoothHeadsetClient.ACTION_CALL_CHANGED)) {
  8. List<BluetoothHeadsetClientCall> calls = Global.mBluetoothHeadsetClient.getCurrentCalls(Global.device);
  9. if (calls.size() > 0 &&
  10. (calls.get(0).getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING
  11. || calls.get(0).getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING)) {
  12. hang.setVisibility(View.VISIBLE);
  13. dial.setVisibility(View.GONE);
  14. Toast.makeText(CallActivity.this, "state: " + calls.get(0).getState(), LENGTH_SHORT).show();
  15. } else {
  16. hang.setVisibility(View.GONE);
  17. dial.setVisibility(View.VISIBLE);
  18. if(calls.size() > 0 && calls.get(0).getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) {
  19. if(!Global.ok) {
  20. Global.ok = true;
  21. intent = new Intent(CallActivity.this, OkActivity.class);
  22. intent.putExtra("call", calls.get(0));
  23. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  24. context.startActivity(intent);
  25. }
  26. }
  27. }
  28. }
  29. }, intentFilter);

来电

对于来电,需要支持来电号码显示,如果在通讯录中,还要显示联系人信息。无论在任何界面,任何活动,如果有来电,应该立刻停止当前的音频,并弹出来电界面。来电见面应该支持挂断电话,接听电话等操作。如果选择挂断,记得断开音频连接;若接听,需要转到接听界面。
因为来电完全是事件触发的,所以建议后继者仔细考虑应对各种情况,增强鲁棒性。

  1. 监听来电,弹出窗口

    在/src/com/example/myapp/Global.java中

    1. case BluetoothHeadsetClientCall.CALL_STATE_INCOMING:
    2. if (!Global.incoming) {
    3. Global.incoming = true;
    4. Toast.makeText(getApplicationContext(), "call changed", Toast.LENGTH_LONG).show();
    5. Intent intent1 = new Intent(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
    6. intent1.putExtra("number", speCall.getNumber());
    7. intent1.putExtra("call", speCall);
    8. intent1.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    9. context.startActivity(intent1);
  2. 来电显示,查找通讯录

    在/src/com/example/myapp/CalledActivity.java中

    1. caller_number = getIntent().getStringExtra("number");
    2. number.setText(caller_number);
    3. VCardEntry caller = Global.lookForPb(caller_number, CalledActivity.this);
    4. caller_name = (caller == null) ? "Unknown Caller" : caller.getDisplayName();
    5. name.setText(caller_name);
  3. 接听电话

    1. accept.setOnClickListener(new View.OnClickListener() {
    2. @Override
    3. public void onClick(View v) {
    4. Global.mBluetoothHeadsetClient.acceptCall(Global.device, BluetoothHeadsetClient.CALL_ACCEPT_NONE);
    5. finish();
    6. }
    7. });
  4. 挂断电话

    1. reject.setOnClickListener(new View.OnClickListener() {
    2. @Override
    3. public void onClick(View v) {
    4. Global.mBluetoothHeadsetClient.rejectCall(Global.device);
    5. finish();
    6. }
    7. });

通话

通话过程中,支持挂断电话,也要监听电话是否被对方挂断。

  1. 主动挂断电话

    1. hangUp.setOnClickListener(new View.OnClickListener() {
    2. @Override
    3. public void onClick(View view) {
    4. Global.mBluetoothHeadsetClient.terminateCall(Global.device, 0);
    5. finish();
    6. }
    7. });
  2. 被对方挂断
    这里采用了一个很愚蠢的方法:当当前通话列表为空时,表示电话被挂断,结束Activity,同样是事件驱动的,注册相应的事件监听者。

    1. void register() {
    2. IntentFilter filter = new IntentFilter(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
    3. registerReceiver(new BroadcastReceiver() {
    4. @Override
    5. public void onReceive(Context context, Intent intent) {
    6. String action = intent.getAction();
    7. if (action.equals(BluetoothHeadsetClient.ACTION_CALL_CHANGED)) { // 当前通话列表变化
    8. List<BluetoothHeadsetClientCall> calls
    9. = Global.mBluetoothHeadsetClient.getCurrentCalls(Global.device);
    10. if (calls.size() == 0) {
    11. finish();
    12. } else {
    13. for (BluetoothHeadsetClientCall any : calls) {
    14. if (any.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
    15. finish();
    16. }
    17. }
    18. }
    19. }
    20. }
    21. }, filter);
    22. }

a2dp

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注