安卓蓝牙通信

来源:互联网 发布:湖南软件外包公司 编辑:程序博客网 时间:2024/05/18 04:00

本来要做BLE的,无意间闯进了android蓝牙通信的区域,这可能是一个小白的日常吧。这点大家一定要分清,就是安卓蓝牙通信和BLE,下面就说一下这段时间的成果把,网上的资料实在是太多了,但是你选一篇就一定要看到底!切记不要每篇文章都看几眼,到头来还是什么都不懂!


AndroidManifest.xml
  我们可以从AndroidManifest.xml文件中找到应用程序启动时所进入的界面Activity等信息,因此下面我们首先打开AndroidManifest.xml文件,代码如下:

view plain
  1. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  2.   package="com.example.android.BluetoothChat"  
  3.   android:versionCode="1"  
  4.   android:versionName="1.0">  
  5.   <!-- 最小的sdk版本 -->>  
  6.   <uses-sdk minSdkVersion="6" />  
  7.   <!-- 权限申明 -->>  
  8.   <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />  
  9.   <uses-permission android:name="android.permission.BLUETOOTH" />  
  10.   
  11.   <application android:label="@string/app_name"  
  12.   android:icon="@drawable/app_icon" >  
  13.   <!-- 默认Activity -->>  
  14.   <activity android:name=".BluetoothChat"  
  15.   android:label="@string/app_name"  
  16.   android:configChanges="orientation|keyboardHidden">  
  17.   <intent-filter>  
  18.   <action android:name="android.intent.action.MAIN" />  
  19.   <category android:name="android.intent.category.LAUNCHER" />  
  20.   </intent-filter>  
  21.   </activity>  
  22.   <!-- 用于显示蓝牙设备列表的Activity -->  
  23.   <activity android:name=".DeviceListActivity"  
  24.   android:label="@string/select_device"  
  25.   android:theme="@android:style/Theme.Dialog"  
  26.   android:configChanges="orientation|keyboardHidden" />  
  27.   </application>  
  28.   </manifest>  

首先minSdkVersion用于说明该应用程序所需要使用的最小SDK版本,这里设置为6,也就是说最小需要使用android1.6版本的sdk,同时Ophone则需要使用oms2.0版本,然后打开了BLUETOOTH和BLUETOOTH_ADMIN两个蓝牙操作相关的权限,最后看到了两个Activity的声明,他们分别是BluetoothChat(默认主Activity)和DeviceListActivity(显示设备列表),其中DeviceListActivity风格被定义为一个对话框风格,下面我们将分析该程序的每个细节。


BluetoothChat
  首先,程序启动进入BluetoothChat,在onCreate函数中对窗口进行了设置,代码如下:

view plain
  1. // 设置窗口布局  
  2.   requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);  
  3.   setContentView(R.layout.main);  
  4.   getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.custom_title);  

这里可以看到将窗口风格设置为自定义风格了,并且指定了自定义title布局为custom_title,其定义代码如下: 

view plain
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.   android:layout_width="match_parent"  
  3.   android:layout_height="match_parent"  
  4.   android:gravity="center_vertical"  
  5.   >  
  6.   <TextView android:id="@+id/title_left_text"  
  7.   android:layout_alignParentLeft="true"  
  8.   android:ellipsize="end"  
  9.   android:singleLine="true"  
  10.   style="?android:attr/windowTitleStyle"  
  11.   android:layout_width="wrap_content"  
  12.   android:layout_height="match_parent"  
  13.   android:layout_weight="1"  
  14.   />  
  15.   <TextView android:id="@+id/title_right_text"  
  16.   android:layout_alignParentRight="true"  
  17.   android:ellipsize="end"  
  18.   android:singleLine="true"  
  19.   android:layout_width="wrap_content"  
  20.   android:layout_height="match_parent"  
  21.   android:textColor="#fff"  
  22.   android:layout_weight="1"   
  23.   />  
  24.   </RelativeLayout>  

该布局将title设置为一个相对布局RelativeLayout,其中包含了两个TextView,一个在左边一个在右边,分别用于显示应用程序的标题title和当前的蓝牙配对链接名称,如下图所示。

  其中左边显示为应用程序名称"BluetoothChat",右边显示一个connected:scort则表示当前配对成功正在进行聊天的链接名称。整个聊天界面的布局在main.xml中实现,代码如下:

