DeviceListActivity.java
该类包含 UI 和操作的 Activity 类,作用是得到系统默认蓝牙设备的已配对设备列表,以及搜索出的未配对的新设备的列表。然后提供点击后发出连接设备请求的功能。
接着上一篇没有完成的任务,我们继续分析这个蓝牙聊天程序的实现,本文主要包括以下两个部分的内容:其一,分析扫描设备部分DeviceListActivity,其二,分析具体的聊天过程的完整通信方案,包括端口监听、链接配对、消息发送和接收等,如果有对上一篇文章不太熟悉的,可以返回去在过一次,这样会有利于本文的理解。
设备扫描(DeviceListActivity)
在上一篇文章的介绍中,当用户点击了扫描按钮之后,则会执行如下代码:
1. // 启动DeviceListActivity查看设备并扫描
2. new Intent(this, DeviceListActivity.class);
3. startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
// 启动DeviceListActivity查看设备并扫描 Intent serverIntent = new Intent(this, DeviceListActivity.class); startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
该代码将跳转到DeviceListActivity进行设备的扫描,并且通过REQUEST_CONNECT_DEVICE来请求链接扫描到的设备。从AndroidManifest.xml文件中我们知道DeviceListActivity将为定义为一个对话框的风格,下图是该应用程序中,扫描蓝牙设备的截图。
device_list.xml
其中DeviceListActivity则为图中对话框部分,其界面的布局如下代码所示。
1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2. "vertical"
3. "match_parent"
4. "match_parent"
5. >
6. <!-- 已经配对的设备 -->
7. "@+id/title_paired_devices"
8. "match_parent"
9. "wrap_content"
10. "@string/title_paired_devices"
11. "gone"
12. "#666"
13. "#fff"
14. "5dp"
15. />
16. <!-- 已经配对的设备信息 -->
17. "@+id/paired_devices"
18. "match_parent"
19. "wrap_content"
20. "true"
21. "1"
22. />
23. <!-- 扫描出来没有经过配对的设备 -->
24. "@+id/title_new_devices"
25. "match_parent"
26. "wrap_content"
27. "@string/title_other_devices"
28. "gone"
29. "#666"
30. "#fff"
31. "5dp"
32. />
33. <!-- 扫描出来没有经过配对的设备信息 -->
34. "@+id/new_devices"
35. "match_parent"
36. "wrap_content"
37. "true"
38. "2"
39. />
40. <!-- 扫描按钮 -->
41. "@+id/button_scan"
42. "match_parent"
43. "wrap_content"
44. "@string/button_scan"
45. />
46. </LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" > <!-- 已经配对的设备 --> <TextView android:id="@+id/title_paired_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/title_paired_devices" android:visibility="gone" android:background="#666" android:textColor="#fff" android:paddingLeft="5dp" /> <!-- 已经配对的设备信息 --> <ListView android:id="@+id/paired_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:stackFromBottom="true" android:layout_weight="1" /> <!-- 扫描出来没有经过配对的设备 --> <TextView android:id="@+id/title_new_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/title_other_devices" android:visibility="gone" android:background="#666" android:textColor="#fff" android:paddingLeft="5dp" /> <!-- 扫描出来没有经过配对的设备信息 --> <ListView android:id="@+id/new_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:stackFromBottom="true" android:layout_weight="2" /> <!-- 扫描按钮 --> <Button android:id="@+id/button_scan" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/button_scan" /> </LinearLayout>
该布局整体由一个线性布局LinearLayout组成,其中包含了两个textview中来显示已经配对的设备和信扫描出来的设备(还没有经过配对)和两个ListView分别用于显示已经配对和没有配对的设备的相关信息。按钮则用于执行扫描过程用,整个结构很简单,下面我们开始分析如何编码实现了。
同样开始之前,我们先确定该类中的变量的作用,定义如下:
定义变量
public class DeviceListActivityextends
1. // Debugging
2. private static final String TAG ="DeviceListActivity";
3. private staticfinalboolean D =true;
4.
5. // Return Intent extra
6. public static String EXTRA_DEVICE_ADDRESS ="device_address";
7.
8. // 蓝牙适配器
9. private
10. //已经配对的蓝牙设备
11. private
12. //新的蓝牙设备
13. private
public class DeviceListActivity extends Activity { // Debugging private static final String TAG = "DeviceListActivity"; private static final boolean D = true; // Return Intent extra public static String EXTRA_DEVICE_ADDRESS = "device_address"; // 蓝牙适配器 private BluetoothAdapter mBtAdapter; //已经配对的蓝牙设备 private ArrayAdapter<String> mPairedDevicesArrayAdapter; //新的蓝牙设备 private ArrayAdapter<String> mNewDevicesArrayAdapter;
其中Debugging部分,同样用于调试,这里定义了一个EXTRA_DEVICE_ADDRESS,用于在通过Intent传递数据时的附加信息,即设备的地址,当扫描出来之后,返回到BluetoothChat中的onActivityResult函数的REQUEST_CONNECT_DEVICE命令,这是我们就需要通过DeviceListActivity.EXTRA_DEVICE_ADDRESS来取得该设备的Mac地址,因此当我们扫描完成之后在反馈扫描结果时就需要绑定设备地址作为EXTRA_DEVICE_ADDRESS的附加值,这和我们上一篇介绍的并不矛盾。另外其他几个变量则分别是本地蓝牙适配器、已经配对的蓝牙列表和扫描出来还没有配对的蓝牙设备列表,稍后我们可以看到对他们的使用。
进入DeviceListActivity之后我们首先分析onCreate,首先通过如下代码对窗口进行了设置:
onCreate_窗口设置、初始化
1. // 设置窗口
2. requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
3. setContentView(R.layout.device_list);
4. setResult(Activity.RESULT_CANCELED);
// 设置窗口 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setContentView(R.layout.device_list); setResult(Activity.RESULT_CANCELED);
这里我们设置了窗口需要带一个进度条,当我们在扫描时就看有很容易的高速用户扫描进度。具体布局则设置为device_list.xml也是我们文本第一段代码的内容,接下来首先初始化扫描按钮,代码如下:
1. // 初始化扫描按钮
2. Button scanButton = (Button) findViewById(R.id.button_scan);
3. new
4. public void
5. doDiscovery();
6. v.setVisibility(View.GONE);
7. }
8. });
// 初始化扫描按钮 Button scanButton = (Button) findViewById(R.id.button_scan); scanButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { doDiscovery(); v.setVisibility(View.GONE); } });
通过doDiscovery函数来执行扫描操作即可,具体扫描过程稍后分析。
然后需要初始化用来显示设备的列表和数据源,使用如下代码即可:
1. //初始化ArrayAdapter,一个是已经配对的设备,一个是新发现的设备
2. new ArrayAdapter<String>(this, R.layout.device_name);
3. new ArrayAdapter<String>(this, R.layout.device_name);
4.
5. // 检测并设置已配对的设备ListView
6. ListView pairedListView = (ListView) findViewById(R.id.paired_devices);
7. pairedListView.setAdapter(mPairedDevicesArrayAdapter);
8. pairedListView.setOnItemClickListener(mDeviceClickListener);
9.
10. // 检查并设置行发现的蓝牙设备ListView
11. ListView newDevicesListView = (ListView) findViewById(R.id.new_devices);
12. newDevicesListView.setAdapter(mNewDevicesArrayAdapter);
13. newDevicesListView.setOnItemClickListener(mDeviceClickListener);
//初始化ArrayAdapter,一个是已经配对的设备,一个是新发现的设备 mPairedDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name); mNewDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name); // 检测并设置已配对的设备ListView ListView pairedListView = (ListView) findViewById(R.id.paired_devices); pairedListView.setAdapter(mPairedDevicesArrayAdapter); pairedListView.setOnItemClickListener(mDeviceClickListener); // 检查并设置行发现的蓝牙设备ListView ListView newDevicesListView = (ListView) findViewById(R.id.new_devices); newDevicesListView.setAdapter(mNewDevicesArrayAdapter); newDevicesListView.setOnItemClickListener(mDeviceClickListener);
并分别对这些列表中的选项设置了监听mDeviceClickListener,用来处理,当选择该选项时,就进行链接和配对操作。既然是扫描,我们就需要对扫描的结果进行监控,这里我们构建了一个广播BroadcastReceiver来对扫描的结果进行处理,代码如下:
1. // 当一个设备被发现时,需要注册一个广播
2. new
3. this.registerReceiver(mReceiver, filter);
4.
5. // 当显示检查完毕的时候,需要注册一个广播
6. new
7. this.registerReceiver(mReceiver, filter);
// 当一个设备被发现时,需要注册一个广播 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); this.registerReceiver(mReceiver, filter); // 当显示检查完毕的时候,需要注册一个广播 filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); this.registerReceiver(mReceiver, filter);
这里我们注册到广播mReceiver的IntentFilter主要包括了发现蓝牙设备(BluetoothDevice.ACTION_FOUND)和扫描结束(BluetoothAdapter.ACTION_DISCOVERY_FINISHED),稍后我们分析如何在mReceiver中来处理这些事件。
最后我们需要取得本地蓝牙适配器和一些初始的蓝牙设备数据显示列表进行处理,代码如下:
getBondedDevices函数_取得本地蓝牙适配器
1. // 得到本地的蓝牙适配器
2. mBtAdapter = BluetoothAdapter.getDefaultAdapter();
3.
4. // 得到一个已经匹配到本地适配器的BluetoothDevice类的对象集合
5. Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices();
6.
7. // 如果有配对成功的设备则添加到ArrayAdapter
8. if (pairedDevices.size() > 0) {
9. findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE);
10. for
11. "\n"
12. }
13. else
14. //否则添加一个没有被配对的字符串
15. String noDevices = getResources().getText(R.string.none_paired).toString();
16. mPairedDevicesArrayAdapter.add(noDevices);
17. }
// 得到本地的蓝牙适配器 mBtAdapter = BluetoothAdapter.getDefaultAdapter(); // 得到一个已经匹配到本地适配器的BluetoothDevice类的对象集合 Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices(); // 如果有配对成功的设备则添加到ArrayAdapter if (pairedDevices.size() > 0) { findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE); for (BluetoothDevice device : pairedDevices) { mPairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } } else { //否则添加一个没有被配对的字符串 String noDevices = getResources().getText(R.string.none_paired).toString(); mPairedDevicesArrayAdapter.add(noDevices); }
首先通过蓝牙适配器的getBondedDevices函数取得已经配对的蓝牙设备,并将其添加到mPairedDevicesArrayAdapter数据源中,会显示到pairedListView列表视图中,如果没有已经配对的蓝牙设备,则显示一个R.string.none_paired字符串表示目前没有配对成功的设备。
onDestroy函数
onDestroy函数中会制定销毁操作,主要包括蓝牙适配器和广播的注销操作,代码如下:
1. @Override
2. protected void
3. super.onDestroy();
4. // 确保我们没有发现,检测设备
5. if (mBtAdapter !=null) {
6. mBtAdapter.cancelDiscovery();
7. }
8. // 卸载所注册的广播
9. this.unregisterReceiver(mReceiver);
10. }
@Override protected void onDestroy() { super.onDestroy(); // 确保我们没有发现,检测设备 if (mBtAdapter != null) { mBtAdapter.cancelDiscovery(); } // 卸载所注册的广播 this.unregisterReceiver(mReceiver); }
对于蓝牙适配器的取消方式则调用cancelDiscovery()函数即可,卸载mReceiver则需要调用unregisterReceiver即可。
做好初始化工作之后,下面我们开始分析扫描函数doDiscovery(),其扫描过程的实现很就简单,代码如下:
doDiscovery()_扫描函数
1. /**
2. * 请求能被发现的设备
3. */
4. private void
5. if (D) Log.d(TAG,"doDiscovery()");
6.
7. // 设置显示进度条
8. true);
9. // 设置title为扫描状态
10. setTitle(R.string.scanning);
11.
12. // 显示新设备的子标题
13. findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE);
14.
15. // 如果已经在请求现实了,那么就先停止
16. if
17. mBtAdapter.cancelDiscovery();
18. }
19.
20. // 请求从蓝牙适配器得到能够被发现的设备
21. mBtAdapter.startDiscovery();
22. }
/** * 请求能被发现的设备 */ private void doDiscovery() { if (D) Log.d(TAG, "doDiscovery()"); // 设置显示进度条 setProgressBarIndeterminateVisibility(true); // 设置title为扫描状态 setTitle(R.string.scanning); // 显示新设备的子标题 findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE); // 如果已经在请求现实了,那么就先停止 if (mBtAdapter.isDiscovering()) { mBtAdapter.cancelDiscovery(); } // 请求从蓝牙适配器得到能够被发现的设备 mBtAdapter.startDiscovery(); }
/** * 请求能被发现的设备 */ private void doDiscovery() { if (D) Log.d(TAG, "doDiscovery()"); // 设置显示进度条 setProgressBarIndeterminateVisibility(true); // 设置title为扫描状态 setTitle(R.string.scanning); // 显示新设备的子标题 findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE); // 如果已经在请求现实了,那么就先停止 if (mBtAdapter.isDiscovering()) { mBtAdapter.cancelDiscovery(); } // 请求从蓝牙适配器得到能够被发现的设备 mBtAdapter.startDiscovery(); }
首先通过setProgressBarIndeterminateVisibility将进度条设置为显示状态,设置标题title为R.string.scanning字符串,表示正在扫描中,代码中所说的新设备的子标题,其实就是上面我们所说的扫描到的没有经过配对的设备的title,对应于R.id.title_new_devices。扫描之前我们首先通过isDiscovering函数检测当前是否正在扫描,如果正在扫描则调用cancelDiscovery函数来取消当前的扫描,最后调用startDiscovery函数开始执行扫描操作。
现在已经开始扫描了,下面我们就需要对扫描过程进行监控和对扫描的结果进行处理。即我们所定义的广播mReceiver,其实现如下所示。
广播mReceiver
1. //监听扫描蓝牙设备
2. private final BroadcastReceiver mReceiver =new
3. @Override
4. public void
5. String action = intent.getAction();
6. // 当发现一个设备时
7. if
8. // 从Intent得到蓝牙设备对象
9. BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
10. // 如果已经配对,则跳过,因为他已经在设备列表中了
11. if
12. //否则添加到设备列表
13. "\n"
14. }
15. // 当扫描完成之后改变Activity的title
16. else if
17. //设置进度条不显示
18. false);
19. //设置title
20. setTitle(R.string.select_device);
21. //如果计数为0,则表示没有发现蓝牙
22. if (mNewDevicesArrayAdapter.getCount() ==0) {
23. String noDevices = getResources().getText(R.string.none_found).toString();
24. mNewDevicesArrayAdapter.add(noDevices);
25. }
26. }
27. }
28. };
//监听扫描蓝牙设备 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // 当发现一个设备时 if (BluetoothDevice.ACTION_FOUND.equals(action)) { // 从Intent得到蓝牙设备对象 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); // 如果已经配对,则跳过,因为他已经在设备列表中了 if (device.getBondState() != BluetoothDevice.BOND_BONDED) { //否则添加到设备列表 mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } // 当扫描完成之后改变Activity的title } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { //设置进度条不显示 setProgressBarIndeterminateVisibility(false); //设置title setTitle(R.string.select_device); //如果计数为0,则表示没有发现蓝牙 if (mNewDevicesArrayAdapter.getCount() == 0) { String noDevices = getResources().getText(R.string.none_found).toString(); mNewDevicesArrayAdapter.add(noDevices); } } } };
其中我们通过intent.getAction()可以取得一个动作,然后判断如果动作为BluetoothDevice.ACTION_FOUND,则表示发现一个蓝牙设备,然后通过BluetoothDevice.EXTRA_DEVICE常量可以取得Intent中的蓝牙设备对象(BluetoothDevice),然后通过条件"device.getBondState() != BluetoothDevice.BOND_BONDED"来判断设备是否配对,如果没有配对则添加到行设备列表数据源mNewDevicesArrayAdapter中,另外,当我们取得的动作为BluetoothAdapter.ACTION_DISCOVERY_FINISHED,则表示扫描过程完毕,这时首先需要设置进度条不现实,并且设置窗口的标题为选择一个设备(R.string.select_device)。当然如果扫描完成之后没有发现新的设备,则添加一个没有发现新的设备字符串(R.string.none_found)到mNewDevicesArrayAdapter中。
最后,扫描界面上还有一个按钮,其监听mDeviceClickListener的实现如下:
监听mDeviceClickListener
1. // ListViews中所有设备的点击事件监听
2. private OnItemClickListener mDeviceClickListener =new
3. public void onItemClick(AdapterView<?> av, View v,int arg2,long
4. // 取消检测扫描发现设备的过程,因为内非常耗费资源
5. mBtAdapter.cancelDiscovery();
6.
7. // 得到mac地址
8. String info = ((TextView) v).getText().toString();
9. 17);
10.
11. // 创建一个包括Mac地址的Intent请求
12. new
13. intent.putExtra(EXTRA_DEVICE_ADDRESS, address);
14.
15. // 设置result并结束Activity
16. setResult(Activity.RESULT_OK, intent);
17. finish();
18. }
19. };
// ListViews中所有设备的点击事件监听 private OnItemClickListener mDeviceClickListener = new OnItemClickListener() { public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) { // 取消检测扫描发现设备的过程,因为内非常耗费资源 mBtAdapter.cancelDiscovery(); // 得到mac地址 String info = ((TextView) v).getText().toString(); String address = info.substring(info.length() - 17); // 创建一个包括Mac地址的Intent请求 Intent intent = new Intent(); intent.putExtra(EXTRA_DEVICE_ADDRESS, address); // 设置result并结束Activity setResult(Activity.RESULT_OK, intent); finish(); } };
EXTRA_DEVICE_ADDRESS将mac地址传递到BluetoothChat中,然后调用finish来结束该界面。这时就会回到上一篇文章我们介绍的BluetoothChat中的
onActivityResult函数
onActivityResult函数中去执行请求代码为REQUEST_CONNECT_DEVICE的片段,用来连接一个设备。