《Android 3D游戏开发技术宝典——OpenGL ES 2.0》——2.7节蓝牙通信

本节书摘来自异步社区《Android 3D游戏开发技术宝典——OpenGL ES 2.0》一书中的第2章,第2.7节蓝牙通信,作者 吴亚峰,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.7 蓝牙通信
Android 3D游戏开发技术宝典——OpenGL ES 2.0
随着硬件设备价格的不断降低,大部分智能手机上都已配备了蓝牙网络模块,Android设备也是如此。如果能为一些小型的休闲娱乐游戏增加蓝牙联网对战的功能,将会大大增加游戏的可玩性。本节将向读者详细介绍如何在Android平台下开发具有蓝牙互联功能的应用程序。

2.7.1 蓝牙通信的基本知识
蓝牙是一种支持设备短距离通信(一般是10m以内)的无线技术,其数据传输时不仅不需要连线,而且传输速率也比传统手持设备的红外模式更加迅速、高效,主要优势如下所列。

免费。
蓝牙无线技术规格供全球的成员公司免费使用。除了设备费用外,制造商不需要为使用蓝牙技术再支付任何知识产权费用,这大大降低了蓝牙技术的普及门槛。

应用范围广。
蓝牙技术得到了广泛的应用,集成该技术的产品从手机、汽车到医疗设备等应有尽有,使用该技术的用户从消费者、工业市场到企业等,不一而足。

易于使用。
蓝牙是一项即时技术,其不要求固定的基础设施,且易于安装和设置,而且无须电缆即可实现连接,使用起来非常方便。

提示 上一小节介绍的Socket技术虽然开发很容易,但对用户而言要么需要使用3G网络,要么需要通过Wifi网络。3G网络虽然没有位置限制,但费用不低;Wifi虽然免费,但必须在AP附近或自己架设AP。可以看出,对于短距离即时互联而言,采用蓝牙技术更为合理、便捷。
全球通用的规格。
蓝牙无线技术是当今市场上支持范围最广泛,功能最丰富且安全的无线标准之一,全球范围内的资格认证程序可以测试成员的产品是否符合标准。

介绍完了蓝牙技术的特点与优势后,下面简单介绍一下蓝牙设备的使用步骤,具体如下所列。

(1)开启要搜索的设备的蓝牙功能,并设置为可见。

(2)在一个设备中开启搜索设备的功能,开始搜索设备。

(3)当搜索到其他设备后,会将搜索的设备显示在本设备的列表中。

(4)选择列表中的某一设备,请求匹配。

(5)被选中的设备收到匹配请求后,经双方验证同意,设备匹配成功。

(6)设备匹配成功后就可以建立连接,并收发数据了。

提示 *蓝牙通信与Socket网络通信的基本思想非常类似,都是连接成功后建立双向数据流收发数据,但开发起来要比Socket复杂一些。这主要是因为蓝牙设备的搜索功能和配对列表的显示需要开发人员自行编写代码实现,下面几个小节的案例将对此进行详细的介绍。

2.7.2 聊天案例概览
本小节将介绍一个用蓝牙技术实现聊天功能的案例,通过对本案例的学习,读者可以掌握关于蓝牙通信的整个开发过程。在开发具体代码之前,首先了解一下本案例的运行方法及运行效果,具体情况如下所列。

(1)准备两部Android手机,在两部手机上都安装本案例的apk(Sample2_10.apk)。

提示 笔者使用的两部手机分别为华为u8800和摩托罗拉ME525,其他支持蓝牙功能的Android智能手机也可以使用。之所以要使用真机是因为模拟器几乎无法进行蓝牙程序的模拟测试,真机则要方便得多。
(2)将两部手机的蓝牙功能都打开,并设置为可见。

(3)在两部手机中同时运行本案例。

(4)在其中的一部手机中单击Menu键(手机上自带的菜单键)弹出设备搜索列表,如图2-31所示。然后单击列表下方的“扫描设备”按钮搜索设备,搜索完毕后列表中出现另一手机的设备名称及硬件地址,如图2-32所示。


ad562ceed0e8193ef8d2e9aa12796184fb8053be

(5)单击列表中搜索到的设备名称,一段时间后两部手机会同时提示连接成功,如图2-33和图2-34所示。


25da46a47e121eb366fbaefb87df9824a8368d95

