2024年8月24日发(作者:学醉蝶)
本文我们将通过学习Android的蓝牙聊天示例应用程序来介绍蓝牙开发包的使用,该
示例程序完整的包含了蓝牙开发的各个部分,将实现两个设备通过蓝牙进行连接并聊
天。
前面我们说过,在使用蓝牙API时就需要开启某些权限,同时我们还可以从And
文件中找到应用程序启动时所进入的界面Activity等信息,因此下
面我们首先打开文件,代码如下:
1.
versionCode="1" android:versionName="1.0"> > l="@string/app_name" android:configChanges="orientation|keybo ardHidden">
- 用于显示蓝牙设备列表的Activity -->
theme="@android:style/" android:configChanges="o
rientation|keyboardHidden" />
t>
首先minSdkVersion用于说明该应用程序所需要使用的最小SDK版本,这里设
置为6,也就是说最小需要使用android1.6版本的sdk,同时Ophone则需要使用o
ms2.0版本,然后打开了BLUETOOTH和BLUETOOTH_ADMIN两个蓝牙操作相关的
权限,最后看到了两个Activity的声明,他们分别是BluetoothChat(默认主Activity)
和DeviceListActivity(显示设备列表),其中DeviceListActivity风格被定义为一个对
话框风格,下面我们将分析该程序的每个细节。
BluetoothChat
首先,程序启动进入BluetoothChat,在onCreate函数中对窗口进行了设置,代
码如下:
1. // 设置窗口布局 requestWindowFeature(E_CUSTO
M_TITLE); setContentView(); getWindow().setFe
atureInt(E_CUSTOM_TITLE, _titl
e);
这里可以看到将窗口风格设置为自定义风格了,并且指定了自定义title布局为c
ustom_title,其定义代码如下:
复制到剪贴板 Java代码
1.
ut_height="match_parent" android:gravity="center_vertical"
> ut_alignParentLeft="true" android:ellipsize="end" android:sin gleLine="true" style="?android:attr/windowTitleStyle" android: layout_width="wrap_content" android:layout_height="match_pare nt" android:layout_weight="1" /> android:ellipsize="end" android:singleLine="true" android:la yout_width="wrap_content" android:layout_height="match_paren t" android:textColor="#fff" android:layout_weight="1" /> 该布局将title设置为一个相对布局RelativeLayout,其中包含了两个TextView, 一个在左边一个在右边,分别用于显示应用程序的标题title和当前的蓝牙配对链接名 称,如下图所示。 其中左边显示为应用程序名称"BluetoothChat",右边显示一个connected:scort 则表示当前配对成功正在进行聊天的链接名称。整个聊天界面的布局在中 实现,代码如下: 复制到剪贴板 Java代码 1. "match_parent" android:layout_height="match_parent" > layout_width="match_parent" android:layout_height="match_par ent" android:stackFromBottom="true" android:transcriptMode ="alwaysScroll" android:layout_weight="1" /> ght="wrap_content" > ght="wrap_content" android:layout_weight="1" android:layou t_gravity="bottom" /> d" android:layout_width="wrap_content" android:layout_heigh t="wrap_content" android:text="@string/send" /> arLayout> 整个界面的布局将是一个线性布局LinearLayout,其中包含了另一个ListView(用 于显示聊天的对话信息)和另外一个线性布局来实现一个发送信息的窗口,发送消息 发送框有一个输入框和一个发送按钮构成。整个界面如下图所示。 布局好界面,下面我们需要进入编码状态,首先看BluetoothChat所要那些成员 变量,如下代码所示: 复制到剪贴板 Java代码 1. public class BluetoothChat extends Activity { // Debugging private static final String TAG = "BluetoothChat"; private static f inal boolean D = true; //从BluetoothChatService Handler发送的消 息类型 public static final int MESSAGE_STATE_CHANGE = 1; pu blic static final int MESSAGE_READ = 2; public static final int MES SAGE_WRITE = 3; public static final int MESSAGE_DEVICE_NAM E = 4; public static final int MESSAGE_TOAST = 5; // 从Blueto othChatService Handler接收消息时使用的键名(键-值模型) public stati c final String DEVICE_NAME = "device_name"; public static final S tring TOAST = "toast"; // Intent请求代码(请求链接,请求可见) private static final int REQUEST_CONNECT_DEVICE = 1; private st atic final int REQUEST_ENABLE_BT = 2; // Layout Views privat e TextView mTitle; private ListView mConversationView; privat e EditText mOutEditText; private Button mSendButton; // 链接 的设备的名称 private String mConnectedDeviceName = null; / / Array adapter for the conversation thread private ArrayAdapter< String> mConversationArrayAdapter; // 将要发送出去的字符串 p rivate StringBuffer mOutStringBuffer; // 本地蓝牙适配器 privat e BluetoothAdapter mBluetoothAdapter = null; // 聊天服务的对象 private BluetoothChatService mChatService = null; //...... 其中Debugging部分则将用于我们在调试程序时通过log打印日志用,其他部分 我们都加入了注释,需要说明的是BluetoothChatService ,它是我们自己定义的一个 用来管理蓝牙的端口监听,链接,管理聊天的程序,后面我们会介绍。在这里需要说 明一点,这些代码都出自google的员工之手,大家在学习时,可以借鉴很多代码编 写的技巧和风格,这都将对我们有非常大的帮助。 然后,我们就需要对界面进行一些设置,如下代码将用来设置我们自定义的标题 title需要显示的内容: 1. // 设置自定义title布局 mTitle = (TextView) findViewById( _left_text); t(_name); mTitle = (Tex tView) findViewById(_right_text); 左边的TextView被设置为显示应用程序名称,右边的则需要我们在链接之后在 设置更新,目前则显示没有链接字样,所以这里我们暂不设置,进一步就需要获取本地 蓝牙适配器BluetoothAdapter了,因为对于有关蓝牙的任何操作都需要首先获得该蓝 牙适配器,获取代码非常简单,如下: 1. // 得到一个本地蓝牙适配器 mBluetoothAdapter = BluetoothAdapter. getDefaultAdapter(); // 如果适配器为null,则不支持蓝牙 if (mBlu etoothAdapter == null) { xt(this, "Bluetooth is not a vailable", _LONG).show(); finish(); return; } getDefaultAdapter()函数用于获取本地蓝牙适配器,然后检测是否为n ull,如果为null则表示没有蓝牙设备的支持,将通过toast告知用户。 在o nStart()函数中,我们将检测蓝牙是否被打开,如果没有打开,则请求打开,否则 就可以设置一些聊天信息的准备工作,代码如下: @Override public v oid onStart() { t(); if(D) Log.e(TAG, "++ ON STA RT ++"); // 如果蓝牙没有打开,则请求打开 // setupChat() will the n be called during onActivityResult if (!l ed()) { Intent enableIntent = new Intent( N_REQUEST_ENABLE); startActivityForResult(enableIntent, REQUE ST_ENABLE_BT); // 否则,设置聊天会话 } else { if (mChatSe rvice == null) setupChat(); } } 如果蓝牙没有打开,我们则通过_REQUEST_ENABLE 来请求打开蓝牙,REQUEST_ENABLE_BT则是我们自己定义的用于请求打开蓝牙的I ntent代码,最后当我们调用startActivityForResult来执行请求时,就会在onActivit yResult函数中得到一个反馈,如果当前蓝牙已经打开,那么就可以调用setupChat 函数来准备蓝牙聊天相关的工作,稍后分析该函数的具体实现。 下面我们分析一下请求打开蓝牙之后,在onActivityResult 中得到的反馈信息, 我们传递了REQUEST_ENABLE_BT代码作为请求蓝牙打开的命令,因此在onActivit yResult 中,需要会得到一个请求代码为REQUEST_ENABLE_B的消息,对于其处理 如下代码所示: 1. 2. 3. 4. 5. 6. 7. 8. 9. case REQUEST_ENABLE_BT: //在请求打开蓝牙时返回的代码 if (resultCode == _OK) { // 蓝牙已经打开,所以设置一个聊天会话 setupChat(); } else { // 请求打开蓝牙出错 Log.d(TAG, "BT not enabled"); xt(this, _not_enabled_leaving, NGTH_SHORT).show(); finish(); 10. } 在请求时,如果返回代码为_OK,则表示请求打开蓝牙成功,那 么我们就可以和上面的操作进度一样,调用setupChat来设置蓝牙聊天相关信息,如 果返回其他代码,则表示请求打开蓝牙失败,这时我们同样通过一个Toast来告诉用 户,同时也需要调用finish()函数来结束应用程序。 如果打开蓝牙无误,那么下面我们开始进入setupChat的设置,其代码实现如下: 1. private void setupChat() { Log.d(TAG, "setupChat()"); // 初 始化对话进程 mConversationArrayAdapter = new ArrayAdapter ng>(this, e); // 初始化对话显示列表 mConversa tionView = (ListView) findViewById(); // 设置话显示列表源 pter(mConversationArrayAdapter); / / 初始化编辑框,并设置一个监听,用于处理按回车键发送消息 mOutEditT ext = (EditText) findViewById(_text_out); mOutEditText.s etOnEditorActionListener(mWriteListener); // 初始化发送按钮,并设置 事件监听 mSendButton = (Button) findViewById(_sen d); lickListener(new OnClickListener() { public void onClick(View v) { // 取得TextView中的内容来发送消息 TextView view = (TextView) findViewById(_text_out); S tring message = t().toString(); sendMessage(messag e); } }); // 初始化BluetoothChatService并执行蓝牙连接 mChatService = new BluetoothChatService(this, mHandler); // 初 始化将要发出的消息的字符串 mOutStringBuffer = new StringBuffer(" "); } 首先构建一个对话进程mConversationArrayAdapter,然后从xml中取得用于显 示对话信息的列表mConversationView,最后将列表的数据来源Adapter设置为mCo nversationArrayAdapter,这里我们可以看到mConversationArrayAdapter所指定的 资源为,其定义实现如下: 1. ight="wrap_content" android:textSize="18sp" android:paddin g="5dp" /> 很简单,就包含了一个TextView用来显示对话内容即可,这里设置了文字标签 的尺寸大小textSize和padding属性。 然后我们取得了编辑框mOutEditText,用于输入聊天内容的输入框,并对其设 置了一个事件监听mWriteListener,其监听函数的实现如下: 1. // The action listener for the EditText widget, to listen for the retur n key private orActionListener mWriteListene r = new orActionListener() { public boolean o nEditorAction(TextView view, int actionId, KeyEvent event) { // 按 下回车键并且是按键弹起的事件时发送消息 if (actionId == EditorInfo.I ME_NULL && ion() == _UP) { Strin g message = t().toString(); sendMessage(messag e); } if(D) Log.i(TAG, "END onEditorAction"); return tru e; } }; 首先在其监听中实现了onEditorAction函数,我们通过判断其参数actionId来确 定事件触发的动作,其中的"_NULL"在Ophone中表示回车键消息,然 后再加上_UP,则表示当用户按下回车键并弹起时才触发消息的处 理,处理过程也很简单,将输入框中内容取出到变量message中,然后调用sendMe ssage函数来发送一条消息,具体的发送细节,我们稍后分析。 在setupChat函数中,我们还对发送消息的按钮进行的初始化,同样为其设置了 事件监听(setOnClickListener),监听的内容则也是取得输入框中的信息,然后调用 sendMessage函数来发送消息,和用户按回车键来发送消息一样。 最后一个重要的操作就是初始化了BluetoothChatService对象mChatService用 来管理蓝牙的链接,聊天的操作,并且设置了其Handler对象mHandler来负责数据的 交换和线程之间的通信。另外还准备了一个空的字符串对象mOutStringBuffer,用于 当我们在发送消息之后,对输入框的清理。 应用菜单 该应用程序除了这些界面的布局之外,我们还为其设置了一个菜单,菜单包括了 "扫描设备"和"使设备可见(能够被其他设备所搜索到)",创建菜单的方式有很多种, 这里gogole的员工,比较喜欢和推崇使用xml布局(将界面和逻辑分开),所以我 们首先看一下对于该应用程序通过xml所定义的菜单布局,代码如下: 1. 这样的定义的确非常的清晰,我们可以随意向这个Menu中添加菜单选项(itme), 这里就定义了上面我们所说的两个菜单。然后再程序中通过onCreateOptionsMenu 函数中来装载该菜单布局,遂于菜单的点击可以通过onOptionsItemSelected函数的 不同参数来辨别,下面是该应用程序中对菜单选项的处理和装载菜单布局: 1. //创建一个菜单 @Override public boolean onCreateOptionsMen u(Menu menu) { MenuInflater inflater = getMenuInflater(); inf e(_menu, menu); return true; } / /处理菜单事件 @Override public boolean onOptionsItemSelected (MenuItem item) { switch (mId()) { case n: // 启动DeviceListActivity查看设备并扫描 Intent serverInten t = new Intent(this, ); startActivityForResu lt(serverIntent, REQUEST_CONNECT_DEVICE); return true; ca se erable: // 确保设备处于可见状态 ensureDiscoverabl e(); return true; } return false; } 装载菜单布局的时候使用了MenuInflater对象,整个过程很简单,大家可以参 考上面的代码实现,在处理菜单事件的时候,通过mId()我们可以得到当 前选择的菜单项的ID,首先是扫描设备(),这里我们有启动了另外一个A ctivity来专门处理扫描设备DeviceListActivity,如果扫描之后我们将通过startActivi tyForResult函数来请求链接该设备,同样我们也会在onActivityResult函数中收到一 个反馈信息,命令代码为REQUEST_CONNECT_DEVICE,如果反馈的请求代码为Act _OK,则表示扫描成功(扫描过程我们稍后介绍),那么下面就可以开 始准备链接了,实现代码如下: 1. case REQUEST_CONNECT_DEVICE: // 当DeviceListActivity返回设 备连接 if (resultCode == _OK) { // 从Intent中 得到设备的MAC地址 String address = ras() .getStrin g(_DEVICE_ADDRESS); // 得到蓝牙设备对 象 BluetoothDevice device = oteDevice (address); // 尝试连接这个设备 t(devic e); } break; 首先我们可以通过_DEVICE_ADDRESS来取得设备的 Mac地址,然后通过Mac地址使用蓝牙适配器mBluetoothAdapter的getRemoteDev ice函数来查找到该地址的设备BluetoothDevice,查询到之后我们可以通过mChatS ervice对象的connect来链接该设备。 上面我们说的是扫描蓝牙设备并链接的过程,一般蓝牙设备在打开之后都需要设 置可见状态,下面我们来看一下另一个菜单选项的实现,用于使设备处于可见状态, 其菜单项的ID为erable,具体实现过程则位于ensureDiscoverable函数 中,其实现如下代码: 1. private void ensureDiscoverable() { if(D) Log.d(TAG, "ensure di scoverable"); //判断扫描模式是否为既可被发现又可以被连接 if (mB nMode() != _MODE _CONNECTABLE_DISCOVERABLE) { //请求可见状态 Intent disc overableIntent = new Intent(_REQUEST_DIS COVERABLE); //添加附加属性,可见状态的时间 discoverableIntent. putExtra(_DISCOVERABLE_DURATION, 30 0); startActivity(discoverableIntent); } } 这里首先通过nMode()函数取得该蓝牙的扫描模式, 然后通过_REQUEST_DISCOVERABLE设置可见属性,在这 里我们加入一个附加属性_DISCOVERABLE_DURATION,用 来设置可见状态的时间,表示在指定的时间中蓝牙处于可见状态,设置好之后通过s tartActivity来执行即可。 这里忧一个需要注意的问题,在链接某个设备之前,我们需要开启一个端口监听, 该应用程序将其放在onResume()函数中来处理了,代码如下: 1. @Override public synchronized void onResume() { super. onResume(); if(D) Log.e(TAG, "+ ON RESUME +"); // Performi ng this check in onResume() covers the case in which BT was // n ot enabled during onStart(), so we were paused to // o nResume() will be called when ACTION_REQUEST_ENABLE activity ret urns. if (mChatService != null) { // 如果当前状态为STATE_NON E,则需要开启蓝牙聊天服务 if (te() == Bluetoot _NONE) { // 开始一个蓝牙聊天服务 mChatSe (); } } } 首先检测mChatService是否被初始化,然后检测其状态是否为STATE_NONE, STATE_NONE表示初始化之后处于等待的状态,当我们在setupChat函数中初始时, 其实就已经将其状态设置为STATE_NONE了(该操作是在BluetoothChatService的 构造函数中处理的),所以这里就可以通过一个start函数来启动一个进程即可,实 际上就是启动了一个端口监听进程,当有设备连接时,该监听进程结束,然后转向链 接进程,链接之后同样又将转换到一个聊天管理进程。 本文主要包括以下两个部分的内容:其一,分析扫描设备部分DeviceListActivit y,其二,分析具体的聊天过程的完整通信方案,包括端口监听、链接配对、消息发 送和接收等,如果有对上一篇文章不太熟悉的,可以返回去在过一次,这样会有利于 本文的理解。 设备扫描(DeviceListActivity) 在上一篇文章的介绍中,当用户点击了扫描按钮之后,则会执行如下代码: 复制到剪贴板 Java代码 1. // 启动DeviceListActivity查看设备并扫描 Intent serverIntent = ne w Intent(this, ); startActivityForResult(serv erIntent, REQUEST_CONNECT_DEVICE); 该代码将跳转到DeviceListActivity进行设备的扫描,并且通过REQUEST_CONN ECT_DEVICE来请求链接扫描到的设备。从文件中我们知道De viceListActivity将为定义为一个对话框的风格,下图是该应用程序中,扫描蓝牙设备 的截图。 其中DeviceListActivity则为图中对话框部分,其界面的布局如下代码所示。 复制到剪贴板 Java代码 1. "match_parent" android:layout_height="match_parent" > _height="wrap_content" android:text="@string/title_paired_devic es" android:visibility="gone" android:background="#666" android:textColor="#fff" android:paddingLeft="5dp" /> -- 已经配对的设备信息 --> ght="wrap_content" android:stackFromBottom="true" android: layout_weight="1" /> width="match_parent" android:layout_height="wrap_content" android:text="@string/title_other_devices" android:visibility="g one" android:background="#666" android:textColor="#fff" android:paddingLeft="5dp" /> layout_width="match_parent" android:layout_height="wrap_conte nt" android:stackFromBottom="true" android:layout_weight=" 2" /> height="wrap_content" android:text="@string/button_scan" /> 该布局整体由一个线性布局LinearLayout组成,其中包含了两个textview中来 显示已经配对的设备和信扫描出来的设备(还没有经过配对)和两个ListView分别用 于显示已经配对和没有配对的设备的相关信息。按钮则用于执行扫描过程用,整个结 构很简单,下面我们开始分析如何编码实现了。 同样开始之前,我们先确定该类中的变量的作用,定义如下: 1. public class DeviceListActivity extends Activity { // Debuggin g private static final String TAG = "DeviceListActivity"; privat e static final boolean D = true; // Return Intent extra public st atic String EXTRA_DEVICE_ADDRESS = "device_address"; // 蓝牙 适配器 private BluetoothAdapter mBtAdapter; //已经配对的蓝牙 设备 private ArrayAdapter r; //新的蓝牙设备 private ArrayAdapter rrayAdapter; 其中Debugging部分,同样用于调试,这里定义了一个EXTRA_DEVICE_ADDRE SS,用于在通过Intent传递数据时的附加信息,即设备的地址,当扫描出来之后, 返回到BluetoothChat中的onActivityResult函数的REQUEST_CONNECT_DEVICE命 令,这是我们就需要通过_DEVICE_ADDRESS来取得该设备 的Mac地址,因此当我们扫描完成之后在反馈扫描结果时就需要绑定设备地址作为E XTRA_DEVICE_ADDRESS的附加值,这和我们上一篇介绍的并不矛盾。另外其他几个 变量则分别是本地蓝牙适配器、已经配对的蓝牙列表和扫描出来还没有配对的蓝牙设 备列表,稍后我们可以看到对他们的使用。 进入DeviceListActivity之后我们首先分析onCreate,首先通过如下代码对窗口 进行了设置: 1. // 设置窗口 requestWindowFeature(E_INDETERMI NATE_PROGRESS); setContentView(_list); setR esult(_CANCELED); 这里我们设置了窗口需要带一个进度条,当我们在扫描时就看有很容易的高速用 户扫描进度。具体布局则设置为device_也是我们文本第一段代码的内容,接 下来首先初始化扫描按钮,代码如下: 1. // 初始化扫描按钮 Button scanButton = (Button) findViewById(. button_scan); lickListener(new OnClickListener () { public void onClick(View v) { doDiscovery(); i bility(); } }); 首先取得按钮对象,然后为其设置一个事件监听,当事件触发时就通过doDisco very函数来执行扫描操作即可,具体扫描过程稍后分析。 然后需要初始化用来显示设备的列表和数据源,使用如下代码即可: 1. //初始化ArrayAdapter,一个是已经配对的设备,一个是新发现的设备 m PairedDevicesArrayAdapter = new ArrayAdapter _name); mNewDevicesArrayAdapter = new ArrayAdapter< String>(this, _name); // 检测并设置已配对的设备List View ListView pairedListView = (ListView) findViewById( d_devices); pter(mPairedDevicesArrayAdapt er); temClickListener(mDeviceClickListene r); // 检查并设置行发现的蓝牙设备ListView ListView newDevicesL istView = (ListView) findViewById(_devices); newDevices pter(mNewDevicesArrayAdapter); newDevicesListV temClickListener(mDeviceClickListener); 并分别对这些列表中的选项设置了监听mDeviceClickListener,用来处理,当 选择该选项时,就进行链接和配对操作。既然是扫描,我们就需要对扫描的结果进行 监控,这里我们构建了一个广播BroadcastReceiver来对扫描的结果进行处理,代码 如下: 1. // 当一个设备被发现时,需要注册一个广播 IntentFilter filter = new Int entFilter(_FOUND); erReceiver (mReceiver, filter); // 当显示检查完毕的时候,需要注册一个广播 fil ter = new IntentFilter(_DISCOVERY_FINISH ED); erReceiver(mReceiver, filter); 这里我们注册到广播mReceiver的IntentFilter主要包括了发现蓝牙设备(Bluet _FOUND)和扫描结束(_DISCOVERY _FINISHED),稍后我们分析如何在mReceiver中来处理这些事件。 最后我们需要取得本地蓝牙适配器和一些初始的蓝牙设备数据显示列表进行处 理,代码如下: 1. // 得到本地的蓝牙适配器 mBtAdapter = aul tAdapter(); // 得到一个已经匹配到本地适配器的BluetoothDevice类的 对象集合 Set ondedDevices(); // 如果有配对成功的设备则添加到ArrayAdapter i f (() > 0) { findViewById(_paired_devic es).setVisibility(E); for (BluetoothDevice device : pair edDevices) { (e () + "n" + ress()); } } else { //否则添加一 个没有被配对的字符串 String noDevices = getResources().getText(R. _paired).toString(); (noDevices); } 首先通过蓝牙适配器的getBondedDevices函数取得已经配对的蓝牙设备,并将 其添加到mPairedDevicesArrayAdapter数据源中,会显示到pairedListView列表视图 中,如果没有已经配对的蓝牙设备,则显示一个_paired字符串表示目 前没有配对成功的设备。 onDestroy函数中会制定销毁操作,主要包括蓝牙适配器和广播的注销操作,代 码如下: 1. @Override protected void onDestroy() { roy (); // 确保我们没有发现,检测设备 if (mBtAdapter != null) { Discovery(); } // 卸载所注册的广播 this.u nregisterReceiver(mReceiver); } 对于蓝牙适配器的取消方式则调用cancelDiscovery()函数即可,卸载mReceive r则需要调用unregisterReceiver即可。 做好初始化工作之后,下面我们开始分析扫描函数doDiscovery(),其扫描过程 的实现很就简单,代码如下: 1. /** * 请求能被发现的设备 */ private void doDiscovery () { if (D) Log.d(TAG, "doDiscovery()"); // 设置显示进度条 setProgressBarIndeterminateVisibility(true); // 设置title为扫描状 态 setTitle(ng); // 显示新设备的子标题 findVie wById(_new_devices).setVisibility(E); // 如果 已经在请求现实了,那么就先停止 if (overing()) { Discovery(); } // 请求从蓝牙适配器得到能够 被发现的设备 iscovery(); } 首先通过setProgressBarIndeterminateVisibility将进度条设置为显示状态,设置 标题title为ng字符串,表示正在扫描中,代码中所说的新设备的子 标题,其实就是上面我们所说的扫描到的没有经过配对的设备的title,对应于 tle_new_devices。扫描之前我们首先通过isDiscovering函数检测当前是否正在扫描, 如果正在扫描则调用cancelDiscovery函数来取消当前的扫描,最后调用startDiscov ery函数开始执行扫描操作。 现在已经开始扫描了,下面我们就需要对扫描过程进行监控和对扫描的结果进行 处理。即我们所定义的广播mReceiver,其实现如下所示。 1. //监听扫描蓝牙设备 private final BroadcastReceiver mReceiver = n ew BroadcastReceiver() { @Override public void onReceive(Co ntext context, Intent intent) { String action = ion (); 2. 3. 4. 5. 6. 7. 8. 9. // 当发现一个设备时 if (_(action)) { // 从Intent得到蓝牙设备对象 BluetoothDevice device = celableExtra(BluetoothDev _DEVICE); // 如果已经配对,则跳过,因为他已经在设备列表中了 if (dState() != _BONDE D) { //否则添加到设备列表 (e() + "n" + devic ress()); 10. } 11. // 当扫描完成之后改变Activity的title 12. } else if (_DISCOVERY_ ls(action)) { 13. //设置进度条不显示 14. setProgressBarIndeterminateVisibility(false); 15. //设置title 16. setTitle(_device); 17. //如果计数为0,则表示没有发现蓝牙 18. if (nt() == 0) { 19. String noDevices = getResources().getText(_found). toString(); 20. (noDevices); 21. } 22. } 23. } 24. }; 其中我们通过ion()可以取得一个动作,然后判断如果动作为Bluet _FOUND,则表示发现一个蓝牙设备,然后通过BluetoothDevice. EXTRA_DEVICE常量可以取得Intent中的蓝牙设备对象(BluetoothDevice),然后 通过条件"dState() != _BONDED"来判断设备 是否配对,如果没有配对则添加到行设备列表数据源mNewDevicesArrayAdapter中, 另外,当我们取得的动作为_DISCOVERY_FINISHED,则 表示扫描过程完毕,这时首先需要设置进度条不现实,并且设置窗口的标题为选择一 个设备(_device)。当然如果扫描完成之后没有发现新的设备,则添 加一个没有发现新的设备字符串(_found)到mNewDevicesArrayAdap ter中。 最后,扫描界面上还有一个按钮,其监听mDeviceClickListener的实现如下: 1. // ListViews中所有设备的点击事件监听 private OnItemClickListene r mDeviceClickListener = new OnItemClickListener() { public voi d onItemClick(AdapterView> av, View v, int arg2, long arg3) { // 取消检测扫描发现设备的过程,因为内非常耗费资源 Discovery(); // 得到mac地址 String info = ((TextView) v).getT ext().toString(); String address = ing(() - 1 7); // 创建一个包括Mac地址的Intent请求 Intent intent = new In tent(); ra(EXTRA_DEVICE_ADDRESS, address); / / 设置result并结束Activity setResult(_OK, inten t); finish(); } }; 当用户点击该按钮时,首先取消扫描进程,因为扫描过程是一个非常耗费资源的 过程,然后去的设备的mac地址,构建一个Intent 对象,通过附加数据EXTRA_DE VICE_ADDRESS将mac地址传递到BluetoothChat中,然后调用finish来结束该界面。 这时就会回到上一篇文章我们介绍的BluetoothChat中的onActivityResult函数中去 执行请求代码为REQUEST_CONNECT_DEVICE的片段,用来连接一个设备。 BluetoothChatService 对于设备的监听,连接管理都将由REQUEST_CONNECT_DEVICE来实现,其中 又包括三个主要部分,三个进程分贝是:请求连接的监听线程(AcceptThread)、连 接一个设备的进程(ConnectThread)、连接之后的管理进程(ConnectedThread)。 同样我们先熟悉一下该类的成员变量的作用,定义如下: 1. // Debugging private static final String TAG = "BluetoothChatSer vice"; private static final boolean D = true; //当创建socket服务 时的SDP名称 private static final String NAME = "BluetoothChat "; // 应用程序的唯一UUID private static final UUID MY_UUI D = ring("fa87c0d0-afac-11de-8a39-0800200c9a66"); // 本地蓝牙适配器 private final BluetoothAdapter mAdapter; / /Handler private final Handler mHandler; //请求链接的监听线 程 private AcceptThread mAcceptThread; //链接一个设备的线 程 private ConnectThread mConnectThread; //已经链接之后的管 理线程 private ConnectedThread mConnectedThread; //当前的 状态 private int mState; // 各种状态 public static final int S TATE_NONE = 0; public static final int STATE_LISTEN = 1; pu blic static final int STATE_CONNECTING = 2; public static final in t STATE_CONNECTED = 3; Debugging为调试相关,NAME 是当我们在创建一个socket监听服务时的一个 SDP名称,另外还包括一个状态变量mState,其值则分别是下面的"各种状态"部分, 另外还有一个本地蓝牙适配器和三个不同的进程对象,由此可见,本地蓝牙适配器的 确是任何蓝牙操作的基础对象,下面我们会分别介绍这些进程的实现。 首先是初始化操作,即构造函数,代码如下: 1. public BluetoothChatService(Context context, Handler handler) { //得到本地蓝牙适配器 mAdapter = aultAd apter(); //设置状态 mState = STATE_NONE; //设置Handle r mHandler = handler; } 取得本地蓝牙适配器、设置状态为STATE_NONE,设置传递进来的mHandler。 接下来需要控制当状态改变之后,我们需要通知UI界面也同时更改状态,下面是得 到状态和设置状态的实现部分,如下: 1. private synchronized void setState(int state) { if (D) Log.d(T AG, "setState() " + mState + " -> " + state); mState = state; // 状态更新之后UI Activity也需要更新 Message(Bl E_STATE_CHANGE, state, -1).sendToTarget(); } public synchronized int getState() { return mState; } 得到状态没有什么特别的,关键在于设置状态之后需要通过obtainMessage来发 送一个消息到Handler,通知UI界面也同时更新其状态,对应的Handler的实现则位 于BluetoothChat中的private final Handler mHandler = new Handler()部分,从 上面的代码中,我们可以看到关于状态更改的之后会发送一个 GE_STATE_CHANGE消息到UI线程中,下面我们看一下UI线程中如何处理这些消息 的,代码如下: 1. case MESSAGE_STATE_CHANGE: if(D) Log.i(TAG, "MESSAGE_ST ATE_CHANGE: " + 1); switch (1) { case Blue _CONNECTED: //设置状态为已经链接 m t(_connected_to); //添加设备名称 mTitl (mConnectedDeviceName); //清理聊天记录 mConversa (); break; case BluetoothChatService.S TATE_CONNECTING: //设置正在链接 t( le_connecting); break; case _LIST EN: case _NONE: //处于监听状态 或者没有准备状态,则显示没有链接 t(_not_c onnected); break; } break; 可以看出,当不同的状态在更改之后会进行不同的设置,但是大多数都是根据不 同的状态设置显示了不同的title,当已经链接(STATE_CONNECTED)之后,设置了标 题为链接的设备名,并同时还mConversationArrayAdapter进行了清除操作,即清除 聊天记录。 现在,初始化操作已经完成了,下面我们可以调用start函数来开启一个服务进 程了,也即是在BluetoothChat中的onResume函数中所调用的start操作,其具体 实现如下: 1. public synchronized void start() { if (D) Log.d(TAG, "start "); // 取消任何线程视图建立一个连接 if (mConnectThread != nul l) {(); mConnectThread = null;} // 取消任 何正在运行的链接 if (mConnectedThread != null) {mConnectedThre (); mConnectedThread = null;} // 启动AcceptThread线程 来监听BluetoothServerSocket if (mAcceptThread == null) { m AcceptThread = new AcceptThread(); (); } //设置状态为监听,,等待链接 2. 3. setState(STATE_LISTEN); } 操作过程很简单,首先取消另外两个进程,新建一个AcceptThread进程,并启 动AcceptThread进程,最后设置状态变为监听(STATE_LISTEN),这时UI界面的titl e也将更新为监听状态,即等待设备的连接。关于AcceptThread的具体实现如下所 示。 1. private class AcceptThread extends Thread { // 本地socket服 务 private final BluetoothServerSocket mmServerSocket; publi c AcceptThread() { BluetoothServerSocket tmp = null; // 创建 一个新的socket服务监听 try { tmp = UsingRfco mmWithServiceRecord(NAME, MY_UUID); } catch (IOExceptio n e) { Log.e(TAG, "listen() failed", e); } mmServerSocke t = tmp; } public void run() { if (D) Log.d(TAG, "BEGIN m AcceptThread" + this); setName("AcceptThread"); BluetoothS ocket socket = null; // 如果当前没有链接则一直监听socket服务 w hile (mState != STATE_CONNECTED) { try { //如果有请求链接, 则接受 //这是一个阻塞调用,将之返回链接成功和一个异常 socke t = (); } catch (IOException e) { Log. e(TAG, "accept() failed", e); break; } // 如果接受了一个链 接 if (socket != null) { synchronized ( s) { switch (mState) { case STATE_LISTEN: case STATE_C ONNECTING: // 如果状态为监听或者正在链接中,,则调用connected来 链接 connected(socket, oteDevice()); break; case STATE_NONE: case STATE_CONNECTED: // 如果为没有 准备或者已经链接,这终止该socket try { (); } cat ch (IOException e) { Log.e(TAG, "Could not close unwanted socke t", e); } break; } } } } if (D) Log.i(TAG, " END mAcceptThread"); } //关闭BluetoothServerSocket pub lic void cancel() { if (D) Log.d(TAG, "cancel " + this); try { (); } catch (IOException e) { Log.e(T AG, "close() of server failed", e); } } } 首先通过listenUsingRfcommWithServiceRecord创建一个socket服务,用来监 听设备的连接,当进程启动之后直到有设备连接时,这段时间都将通过accept来监 听和接收一个连接请求,如果连接无效则调用close来关闭即可,如果连接有效则调 用connected进入连接进程,进入连接进程之后会取消当前的监听进程,取消过程则 直接调用cancel通过()来关闭即可。下面我们分析连接函数c onnect的实现,如下: 1. public synchronized void connect(BluetoothDevice device) { i f (D) Log.d(TAG, "connect to: " + device); // 取消任何链接线程,视图 建立一个链接 if (mState == STATE_CONNECTING) { if (mConne ctThread != null) {(); mConnectThread = nul l;} } // 取消任何正在运行的线程 if (mConnectedThread != nu ll) {(); mConnectedThread = null;} // 启 动一个链接线程链接指定的设备 mConnectThread = new ConnectThrea d(device); (); setState(STATE_CONNEC TING); } 同样,首先关闭其他两个进程,然后新建一个ConnectThread进程,并启动, 通知UI界面状态更改为正在连接的状态(STATE_CONNECTING)。具体的连接进程 由ConnectThread来实现,如下: 1. private class ConnectThread extends Thread { //蓝牙Socket private final BluetoothSocket mmSocket; //蓝牙设备 private f inal BluetoothDevice mmDevice; public ConnectThread(BluetoothD evice device) { mmDevice = device; BluetoothSocket tmp = n ull; //得到一个给定的蓝牙设备的BluetoothSocket try { tm p = RfcommSocketToServiceRecord(MY_UUID); } cat ch (IOException e) { Log.e(TAG, "create() failed", e); } m mSocket = tmp; } public void run() { Log.i(TAG, "BEGI N mConnectThread"); setName("ConnectThread"); // 取消可见 状态,将会进行链接 Discovery(); // 创建一个Blu etoothSocket链接 try { //同样是一个阻塞调用,返回成功和异常 t(); } catch (IOException e) { //链接失 败 connectionFailed(); // 如果异常则关闭socket try { m (); } catch (IOException e2) { Log.e(TAG, "una ble to close() socket during connection failure", e2); } // 重新 启动监听服务状态 (); return; } // 完成则重置ConnectThread 2. 3. 4. 5. 6. 7. 8. 9. synchronized () { mConnectThread = null; } // 开启ConnectedThread(正在运行中...)线程 connected(mmSocket, mmDevice); } //取消链接线程ConnectThread 10. public void cancel() { 11. try { 12. (); 13. } catch (IOException e) { 14. Log.e(TAG, "close() of connect socket failed", e); 15. } 16. } 17. } 在创建该进程时,就已经知道当前需要被连接的蓝牙设备,然后通过createRfc ommSocketToServiceRecord可以构建一个蓝牙设备的BluetoothSocket对象,当进 入连接状态时,就可以调用cancelDiscovery来取消蓝牙的可见状态,然后通过调用 connect函数进行链接操作,如果出现异常则表示链接失败,则调用connectionFaile d函数通知UI进程更新界面的显示为链接失败状态,然后关闭BluetoothSocket,调 用start函数重新开启一个监听服务AcceptThread,对于链接失败的处理实现如下: 1. private void connectionFailed() { setState(STATE_LISTEN); // 发送链接失败的消息到UI界面 Message msg = Mes sage(E_TOAST); Bundle bundle = new Bun dle(); ing(, "Unable to connec t device"); a(bundle); ssage(ms g); } 首先更改状态为STATE_LISTEN,然后发送一个Message带UI界面,通知UI 更新,显示一个Toast告知用户,当BluetoothChat中的mHandler接收到Bluetooth 消息时,就会直接更新UI界面的显示,如果连接成功则将调用connect ed函数进入连接管理进程,其实现如下: 1. public synchronized void connected(BluetoothSocket socket, Bluet oothDevice device) { if (D) Log.d(TAG, "connected"); // 取消C onnectThread链接线程 if (mConnectThread != null) {mConnectThre (); mConnectThread = null;} // 取消所有正在链接的线程 if (mConnectedThread != null) {(); mCon nectedThread = null;} // 取消所有的监听线程,因为我们已经链接了一个 设备 if (mAcceptThread != null) {(); mAccep tThread = null;} // 启动ConnectedThread线程来管理链接和执行翻 译 mConnectedThread = new ConnectedThread(socket); mCon (); // 发送链接的设备名称到UI Activity界面 M essage msg = Message(E_DEV ICE_NAME); Bundle bundle = new Bundle(); ing (_NAME, e()); a(b undle); ssage(msg); //状态变为已经链接,即正 在运行中 setState(STATE_CONNECTED); } 首先,关闭所有的进程,构建一个ConnectedThread进程,并准备一个Messag e消息,就设备名称(_NAME)也发送到UI进程,因为UI进 程需要显示当前连接的设备名称,当UI进程收到E_DEVICE_ NAME消息时就会更新相应的UI界面,就是设置窗口的title,这里我们就不贴出代 码了,下面我们分析一下ConnectedThread的实现,代码如下: 1. private class ConnectedThread extends Thread { //BluetoothS ocket private final BluetoothSocket mmSocket; //输入输出流 private final InputStream mmInStream; private final OutputStre am mmOutStream; public ConnectedThread(BluetoothSocket sock et) { Log.d(TAG, "create ConnectedThread"); mmSocket = soc ket; InputStream tmpIn = null; OutputStream tmpOut = nul l; // 得到BluetoothSocket的输入输出流 try { tmpIn = socke utStream(); tmpOut = putStream(); } c atch (IOException e) { Log.e(TAG, "temp sockets not created ", e); } mmInStream = tmpIn; mmOutStream = tmpOu t; } public void run() { Log.i(TAG, "BEGIN mConnectedThr ead"); byte[] buffer = new byte[1024]; int bytes; // 监听 输入流 while (true) { try { // 从输入流中读取数据 byte s = (buffer); // 发送一个消息到UI线程进行更新 Message(E_READ, bytes, - 1, buffer) .sendToTarget(); } catch (IOException e) { //出 现异常,则链接丢失 Log.e(TAG, "disconnected", e); connectionLo st(); break; } } } /** * 写入药发送的消息 * @param buffer The bytes to write */ public void write(byte [] buffer) { try { (buffer); // 将写的消 息同时传递给UI界面 Message( AGE_WRITE, -1, -1, buffer) .sendToTarget(); } catch (IOExcep tion e) { Log.e(TAG, "Exception during write", e); } } //取消ConnectedThread链接管理线程 public void cancel() { tr y { (); } catch (IOException e) { Log.e(TA G, "close() of connect socket failed", e); 2. 3. 4. } } } 连接之后的主要操作就是发送和接收聊天消息了,因为需要通过其输入(出)流 来操作具体信息,进程会一直从输入流中读取信息,并通过obtainMessage函数将读 取的信息以E_READ命令发送到UI进程,到UI进程收到是, 就需要将其显示到消息列表之中,同时对于发送消息,需要实行写操作write,其操 作就是将要发送的消息写入到输出流mmOutStream中,并且以 SAGE_WRITE命令的方式发送到UI进程中,进行同步更新,如果在读取消息时失败 或者产生了异常,则表示连接丢失,这是就调用connectionLost函数来处理连接丢失, 代码如下: 1. private void connectionLost() { setState(STATE_LISTEN); / / 发送失败消息到UI界面 Message msg = Message(B E_TOAST); Bundle bundle = new Bundle (); ing(, "Device connection wa s lost"); a(bundle); ssage(ms g); } 操作同样简单,首先改变状态为STATE_LISTEN,然后E _TOAST命令发送一个消息Message到UI进程,通知UI进程更新显示画面即可。对 于写操作,是调用了来实现,其实现代码如下: 1. //写入自己要发送出来的消息 public void write(byte[] out) { // C reate temporary object ConnectedThread r; // Synchronize a c opy of the ConnectedThread synchronized (this) { //判断是否 处于已经链接状态 if (mState != STATE_CONNECTED) return; r = mConnectedThread; } // 执行写 (out); } 其实就是检测,当前的状态是否处于已经链接状态STATE_CONNECTED,然后 调用ConnectedThread 进程中的write操作,来完成消息的发送。因此这时我们可以 回过头来看BluetoothChat中的sendMessage的实现了,如下所示: 1. private void sendMessage(String message) { // 检查是否处于连接 状态 if (te() != _ CONNECTED) { xt(this, _connected, Toa _SHORT).show(); return; } // 如果输入的消息不 为空才发送,否则不发送 if (() > 0) { // Get the m essage bytes and tell the BluetoothChatService to write byte[] sen d = es(); (send); // Rese t out string buffer to zero and clear the edit text field mOutString gth(0); t(mOutStringBuffer); } } 同样首先检测了当前的状态是否为已经连接状态,然后对要发送的消息是否为n ull进行了判断,如果为空则不需要发送,否则调用(即上面所说 的ConnectedThread 中的wirte操作)来发送消息。然后一个小的细节就是设置编辑 框的内容为null即可。最后我们可以看一下在BluetoothChat中如何处理这些接收到 的消息,主要位于mHandler中的handleMessage函数中,对于状态改变的消息我们 已经分析过了,下面是其他几个消息的处理: 1. case MESSAGE_WRITE: byte[] writeBuf = (byte[]) ; // 将自己写入的消息也显示到会话列表中 String writeMessage = ne w String(writeBuf); ("Me: " + writ eMessage); break; case MESSAGE_READ: byte[] readBu f = (byte[]) ; // 取得内容并添加到聊天对话列表中 String r eadMessage = new String(readBuf, 0, 1); mConversationA (mConnectedDeviceName+": " + readMessage); b reak; case MESSAGE_DEVICE_NAME: // 保存链接的设备名称,并 显示一个toast提示 mConnectedDeviceName = a().getSt ring(DEVICE_NAME); xt(getApplicationContext(), "C onnected to " + mConnectedDeviceName, _SHORT). show(); break; case MESSAGE_TOAST: //处理链接(发送)失 败的消息 xt(getApplicationContext(), a(). getString(TOAST), _SHORT).show(); break; 分别是读取消息和写消息(发送消息),对于一些信息提示消息MESSAGE_T OAST,则通过Toast显示出来即可。如果消息是设备名称MESSAGE_DEVICE_NAME, 则提示用户当前连接的设备的名称。对于写消息(MESSAGE_WRITE)和读消息(ME SSAGE_READ)我们就不重复了,大家看看代码都已经加入了详细的注释了。 最后当我们在需要停止这些进程时就看有直接调用stop即可,具体实现如下: 1. //停止所有的线程 public synchronized void stop() { if (D) Log. d(TAG, "stop"); if (mConnectThread != null) { ncel(); mConnectThread = null;} if (mConnectedThread != nul l) {(); mConnectedThread = null;} if (m AcceptThread != null) {(); mAcceptThread = nu ll;} //状态设置为准备状态 setState(STATE_NONE); } 分别检测三个进程是否为null,然后调用各自的cancel函数来取消进程,最后 不要忘记将状态恢复到STATE_NONE即可。 2024年8月24日发(作者:学醉蝶) 本文我们将通过学习Android的蓝牙聊天示例应用程序来介绍蓝牙开发包的使用,该 示例程序完整的包含了蓝牙开发的各个部分,将实现两个设备通过蓝牙进行连接并聊 天。 前面我们说过,在使用蓝牙API时就需要开启某些权限,同时我们还可以从And 文件中找到应用程序启动时所进入的界面Activity等信息,因此下 面我们首先打开文件,代码如下: 1. versionCode="1" android:versionName="1.0"> > l="@string/app_name" android:configChanges="orientation|keybo ardHidden"> - 用于显示蓝牙设备列表的Activity --> theme="@android:style/" android:configChanges="o rientation|keyboardHidden" /> t> 首先minSdkVersion用于说明该应用程序所需要使用的最小SDK版本,这里设 置为6,也就是说最小需要使用android1.6版本的sdk,同时Ophone则需要使用o ms2.0版本,然后打开了BLUETOOTH和BLUETOOTH_ADMIN两个蓝牙操作相关的 权限,最后看到了两个Activity的声明,他们分别是BluetoothChat(默认主Activity) 和DeviceListActivity(显示设备列表),其中DeviceListActivity风格被定义为一个对 话框风格,下面我们将分析该程序的每个细节。 BluetoothChat 首先,程序启动进入BluetoothChat,在onCreate函数中对窗口进行了设置,代 码如下: 1. // 设置窗口布局 requestWindowFeature(E_CUSTO M_TITLE); setContentView(); getWindow().setFe atureInt(E_CUSTOM_TITLE, _titl e); 这里可以看到将窗口风格设置为自定义风格了,并且指定了自定义title布局为c ustom_title,其定义代码如下: 复制到剪贴板 Java代码 1. ut_height="match_parent" android:gravity="center_vertical" > ut_alignParentLeft="true" android:ellipsize="end" android:sin gleLine="true" style="?android:attr/windowTitleStyle" android: layout_width="wrap_content" android:layout_height="match_pare nt" android:layout_weight="1" /> android:ellipsize="end" android:singleLine="true" android:la yout_width="wrap_content" android:layout_height="match_paren t" android:textColor="#fff" android:layout_weight="1" /> 该布局将title设置为一个相对布局RelativeLayout,其中包含了两个TextView, 一个在左边一个在右边,分别用于显示应用程序的标题title和当前的蓝牙配对链接名 称,如下图所示。 其中左边显示为应用程序名称"BluetoothChat",右边显示一个connected:scort 则表示当前配对成功正在进行聊天的链接名称。整个聊天界面的布局在中 实现,代码如下: 复制到剪贴板 Java代码 1. "match_parent" android:layout_height="match_parent" > layout_width="match_parent" android:layout_height="match_par ent" android:stackFromBottom="true" android:transcriptMode ="alwaysScroll" android:layout_weight="1" /> ght="wrap_content" > ght="wrap_content" android:layout_weight="1" android:layou t_gravity="bottom" /> d" android:layout_width="wrap_content" android:layout_heigh t="wrap_content" android:text="@string/send" /> arLayout> 整个界面的布局将是一个线性布局LinearLayout,其中包含了另一个ListView(用 于显示聊天的对话信息)和另外一个线性布局来实现一个发送信息的窗口,发送消息 发送框有一个输入框和一个发送按钮构成。整个界面如下图所示。 布局好界面,下面我们需要进入编码状态,首先看BluetoothChat所要那些成员 变量,如下代码所示: 复制到剪贴板 Java代码 1. public class BluetoothChat extends Activity { // Debugging private static final String TAG = "BluetoothChat"; private static f inal boolean D = true; //从BluetoothChatService Handler发送的消 息类型 public static final int MESSAGE_STATE_CHANGE = 1; pu blic static final int MESSAGE_READ = 2; public static final int MES SAGE_WRITE = 3; public static final int MESSAGE_DEVICE_NAM E = 4; public static final int MESSAGE_TOAST = 5; // 从Blueto othChatService Handler接收消息时使用的键名(键-值模型) public stati c final String DEVICE_NAME = "device_name"; public static final S tring TOAST = "toast"; // Intent请求代码(请求链接,请求可见) private static final int REQUEST_CONNECT_DEVICE = 1; private st atic final int REQUEST_ENABLE_BT = 2; // Layout Views privat e TextView mTitle; private ListView mConversationView; privat e EditText mOutEditText; private Button mSendButton; // 链接 的设备的名称 private String mConnectedDeviceName = null; / / Array adapter for the conversation thread private ArrayAdapter< String> mConversationArrayAdapter; // 将要发送出去的字符串 p rivate StringBuffer mOutStringBuffer; // 本地蓝牙适配器 privat e BluetoothAdapter mBluetoothAdapter = null; // 聊天服务的对象 private BluetoothChatService mChatService = null; //...... 其中Debugging部分则将用于我们在调试程序时通过log打印日志用,其他部分 我们都加入了注释,需要说明的是BluetoothChatService ,它是我们自己定义的一个 用来管理蓝牙的端口监听,链接,管理聊天的程序,后面我们会介绍。在这里需要说 明一点,这些代码都出自google的员工之手,大家在学习时,可以借鉴很多代码编 写的技巧和风格,这都将对我们有非常大的帮助。 然后,我们就需要对界面进行一些设置,如下代码将用来设置我们自定义的标题 title需要显示的内容: 1. // 设置自定义title布局 mTitle = (TextView) findViewById( _left_text); t(_name); mTitle = (Tex tView) findViewById(_right_text); 左边的TextView被设置为显示应用程序名称,右边的则需要我们在链接之后在 设置更新,目前则显示没有链接字样,所以这里我们暂不设置,进一步就需要获取本地 蓝牙适配器BluetoothAdapter了,因为对于有关蓝牙的任何操作都需要首先获得该蓝 牙适配器,获取代码非常简单,如下: 1. // 得到一个本地蓝牙适配器 mBluetoothAdapter = BluetoothAdapter. getDefaultAdapter(); // 如果适配器为null,则不支持蓝牙 if (mBlu etoothAdapter == null) { xt(this, "Bluetooth is not a vailable", _LONG).show(); finish(); return; } getDefaultAdapter()函数用于获取本地蓝牙适配器,然后检测是否为n ull,如果为null则表示没有蓝牙设备的支持,将通过toast告知用户。 在o nStart()函数中,我们将检测蓝牙是否被打开,如果没有打开,则请求打开,否则 就可以设置一些聊天信息的准备工作,代码如下: @Override public v oid onStart() { t(); if(D) Log.e(TAG, "++ ON STA RT ++"); // 如果蓝牙没有打开,则请求打开 // setupChat() will the n be called during onActivityResult if (!l ed()) { Intent enableIntent = new Intent( N_REQUEST_ENABLE); startActivityForResult(enableIntent, REQUE ST_ENABLE_BT); // 否则,设置聊天会话 } else { if (mChatSe rvice == null) setupChat(); } } 如果蓝牙没有打开,我们则通过_REQUEST_ENABLE 来请求打开蓝牙,REQUEST_ENABLE_BT则是我们自己定义的用于请求打开蓝牙的I ntent代码,最后当我们调用startActivityForResult来执行请求时,就会在onActivit yResult函数中得到一个反馈,如果当前蓝牙已经打开,那么就可以调用setupChat 函数来准备蓝牙聊天相关的工作,稍后分析该函数的具体实现。 下面我们分析一下请求打开蓝牙之后,在onActivityResult 中得到的反馈信息, 我们传递了REQUEST_ENABLE_BT代码作为请求蓝牙打开的命令,因此在onActivit yResult 中,需要会得到一个请求代码为REQUEST_ENABLE_B的消息,对于其处理 如下代码所示: 1. 2. 3. 4. 5. 6. 7. 8. 9. case REQUEST_ENABLE_BT: //在请求打开蓝牙时返回的代码 if (resultCode == _OK) { // 蓝牙已经打开,所以设置一个聊天会话 setupChat(); } else { // 请求打开蓝牙出错 Log.d(TAG, "BT not enabled"); xt(this, _not_enabled_leaving, NGTH_SHORT).show(); finish(); 10. } 在请求时,如果返回代码为_OK,则表示请求打开蓝牙成功,那 么我们就可以和上面的操作进度一样,调用setupChat来设置蓝牙聊天相关信息,如 果返回其他代码,则表示请求打开蓝牙失败,这时我们同样通过一个Toast来告诉用 户,同时也需要调用finish()函数来结束应用程序。 如果打开蓝牙无误,那么下面我们开始进入setupChat的设置,其代码实现如下: 1. private void setupChat() { Log.d(TAG, "setupChat()"); // 初 始化对话进程 mConversationArrayAdapter = new ArrayAdapter ng>(this, e); // 初始化对话显示列表 mConversa tionView = (ListView) findViewById(); // 设置话显示列表源 pter(mConversationArrayAdapter); / / 初始化编辑框,并设置一个监听,用于处理按回车键发送消息 mOutEditT ext = (EditText) findViewById(_text_out); mOutEditText.s etOnEditorActionListener(mWriteListener); // 初始化发送按钮,并设置 事件监听 mSendButton = (Button) findViewById(_sen d); lickListener(new OnClickListener() { public void onClick(View v) { // 取得TextView中的内容来发送消息 TextView view = (TextView) findViewById(_text_out); S tring message = t().toString(); sendMessage(messag e); } }); // 初始化BluetoothChatService并执行蓝牙连接 mChatService = new BluetoothChatService(this, mHandler); // 初 始化将要发出的消息的字符串 mOutStringBuffer = new StringBuffer(" "); } 首先构建一个对话进程mConversationArrayAdapter,然后从xml中取得用于显 示对话信息的列表mConversationView,最后将列表的数据来源Adapter设置为mCo nversationArrayAdapter,这里我们可以看到mConversationArrayAdapter所指定的 资源为,其定义实现如下: 1. ight="wrap_content" android:textSize="18sp" android:paddin g="5dp" /> 很简单,就包含了一个TextView用来显示对话内容即可,这里设置了文字标签 的尺寸大小textSize和padding属性。 然后我们取得了编辑框mOutEditText,用于输入聊天内容的输入框,并对其设 置了一个事件监听mWriteListener,其监听函数的实现如下: 1. // The action listener for the EditText widget, to listen for the retur n key private orActionListener mWriteListene r = new orActionListener() { public boolean o nEditorAction(TextView view, int actionId, KeyEvent event) { // 按 下回车键并且是按键弹起的事件时发送消息 if (actionId == EditorInfo.I ME_NULL && ion() == _UP) { Strin g message = t().toString(); sendMessage(messag e); } if(D) Log.i(TAG, "END onEditorAction"); return tru e; } }; 首先在其监听中实现了onEditorAction函数,我们通过判断其参数actionId来确 定事件触发的动作,其中的"_NULL"在Ophone中表示回车键消息,然 后再加上_UP,则表示当用户按下回车键并弹起时才触发消息的处 理,处理过程也很简单,将输入框中内容取出到变量message中,然后调用sendMe ssage函数来发送一条消息,具体的发送细节,我们稍后分析。 在setupChat函数中,我们还对发送消息的按钮进行的初始化,同样为其设置了 事件监听(setOnClickListener),监听的内容则也是取得输入框中的信息,然后调用 sendMessage函数来发送消息,和用户按回车键来发送消息一样。 最后一个重要的操作就是初始化了BluetoothChatService对象mChatService用 来管理蓝牙的链接,聊天的操作,并且设置了其Handler对象mHandler来负责数据的 交换和线程之间的通信。另外还准备了一个空的字符串对象mOutStringBuffer,用于 当我们在发送消息之后,对输入框的清理。 应用菜单 该应用程序除了这些界面的布局之外,我们还为其设置了一个菜单,菜单包括了 "扫描设备"和"使设备可见(能够被其他设备所搜索到)",创建菜单的方式有很多种, 这里gogole的员工,比较喜欢和推崇使用xml布局(将界面和逻辑分开),所以我 们首先看一下对于该应用程序通过xml所定义的菜单布局,代码如下: 1. 这样的定义的确非常的清晰,我们可以随意向这个Menu中添加菜单选项(itme), 这里就定义了上面我们所说的两个菜单。然后再程序中通过onCreateOptionsMenu 函数中来装载该菜单布局,遂于菜单的点击可以通过onOptionsItemSelected函数的 不同参数来辨别,下面是该应用程序中对菜单选项的处理和装载菜单布局: 1. //创建一个菜单 @Override public boolean onCreateOptionsMen u(Menu menu) { MenuInflater inflater = getMenuInflater(); inf e(_menu, menu); return true; } / /处理菜单事件 @Override public boolean onOptionsItemSelected (MenuItem item) { switch (mId()) { case n: // 启动DeviceListActivity查看设备并扫描 Intent serverInten t = new Intent(this, ); startActivityForResu lt(serverIntent, REQUEST_CONNECT_DEVICE); return true; ca se erable: // 确保设备处于可见状态 ensureDiscoverabl e(); return true; } return false; } 装载菜单布局的时候使用了MenuInflater对象,整个过程很简单,大家可以参 考上面的代码实现,在处理菜单事件的时候,通过mId()我们可以得到当 前选择的菜单项的ID,首先是扫描设备(),这里我们有启动了另外一个A ctivity来专门处理扫描设备DeviceListActivity,如果扫描之后我们将通过startActivi tyForResult函数来请求链接该设备,同样我们也会在onActivityResult函数中收到一 个反馈信息,命令代码为REQUEST_CONNECT_DEVICE,如果反馈的请求代码为Act _OK,则表示扫描成功(扫描过程我们稍后介绍),那么下面就可以开 始准备链接了,实现代码如下: 1. case REQUEST_CONNECT_DEVICE: // 当DeviceListActivity返回设 备连接 if (resultCode == _OK) { // 从Intent中 得到设备的MAC地址 String address = ras() .getStrin g(_DEVICE_ADDRESS); // 得到蓝牙设备对 象 BluetoothDevice device = oteDevice (address); // 尝试连接这个设备 t(devic e); } break; 首先我们可以通过_DEVICE_ADDRESS来取得设备的 Mac地址,然后通过Mac地址使用蓝牙适配器mBluetoothAdapter的getRemoteDev ice函数来查找到该地址的设备BluetoothDevice,查询到之后我们可以通过mChatS ervice对象的connect来链接该设备。 上面我们说的是扫描蓝牙设备并链接的过程,一般蓝牙设备在打开之后都需要设 置可见状态,下面我们来看一下另一个菜单选项的实现,用于使设备处于可见状态, 其菜单项的ID为erable,具体实现过程则位于ensureDiscoverable函数 中,其实现如下代码: 1. private void ensureDiscoverable() { if(D) Log.d(TAG, "ensure di scoverable"); //判断扫描模式是否为既可被发现又可以被连接 if (mB nMode() != _MODE _CONNECTABLE_DISCOVERABLE) { //请求可见状态 Intent disc overableIntent = new Intent(_REQUEST_DIS COVERABLE); //添加附加属性,可见状态的时间 discoverableIntent. putExtra(_DISCOVERABLE_DURATION, 30 0); startActivity(discoverableIntent); } } 这里首先通过nMode()函数取得该蓝牙的扫描模式, 然后通过_REQUEST_DISCOVERABLE设置可见属性,在这 里我们加入一个附加属性_DISCOVERABLE_DURATION,用 来设置可见状态的时间,表示在指定的时间中蓝牙处于可见状态,设置好之后通过s tartActivity来执行即可。 这里忧一个需要注意的问题,在链接某个设备之前,我们需要开启一个端口监听, 该应用程序将其放在onResume()函数中来处理了,代码如下: 1. @Override public synchronized void onResume() { super. onResume(); if(D) Log.e(TAG, "+ ON RESUME +"); // Performi ng this check in onResume() covers the case in which BT was // n ot enabled during onStart(), so we were paused to // o nResume() will be called when ACTION_REQUEST_ENABLE activity ret urns. if (mChatService != null) { // 如果当前状态为STATE_NON E,则需要开启蓝牙聊天服务 if (te() == Bluetoot _NONE) { // 开始一个蓝牙聊天服务 mChatSe (); } } } 首先检测mChatService是否被初始化,然后检测其状态是否为STATE_NONE, STATE_NONE表示初始化之后处于等待的状态,当我们在setupChat函数中初始时, 其实就已经将其状态设置为STATE_NONE了(该操作是在BluetoothChatService的 构造函数中处理的),所以这里就可以通过一个start函数来启动一个进程即可,实 际上就是启动了一个端口监听进程,当有设备连接时,该监听进程结束,然后转向链 接进程,链接之后同样又将转换到一个聊天管理进程。 本文主要包括以下两个部分的内容:其一,分析扫描设备部分DeviceListActivit y,其二,分析具体的聊天过程的完整通信方案,包括端口监听、链接配对、消息发 送和接收等,如果有对上一篇文章不太熟悉的,可以返回去在过一次,这样会有利于 本文的理解。 设备扫描(DeviceListActivity) 在上一篇文章的介绍中,当用户点击了扫描按钮之后,则会执行如下代码: 复制到剪贴板 Java代码 1. // 启动DeviceListActivity查看设备并扫描 Intent serverIntent = ne w Intent(this, ); startActivityForResult(serv erIntent, REQUEST_CONNECT_DEVICE); 该代码将跳转到DeviceListActivity进行设备的扫描,并且通过REQUEST_CONN ECT_DEVICE来请求链接扫描到的设备。从文件中我们知道De viceListActivity将为定义为一个对话框的风格,下图是该应用程序中,扫描蓝牙设备 的截图。 其中DeviceListActivity则为图中对话框部分,其界面的布局如下代码所示。 复制到剪贴板 Java代码 1. "match_parent" android:layout_height="match_parent" > _height="wrap_content" android:text="@string/title_paired_devic es" android:visibility="gone" android:background="#666" android:textColor="#fff" android:paddingLeft="5dp" /> -- 已经配对的设备信息 --> ght="wrap_content" android:stackFromBottom="true" android: layout_weight="1" /> width="match_parent" android:layout_height="wrap_content" android:text="@string/title_other_devices" android:visibility="g one" android:background="#666" android:textColor="#fff" android:paddingLeft="5dp" /> layout_width="match_parent" android:layout_height="wrap_conte nt" android:stackFromBottom="true" android:layout_weight=" 2" /> height="wrap_content" android:text="@string/button_scan" /> 该布局整体由一个线性布局LinearLayout组成,其中包含了两个textview中来 显示已经配对的设备和信扫描出来的设备(还没有经过配对)和两个ListView分别用 于显示已经配对和没有配对的设备的相关信息。按钮则用于执行扫描过程用,整个结 构很简单,下面我们开始分析如何编码实现了。 同样开始之前,我们先确定该类中的变量的作用,定义如下: 1. public class DeviceListActivity extends Activity { // Debuggin g private static final String TAG = "DeviceListActivity"; privat e static final boolean D = true; // Return Intent extra public st atic String EXTRA_DEVICE_ADDRESS = "device_address"; // 蓝牙 适配器 private BluetoothAdapter mBtAdapter; //已经配对的蓝牙 设备 private ArrayAdapter r; //新的蓝牙设备 private ArrayAdapter rrayAdapter; 其中Debugging部分,同样用于调试,这里定义了一个EXTRA_DEVICE_ADDRE SS,用于在通过Intent传递数据时的附加信息,即设备的地址,当扫描出来之后, 返回到BluetoothChat中的onActivityResult函数的REQUEST_CONNECT_DEVICE命 令,这是我们就需要通过_DEVICE_ADDRESS来取得该设备 的Mac地址,因此当我们扫描完成之后在反馈扫描结果时就需要绑定设备地址作为E XTRA_DEVICE_ADDRESS的附加值,这和我们上一篇介绍的并不矛盾。另外其他几个 变量则分别是本地蓝牙适配器、已经配对的蓝牙列表和扫描出来还没有配对的蓝牙设 备列表,稍后我们可以看到对他们的使用。 进入DeviceListActivity之后我们首先分析onCreate,首先通过如下代码对窗口 进行了设置: 1. // 设置窗口 requestWindowFeature(E_INDETERMI NATE_PROGRESS); setContentView(_list); setR esult(_CANCELED); 这里我们设置了窗口需要带一个进度条,当我们在扫描时就看有很容易的高速用 户扫描进度。具体布局则设置为device_也是我们文本第一段代码的内容,接 下来首先初始化扫描按钮,代码如下: 1. // 初始化扫描按钮 Button scanButton = (Button) findViewById(. button_scan); lickListener(new OnClickListener () { public void onClick(View v) { doDiscovery(); i bility(); } }); 首先取得按钮对象,然后为其设置一个事件监听,当事件触发时就通过doDisco very函数来执行扫描操作即可,具体扫描过程稍后分析。 然后需要初始化用来显示设备的列表和数据源,使用如下代码即可: 1. //初始化ArrayAdapter,一个是已经配对的设备,一个是新发现的设备 m PairedDevicesArrayAdapter = new ArrayAdapter _name); mNewDevicesArrayAdapter = new ArrayAdapter< String>(this, _name); // 检测并设置已配对的设备List View ListView pairedListView = (ListView) findViewById( d_devices); pter(mPairedDevicesArrayAdapt er); temClickListener(mDeviceClickListene r); // 检查并设置行发现的蓝牙设备ListView ListView newDevicesL istView = (ListView) findViewById(_devices); newDevices pter(mNewDevicesArrayAdapter); newDevicesListV temClickListener(mDeviceClickListener); 并分别对这些列表中的选项设置了监听mDeviceClickListener,用来处理,当 选择该选项时,就进行链接和配对操作。既然是扫描,我们就需要对扫描的结果进行 监控,这里我们构建了一个广播BroadcastReceiver来对扫描的结果进行处理,代码 如下: 1. // 当一个设备被发现时,需要注册一个广播 IntentFilter filter = new Int entFilter(_FOUND); erReceiver (mReceiver, filter); // 当显示检查完毕的时候,需要注册一个广播 fil ter = new IntentFilter(_DISCOVERY_FINISH ED); erReceiver(mReceiver, filter); 这里我们注册到广播mReceiver的IntentFilter主要包括了发现蓝牙设备(Bluet _FOUND)和扫描结束(_DISCOVERY _FINISHED),稍后我们分析如何在mReceiver中来处理这些事件。 最后我们需要取得本地蓝牙适配器和一些初始的蓝牙设备数据显示列表进行处 理,代码如下: 1. // 得到本地的蓝牙适配器 mBtAdapter = aul tAdapter(); // 得到一个已经匹配到本地适配器的BluetoothDevice类的 对象集合 Set ondedDevices(); // 如果有配对成功的设备则添加到ArrayAdapter i f (() > 0) { findViewById(_paired_devic es).setVisibility(E); for (BluetoothDevice device : pair edDevices) { (e () + "n" + ress()); } } else { //否则添加一 个没有被配对的字符串 String noDevices = getResources().getText(R. _paired).toString(); (noDevices); } 首先通过蓝牙适配器的getBondedDevices函数取得已经配对的蓝牙设备,并将 其添加到mPairedDevicesArrayAdapter数据源中,会显示到pairedListView列表视图 中,如果没有已经配对的蓝牙设备,则显示一个_paired字符串表示目 前没有配对成功的设备。 onDestroy函数中会制定销毁操作,主要包括蓝牙适配器和广播的注销操作,代 码如下: 1. @Override protected void onDestroy() { roy (); // 确保我们没有发现,检测设备 if (mBtAdapter != null) { Discovery(); } // 卸载所注册的广播 this.u nregisterReceiver(mReceiver); } 对于蓝牙适配器的取消方式则调用cancelDiscovery()函数即可,卸载mReceive r则需要调用unregisterReceiver即可。 做好初始化工作之后,下面我们开始分析扫描函数doDiscovery(),其扫描过程 的实现很就简单,代码如下: 1. /** * 请求能被发现的设备 */ private void doDiscovery () { if (D) Log.d(TAG, "doDiscovery()"); // 设置显示进度条 setProgressBarIndeterminateVisibility(true); // 设置title为扫描状 态 setTitle(ng); // 显示新设备的子标题 findVie wById(_new_devices).setVisibility(E); // 如果 已经在请求现实了,那么就先停止 if (overing()) { Discovery(); } // 请求从蓝牙适配器得到能够 被发现的设备 iscovery(); } 首先通过setProgressBarIndeterminateVisibility将进度条设置为显示状态,设置 标题title为ng字符串,表示正在扫描中,代码中所说的新设备的子 标题,其实就是上面我们所说的扫描到的没有经过配对的设备的title,对应于 tle_new_devices。扫描之前我们首先通过isDiscovering函数检测当前是否正在扫描, 如果正在扫描则调用cancelDiscovery函数来取消当前的扫描,最后调用startDiscov ery函数开始执行扫描操作。 现在已经开始扫描了,下面我们就需要对扫描过程进行监控和对扫描的结果进行 处理。即我们所定义的广播mReceiver,其实现如下所示。 1. //监听扫描蓝牙设备 private final BroadcastReceiver mReceiver = n ew BroadcastReceiver() { @Override public void onReceive(Co ntext context, Intent intent) { String action = ion (); 2. 3. 4. 5. 6. 7. 8. 9. // 当发现一个设备时 if (_(action)) { // 从Intent得到蓝牙设备对象 BluetoothDevice device = celableExtra(BluetoothDev _DEVICE); // 如果已经配对,则跳过,因为他已经在设备列表中了 if (dState() != _BONDE D) { //否则添加到设备列表 (e() + "n" + devic ress()); 10. } 11. // 当扫描完成之后改变Activity的title 12. } else if (_DISCOVERY_ ls(action)) { 13. //设置进度条不显示 14. setProgressBarIndeterminateVisibility(false); 15. //设置title 16. setTitle(_device); 17. //如果计数为0,则表示没有发现蓝牙 18. if (nt() == 0) { 19. String noDevices = getResources().getText(_found). toString(); 20. (noDevices); 21. } 22. } 23. } 24. }; 其中我们通过ion()可以取得一个动作,然后判断如果动作为Bluet _FOUND,则表示发现一个蓝牙设备,然后通过BluetoothDevice. EXTRA_DEVICE常量可以取得Intent中的蓝牙设备对象(BluetoothDevice),然后 通过条件"dState() != _BONDED"来判断设备 是否配对,如果没有配对则添加到行设备列表数据源mNewDevicesArrayAdapter中, 另外,当我们取得的动作为_DISCOVERY_FINISHED,则 表示扫描过程完毕,这时首先需要设置进度条不现实,并且设置窗口的标题为选择一 个设备(_device)。当然如果扫描完成之后没有发现新的设备,则添 加一个没有发现新的设备字符串(_found)到mNewDevicesArrayAdap ter中。 最后,扫描界面上还有一个按钮,其监听mDeviceClickListener的实现如下: 1. // ListViews中所有设备的点击事件监听 private OnItemClickListene r mDeviceClickListener = new OnItemClickListener() { public voi d onItemClick(AdapterView> av, View v, int arg2, long arg3) { // 取消检测扫描发现设备的过程,因为内非常耗费资源 Discovery(); // 得到mac地址 String info = ((TextView) v).getT ext().toString(); String address = ing(() - 1 7); // 创建一个包括Mac地址的Intent请求 Intent intent = new In tent(); ra(EXTRA_DEVICE_ADDRESS, address); / / 设置result并结束Activity setResult(_OK, inten t); finish(); } }; 当用户点击该按钮时,首先取消扫描进程,因为扫描过程是一个非常耗费资源的 过程,然后去的设备的mac地址,构建一个Intent 对象,通过附加数据EXTRA_DE VICE_ADDRESS将mac地址传递到BluetoothChat中,然后调用finish来结束该界面。 这时就会回到上一篇文章我们介绍的BluetoothChat中的onActivityResult函数中去 执行请求代码为REQUEST_CONNECT_DEVICE的片段,用来连接一个设备。 BluetoothChatService 对于设备的监听,连接管理都将由REQUEST_CONNECT_DEVICE来实现,其中 又包括三个主要部分,三个进程分贝是:请求连接的监听线程(AcceptThread)、连 接一个设备的进程(ConnectThread)、连接之后的管理进程(ConnectedThread)。 同样我们先熟悉一下该类的成员变量的作用,定义如下: 1. // Debugging private static final String TAG = "BluetoothChatSer vice"; private static final boolean D = true; //当创建socket服务 时的SDP名称 private static final String NAME = "BluetoothChat "; // 应用程序的唯一UUID private static final UUID MY_UUI D = ring("fa87c0d0-afac-11de-8a39-0800200c9a66"); // 本地蓝牙适配器 private final BluetoothAdapter mAdapter; / /Handler private final Handler mHandler; //请求链接的监听线 程 private AcceptThread mAcceptThread; //链接一个设备的线 程 private ConnectThread mConnectThread; //已经链接之后的管 理线程 private ConnectedThread mConnectedThread; //当前的 状态 private int mState; // 各种状态 public static final int S TATE_NONE = 0; public static final int STATE_LISTEN = 1; pu blic static final int STATE_CONNECTING = 2; public static final in t STATE_CONNECTED = 3; Debugging为调试相关,NAME 是当我们在创建一个socket监听服务时的一个 SDP名称,另外还包括一个状态变量mState,其值则分别是下面的"各种状态"部分, 另外还有一个本地蓝牙适配器和三个不同的进程对象,由此可见,本地蓝牙适配器的 确是任何蓝牙操作的基础对象,下面我们会分别介绍这些进程的实现。 首先是初始化操作,即构造函数,代码如下: 1. public BluetoothChatService(Context context, Handler handler) { //得到本地蓝牙适配器 mAdapter = aultAd apter(); //设置状态 mState = STATE_NONE; //设置Handle r mHandler = handler; } 取得本地蓝牙适配器、设置状态为STATE_NONE,设置传递进来的mHandler。 接下来需要控制当状态改变之后,我们需要通知UI界面也同时更改状态,下面是得 到状态和设置状态的实现部分,如下: 1. private synchronized void setState(int state) { if (D) Log.d(T AG, "setState() " + mState + " -> " + state); mState = state; // 状态更新之后UI Activity也需要更新 Message(Bl E_STATE_CHANGE, state, -1).sendToTarget(); } public synchronized int getState() { return mState; } 得到状态没有什么特别的,关键在于设置状态之后需要通过obtainMessage来发 送一个消息到Handler,通知UI界面也同时更新其状态,对应的Handler的实现则位 于BluetoothChat中的private final Handler mHandler = new Handler()部分,从 上面的代码中,我们可以看到关于状态更改的之后会发送一个 GE_STATE_CHANGE消息到UI线程中,下面我们看一下UI线程中如何处理这些消息 的,代码如下: 1. case MESSAGE_STATE_CHANGE: if(D) Log.i(TAG, "MESSAGE_ST ATE_CHANGE: " + 1); switch (1) { case Blue _CONNECTED: //设置状态为已经链接 m t(_connected_to); //添加设备名称 mTitl (mConnectedDeviceName); //清理聊天记录 mConversa (); break; case BluetoothChatService.S TATE_CONNECTING: //设置正在链接 t( le_connecting); break; case _LIST EN: case _NONE: //处于监听状态 或者没有准备状态,则显示没有链接 t(_not_c onnected); break; } break; 可以看出,当不同的状态在更改之后会进行不同的设置,但是大多数都是根据不 同的状态设置显示了不同的title,当已经链接(STATE_CONNECTED)之后,设置了标 题为链接的设备名,并同时还mConversationArrayAdapter进行了清除操作,即清除 聊天记录。 现在,初始化操作已经完成了,下面我们可以调用start函数来开启一个服务进 程了,也即是在BluetoothChat中的onResume函数中所调用的start操作,其具体 实现如下: 1. public synchronized void start() { if (D) Log.d(TAG, "start "); // 取消任何线程视图建立一个连接 if (mConnectThread != nul l) {(); mConnectThread = null;} // 取消任 何正在运行的链接 if (mConnectedThread != null) {mConnectedThre (); mConnectedThread = null;} // 启动AcceptThread线程 来监听BluetoothServerSocket if (mAcceptThread == null) { m AcceptThread = new AcceptThread(); (); } //设置状态为监听,,等待链接 2. 3. setState(STATE_LISTEN); } 操作过程很简单,首先取消另外两个进程,新建一个AcceptThread进程,并启 动AcceptThread进程,最后设置状态变为监听(STATE_LISTEN),这时UI界面的titl e也将更新为监听状态,即等待设备的连接。关于AcceptThread的具体实现如下所 示。 1. private class AcceptThread extends Thread { // 本地socket服 务 private final BluetoothServerSocket mmServerSocket; publi c AcceptThread() { BluetoothServerSocket tmp = null; // 创建 一个新的socket服务监听 try { tmp = UsingRfco mmWithServiceRecord(NAME, MY_UUID); } catch (IOExceptio n e) { Log.e(TAG, "listen() failed", e); } mmServerSocke t = tmp; } public void run() { if (D) Log.d(TAG, "BEGIN m AcceptThread" + this); setName("AcceptThread"); BluetoothS ocket socket = null; // 如果当前没有链接则一直监听socket服务 w hile (mState != STATE_CONNECTED) { try { //如果有请求链接, 则接受 //这是一个阻塞调用,将之返回链接成功和一个异常 socke t = (); } catch (IOException e) { Log. e(TAG, "accept() failed", e); break; } // 如果接受了一个链 接 if (socket != null) { synchronized ( s) { switch (mState) { case STATE_LISTEN: case STATE_C ONNECTING: // 如果状态为监听或者正在链接中,,则调用connected来 链接 connected(socket, oteDevice()); break; case STATE_NONE: case STATE_CONNECTED: // 如果为没有 准备或者已经链接,这终止该socket try { (); } cat ch (IOException e) { Log.e(TAG, "Could not close unwanted socke t", e); } break; } } } } if (D) Log.i(TAG, " END mAcceptThread"); } //关闭BluetoothServerSocket pub lic void cancel() { if (D) Log.d(TAG, "cancel " + this); try { (); } catch (IOException e) { Log.e(T AG, "close() of server failed", e); } } } 首先通过listenUsingRfcommWithServiceRecord创建一个socket服务,用来监 听设备的连接,当进程启动之后直到有设备连接时,这段时间都将通过accept来监 听和接收一个连接请求,如果连接无效则调用close来关闭即可,如果连接有效则调 用connected进入连接进程,进入连接进程之后会取消当前的监听进程,取消过程则 直接调用cancel通过()来关闭即可。下面我们分析连接函数c onnect的实现,如下: 1. public synchronized void connect(BluetoothDevice device) { i f (D) Log.d(TAG, "connect to: " + device); // 取消任何链接线程,视图 建立一个链接 if (mState == STATE_CONNECTING) { if (mConne ctThread != null) {(); mConnectThread = nul l;} } // 取消任何正在运行的线程 if (mConnectedThread != nu ll) {(); mConnectedThread = null;} // 启 动一个链接线程链接指定的设备 mConnectThread = new ConnectThrea d(device); (); setState(STATE_CONNEC TING); } 同样,首先关闭其他两个进程,然后新建一个ConnectThread进程,并启动, 通知UI界面状态更改为正在连接的状态(STATE_CONNECTING)。具体的连接进程 由ConnectThread来实现,如下: 1. private class ConnectThread extends Thread { //蓝牙Socket private final BluetoothSocket mmSocket; //蓝牙设备 private f inal BluetoothDevice mmDevice; public ConnectThread(BluetoothD evice device) { mmDevice = device; BluetoothSocket tmp = n ull; //得到一个给定的蓝牙设备的BluetoothSocket try { tm p = RfcommSocketToServiceRecord(MY_UUID); } cat ch (IOException e) { Log.e(TAG, "create() failed", e); } m mSocket = tmp; } public void run() { Log.i(TAG, "BEGI N mConnectThread"); setName("ConnectThread"); // 取消可见 状态,将会进行链接 Discovery(); // 创建一个Blu etoothSocket链接 try { //同样是一个阻塞调用,返回成功和异常 t(); } catch (IOException e) { //链接失 败 connectionFailed(); // 如果异常则关闭socket try { m (); } catch (IOException e2) { Log.e(TAG, "una ble to close() socket during connection failure", e2); } // 重新 启动监听服务状态 (); return; } // 完成则重置ConnectThread 2. 3. 4. 5. 6. 7. 8. 9. synchronized () { mConnectThread = null; } // 开启ConnectedThread(正在运行中...)线程 connected(mmSocket, mmDevice); } //取消链接线程ConnectThread 10. public void cancel() { 11. try { 12. (); 13. } catch (IOException e) { 14. Log.e(TAG, "close() of connect socket failed", e); 15. } 16. } 17. } 在创建该进程时,就已经知道当前需要被连接的蓝牙设备,然后通过createRfc ommSocketToServiceRecord可以构建一个蓝牙设备的BluetoothSocket对象,当进 入连接状态时,就可以调用cancelDiscovery来取消蓝牙的可见状态,然后通过调用 connect函数进行链接操作,如果出现异常则表示链接失败,则调用connectionFaile d函数通知UI进程更新界面的显示为链接失败状态,然后关闭BluetoothSocket,调 用start函数重新开启一个监听服务AcceptThread,对于链接失败的处理实现如下: 1. private void connectionFailed() { setState(STATE_LISTEN); // 发送链接失败的消息到UI界面 Message msg = Mes sage(E_TOAST); Bundle bundle = new Bun dle(); ing(, "Unable to connec t device"); a(bundle); ssage(ms g); } 首先更改状态为STATE_LISTEN,然后发送一个Message带UI界面,通知UI 更新,显示一个Toast告知用户,当BluetoothChat中的mHandler接收到Bluetooth 消息时,就会直接更新UI界面的显示,如果连接成功则将调用connect ed函数进入连接管理进程,其实现如下: 1. public synchronized void connected(BluetoothSocket socket, Bluet oothDevice device) { if (D) Log.d(TAG, "connected"); // 取消C onnectThread链接线程 if (mConnectThread != null) {mConnectThre (); mConnectThread = null;} // 取消所有正在链接的线程 if (mConnectedThread != null) {(); mCon nectedThread = null;} // 取消所有的监听线程,因为我们已经链接了一个 设备 if (mAcceptThread != null) {(); mAccep tThread = null;} // 启动ConnectedThread线程来管理链接和执行翻 译 mConnectedThread = new ConnectedThread(socket); mCon (); // 发送链接的设备名称到UI Activity界面 M essage msg = Message(E_DEV ICE_NAME); Bundle bundle = new Bundle(); ing (_NAME, e()); a(b undle); ssage(msg); //状态变为已经链接,即正 在运行中 setState(STATE_CONNECTED); } 首先,关闭所有的进程,构建一个ConnectedThread进程,并准备一个Messag e消息,就设备名称(_NAME)也发送到UI进程,因为UI进 程需要显示当前连接的设备名称,当UI进程收到E_DEVICE_ NAME消息时就会更新相应的UI界面,就是设置窗口的title,这里我们就不贴出代 码了,下面我们分析一下ConnectedThread的实现,代码如下: 1. private class ConnectedThread extends Thread { //BluetoothS ocket private final BluetoothSocket mmSocket; //输入输出流 private final InputStream mmInStream; private final OutputStre am mmOutStream; public ConnectedThread(BluetoothSocket sock et) { Log.d(TAG, "create ConnectedThread"); mmSocket = soc ket; InputStream tmpIn = null; OutputStream tmpOut = nul l; // 得到BluetoothSocket的输入输出流 try { tmpIn = socke utStream(); tmpOut = putStream(); } c atch (IOException e) { Log.e(TAG, "temp sockets not created ", e); } mmInStream = tmpIn; mmOutStream = tmpOu t; } public void run() { Log.i(TAG, "BEGIN mConnectedThr ead"); byte[] buffer = new byte[1024]; int bytes; // 监听 输入流 while (true) { try { // 从输入流中读取数据 byte s = (buffer); // 发送一个消息到UI线程进行更新 Message(E_READ, bytes, - 1, buffer) .sendToTarget(); } catch (IOException e) { //出 现异常,则链接丢失 Log.e(TAG, "disconnected", e); connectionLo st(); break; } } } /** * 写入药发送的消息 * @param buffer The bytes to write */ public void write(byte [] buffer) { try { (buffer); // 将写的消 息同时传递给UI界面 Message( AGE_WRITE, -1, -1, buffer) .sendToTarget(); } catch (IOExcep tion e) { Log.e(TAG, "Exception during write", e); } } //取消ConnectedThread链接管理线程 public void cancel() { tr y { (); } catch (IOException e) { Log.e(TA G, "close() of connect socket failed", e); 2. 3. 4. } } } 连接之后的主要操作就是发送和接收聊天消息了,因为需要通过其输入(出)流 来操作具体信息,进程会一直从输入流中读取信息,并通过obtainMessage函数将读 取的信息以E_READ命令发送到UI进程,到UI进程收到是, 就需要将其显示到消息列表之中,同时对于发送消息,需要实行写操作write,其操 作就是将要发送的消息写入到输出流mmOutStream中,并且以 SAGE_WRITE命令的方式发送到UI进程中,进行同步更新,如果在读取消息时失败 或者产生了异常,则表示连接丢失,这是就调用connectionLost函数来处理连接丢失, 代码如下: 1. private void connectionLost() { setState(STATE_LISTEN); / / 发送失败消息到UI界面 Message msg = Message(B E_TOAST); Bundle bundle = new Bundle (); ing(, "Device connection wa s lost"); a(bundle); ssage(ms g); } 操作同样简单,首先改变状态为STATE_LISTEN,然后E _TOAST命令发送一个消息Message到UI进程,通知UI进程更新显示画面即可。对 于写操作,是调用了来实现,其实现代码如下: 1. //写入自己要发送出来的消息 public void write(byte[] out) { // C reate temporary object ConnectedThread r; // Synchronize a c opy of the ConnectedThread synchronized (this) { //判断是否 处于已经链接状态 if (mState != STATE_CONNECTED) return; r = mConnectedThread; } // 执行写 (out); } 其实就是检测,当前的状态是否处于已经链接状态STATE_CONNECTED,然后 调用ConnectedThread 进程中的write操作,来完成消息的发送。因此这时我们可以 回过头来看BluetoothChat中的sendMessage的实现了,如下所示: 1. private void sendMessage(String message) { // 检查是否处于连接 状态 if (te() != _ CONNECTED) { xt(this, _connected, Toa _SHORT).show(); return; } // 如果输入的消息不 为空才发送,否则不发送 if (() > 0) { // Get the m essage bytes and tell the BluetoothChatService to write byte[] sen d = es(); (send); // Rese t out string buffer to zero and clear the edit text field mOutString gth(0); t(mOutStringBuffer); } } 同样首先检测了当前的状态是否为已经连接状态,然后对要发送的消息是否为n ull进行了判断,如果为空则不需要发送,否则调用(即上面所说 的ConnectedThread 中的wirte操作)来发送消息。然后一个小的细节就是设置编辑 框的内容为null即可。最后我们可以看一下在BluetoothChat中如何处理这些接收到 的消息,主要位于mHandler中的handleMessage函数中,对于状态改变的消息我们 已经分析过了,下面是其他几个消息的处理: 1. case MESSAGE_WRITE: byte[] writeBuf = (byte[]) ; // 将自己写入的消息也显示到会话列表中 String writeMessage = ne w String(writeBuf); ("Me: " + writ eMessage); break; case MESSAGE_READ: byte[] readBu f = (byte[]) ; // 取得内容并添加到聊天对话列表中 String r eadMessage = new String(readBuf, 0, 1); mConversationA (mConnectedDeviceName+": " + readMessage); b reak; case MESSAGE_DEVICE_NAME: // 保存链接的设备名称,并 显示一个toast提示 mConnectedDeviceName = a().getSt ring(DEVICE_NAME); xt(getApplicationContext(), "C onnected to " + mConnectedDeviceName, _SHORT). show(); break; case MESSAGE_TOAST: //处理链接(发送)失 败的消息 xt(getApplicationContext(), a(). getString(TOAST), _SHORT).show(); break; 分别是读取消息和写消息(发送消息),对于一些信息提示消息MESSAGE_T OAST,则通过Toast显示出来即可。如果消息是设备名称MESSAGE_DEVICE_NAME, 则提示用户当前连接的设备的名称。对于写消息(MESSAGE_WRITE)和读消息(ME SSAGE_READ)我们就不重复了,大家看看代码都已经加入了详细的注释了。 最后当我们在需要停止这些进程时就看有直接调用stop即可,具体实现如下: 1. //停止所有的线程 public synchronized void stop() { if (D) Log. d(TAG, "stop"); if (mConnectThread != null) { ncel(); mConnectThread = null;} if (mConnectedThread != nul l) {(); mConnectedThread = null;} if (m AcceptThread != null) {(); mAcceptThread = nu ll;} //状态设置为准备状态 setState(STATE_NONE); } 分别检测三个进程是否为null,然后调用各自的cancel函数来取消进程,最后 不要忘记将状态恢复到STATE_NONE即可。