view plain
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.   android:orientation="vertical"  
  3.   android:layout_width="match_parent"  
  4.   android:layout_height="match_parent"  
  5.   >  
  6.   <!-- 显示设备列表 -->  
  7.   <ListView android:id="@+id/in"  
  8.   android:layout_width="match_parent"  
  9.   android:layout_height="match_parent"  
  10.   android:stackFromBottom="true"  
  11.   android:transcriptMode="alwaysScroll"  
  12.   android:layout_weight="1"  
  13.   />  
  14.   <!-- 显示发送消息的编辑框 -->  
  15.   <LinearLayout  
  16.   android:orientation="horizontal"  
  17.   android:layout_width="match_parent"  
  18.   android:layout_height="wrap_content"  
  19.   >  
  20.   <EditText android:id="@+id/edit_text_out"  
  21.   android:layout_width="wrap_content"  
  22.   android:layout_height="wrap_content"  
  23.   android:layout_weight="1"  
  24.   android:layout_gravity="bottom"  
  25.   />  
  26.   <Button android:id="@+id/button_send"  
  27.   android:layout_width="wrap_content"  
  28.   android:layout_height="wrap_content"  
  29.   android:text="@string/send"  
  30.   />  
  31.   </LinearLayout>  
  32.   </LinearLayout>  

整个界面的布局将是一个线性布局LinearLayout,其中包含了另一个ListView(用于显示聊天的对话信息)和另外一个线性布局来实现一个发送信息的窗口,发送消息发送框有一个输入框和一个发送按钮构成。整个界面如下图所示。

  布局好界面,下面我们需要进入编码状态,首先看BluetoothChat所要那些成员变量,如下代码所示:

view plain
  1. public class BluetoothChat extends Activity {  
  2.   // Debugging  
  3.   private static final String TAG = "BluetoothChat";  
  4.   private static final boolean D = true;  
  5.   
  6.   //从BluetoothChatService Handler发送的消息类型  
  7.   public static final int MESSAGE_STATE_CHANGE = 1;  
  8.   public static final int MESSAGE_READ = 2;  
  9.   public static final int MESSAGE_WRITE = 3;  
  10.   public static final int MESSAGE_DEVICE_NAME = 4;  
  11.   public static final int MESSAGE_TOAST = 5;  
  12.   
  13.   // 从BluetoothChatService Handler接收消息时使用的键名(键-值模型)  
  14.   public static final String DEVICE_NAME = "device_name";  
  15.   public static final String TOAST = "toast";  
  16.   
  17.   // Intent请求代码(请求链接,请求可见)  
  18.   private static final int REQUEST_CONNECT_DEVICE = 1;  
  19.   private static final int REQUEST_ENABLE_BT = 2;  
  20.   
  21.   // Layout Views  
  22.   private TextView mTitle;  
  23.   private ListView mConversationView;  
  24.   private EditText mOutEditText;  
  25.   private Button mSendButton;  
  26.   
  27.   // 链接的设备的名称  
  28.   private String mConnectedDeviceName = null;  
  29.   // Array adapter for the conversation thread  
  30.   private ArrayAdapter<String> mConversationArrayAdapter;  
  31.   // 将要发送出去的字符串  
  32.   private StringBuffer mOutStringBuffer;  
  33.   // 本地蓝牙适配器  
  34.   private BluetoothAdapter mBluetoothAdapter = null;  
  35.   // 聊天服务的对象  
  36.   private BluetoothChatService mChatService = null;  
  37.   //......  

其中Debugging部分则将用于我们在调试程序时通过log打印日志用,其他部分我们都加入了注释,需要说明的是BluetoothChatService ,它是我们自己定义的一个用来管理蓝牙的端口监听,链接,管理聊天的程序,后面我们会介绍。在这里需要说明一点,这些代码都出自google的员工之手,大家在学习时,可以借鉴很多代码编写的技巧和风格,这都将对我们有非常大的帮助。 
然后,我们就需要对界面进行一些设置,如下代码将用来设置我们自定义的标题title需要显示的内容: 

view plain
  1. // 设置自定义title布局  
  2.   mTitle = (TextView) findViewById(R.id.title_left_text);  
  3.   mTitle.setText(R.string.app_name);  
  4.   mTitle = (TextView) findViewById(R.id.title_right_text);  

左边的TextView被设置为显示应用程序名称,右边的则需要我们在链接之后在设置更新,目前则显示没有链接字样,所以这里我们暂不设置,进一步就需要获取本地蓝牙适配器BluetoothAdapter了,因为对于有关蓝牙的任何操作都需要首先获得该蓝牙适配器,获取代码非常简单,如下: 

view plain
  1. // 得到一个本地蓝牙适配器  
  2.   mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();  
  3.   
  4.   // 如果适配器为null,则不支持蓝牙  
  5.   if (mBluetoothAdapter == null) {  
  6.   Toast.makeText(this"Bluetooth is not available", Toast.LENGTH_LONG).show();  
  7.   finish();  
  8.   return;  
  9.   }  
  10.   getDefaultAdapter()函数用于获取本地蓝牙适配器,然后检测是否为null,如果为null则表示没有蓝牙设备的支持,将通过toast告知用户。  
  11.   在onStart()函数中,我们将检测蓝牙是否被打开,如果没有打开,则请求打开,否则就可以设置一些聊天信息的准备工作,代码如下:  
  12.   @Override  
  13.   public void onStart() {  
  14.   super.onStart();  
  15.   if(D) Log.e(TAG, "++ ON START ++");  
  16.   
  17.   // 如果蓝牙没有打开,则请求打开  
  18.   // setupChat() will then be called during onActivityResult  
  19.   if (!mBluetoothAdapter.isEnabled()) {  
  20.   Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);  
  21.   startActivityForResult(enableIntent, REQUEST_ENABLE_BT);  
  22.   // 否则,设置聊天会话  
  23.   } else {  
  24.   if (mChatService == null) setupChat();  
  25.   }  
  26.   }  