(6)在一部手机聊天界面的文本框中输入一些文本信息,再单击“发送”按钮,另一部手机会弹出该手机的名称及接收到的消息,如图2-35和图2-36所示。


a10efd94efdf2ef992e6b7dcbbcf20c0d4db1f38

提示 本案例运行时的界面还有很多,由于篇幅所限,这里只给出主要的运行界面,其他界面请读者自行运行随书光盘中的本案例进行测试。

2.7.3 聊天案例的开发过程
介绍完本案例的运行过程及效果后,本小节将详细介绍本案例的开发过程。由于本案例中涉及的类比较多,因此在开发代码之前首先介绍一下本案例中各个类之间的关系及各自的用途,如图2-37所示。


cdac327fc874902db3ae79398fa3437ed643777f

从图2-37中可以看出,本案例中包含两个Activity、一个Service、3个线程,各自的用途如下所列。

Sample2_10_Activity为本案例的主Activity,程序一启动其就开始运行,主要功能为用来显示对话信息;MyDeviceListActivity用于显示搜索到的可连接设备列表,通过菜单启动;MyService则用来在后台管理蓝牙连接。
AcceptThread用于在蓝牙连接请求接收方监听连接请求;ConnectThread用来向别的设备发出连接请求;ConnectedThread用来在蓝牙连接建立后接收对方设备发送过来的消息。
了解完本案例中各个类之间的关系及各自的用途后,就可以进行代码的开发了,具体步骤如下所列。

(1)首先开发本案例的主控制类——Sample2_10_Activity,该类在程序开始时执行,其中重写了onCreate方法、onStart方法以及onDestroy等方法,具体代码如下。

1 package com.bn.pp10;          //声明包
2 import android.app.Activity;        //引入相关类
3 ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4 import android.widget.Toast;        //引入相关类
5 public class Sample2_10_Activity extends Activity {
6  private EditText outEt;        // 布局中的控件引用
7  private Button sendBtn;
8  private String connectedNameStr = null;     // 已连接的设备名称
9  private StringBuffer outSb;       // 发送的字符信息
10  private BluetoothAdapter btAdapter = null;   // 本地蓝牙适配器
11  private MyService myService = null;    // Service引用
12  public void onCreate(Bundle savedInstanceState) {
13   super.onCreate(savedInstanceState);
14   setContentView(R.layout.main);
15   btAdapter = BluetoothAdapter.getDefaultAdapter();// 获取本地蓝牙适配器
16  }
17  public void onStart() {
18   super.onStart();
19         // 如果蓝牙没有开启,提示开启蓝牙,并退出Activity
20   if (!btAdapter.isEnabled()) {
21    Toast.makeText(this, "请先开启蓝牙!", Toast.LENGTH_LONG).show();
22    finish();
23   } else {          // 否则初始化聊天的控件
24    if (myService == null)
25     initChat();
26  }}
27  public synchronized void onResume() {
28   super.onResume();  
29   if (myService != null) {      // 创建并开启Service
30    if (myService.getState() == MyService.STATE_NONE) {// 如果Service为空状态
31     myService.start();     // 开启Service
32  }}}
33  private void initChat() {
34   outEt = (EditText) findViewById(R.id.edit_text_out); // 获取编辑文本框的引用
35   sendBtn = (Button) findViewById(R.id.button_send);
            // 获取发送按钮引用,并为其添加监听
36   sendBtn.setOnClickListener(new OnClickListener() {
37    public void onClick(View v) {
38           // 获取编辑文本框中的文本内容,并发送消息
39     TextView view = (TextView) findViewById(R.id.edit_text_out);
40     String message = view.getText().toString();
41     sendMessage(message);
42   }});
43   myService = new MyService(this, mHandler);   // 创建Service对象
44   outSb = new StringBuffer("");   // 初始化存储发送消息的StringBuffer
45  }
46  public void onDestroy() {
47   super.onDestroy();
48   if (myService != null) {       // 停止Service
49    myService.stop();
50  }}
51  private void sendMessage(String message) {    // 发送消息的方法
52   ……//此处省略了部分源代码,将在后面步骤中给出
53  }}
54            //处理从Service发来消息的Handler
55  private final Handler mHandler = new Handler() {
56   ……//此处省略了部分源代码,将在后面步骤中给出
57  }}};
58  public void onActivityResult(int requestCode, int resultCode, Intent data) {
59   ……//此处省略了部分源代码,将在后面步骤中给出
60  }}
61  public boolean onPrepareOptionsMenu(Menu menu) {
62            // 启动设备列表Activity搜索设备
63   Intent serverIntent = new Intent(this, MyDeviceListActivity.class);
64   startActivityForResult(serverIntent, 1);
65   return true;
66 }}

