话说上一回,刘关张三人在孙权的公司各种做项目。三个月的时间“唰”的一下过去了。在刘关张三人的努力下,项目终于突飞猛进,获得了异常的成功,遭到了孙权猛烈的赞赏。
孙权:“三位在我这里辛苦了,现在项目结了,我也没什么好送给你们的,送个宝马给你们吧!”
关羽:“哦,几系的?”
孙权:“开玩笑,限量版的,中文名叫赤兔,英文名叫Naked Rabbit!”
关羽:“这么贵重的礼物怎么好意思呢?”
刘备:“怎么不好意思,你不要我要,上牌照了吗?百公里耗多少粮草?做一次保养多少钱?”
孙权:“三位千万不要客气,你们在我这里辛苦了这么久,这点小意思不算什么!赤兔就跟楼下停着呢,钥匙在这里。”
刘备一把夺过钥匙,关羽和张飞只好跟下楼去。等三人走后,鲁肃问孙权:“赤兔如此贵重,主公你怎么这么大方就送出去了?”
孙权擦了擦汗:“赤兔虽然贵,但我算了算……没他们回去的高铁票贵啊,至少把票钱省下了。”
鲁肃竖起大拇指:“主公高明!”
要走,仍是鲁肃送出门。四人再三拜别,依依不舍,最后鲁肃深情地拉住刘备的手,说:“送君千里终有一别,有句话不知当问不当问?”
刘备:“有什么话不用顾忌,尽管讲来!”
鲁肃:“你们确定不需要买点发票吗?”
三人上路后,发现赤兔走得好慢。张飞一个劲儿的发牢骚。关羽说:“你也别怪赤兔马力不行,他一个驼咱三个,咱仨加起来少说六百斤,它能起来已经很不容易啦!”
张飞:“我只是觉得无聊啊,这要猴年马月才能回去呀。”
关羽:“无聊就玩玩手机呗。不好!现在新交规对驾车打手机加大了处罚力度,要扣分的!”
刘备:“二弟呀,这赤兔带蓝牙不?咱连上蓝牙不就不用拿着手机打了嘛。”
关羽:“还是大哥想得周到,吁~~~~来,好兔兔,张开嘴让我看看牙口有蓝的不?”
1.1. 蓝牙介绍
如今的移动应用基本上都很难脱离网络单独存在,网络编程对开发者来说是必不可少的一项能力。前面介绍了Socket、HTTP等网络通信方式,这一回将介绍如何使用蓝牙技术进行网络应用的开发。蓝牙是一种短距离的通信技术,利用它可以去掉设备的连接线,真正实现无线连接。
1.1.1.什么是蓝牙
蓝牙是爱立信公司于1994年起研发的一种支持设备短距离通信的无线电技术。它能在手机、PDA、无线耳机、笔记本电脑等设备之间进行无线信息交换。采用的是分散式网络结构及其跳频和短包技术,支持点对点或者是点对多点的通信,一般工作在2.4GHz ISM频段,支持的传输速度为1Mbit/s,采用时分双工传输方案实现全双工传输。
蓝牙技术的应用主要有以下几点优势:(1)全球可用,使用蓝牙不需要支付任何的费用;(2)支持蓝牙的设备多,例如:手机、打印机、汽车等;(3)蓝牙易于使用,只要设备支持蓝牙技术,那么设备都可以使用蓝牙;(4)全球统一规范,只要符合蓝牙标准的设备,都可以轻易的进行连接。
张飞:大哥,蓝牙是个什么东东,咋各种专业名词,搞得我小飞飞很慌张嘛! 刘备:小飞飞,蓝牙简单来说就是一种支持设备短距离通信的无线电技术。 张飞:哦,早说嘛,就是一种通信技术嘛,和HTTP、TCP一样,可以进行数据传输嘛!了解了! |
1.1.2.蓝牙的应用
蓝牙技术是一项即时的通信技术。它不要求固定的基础设施且易于安装和设置,不需要电缆即可实现连接。这使得许多设备都已经使用上蓝牙技术。蓝牙技术的应用也越来越广泛。日常生活中蓝牙技术最广泛应用的就是在支持蓝牙的手机设备上,如手机蓝牙耳机、车载免提等。利用蓝牙技术,可以从手机向计算机、打印机发送文件;同步手机上的联系人、日历;连接手机至扬声器召开免提电话会议;收听手机电话和手机音乐等。
蓝牙技术是一种能够真正实现无线娱乐的技术,使用蓝牙技术的主要步骤为:(1)为应用程序添加蓝牙通信权限和蓝牙开关权限;(2)搜索附件可用的蓝牙设备;(3)建立连接;(4)进行通信。
1.2. Android蓝牙接口
蓝牙几乎是现在每部手机标准配备的功能。Android SDK从2.0版本开始支持蓝牙开发,但模拟器目前还不支持。Android关于蓝牙开发的类都在android.bluetooth包下,主要有:BluetoothAdapter 本地蓝牙适配器、BluetoothClass蓝牙类、BluetoothClass.Device蓝牙设备、BluetoothClass.Device.Major蓝牙设备管理、BluetoothClass.Service蓝牙服务、BluetoothDevice远程蓝牙设备、BluetoothSocket 蓝牙Socket客户端和BluetoothServerSocket 蓝牙Socket服务器端。下面将详细地介绍其中常用的四个类:BluetoothAdapter、BluetoothDevice、BluetoothServerSocket和BluetoothSocket。
1.2.1.BluetoothAdapter
BluetoothAdapter代表本地的蓝牙适配器设备。BluetoothAdapter类让用户能执行基本的蓝牙任务,例如:初始化设备的搜索、查询可匹配的设备集、使用一个已知的MAC地址来初始化一个BluetoothDevice类、创建一个 BluetoothServerSocket类用于监听其它设备对本机的连接请求等。可以调用BluetoothAdapter.getDefaultAdapter()静态方法来获取本地BluetoothAdapter类,如果调用方法返回null,则表示本地不支持蓝牙。当获取BluetoothAdapter后,可以获得一系列的BluetoothDevice对象。下面详细地介绍BluetoothAdapter类常用的常量:
表20-1 BluetoothAdapter类常用的常量
类型 | 常量名 | 说明 |
String | ACTION_DISCOVERY_FINISHED | 广播活动,本地蓝牙适配器已经完成设备的搜寻过程。 |
String | ACTION_DISCOVERY_STARTED | 广播活动,本地蓝牙适配器已经开始对远程设备的搜寻过程。 |
String | ACTION_LOCAL_NAME_CHANGED | 广播活动,本地蓝牙适配器已经更改了它的蓝牙名称。 |
String | ACTION_REQUEST_ENABLE | Activity活动,显示一个允许用户打开蓝牙模块的系统活动。 |
String | ACTION_REQUEST_DISCOVERABLE | Activity活动,显示一个请求被搜寻模式的系统活动。如果蓝牙模块当前未打开,该活动也将请求用户打开蓝牙模块。 |
String | ACTION_SCAN_MODE_CHANGED | 广播活动,指明蓝牙扫描模块或者本地适配器已经发生变化。 |
String | ACTION_STATE_CHANGED | 广播活动,本地的蓝牙适配器的状态已经改变。例如:蓝牙模块已经被打开或者关闭。 |
int | ERROR | 标记发生错误时,返回的值,确保和该类中的任意其它整数常量不相等。 |
int | STATE_OFF | 指明本地蓝牙适配器模块已经关闭。 |
int | STATE_ON | 指明本地蓝牙适配器模块已经打开,并且准备被使用。 |
int | STATE_TURNING_OFF | 指明本地蓝牙适配器模块正在关闭。本地客户端可以立刻尝试友好地断开任意外部连接。 |
int | STATE_TURNING_ON | 指明本地蓝牙适配器模块正在打开。 |
上表20-1中主要列举了BluetoothAdapter用于活动和表明当前状态的常量。那么如何使用这些常量?如下的代码利用ACTION_REQUEST_ENABLE常量显示了请求使用蓝牙的操作:
Intent intent=newIntent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent,REQUEST_ENABLE_BT);
值得注意的是在使用ACTION_DISCOVERY_STARTED时,通常牵涉到一个大概需要12秒的查询扫描设备的过程,当查找正在进行的时候,用户不能尝试对新的远程蓝牙设备进行连接,同时存在的连接将获得有限制的带宽和高等待时间。因此一旦找到需要的设备之后,在发起连接请求之前,需要确保程序调用cancelDiscovery()方法停止扫描。如果已经连接上一个设备,启动扫描会减少通信带宽。BluetoothAdapter类的常用方法如表20-2所示:
表20-2 BluetoothAdapter类的常用方法
方法 | 说明 |
public static synchronized BluetoothAdapter getDefaultAdapter () | 获取对默认本地蓝牙适配器的的操作权限。 |
public BluetoothDevice getRemoteDevice (String address) | 为给予的蓝牙硬件地址获取一个BluetoothDevice对象。 |
public boolean isEnabled () | 如果蓝牙正处于打开状态并可用,则返回真值。 |
public boolean disable () | 这个方法友好地停止所有的蓝牙连接,停止蓝牙系统服务,以及对所有基础蓝牙硬件进行断电。 |
public boolean enable () | 打开本地蓝牙适配器。 |
public boolean startDiscovery () | 开始对远程设备进行查找的进程。 |
public boolean isDiscovering () | 如果当前蓝牙适配器正处于设备发现查找进程中,则返回真值。 |
public boolean cancelDiscovery () | 取消当前的设备发现查找进程。 |
public String getAddress () | 返回本地蓝牙适配器的硬件地址。 |
public Set<BluetoothDevice> getBondedDevices () | 返回已经匹配到本地适配器的BluetoothDevice类的对象集合。如果蓝牙状态不是STATE_ON,将返回false。 |
public int getState () | 获取本地蓝牙适配器的当前状态。 |
public int getScanMode () | 获取本地蓝牙适配器的当前蓝牙扫描模式 |
public static boolean checkBluetoothAddress (String address) | 验证蓝牙地址,字母必须为大写才有效。address表示字符串形式的蓝牙地址。地址正确则返回true,否则返回false。 |
public boolean setName (String name) | 设置蓝牙或者本地蓝牙适配器的昵称。 |
public String getName () | 获取本地蓝牙适配器的蓝牙名称。 |
一般如果没有用户的直接同意,蓝牙状态的改变将被禁止。需要注意的是当调用enable()和disable()方法时,系统不会弹出一个对话框来让用户确认是否真的要改变蓝牙当前的状态,而是直接打开蓝牙设备。当采用BluetoothAdapter.ACTION_REQUEST_ENABLE打开蓝牙时,Android系统会弹出一个提示框,提示用户是否开启蓝牙设备。
1.2.2.BluetoothDevice
BluetoothDevice类是一个远程蓝牙设备包装器类。该类可以通过一个 BlueSocket去请求一个与远程设备的连接,查询其名称、地址和连接状态等信息。BluetoothDevice类的对象是不可改变的。为了获得BluetoothDevice类,可以使用BluetoothAdapter.getRemoteDevice(String)方法创建一个指定MAC地址的设备或者通过BluetoothAdapter.getBondedDevices()方法得到有联系的设备集合。
BluetoothDevice类的常用方法如表20-3所示:
表20-3 BluetoothDevice类的常用方法
方法 | 说明 |
public String getAddress () | 返回该蓝牙设备的硬件地址。 |
public int getBondState () | 获取远程设备的连接状态。 |
public boolean equals (Object o) | 比较带有特定目标的常量。 |
public BluetoothClass getBluetoothClass() | 获取远程设备的蓝牙类。 |
public BluetoothSocket createRfcommSocketToServiceRecord (UUID uuid) | 根据UUID创建并返回一个BluetoothSocket。 |
1.2.3.BluetoothServerSocket
BlueboothServerSocket类代表服务器端打开服务连接并监听可能到来的连接请求。蓝牙端口监听接口和TCP端口类似,为了连接两个蓝牙设备必须有一个设备作为服务器打开一个服务套接字。当远端设备发起连接请求时,并且连接到来时,BlueboothServerSocket类将会返回一个BluetoothSocket,用于管理该连接。
BlueboothServerSocket类有三个方法:
l public BluetoothSocket accept():阻塞直到一个连接已经建立。
l public BluetoothSocket accept(inttimeout):阻塞直到超时时间内的连接建立。
l public void close():关闭端口,并释放所有相关的资源。
值得注意的是,当其他线程的调用close()方法时,会使系统马上抛出一个IO异常并关闭BluetoothServerSocket,但不会关闭接受自accept()的任何BluetoothSocket。
1.2.4.BluetoothSocket
在应用程序中,想建立两个蓝牙设备之间的连接,必须实现客户端和服务器端的代码。当客户端和服务器端都拥有一个蓝牙套接字并在同一RFECOMM信道上时,可以认为它们之间已经连接上了。在Android中提供了BluetoothServerSocket类用于处理服务器端的信息,而在客户端,使用BluetoothSocket类去初始化一个外部连接和管理该连接。
一般使用BluetoothDevice.createRfcommSocketToServiceRecord()方法创建一个BluetoothSocket去连接一个已知设备。当该端口连接成功后,无论初始化为客户端,或者被接受作为服务器端,都通过getInputStream()和getOutputStream()来打开IO流,从而获得各自的InputStream和OutputStream对象。特别的,close()方法总会马上放弃外界操作并关闭服务器端口。BluetoothSocket常用的方法如下表20-4所示:
表20-4 BluetoothSocket常用的方法
方法 | 说明 |
public void close () | 马上关闭该端口并且释放所有相关的资源。其它线程的该端口中引起阻塞,从而使系统马上抛出一个IO异常。 |
public void connect () | 尝试连接到远程设备。如果该方法没有返回异常值,则该端口现在已经建立。 |
public InputStream getInputStream () | 通过连接的端口获得输入数据流。 |
public OutputStream getOutputStream () | 通过连接的端口获得输出数据流。 |
public BluetoothDevice getRemoteDevice() | 获得该端口正在连接或者已经连接的远程设备。 |
1.3. 蓝牙通信实例
本节通过详细的讲解Android SDK自带的蓝牙聊天程序BluetoothChat例子介绍如何使用蓝牙,源代码在“\android-sdk-windows\samples\android-8\BluetoothChat”目录下。测试蓝牙程序需要至少2台真机。运行程序,结果如图20-1所示:
图20-1 蓝牙通信聊天
Google 提供的关于Bluetooth开发的例程BluetoothChat,主程序文件共有三个: BluetoothChat.java、BluetoothChatService.java和DeviceListActivity.java。下面依次讲解每个类:
BluetoothChat.java代码清单20-3-0:
/**
* @author张飞:大哥!跟你当这么久的兄弟,你一直都很关心我,我却时常给你添麻烦,真不知该怎么报答你...所以...下辈子作牛作马....我一定会拔草给你吃的...
刘备:好好编代码!
*/
public class BluetoothChat extends Activity {
// 设置从BluetoothChatService Handler返回的Message类型
// 聊天状态发送改变时
public static final intMESSAGE_STATE_CHANGE = 1;
// 当前的读消息状态
public static final intMESSAGE_READ = 2;
// 当前是写消息状态
public static final intMESSAGE_WRITE = 3;
// 显示连接设备的名字
public static final intMESSAGE_DEVICE_NAME = 4;
// 异常操作
public static final intMESSAGE_TOAST = 5;
// 设置从BluetoothChatService Handler获取信息的Key值
public static final StringDEVICE_NAME = "device_name";
public static final StringTOAST = "toast";
// 设置回调函数的编码
private static final intREQUEST_CONNECT_DEVICE = 1;
private static final intREQUEST_ENABLE_BT = 2;
// Views组件
private TextView mTitle;
private ListViewmConversationView;
private EditTextmOutEditText;
private ButtonmSendButton;
// 连接设备的名字
private StringmConnectedDeviceName = null;
// 聊天记录数组
privateArrayAdapter<String> mConversationArrayAdapter;
// 输出信息的StringBuffer
private StringBuffer mOutStringBuffer;
// 本地蓝牙适配器
private BluetoothAdaptermBluetoothAdapter = null;
// 蓝牙具体操作类
privateBluetoothChatService mChatService = null;
@Override
public voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 自定义标题,标题是一个两个Textview
requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
setContentView(R.layout.main);
getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,R.layout.custom_title);
// title_left_text用于应用程序名字,title_right_text用于显示当前蓝牙连接状态
mTitle =(TextView)findViewById(R.id.title_left_text);
mTitle.setText(R.string.app_name);
mTitle =(TextView)findViewById(R.id.title_right_text);
// 获取本地蓝牙适配器
mBluetoothAdapter =BluetoothAdapter.getDefaultAdapter();
// 如果获取的本地蓝牙适配器为空,表示设备不支持蓝牙,结束应用
if (mBluetoothAdapter== null) {
Toast.makeText(this, "蓝牙设备不可用", Toast.LENGTH_LONG).show();
finish();
return;
}
}
@Override
public void onStart() {
super.onStart();
// 如果蓝牙当时出于关闭状态,则利用intent提示用户打开蓝牙
if(!mBluetoothAdapter.isEnabled()) {
IntentenableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
} else {
// 如果蓝牙连接类为初始化,则初始化蓝牙连接类
if (mChatService== null)
setupChat();
}
}
@Override
public synchronized voidonResume() {
super.onResume();
if (mChatService !=null) {
// 如果是从后台切换到前台当前状态是STATE_LISTEN,无需再次启动,所以需要
// 在onResume()方法里进行状态判断,当是STATE_NONE时,需要启动
if(mChatService.getState() == BluetoothChatService.STATE_NONE) {
mChatService.start();
}
}
}
private void setupChat() {
// 显示谈话的列表控件初始化
mConversationArrayAdapter = new ArrayAdapter<String>(this,R.layout.message);
mConversationView =(ListView)findViewById(R.id.in);
mConversationView.setAdapter(mConversationArrayAdapter);
// 编辑框初始化,设置编辑监听
mOutEditText =(EditText)findViewById(R.id.edit_text_out);
mOutEditText.setOnEditorActionListener(mWriteListener);
// 发送按钮初始化,增加点击监听器
mSendButton =(Button)findViewById(R.id.button_send);
mSendButton.setOnClickListener(new OnClickListener() {
public voidonClick(View v) {
// 从编辑框中获取发送信息,发送消息
TextView view= (TextView)findViewById(R.id.edit_text_out);
String message= view.getText().toString();
sendMessage(message);
}
});
/*创建 BluetoothChatService 对象,该对象在整个应用过程中存在,并执行蓝牙连接建*立、消息发送接受等实际的行为。
*/
mChatService = newBluetoothChatService(this, mHandler);
// 初始化StringBuffer输出消息
mOutStringBuffer = newStringBuffer("");
}
@Override
public synchronized voidonPause() {
super.onPause();
}
@Override
public void onStop() {
super.onStop();
}
@Override
public void onDestroy() {
super.onDestroy();
// UI关闭时,蓝牙服务应也停止
if (mChatService !=null)
mChatService.stop();
}
/**
* @author张飞蓝牙不可见,怎么连接蓝牙,看我小飞飞的神功,使蓝牙可见
*/
private voidensureDiscoverable() {
if(mBluetoothAdapter.getScanMode() !=BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
// 如果蓝牙状态为不可被发现则请求开启为可被发现,可见时间为300秒
IntentdiscoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,300);
startActivity(discoverableIntent);
}
}
/**
* @author张飞唛哩唛哩轰,发送消息!
*/
private voidsendMessage(String message) {
// 在发送前检查当前状态是不是在远程连接一个设备,不是则退出
if(mChatService.getState() != BluetoothChatService.STATE_CONNECTED) {
Toast.makeText(this, R.string.not_connected, Toast.LENGTH_SHORT).show();
return;
}
// 检查发送的字符串是否有内容
if (message.length()> 0) {
// 转换字符串为bytes,告诉BluetoothChatService发送消息
byte[] send =message.getBytes();
mChatService.write(send);
mOutStringBuffer.setLength(0);
// 清除编辑框的内容
mOutEditText.setText(mOutStringBuffer);
}
}
// 编辑框编辑事件监听
private TextView.OnEditorActionListener mWriteListener = newTextView.OnEditorActionListener(){
public booleanonEditorAction(TextView view, int actionId, KeyEvent event) {
// 点击回车键后放开时发送消息
if (actionId ==EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_UP) {
String message= view.getText().toString();
sendMessage(message);
}
return true;
}
};
// 更新标题栏右边状态和读写状态的Handler
private final HandlermHandler = new Handler() {
@Override
public voidhandleMessage(Message msg) {
switch (msg.what){
// 聊天状态发生改变
caseMESSAGE_STATE_CHANGE:
switch(msg.arg1) {
// 连接成功,开始对话
caseBluetoothChatService.STATE_CONNECTED:
mTitle.setText(R.string.title_connected_to);
mTitle.append(mConnectedDeviceName);
mConversationArrayAdapter.clear();
break;
// 正在连接
caseBluetoothChatService.STATE_CONNECTING:
mTitle.setText(R.string.title_connecting);
break;
// 监听新的连接和什么也不做
caseBluetoothChatService.STATE_LISTEN:
caseBluetoothChatService.STATE_NONE:
mTitle.setText(R.string.title_not_connected);
break;
}
break;
// 状态为发送给对方的消息,显示发送的消息
caseMESSAGE_WRITE:
byte[]writeBuf = (byte[])msg.obj;
StringwriteMessage = new String(writeBuf);
mConversationArrayAdapter.add("Me: " + writeMessage);
break;
// 状态为获取对方的消息,显示获取的消息
caseMESSAGE_READ:
byte[]readBuf = (byte[])msg.obj;
StringreadMessage = new String(readBuf, 0, msg.arg1);
mConversationArrayAdapter.add(mConnectedDeviceName + ": " + readMessage);
break;
// 显示连接设备的名字
caseMESSAGE_DEVICE_NAME:
mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);
Toast.makeText(getApplicationContext(), "Connected to " +mConnectedDeviceName,
Toast.LENGTH_SHORT).show();
break;
caseMESSAGE_TOAST:
Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST),
Toast.LENGTH_SHORT).show();
break;
}
}
};
/**
* @张飞:幸亏我Activity学的好,知道这是回调Activity的方法,获得DeviceListActivity的返回结果
*/
public voidonActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
caseREQUEST_CONNECT_DEVICE:
// 当DeviceListActivity 返回一个设备需要连接时
if (resultCode == Activity.RESULT_OK) {
// 获得设备的物理地址
Stringaddress = data.getExtras().getString(
DeviceListActivity.EXTRA_DEVICE_ADDRESS);
// 根据物理地址获得设备
BluetoothDevice device =mBluetoothAdapter.getRemoteDevice(address);
// 建立蓝牙连接
mChatService.connect(device);
}
break;
caseREQUEST_ENABLE_BT:
// 请求为确保蓝牙可用
if (resultCode== Activity.RESULT_OK) {
// 当蓝牙可用,启动一个蓝牙会话连接
setupChat();
} else {
/*当用户不打开蓝牙设备或者发生错误情况时,显示蓝牙设备不可用,退*出。
*/
Toast.makeText(this, R.string.bt_not_enabled_leaving,Toast.LENGTH_SHORT)
.show();
finish();
}
}
}
/**
* @张飞:创建菜单都不知道,侮辱我小飞飞的智商
*/
@Override
public booleanonCreateOptionsMenu(Menu menu) {
/*从Menu资源中初始化Menu 返回true则弹出默认menu,false则不弹出Menu,须自定义
*/
MenuInflater inflater= getMenuInflater();
inflater.inflate(R.menu.option_menu, menu);
return true;
}
@Override
public booleanonOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
// 打开DeviceListActivity,用于扫描周围的设备和连接已经扫描到的设备
case R.id.scan:
IntentserverIntent = new Intent(this, DeviceListActivity.class);
startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
return true;
caseR.id.discoverable:
// 确保设备蓝牙打开,同时对其他蓝牙设备保持可见
ensureDiscoverable();
return true;
}
return false;
}
}
BluetoothChat.java是例子的主Activity。BluetoothChat.java的onCreate()方法获得本地BluetoothAdapter 设备,检查是否支持;onStart()方法检查是否启用蓝牙,并请求启用,然后执行 setupChat()方法。setupChat()方法先对界面中的控件进行初始化增加点击监听器等,然后创建 BluetoothChatService对象,该对象在整个应用过程中存在,并执行蓝牙连接、消息发送接收等行为。
DeviceListActivity.java代码清单20-3-0:
/**
* @author张飞:这是一个对话框形式的Activity,用于列表显示已经配对的蓝牙设备和通过搜索找到附件的蓝牙设备列表。当用户选中其中的一个设备,
*设备的物理地址将返回到父Activity中,用于建立连接。
*/
public class DeviceListActivity extends Activity {
// 获得设备的物理地址的key值
public static StringEXTRA_DEVICE_ADDRESS = "device_address";
// 本地蓝牙适配器
private BluetoothAdaptermBtAdapter;
// 已配对设备的数组适配器
privateArrayAdapter<String> mPairedDevicesArrayAdapter;
// 新发现设备的数组适配器
private ArrayAdapter<String>mNewDevicesArrayAdapter;
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.device_list);
// 设置回调方法的Activity结果
setResult(Activity.RESULT_CANCELED);
// 初始化扫描设备的按钮
Button scanButton =(Button)findViewById(R.id.button_scan);
scanButton.setOnClickListener(new OnClickListener() {
public voidonClick(View v) {
doDiscovery();
v.setVisibility(View.GONE);
}
});
// 初始化已配对和新发现设备的数据适配器
mPairedDevicesArrayAdapter = new ArrayAdapter<String>(this,R.layout.device_name);
mNewDevicesArrayAdapter = new ArrayAdapter<String>(this,R.layout.device_name);
// 初始化已配对列表和监听
ListViewpairedListView = (ListView)findViewById(R.id.paired_devices);
pairedListView.setAdapter(mPairedDevicesArrayAdapter);
pairedListView.setOnItemClickListener(mDeviceClickListener);
// 初始化新发现设备列表和监听
ListViewnewDevicesListView = (ListView)findViewById(R.id.new_devices);
newDevicesListView.setAdapter(mNewDevicesArrayAdapter);
newDevicesListView.setOnItemClickListener(mDeviceClickListener);
// 注册当一个远程设备被发现,发送广播
IntentFilter filter =new IntentFilter(BluetoothDevice.ACTION_FOUND);
this.registerReceiver(mReceiver, filter);
// 注册当本地蓝牙适配器扫描完成,发送广播
filter = newIntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
this.registerReceiver(mReceiver, filter);
// 获得本地蓝牙适配器
mBtAdapter =BluetoothAdapter.getDefaultAdapter();
// 获得当前已经适配的设备
Set<BluetoothDevice> pairedDevices =mBtAdapter.getBondedDevices();
// 将当前已适配的设备信息加入到数据适配器中
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);
}
}
@Override
protected void onDestroy(){
super.onDestroy();
// 确保扫描和Activity一起结束
if (mBtAdapter !=null) {
mBtAdapter.cancelDiscovery();
}
// 取消广播
this.unregisterReceiver(mReceiver);
}
/**
* @author张飞利用本地蓝牙适配器开始扫描设备
*/
private void doDiscovery(){
// 开启标题圆形进度条
setProgressBarIndeterminateVisibility(true);
setTitle(R.string.scanning);
findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE);
// 如果正在扫描,那么结束当前扫描
if(mBtAdapter.isDiscovering()) {
mBtAdapter.cancelDiscovery();
}
// 开启扫描
mBtAdapter.startDiscovery();
}
privateOnItemClickListener mDeviceClickListener = new OnItemClickListener() {
public voidonItemClick(AdapterView<?> av, View v, int arg2, long arg3) {
// 当选择一个设备时,结束当前扫描
mBtAdapter.cancelDiscovery();
// 获取设备的物理地址
String info =((TextView)v).getText().toString();
String address =info.substring(info.length() - 17);
// 创建返回父Activity的Intent,包含物理地址
Intent intent =new Intent();
intent.putExtra(EXTRA_DEVICE_ADDRESS,address);
// Set result andfinish this Activity
setResult(Activity.RESULT_OK, intent);
finish();
}
};
// BroadcastReceiver用于监听发现设备和扫描完成
private finalBroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public voidonReceive(Context context, Intent intent) {
String action =intent.getAction();
// 当发现一个设备
if (BluetoothDevice.ACTION_FOUND.equals(action)){
// 获取设备
BluetoothDevice device =intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
/*如果设备已经存在,则不操作,如果不存在,则加入到新发现设备数据适配器中
*/
if(device.getBondState() != BluetoothDevice.BOND_BONDED) {
mNewDevicesArrayAdapter.add(device.getName() + "\n" +device.getAddress());
}
// 当扫描完成时,改变标题中加载的状态
} else if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
setProgressBarIndeterminateVisibility(false);
setTitle(R.string.select_device);
if(mNewDevicesArrayAdapter.getCount() == 0) {
StringnoDevices = getResources().getText(R.string.none_found).toString();
mNewDevicesArrayAdapter.add(noDevices);
}
}
}
};
}
DeviceListActivity是一个对话框的Activity,作用是得到系统默认蓝牙设备的已配对设备列表,以及搜索出的未配对的新设备的列表,然后提供点击后发出连接设备请求的功能。
BluetoothChatService.java代码清单20-3:
/**
* @author张飞:这个类包含蓝牙连接的所有操作启动三个线程,一个线程用于监听连接消息,*一个用于连接设备,一个用于数据交换。
*/
public class BluetoothChatService {
private static final String TAG = "BluetoothChatService";
// 当创建一个服务器socket时,为此次会话描述
private static final String NAME = "BluetoothChat";
// 为当前应用设定的特殊UUID,根据需要交换数据的类别,UUID有统一的设置
private static final UUIDMY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
// 成员变量
private final BluetoothAdapter mAdapter;
private final Handler mHandler;
private AcceptThread mAcceptThread;
private ConnectThread mConnectThread;
private ConnectedThread mConnectedThread;
private int mState;
// 常量,表示当前连接状态
// 当前无操作
public static final int STATE_NONE = 0;
// 监听连接状态
public static final int STATE_LISTEN = 1;
// 初始化连接
public static final int STATE_CONNECTING = 2;
// 已经连接上一个远程设备
public static final int STATE_CONNECTED = 3;
/**
* @author张飞构造函数,初始化开启一个蓝牙连接所需的类.
* @param context The UI Activity Context
* @param handler Handler 发送消息回父Activity
*/
public BluetoothChatService(Context context, Handler handler) {
mAdapter = BluetoothAdapter.getDefaultAdapter();
// 当前出于无操作状态
mState = STATE_NONE;
mHandler = handler;
}
/**
* 设置当前聊天的状态
*/
private synchronized void setState(int state) {
mState = state;
// 通过Handler更新Activity的UI
mHandler.obtainMessage(BluetoothChat.MESSAGE_STATE_CHANGE, state,-1).sendToTarget();
}
/**
* 获取当前聊天的状态
*/
public synchronized int getState() {
return mState;
}
/**
* 开始聊天
*/
public synchronized void start() {
// 取消任何连接线程
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
// 取消已经连接的线程
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// 开启一个线程,启动BluetoothServerSocket
if (mAcceptThread == null) {
mAcceptThread = new AcceptThread();
mAcceptThread.start();
}
// 更改当前状态为监听
setState(STATE_LISTEN);
}
/**
* 开启一个ConnectThread初始化远程连接设备
*/
public synchronized void connect(BluetoothDevice device) {
// 取消任何连接线程
if (mState == STATE_CONNECTING) {
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
}
// 取消已经连接的线程
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// 开启一个ConnectThread线程连接所给的设备
mConnectThread = new ConnectThread(device);
mConnectThread.start();
// 修改状态为正在连接
setState(STATE_CONNECTING);
}
/**
* 开启一个ConnectedThread现场管理设备连接参数Socket是本次连接创建的套接字
*/
public synchronized void connected(BluetoothSocket socket,BluetoothDevice device) {
// 取消任何连接线程
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
// 取消已经连接的线程
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// 取消监听线程
if (mAcceptThread != null) {
mAcceptThread.cancel();
mAcceptThread = null;
}
// 新建ConnectedThread,用于数据交互
mConnectedThread = new ConnectedThread(socket);
mConnectedThread.start();
// 发送连接设备的名字回Activity
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.DEVICE_NAME, device.getName());
msg.setData(bundle);
mHandler.sendMessage(msg);
// 设置当前状态为已连接
setState(STATE_CONNECTED);
}
/**
* 停止所有的线程
*/
public synchronized void stop() {
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
if (mAcceptThread != null) {
mAcceptThread.cancel();
mAcceptThread = null;
}
// 设置当前状态为无操作
setState(STATE_NONE);
}
public void write(byte[] out) {
ConnectedThread r;
// 如果当前状态为已连接,那么就利用ConnectedThread将数据写入
synchronized (this) {
if (mState != STATE_CONNECTED)
return;
r = mConnectedThread;
}
r.write(out);
}
/**
* 连接失败,发送连接失败消息回Activity,设置状态为继续监听.
*/
private void connectionFailed() {
setState(STATE_LISTEN);
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.TOAST, "不能连接设备");
msg.setData(bundle);
mHandler.sendMessage(msg);
}
/**
* 连接中断,发送连接中断消息回Activity,设置状态为继续监听.
*/
private void connectionLost() {
setState(STATE_LISTEN);
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.TOAST, "连接中断");
msg.setData(bundle);
mHandler.sendMessage(msg);
}
/**
* 线程用于监听即将到来的连接
*/
private class AcceptThread extends Thread {
// 本地的server socket
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
BluetoothServerSocket tmp = null;
// 创建一个监听server socket
try {
tmp =mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) {
Log.e(TAG, "listen()failed", e);
}
mmServerSocket = tmp;
}
public void run() {
setName("AcceptThread");
BluetoothSocket socket = null;
// 当我们的连接状态不是已连接,就持续监听
while (mState != STATE_CONNECTED) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "accept()failed", e);
break;
}
// 一个连接成功
if (socket != null) {
synchronized(BluetoothChatService.this) {
switch (mState) {
case STATE_LISTEN:
caseSTATE_CONNECTING:
// 状态正常,连接远程设备.
connected(socket,socket.getRemoteDevice());
break;
case STATE_NONE:
caseSTATE_CONNECTED:
// 连接不正常,关闭连接
try {
socket.close();
} catch(IOException e) {
Log.e(TAG,"Could not close unwanted socket", e);
}
break;
}
}
}
}
}
// 关闭ServerSocket
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() ofserver failed", e);
}
}
}
/**
* 这个线程用于和传入的设备建立连接
*/
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
mmDevice = device;
BluetoothSocket tmp = null;
// 获得与传人设备连接的BluetoothSocket
try {
tmp =device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.e(TAG, "create()failed", e);
}
mmSocket = tmp;
}
public void run() {
Log.i(TAG, "BEGINmConnectThread");
setName("ConnectThread");
// 开始连接,取消扫描
mAdapter.cancelDiscovery();
try {
mmSocket.connect();
} catch (IOException e) {
connectionFailed();
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG, "unable toclose() socket during connection failure", e2);
}
// 连接失败,重启监听
BluetoothChatService.this.start();
return;
}
// 完成连接操作,释放连接线程
synchronized(BluetoothChatService.this) {
mConnectThread = null;
}
// 开启已连接线程
connected(mmSocket, mmDevice);
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() ofconnect socket failed", e);
}
}
}
/**
* 这个线程和远程设备交换数据.
*/
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
Log.d(TAG, "createConnectedThread");
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// 获得BluetoothSocket的输入输出流
try {
tmpIn =socket.getInputStream();
tmpOut =socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp socketsnot created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
Log.i(TAG, "BEGINmConnectedThread");
byte[] buffer = new byte[1024];
int bytes;
// 当连接存在时,保存对输入流的监听
while (true) {
try {
// 读入InputStream
bytes =mmInStream.read(buffer);
// 发送读取的数据
mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG,"disconnected", e);
connectionLost();
break;
}
}
}
/**
* 写数据到连接输出流中
*/
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
// 发送message回 Activity
mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1,buffer).sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Exceptionduring write", e);
}
}
// 关闭socket
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() ofconnect socket failed", e);
}
}
}
}
BluetoothChatService类包含了蓝牙连接的所有操作和三个线程内部类,分别如下所示:
l AcceptThread线程:创建蓝牙监听线程,调用BluetoothServerSocket.accept()方法接受客户端的新连接,同时提供cancel()方法用于关闭 Socket。
l ConnectThread线程:连接蓝牙设备,BluetoothDevice.createRfcommSocketToServiceRecord()方法从待连接的设备中产生BluetoothSocket,然后在run ()方法中调用BluetoothChatSevice的connected()方法,启动数据交换线程。
l ConnectedThread线程:这是蓝牙服务器和客户端连接成功后一直运行的线程,构造函数中设置输入输出流。线程run()方法中使用阻塞模式的InputStream.read()循环读取输入流,然后通过Handler将信息传递到ActivityUI中更新聊天消息。ConnectedThread线程也提供了write()方法将聊天消息写入到输出流中,传输至客户端。
值得注意的是在Android蓝牙开发时需要添加相应的权限,如下所示:
<uses-permissionandroid:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permissionandroid:name="android.permission.BLUETOOTH" />
1.4. 玄德有话说
刘备:阻塞是个啥情况?
孔明:Socket编程中,有时会需要这样一种情况:服务器不断的向输出流中输出数据,客户端不断的读取数据。同时,只要客户端的输入流中有可读的数据,立即读取,如果没有,则阻塞等待。但是客户端的输入流不可能保证在它想读数据的时候就一定会有可读的数据,如果直接去读的话就会抛出异常,为了避免这种情况的发生,一般的做法是在读取数据前先检查输入流中是否有可读数据:有,读取;没有,等待,此时就发生阻塞了。
刘备:UUID是啥?
孔明:UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。常见的UUID设置有:
蓝牙串口服务SerialPortServiceClass_UUID ='{00001101-0000-1000-8000-00805F9B34FB}'
拨号网络服务DialupNetworkingServiceClass_UUID ='{00001103-0000-1000-8000-00805F9B34FB}'
文件传输服务OBEXFileTransferServiceClass_UUID ='{00001106-0000-1000-8000-00805F9B34FB}'
蓝牙传真服务FaxServiceClass_UUID ='{00001111-0000-1000-8000-00805F9B34FB}'
蓝牙打印服务HCRPrintServiceClass_UUID ='{00001126-0000-1000-8000-00805F9B34FB}'
等。
刘备:RFCOMM 通信是啥?
孔明:RFCOMM是一个简单传输协议,其目的是针对如何在两个不同设备上的应用之间保证一条完整的通信路径,并在它们之间保持一通信段。除了RFCOMM通信外,Android上关于Bluetooth的还有SDP、GAP、耳机设备连接等内容。