如果蓝牙没有打开,我们则通过BluetoothAdapter.ACTION_REQUEST_ENABLE来请求打开蓝牙,REQUEST_ENABLE_BT则是我们自己定义的用于请求打开蓝牙的Intent代码,最后当我们调用startActivityForResult来执行请求时,就会在onActivityResult函数中得到一个反馈,如果当前蓝牙已经打开,那么就可以调用setupChat函数来准备蓝牙聊天相关的工作,稍后分析该函数的具体实现。 
下面我们分析一下请求打开蓝牙之后,在onActivityResult 中得到的反馈信息,我们传递了REQUEST_ENABLE_BT代码作为请求蓝牙打开的命令,因此在onActivityResult 中,需要会得到一个请求代码为REQUEST_ENABLE_B的消息,对于其处理如下代码所示: 

view plain
  1. case REQUEST_ENABLE_BT:  
  2.   // 在请求打开蓝牙时返回的代码  
  3.   if (resultCode == Activity.RESULT_OK) {  
  4.   // 蓝牙已经打开,所以设置一个聊天会话  
  5.   setupChat();  
  6.   } else {  
  7.   // 请求打开蓝牙出错  
  8.   Log.d(TAG, "BT not enabled");  
  9.   Toast.makeText(this, R.string.bt_not_enabled_leaving, Toast.LENGTH_SHORT).show();  
  10.   finish();  
  11.   }  

在请求时,如果返回代码为Activity.RESULT_OK,则表示请求打开蓝牙成功,那么我们就可以和上面的操作进度一样,调用setupChat来设置蓝牙聊天相关信息,如果返回其他代码,则表示请求打开蓝牙失败,这时我们同样通过一个Toast来告诉用户,同时也需要调用finish()函数来结束应用程序。 
如果打开蓝牙无误,那么下面我们开始进入setupChat的设置,其代码实现如下: 

view plain
  1. private void setupChat() {  
  2.   Log.d(TAG, "setupChat()");  
  3.   
  4.   // 初始化对话进程  
  5.   mConversationArrayAdapter = new ArrayAdapter<String>(this, R.layout.message);  
  6.   // 初始化对话显示列表  
  7.   mConversationView = (ListView) findViewById(R.id.in);  
  8.   // 设置话显示列表源  
  9.   mConversationView.setAdapter(mConversationArrayAdapter);  
  10.   
  11.   // 初始化编辑框,并设置一个监听,用于处理按回车键发送消息  
  12.   mOutEditText = (EditText) findViewById(R.id.edit_text_out);  
  13.   mOutEditText.setOnEditorActionListener(mWriteListener);  
  14.   
  15.   // 初始化发送按钮,并设置事件监听  
  16.   mSendButton = (Button) findViewById(R.id.button_send);  
  17.   mSendButton.setOnClickListener(new OnClickListener() {  
  18.   public void onClick(View v) {  
  19.   // 取得TextView中的内容来发送消息  
  20.   TextView view = (TextView) findViewById(R.id.edit_text_out);  
  21.   String message = view.getText().toString();  
  22.   sendMessage(message);  
  23.   }  
  24.   });  
  25.   
  26.   // 初始化BluetoothChatService并执行蓝牙连接  
  27.   mChatService = new BluetoothChatService(this, mHandler);  
  28.   
  29.   // 初始化将要发出的消息的字符串  
  30.   mOutStringBuffer = new StringBuffer("");  
  31.   }  