第6-16行是该类成员变量的定义及重写的onCreate方法,其主要工作为跳转到主界面并通过BluetoothAdapter类的静态方法getDefaultAdapter获取蓝牙适配器对象的引用。
第17-26行重写了onStart方法。在该方法中查看蓝牙是否开启,如果蓝牙没有开启,则提示开启蓝牙,并退出Activity;否则调用initChat方法初始化聊天界面。
第27-32行重写了onResume方法。在该方法中检测是否已经启动了后台服务,若后台服务为空,则调用其start方法,开启后台服务。此后台服务用于管理蓝牙的连接及数据的收发,具体开发在后面进行介绍。
第33-45行为初始化聊天界面的方法。其中首先为发送按钮添加了监听器,接着创建了后台服务对象与字符串缓冲对象。
第51-60行省略了发送消息的sendMessage方法、onActivityResult方法及Handler内部类的代码,这些省略部分将在后面一一进行详细介绍。
第61-66行为每次按下Menu键调用的方法。在该方法中创建一个Intent消息,通过其启动搜索设备列表的Activity。其中的“1”与onActivityResult方法中的“1”相对应。
(2)接下来介绍Sample2_10_Activity类中省略的sendMessage方法。该方法负责发送消息,其代码如下。

1 private void sendMessage(String message) {   // 发送消息的方法
2             // 先检查是否已经连接到设备
3  if (myService.getState() != MyService.STATE_CONNECTED) {
4   Toast.makeText(this, R.string.not_connected, Toast.LENGTH_SHORT)
5     .show();
6   return;
7  }
8  if (message.length() > 0) {     // 如果消息不为空再发送消息
9   byte[] send = message.getBytes();   // 获取发送消息的字节数组,并发送
10   myService.write(send);
11   outSb.setLength(0);     //消除StringBuffer和文本框的内容
12   outEt.setText(outSb);
13 }};

第1-7行检查是否已成功经连接到设备,如果没有连接成功,则弹出提示信息。
第8-12行为在连接成功的情况下,判断待发送的消息是否为空,若消息不为空,则再将信息以字节数组的形式,通过myService的write方法发送出去。
(3)然后介绍前面省略的处理从Service发来的消息的Handler类的开发,用下列代码替代前面Sample2_10_Activity类中的第55-57行代码。

1 // 处理从Service发来的消息的Handler
2 private final Handler mHandler = new Handler() {
3  public void handleMessage(Message msg) {
4   switch (msg.what) {
5   case Constant.MSG_READ:
6    byte[] readBuf = (byte[]) msg.obj;
7    String readMessage = new String(readBuf, 0, msg.arg1);//创建要发送信息的字符串
8    Toast.makeText(Sample2_10_Activity.this,
9      connectedNameStr + ":  " + readMessage,
10      Toast.LENGTH_LONG).show();
11    break;
12   case Constant.MSG_DEVICE_NAME:
13              // 获取已连接的设备名称
14    connectedNameStr = msg.getData().getString(
15      Constant.DEVICE_NAME);
16    Toast.makeText(getApplicationContext(),  //并弹出提示信息
17      "已连接到 " + connectedNameStr, Toast.LENGTH_SHORT)
18      .show();
19    break;
20 }}};

第4-11行为收到待读数据信息时的处理代码,其功能为从msg中获取字节数组形式的信息,将其转化成字符串,并以Toast的形式显示在屏幕上。
第12-19行为收到已连接设备名称信息的处理代码,其功能为获取已连接设备的名称,并弹出Toast提示信息。
(4)接下来开发重写的onActivityResult方法,用下列代码替代前面Sample2_10_Activity类中的第58-60行代码。

1 public void onActivityResult(int requestCode, int resultCode, Intent data) {
2  switch (requestCode) {
3  case 1:       // 如果设备列表Activity返回一个连接的设备
4   if (resultCode == Activity.RESULT_OK) {
5    String address = data.getExtras().getString(// 获取设备的MAC地址
6      MyDeviceListActivity.EXTRA_DEVICE_ADDR);
7    BluetoothDevice device = btAdapter
8      .getRemoteDevice(address); // 获取BLuetoothDevice对象
9    myService.connect(device);   // 连接该设备
10   }
11   break;
12 }};

第1行为该方法的签名,其中requestCode参数用来标识是从哪一个Activity跳转到该Activity,resultCode参数用来表示返回值的状态,data参数里包含了返回的数据。
第2-12行功能为:若设备列表Activity成功返回了选中的目标连接设备,则调用蓝牙适配器对象的getRemoteDevice方法获取远端设备对象,然后将远端设备对象传递给自定义的后台服务连接该设备。
(5)下一步将开发用于显示可连接设备列表的Activity类——MyDeviceListActivity类,其代码如下。

1 package com.bn.pp10;           //声明包
2 import java.util.Set;           //引入相关类
3 ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4 import android.widget.AdapterView.OnItemClickListener;   //引入相关类
5 public class MyDeviceListActivity extends Activity {
6  public static String EXTRA_DEVICE_ADDR = "device_address"; // extra信息名称
7  private BluetoothAdapter myBtAdapter;      // 成员变量
8  private ArrayAdapter<String> myAdapterPaired;
9  private ArrayAdapter<String> myAdapterNew;
10  protected void onCreate(Bundle savedInstanceState) {
11   super.onCreate(savedInstanceState);
12   requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);// 设置窗口
13   setContentView(R.layout.device_list);
14   // 设置为当结果是Activity.RESULT_CANCELED时,返回到该Activity的调用者
15   setResult(Activity.RESULT_CANCELED);
16   Button scanBtn = (Button) findViewById(R.id.button_scan);// 初始化搜索按钮
17   scanBtn.setOnClickListener(new OnClickListener() {
18    public void onClick(View v) {
19     doDiscovery();
20     v.setVisibility(View.GONE);    // 使按钮不可见
21    }
22   });
23               // 初始化适配器
24   myAdapterPaired = new ArrayAdapter<String>(this,
25     R.layout.device_name);    // 已配对的
26   myAdapterNew = new ArrayAdapter<String>(this,
27     R.layout.device_name);    // 新发现的
28              // 将已配对的设备放入列表中
29   ListView lvPaired = (ListView) findViewById(R.id.paired_devices);
30   lvPaired.setAdapter(myAdapterPaired);
31   lvPaired.setOnItemClickListener(mDeviceClickListener);
32              // 将新发现的设备放入列表中
33   ListView lvNewDevices = (ListView) findViewById(R.id.new_devices);
34   lvNewDevices.setAdapter(myAdapterNew);
35   lvNewDevices.setOnItemClickListener(mDeviceClickListener);
36              // 注册发现设备时的广播
37   IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
38   this.registerReceiver(mReceiver, filter);
39              // 注册搜索完成时的广播
40   filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
41   this.registerReceiver(mReceiver, filter);
42   myBtAdapter = BluetoothAdapter.getDefaultAdapter(); // 获取本地蓝牙适配器
43  Set<BluetoothDevice> pairedDevices = myBtAdapter.getBondedDevices();
               // 获取已配对的设备
44            // 将所有已配对设备信息放入列表中
45   if (pairedDevices.size() > 0) {
46    findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE);
47    for (BluetoothDevice device : pairedDevices) {
48     myAdapterPaired.add(device.getName() + "\n"
49       + device.getAddress());
50    }
51   } else {
52    String noDevices = getResources().getText(R.string.none_paired)
53      .toString();
54    myAdapterPaired.add(noDevices);
55  }}
56  protected void onDestroy() {
57   ……//此处省略了部分源代码,该方法非常简单,请读者自行查看随书光盘中的源代码。
58  }
59            //用蓝牙适配器搜索设备的方法
60  private void doDiscovery() {
61   ……//此处省略了部分源代码,将在后面步骤中给出
62  }
63            // 列表中设备按下时的监听器
64  private OnItemClickListener mDeviceClickListener = new OnItemClickListener() {
65   ……//此处省略了部分源代码,将在后面步骤中给出
66  };
67           // 监听搜索到的设备的BroadcastReceiver
68  private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
69   ……//此处省略了部分源代码,将在后面步骤中给出
70 };}