首先构建一个对话进程mConversationArrayAdapter,然后从xml中取得用于显示对话信息的列表mConversationView,最后将列表的数据来源Adapter设置为mConversationArrayAdapter,这里我们可以看到mConversationArrayAdapter所指定的资源为message.xml,其定义实现如下: 

view plain
  1. <TextView xmlns:android="http://schemas.android.com/apk/res/android"  
  2.   android:layout_width="match_parent"  
  3.   android:layout_height="wrap_content"  
  4.   android:textSize="18sp"  
  5.   android:padding="5dp"  
  6.   />  

很简单,就包含了一个TextView用来显示对话内容即可,这里设置了文字标签的尺寸大小textSize和padding属性。 
然后我们取得了编辑框mOutEditText,用于输入聊天内容的输入框,并对其设置了一个事件监听mWriteListener,其监听函数的实现如下: 

view plain
  1. // The action listener for the EditText widget, to listen for the return key  
  2.   private TextView.OnEditorActionListener mWriteListener =  
  3.   new TextView.OnEditorActionListener() {  
  4.   public boolean onEditorAction(TextView view, int actionId, KeyEvent event) {  
  5.   // 按下回车键并且是按键弹起的事件时发送消息  
  6.   if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_UP) {  
  7.   String message = view.getText().toString();  
  8.   sendMessage(message);  
  9.   }  
  10.   if(D) Log.i(TAG, "END onEditorAction");  
  11.   return true;  
  12.   }  
  13.   };  

首先在其监听中实现了onEditorAction函数,我们通过判断其参数actionId来确定事件触发的动作,其中的"EditorInfo.IME_NULL"在Ophone中表示回车键消息,然后再加上KeyEvent.ACTION_UP,则表示当用户按下回车键并弹起时才触发消息的处理,处理过程也很简单,将输入框中内容取出到变量message中,然后调用sendMessage函数来发送一条消息,具体的发送细节,我们稍后分析。
在setupChat函数中,我们还对发送消息的按钮进行的初始化,同样为其设置了事件监听(setOnClickListener),监听的内容则也是取得输入框中的信息,然后调用sendMessage函数来发送消息,和用户按回车键来发送消息一样。
最后一个重要的操作就是初始化了BluetoothChatService对象mChatService用来管理蓝牙的链接,聊天的操作,并且设置了其Handler对象mHandler来负责数据的交换和线程之间的通信。另外还准备了一个空的字符串对象mOutStringBuffer,用于当我们在发送消息之后,对输入框的清理。


应用菜单
  该应用程序除了这些界面的布局之外,我们还为其设置了一个菜单,菜单包括了"扫描设备"和"使设备可见(能够被其他设备所搜索到)",创建菜单的方式有很多种,这里gogole的员工,比较喜欢和推崇使用xml布局(将界面和逻辑分开),所以我们首先看一下对于该应用程序通过xml所定义的菜单布局,代码如下:

view plain
  1. <menu xmlns:android="http://schemas.android.com/apk/res/android">  
  2.    <!-- 扫描菜单 -->  
  3.   <item android:id="@+id/scan"  
  4.   android:icon="@android:drawable/ic_menu_search"  
  5.   android:title="@string/connect" />  
  6.   <!-- 可见操作 -->  
  7.   <item android:id="@+id/discoverable"  
  8.   android:icon="@android:drawable/ic_menu_mylocation"  
  9.   android:title="@string/discoverable" />  
  10.   </menu>  

这样的定义的确非常的清晰,我们可以随意向这个Menu中添加菜单选项(itme),这里就定义了上面我们所说的两个菜单。然后再程序中通过onCreateOptionsMenu函数中来装载该菜单布局,遂于菜单的点击可以通过onOptionsItemSelected函数的不同参数来辨别,下面是该应用程序中对菜单选项的处理和装载菜单布局: 

view plain
  1. //创建一个菜单  
  2.   @Override  
  3.   public boolean onCreateOptionsMenu(Menu menu) {  
  4.   MenuInflater inflater = getMenuInflater();  
  5.   inflater.inflate(R.menu.option_menu, menu);  
  6.   return true;  
  7.   }  
  8.   //处理菜单事件  
  9.   @Override  
  10.   public boolean onOptionsItemSelected(MenuItem item) {  
  11.   switch (item.getItemId()) {  
  12.   case R.id.scan:  
  13.   // 启动DeviceListActivity查看设备并扫描  
  14.   Intent serverIntent = new Intent(this, DeviceListActivity.class);  
  15.   startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);  
  16.   return true;  
  17.   case R.id.discoverable:  
  18.   // 确保设备处于可见状态  
  19.   ensureDiscoverable();  
  20.   return true;  
  21.   }  
  22.   return false;  
  23.   }  