第6-9行为该类成员变量的定义。其中有extra信息的名称、蓝牙适配器对象的引用、已配对设备和新搜索到设备的列表对应适配器的声明。
第10-55行为重写的onCreate方法,其中首先初始化了成员变量及布局文件中的相关资源,然后注册发现设备和搜索完成时的广播接收器,并初始化了已配对设备列表。
第60-70行省略了doDiscovery方法、mDeviceClickListener监听器及mReceiver内部类的具体代码,这些代码将在后面一一进行详细介绍。

提示 由于篇幅所限,设备列表布局文件device_list.xml代码的开发在此没有进行介绍,需要的读者请自行查看随书光盘中的源代码。
(6)接下来将开发前面省略的负责搜索设备的doDiscovery方法,其代码如下。

1 private void doDiscovery() {
2              // 在标题上显示正在搜索的标志
3  setProgressBarIndeterminateVisibility(true);
4  setTitle(R.string.scanning);
5              // 显示搜索到的新设备的副标题
6  findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE);
7  if (myBtAdapter.isDiscovering()) {     // 如果正在搜索,取消本次搜索
8   myBtAdapter.cancelDiscovery();
9  }
10  myBtAdapter.startDiscovery();     // 开始搜索
11 };

说明 在该方法中首先更改UI界面(如在标题上显示正在搜索的标志),完成相关操作后,调用蓝牙适配器的startDiscovery方法进行设备搜索。
(7)下一步将开发前面省略的监听器mDeviceClickListener的对应代码,具体内容如下。

1 private OnItemClickListener mDeviceClickListener = new OnItemClickListener() {
2  public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) {
3   myBtAdapter.cancelDiscovery();     // 取消搜索
4   String msg = ((TextView) v).getText().toString(); // 获取设备的MAC地址
5   String address = msg.substring(msg.length() - 17);
6   Intent intent = new Intent();     // 创建带有MAC地址的Intent
7   intent.putExtra(EXTRA_DEVICE_ADDR, address);
8   setResult(Activity.RESULT_OK, intent);   // 设置结果并退出Activity
9   finish();
10  }};

上述实现监听功能的onItemClick方法中,首先取消搜索,然后获取并发送设备的MAC地址给主界面Activity。
第8行setResult方法中的参数Activity.RESULT_OK表示成功地获取了要连接的设备的硬件地址。接收方的Activity根据Activity.RESULT_OK会做出相应的处理,前面步骤4中已经进行了介绍。
(8)接下来将开发前面省略的用于接收设备搜索到系统广播的广播接收器——mReceiver,其代码如下。

``
1 // 监听搜索到的设备的BroadcastReceiver
2 private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
3 @Override
4 public void onReceive(Context context, Intent intent) {
5 String action = intent.getAction();
6 if (BluetoothDevice.ACTION_FOUND.equals(action)) {// 如果找到设备
7 // 从Intent中获取BluetoothDevice对象
8 BluetoothDevice device = intent
9 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
10 // 如果没有配对,将设备加入新设备列表
11 if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
12 myAdapterNew.add(device.getName() + "n"
13 + device.getAddress());
14 }
15 // 当搜索完成后,改变Activity的标题
16 } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED
17 .equals(action)) {
18 setProgressBarIndeterminateVisibility(false);
19 setTitle(R.string.select_device);
20 if (myAdapterNew.getCount() == 0) { //没有找到设备
21 String noDevices = getResources().getText(
22 R.string.none_found).toString();
23 myAdapterNew.add(noDevices);
24 }
25 }}};

第6-14行为此广播接收器收到找到设备信息时对应的处理代码。首先从Intent中获取BluetoothDevice对象,然后判断该设备是否已经配对。若没有配对,则将设备加入新设备列表,否则不进行任何处理。
第16-24行为此广播接收器收到完成设备搜索信息时对应的处理代码,主要是改变对话框的标题等信息。如果没有找到任何设备,则在界面中显示没有找到设备的信息。

(9)再接着将开发自定义的用于后台服务的MyService类,其提供的后台服务主要是关于蓝牙设备的连接、数据的收发等方面,具体代码如下。

1 package com.bn.pp10; //声明包
2 import java.io.IOException; //引入相关类
3 ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4 import android.os.Message; //引入相关类
5 public class MyService { //用于管理连接的Service
6 // 本应用的唯一 UUID
7 private static final UUID MY_UUID =
8 UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");
9 private final BluetoothAdapter btAdapter; // 成员变量
10 private final Handler myHandler;
11 private AcceptThread myAcceptThread;
12 private ConnectThread myConnectThread;
13 private ConnectedThread myConnectedThread;
14 private int myState;
15 // 表示当前连接状态的常量
16 public static final int STATE_NONE = 0; // 什么也没做
17 public static final int STATE_LISTEN = 1; // 正在监听连接
18 public static final int STATE_CONNECTING = 2; // 正在连接
19 public static final int STATE_CONNECTED = 3; // 已连接到设备
20 // 构造器
21 public MyService(Context context, Handler handler) {
22 btAdapter = BluetoothAdapter.getDefaultAdapter();
23 myState = STATE_NONE;
24 myHandler = handler;
25 }
26 private synchronized void setState(int state) { //设置当前连接状态的方法
27 myState = state;
28 }
29 public synchronized int getState() { //获取当前连接状态的方法
30 return myState;
31 }
32 public synchronized void start() { //开启Service的方法
33 // 关闭不必要的线程
34 if (myConnectThread != null) {myConnectThread.cancel(); myConnectThread = null;}
35 if (myConnectedThread != null) {myConnectedThread.cancel(); myConnectedThread

   = null;}

36 if (myAcceptThread == null) { // 开启线程监听连接
37 myAcceptThread = new AcceptThread();
38 myAcceptThread.start();
39 }
40 setState(STATE_LISTEN);
41 }
42 public synchronized void stop() { //停止所有线程的方法
43 if (myConnectThread != null) {myConnectThread.cancel(); myConnectThread =

      null;}

44 if (myConnectedThread != null) {myConnectedThread.cancel(); myConnectedThread

     = null;}

45 if (myAcceptThread != null) {myAcceptThread.cancel(); myAcceptThread = null;}
46 setState(STATE_NONE);
47 }
48 public void write(byte[] out) { //向ConnectedThread写入数据的方法
49 ConnectedThread tmpCt; // 创建临时对象引用
50 synchronized (this) { // 锁定ConnectedThread
51 if (myState != STATE_CONNECTED) return;
52 tmpCt = myConnectedThread;
53 }
54 tmpCt.write(out); // 写入数据
55 }
56 public synchronized void connect(BluetoothDevice device) {//连接设备的方法
57 ……//此处省略了部分源代码,将在后面步骤中给出
58 }
59 //开启管理和已连接的设备间通话的线程的方法
60 public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
61 ……//此处省略了部分源代码,将在后面步骤中给出
62 }
63 ……//以下省略了3个主要线程类的代码,将在后面介绍
64 }

第6-19行为成员变量的定义,其中第7行定义了本应用的UUID(全局唯一标识)。第16-19行定义了表示当前连接状态的一些常量,各常量代表的含义详见代码注释。
第26-31行为设置和获取当前连接状态的方法,这两个方法都是用synchronized修饰符修饰的,这是为了避免异步线程可能同时读取myState时产生的问题。
第32-41行为开启后台服务的方法。在该方法中首先关闭不必要的线程,然后开启线程监听连接,并设置当前状态为正在监听。
第42-47行为停止所有线程的stop方法,主要工作是调用各线程的cancel方法。
第48-55行为向连接线程ConnectedThread写入数据的write方法,只要调用ConnectedThread对象的write方法即可。


提示 第56-63行省略了connect 方法、connected方法及3个主要线程类的代码,这些代码的开发将在下面进行详细讲解。
(10)接下来将开发负责连接设备的connect方法,其代码如下。

1 public synchronized void connect(BluetoothDevice device) {//连接设备的方法
2 // 关闭不必要的线程
3 if (myState == STATE_CONNECTING) {
4 if (myConnectThread != null) {myConnectThread.cancel(); myConnectThread = null;}
5 }
6 if (myConnectedThread != null) {myConnectedThread.cancel(); myConnectedThread = null;}
7 myConnectThread = new ConnectThread(device); // 开启线程连接设备
8 myConnectThread.start();
9 setState(STATE_CONNECTING);
10 }
``