装载菜单布局的时候使用了MenuInflater对象,整个过程很简单,大家可以参考上面的代码实现,在处理菜单事件的时候,通过item.getItemId()我们可以得到当前选择的菜单项的ID,首先是扫描设备(R.id.scan),这里我们有启动了另外一个Activity来专门处理扫描设备DeviceListActivity,如果扫描之后我们将通过startActivityForResult函数来请求链接该设备,同样我们也会在onActivityResult函数中收到一个反馈信息,命令代码为REQUEST_CONNECT_DEVICE,如果反馈的请求代码为Activity.RESULT_OK,则表示扫描成功(扫描过程我们稍后介绍),那么下面就可以开始准备链接了,实现代码如下:
view plain
  1. case REQUEST_CONNECT_DEVICE:  
  2.   // 当DeviceListActivity返回设备连接  
  3.   if (resultCode == Activity.RESULT_OK) {  
  4.   // 从Intent中得到设备的MAC地址  
  5.   String address = data.getExtras()  
  6.   .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);  
  7.   // 得到蓝牙设备对象  
  8.   BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);  
  9.   // 尝试连接这个设备  
  10.   mChatService.connect(device);  
  11.   }  
  12.   break;  

首先我们可以通过DeviceListActivity.EXTRA_DEVICE_ADDRESS来取得设备的Mac地址,然后通过Mac地址使用蓝牙适配器mBluetoothAdapter的getRemoteDevice函数来查找到该地址的设备BluetoothDevice,查询到之后我们可以通过mChatService对象的connect来链接该设备。 
上面我们说的是扫描蓝牙设备并链接的过程,一般蓝牙设备在打开之后都需要设置可见状态,下面我们来看一下另一个菜单选项的实现,用于使设备处于可见状态,其菜单项的ID为R.id.discoverable,具体实现过程则位于ensureDiscoverable函数中,其实现如下代码: 

view plain
  1. private void ensureDiscoverable() {  
  2.   if(D) Log.d(TAG, "ensure discoverable");  
  3.   //判断扫描模式是否为既可被发现又可以被连接  
  4.   if (mBluetoothAdapter.getScanMode() !=  
  5.   BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {  
  6.    //请求可见状态  
  7.   Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);  
  8.   //添加附加属性,可见状态的时间  
  9.   discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);  
  10.   startActivity(discoverableIntent);  
  11.   }  
  12.   }  

这里首先通过mBluetoothAdapter.getScanMode()函数取得该蓝牙的扫描模式,然后通过BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE设置可见属性,在这里我们加入一个附加属性BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,用来设置可见状态的时间,表示在指定的时间中蓝牙处于可见状态,设置好之后通过startActivity来执行即可。 
这里忧一个需要注意的问题,在链接某个设备之前,我们需要开启一个端口监听,该应用程序将其放在onResume()函数中来处理了,代码如下:
view plain
  1. @Override  
  2.   public synchronized void onResume() {  
  3.   super.onResume();  
  4.   if(D) Log.e(TAG, "+ ON RESUME +");  
  5.   
  6.   // Performing this check in onResume() covers the case in which BT was  
  7.   // not enabled during onStart(), so we were paused to enable it...  
  8.   // onResume() will be called when ACTION_REQUEST_ENABLE activity returns.  
  9.   if (mChatService != null) {  
  10.   // 如果当前状态为STATE_NONE,则需要开启蓝牙聊天服务  
  11.   if (mChatService.getState() == BluetoothChatService.STATE_NONE) {  
  12.   // 开始一个蓝牙聊天服务  
  13.   mChatService.start();  
  14.   }  
  15.   }  
  16.   }  

首先检测mChatService是否被初始化,然后检测其状态是否为STATE_NONE,STATE_NONE表示初始化之后处于等待的状态,当我们在setupChat函数中初始时,其实就已经将其状态设置为STATE_NONE了(该操作是在BluetoothChatService的构造函数中处理的),所以这里就可以通过一个start函数来启动一个进程即可,实际上就是启动了一个端口监听进程,当有设备连接时,该监听进程结束,然后转向链接进程,链接之后同样又将转换到一个聊天管理进程,对于这些链接过程和通信模块在下一篇文章介绍。 
0 0
原创粉丝点击