说明 该方法的实现比较简单,首先关闭不必要的线程,然后开启线程连接设备,并设置当前状态为正在连接。
(11)接着将开发负责开启连接线程(ConnectedThread)并向myHandler发送设备名称消息的connected方法,其代码如下。

1     public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
2                   // 关闭不必要的线程
3         if (myConnectThread != null) {myConnectThread.cancel(); myConnectThread = null;}
4        if (myConnectedThread != null) {myConnectedThread.cancel(); myConnectedThread = null;}
5         if (myAcceptThread != null) {myAcceptThread.cancel(); myAcceptThread = null;}
6         myConnectedThread = new ConnectedThread(socket); // 创建并启动ConnectedThread
7         myConnectedThread.start();
8                   // 发送已连接的设备名称到主界面Activity
9         Message msg = myHandler.obtainMessage(Constant.MSG_DEVICE_NAME);
10         Bundle bundle = new Bundle();
11         bundle.putString(Constant.DEVICE_NAME, device.getName());
12         msg.setData(bundle);
13         myHandler.sendMessage(msg);
14         setState(STATE_CONNECTED);
15     }

提示 在该方法中首先要关闭不必要的线程,然后创建并开启ConnectedThread线程,同时发送已连接的设备名称到主界面Activity,最后设置当前状态为已连接。
(12)接下来将开发前面省略的MyService类中的3个主要线程,首先开发负责监听连接请求的AcceptThread,其代码如下。

1     private class AcceptThread extends Thread {  //用于监听连接的线程
2                    // 本地服务器端ServerSocket
3         private final BluetoothServerSocket mmServerSocket;
4         public AcceptThread() {
5             BluetoothServerSocket tmpSS = null;
6             try {        // 创建用于监听的服务器端ServerSocket
7          tmpSS = btAdapter.listenUsingRfcommWithServiceRecord("BluetoothChat", MY_UUID);
8             } catch (IOException e) {e.printStackTrace();}            
9             mmServerSocket = tmpSS;
10         }
11         public void run() {
12             setName("AcceptThread");     //设置线程名称
13             BluetoothSocket socket = null;
14             while (myState != STATE_CONNECTED) {  //如果没有连接到设备
15                 try {
16                     socket = mmServerSocket.accept(); //获取连接的Socket
17                 } catch (IOException e) {e.printStackTrace();break;}
18                 if (socket != null) {     // 如果连接成功
19                     synchronized (MyService.this) {
20                         switch (myState) {
21                         case STATE_LISTEN:
22                         case STATE_CONNECTING:
23                                    // 开启管理连接后数据交流的线程
24                             connected(socket, socket.getRemoteDevice());
25                             break;
26                         case STATE_NONE:
27                         case STATE_CONNECTED:
28                             try {      // 关闭新Socket
29                                 socket.close();
30                             } catch (IOException e) {
31                              e.printStackTrace();
32                             }
33                             break;
34         }}}}}
35         public void cancel() {//关闭本地服务器端ServerSocket的方法
36             try {
37                 mmServerSocket.close(); //调用close方法关闭ServerSocket
38             } catch (IOException e) {e.printStackTrace();}
39     }}

第3-10行为该线程类的成员变量及构造器声明,在构造器中创建了用于在服务端监听并接收连接请求的BluetoothServerSocket对象。
第11-34行为重写的用于描述线程任务的run方法。在该方法的while循环中,如果没有连接到设备,则一直调用BluetoothServerSocket的accept方法进入阻塞状态,等待连接请求的到来。

(13)下面将开发负责用于尝试连接其他设备的ConnectThread,其代码如下。

1     private class ConnectThread extends Thread {  //用于尝试连接其他设备的线程
2         private final BluetoothSocket myBtSocket;
3         private final BluetoothDevice mmDevice;
4         public ConnectThread(BluetoothDevice device) {
5             mmDevice = device;
6             BluetoothSocket tmp = null;
7                     // 通过正在连接的设备获取BluetoothSocket
8             try {
9                 tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
10             } catch (IOException e) {e.printStackTrace();}
11             myBtSocket = tmp;
12         }
13         public void run() {
14             setName("ConnectThread");
15             btAdapter.cancelDiscovery();   // 取消搜索设备
16             try {        // 连接到BluetoothSocket
17                 myBtSocket.connect();    //尝试连接
18             } catch (IOException e) {
19              setState(STATE_LISTEN);   //连接断开后设置状态为正在监听
20                 try {       // 关闭Socket
21                     myBtSocket.close();
22                 } catch (IOException e2) {e.printStackTrace();}
23                 MyService.this.start();   //如果连接不成功,重新开启Service
24                 return;
25             }
26             synchronized (MyService.this) {  // 将ConnectThread线程置空
27                 myConnectThread = null;
28             }
29             connected(myBtSocket, mmDevice);   // 开启管理连接后数据交流的线程
30         }
31         public void cancel() {
32             try {
33                 myBtSocket.close();
34             } catch (IOException e) {e.printStackTrace();}
35         } }

第2-12行为该线程类的成员变量及构造器的声明。在构造器中通过正在连接的设备获取BluetoothSocket对象的引用。
第13-30行为重写的run方法,在该方法中主要调用BluetoothServerSocket 类的connect方法尝试连接。如果连接被断开,改变相应的状态,并释放资源。
(14)接下来将开发负责连接成功后信息收发的ConnectedThread,其代码如下。

1     private class ConnectedThread extends Thread {
2         private final BluetoothSocket myBtSocket;
3         private final InputStream mmInStream;
4         private final OutputStream myOs;
5         public ConnectedThread(BluetoothSocket socket) {
6             myBtSocket = socket;
7             InputStream tmpIn = null;
8             OutputStream tmpOut = null;
9             try {         //获取BluetoothSocket的输入输出流
10                 tmpIn = socket.getInputStream();
11                 tmpOut = socket.getOutputStream();
12             } catch (IOException e) {e.printStackTrace();} //打印异常
13             mmInStream = tmpIn;
14             myOs = tmpOut;
15         }
16         public void run() {
17             byte[] buffer = new byte[1024];
18             int bytes;
19             while (true) {      // 一直监听输入流
20                 try {
21                     bytes = mmInStream.read(buffer);// 从输入流中读入数据
22                             //将读入的数据发送到主界面Activity
23                     myHandler.obtainMessage(Constant.MSG_READ, bytes, -1, buffer)
24                             .sendToTarget();
25                 } catch (IOException e) {
26                  e.printStackTrace();
27                  setState(STATE_LISTEN);  //连接断开后设置状态为正在监听
28                     break;
29         }}}
30         public void write(byte[] buffer) {  //向输出流中写入数据的方法
31             try {
32                 myOs.write(buffer);
33             } catch (IOException e) {e.printStackTrace();}
34         }
35         public void cancel() {
36             try {
37                 myBtSocket.close();     //关闭Socket
38             } catch (IOException e) {e.printStackTrace();}
39         }}

第2-15行为该线程类的成员变量及构造器的声明。在构造器中通过传递进来的BluetoothSocket对象的引用获取基于蓝牙连接的输入输出流。
第16-29行为重写的run方法。在该方法中通过while循环一直监听输入流,一旦输入流中有数据,便从输入流中读出数据,同时将数据发送到主界面Activity。
第30-34行为向输出流中写入数据的方法,该方法只是对输出流对象的write方法进行了简单的封装。
(15)代码开发完成后还需要在AndroidManifest.xml中声明BLUETOOTH权限,其代码如下。

1             <!--声明BLUETOOTH权限-->
2     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
3     <uses-permission android:name="android.permission.BLUETOOTH" />

提示 上述代码应插入到AndroidManifest.xml中的“”标签之前。
(16)在AndroidManifest.xml文件中声明完BLUETOOTH权限后,接下来需要在AndroidManifest.xml文件中注册自己开发的MyDeviceListActivity,具体代码如下。

1        <!--注册MyDeviceListActivity-->
2       <activity android:name=".MyDeviceListActivity"
3                   android:label="@string/select_device"
4                   android:theme="@android:style/Theme.Dialog"
5                   android:configChanges="orientation|keyboardHidden" 
6         />

提示 上述代码应插入到AndroidManifest.xml中的与标签之间。请读者注意的是,在新建项目时指定的Activity其配置代码系统会自动加入到AndroidManifest.xml文件中,但自己在项目中再开发其他的Activity时就需要自行添加相应的配置代码到AndroidManifest.xml文件中了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值