二十五岁时我们都一样愚蠢、多愁善感,喜欢故弄玄虚,可如果不那样的话,五十岁时也就不会如此明智。
标题:8.9 Android蓝牙开发实例演示
在上一节《Android蓝牙(Bluetooth)连接与通讯》教程中我们已经了解如何探测并开启手机的蓝牙功能、蓝牙服务搜索和建立蓝牙连接。下面我们编写实例 BluetoothDemo。
实例 BluetoothDemo 演示了使用蓝牙功能对其他蓝牙设备进行搜寻、连接并进行数据传输的过程。该应用程序的运行效果如图 1 所示。
图 1 实例 BluetoothDemo 的运行效果
该视图整体使用 LinearLayout 布局,使用 ListView 显示聊天内容,下方的横向 LinearLayout 布局中放置了一个用于输入文本的 EditText 和一个按钮。对应的布局文件 main.xml 的内容如下:<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:myapp="http://schemas.android.com/apk/res/android.BluetoothChat" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/bg01"> <ListView android:id="@+id/in" android:layout_width="match_parent" android:layout_height="match_parent" android:stackFromBottom="true" android:transcriptMode="alwaysScroll" android:layout_weight="1"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_parent" android:orientation="horizontal" > <EditText android:id="@+id/edit_text_out" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:layout_gravity="bottom"/> <ListView android:id="@+id/button_send" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/send" /> </LinearLayout> </LinearLayout>实例 BluetoothDemo 的 AndroidManifest.xml 文件的内容如下:<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="introduction.android.BluetoothChat" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="14" /> <uses-permission android:name="android.permission.BLUE TOOTH_ADMIN"> <uses-permission android:name="android.permission.BLUETOOTH" /> <application android:icon="@drawable/app_icon" android:label="@string/app_name"> <activity android:name=".BluetoothChat" android:configChanges="orientation\keyboardHidden" android:label="@string/app_name"> <intent-filter> <action android:name="android.in tent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".DeviceList" android:configChanges="orientation\keyboardHidden" android:label="@string/select_device" android:theme="@android:style/Theme.Dialog" /> </application> </manifest>实例 BluetoothDemo 的主 Activity 为 BluetoothChat,其对应的文件内容如下:package introduction.android.mydbdemo; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.view.inputmethod.EditorInfo; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; public class BluetoothChat extends Activity { public static final int MESSAGE_STATE_CHANGE = 1; public static final int MESSAGE_READ = 2; public static final int MESSAGE_WRITE = 3; public static final int MESSAGE_DEVICE_NAME = 4; public static final int MESSAGE_TOAST = 5; public static final String DEVICE_NAME = "device_name"; public static final String TOAST = "toast"; private static final int REQUEST_C0NNECT_DEVICE = 1; private static final int REQUEST_ENABLE_BT = 2; private TextView mTitle; private ListView mConversationView; private EditText mOutEditText; private Button mSendButton; private String mConnectedDeviceName = null; private ArrayAdapter<String> mConversationArrayAdapter; private StringBuffer mOutStringBuffer; private BluetoothAdapter mBluetoothAdapter = null; private ChatService mChatService = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //设置窗口布局为自定义标题 requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); setContentView(R.layout.main); //设置窗口标题为布局文件 getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.custom_title); 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(); if (!mBluetoothAdapter.isEnabled()) { Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableIntent, REQUEST_ENABLE_BT); } else { if (mChatService == null) setupChat(); } } @Override public synchronized void onResume() { super.onResume(); if (mChatService != null) { if (mChatService.getState() == ChatService.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 void onClick(View v) { TextView view = (TextView) findViewById(R.id.edit_text_out); String message = view.getText().toString(); sendMessage(message); } }); mChatService = new ChatService(this, mHandler); mOutStringBuffer = new StringBuffer(""); } @Override public synchronized void onPause() { super.onPause(); } @Override public void onStop() { super.onStop(); } @Override public void onDestroy() { super.onDestroy(); if (mChatService != null) mChatService.stop(); } private void ensureDiscoverable() { if (mBluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); startActivity(discoverableIntent); } } private void sendMessage(String message) { if (mChatService.getState() != ChatService.STATE_CONNECTED) { Toast.makeText(this, R.string.not_connected, Toast.LENGTH_SHORT).show(); return; } if (message.length() > 0) { byte[] send = message.getBytes(); mChatService.write(send); mOutStringBuffer.setLength(0); mOutEditText.setText(mOutStringBuffer); } } private TextView.OnEditorActionListener mWriteListener = new TextView.OnEditorActionListener() { public boolean onEditorAction(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; } }; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_STATE_CHANGE: switch (msg.argl) { case ChatService.STATE_CONNECTED: mTitle.setText(R.string.title_connected_to); mTitle.append(mConnectedDeviceName); mConversationArrayAdapter.clear(); break; case ChatService.STATE_CONNECTING: mTitle.setText(R.string.title_connecting); break; case ChatService.STATE_LISTEN: case ChatService.STATE_NONE: mTitle.setText(R.string.title_not_connected); break; } break; case MESSAGE_WRITE: byte[] writeBuf = (byte[]) msg.obj; String writeMessage = new String(writeBuf); mConversationArrayAdapter.add("我: " + writeMessage); break; case MESSAGE_READ: byte[] readBuf = (byte[]) msg.obj; String readMessage = new String(readBuf, 0, msg.argl); mConversationArrayAdapter.add(mConnectedDeviceName + ":" + readMessage); break; case MESSAGE_DEVICE_NAME: mConnectedDeviceName = msg.getData().getString(DEVICE_NAME); Toast.makeText(getApplicationContext(), "链接到" + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); break; case MESSAGE_TOAST: Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST), Toast.LENGTH_SHORT).show(); break; } } }; public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CONNECT_DEVICE: if (resultCode == Activity.RESULT_OK) { String address = data.getExtras().getString(DeviceList.EXTRA_DEVICE_ADDRESS); BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); mChatService.connect(device); } break; case REQUEST_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 boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.option_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.scan: Intent serverIntent = new Intent(this, DeviceList.class); startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE); return true; case R.id.discoverable: ensureDiscoverable(); return true; case R.id.back: finish(); System.exit(0); return true; } return false; } }Activity BluetoothChat 的 onCreate() 方法检查当前设备是否支持蓝牙功能,并得到本地的 BluetoothAdapter 设备。
在 onStart() 中检查是否启用了蓝牙功能,若未启用,则请求启用,然后通过 setupChat() 方法对界面中的控件进行初始化、增加单击监听器等,BluetoothChat 创建了 ChatService 对象,该对象在整个应用过程中存在,并完成了蓝牙连接的建立、消息发送与接收等功能。
ChatService.java 的代码如下:package introduction.android.BluetoothChat; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.UUID; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Message; import introduction.android.mydbdemo.BluetoothChat; public class ChatService { private static final String NAME = "BluetoothChat"; // UUID-->通用唯一识别码,能唯一辨识资讯 private static final UUID MY_UUID = UUID .fromString("fa87c0d0-afac-llde-8a39-0800200c9a66"); 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; public ChatService(Context context, Handler handler) { mAdapter = BluetoothAdapter.getDefaultAdapter(); mState = STATE_NONE; mHandler = handler; } private synchronized void setState(int state) { mState = state; 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; } if (mAcceptThread == null) { mAcceptThread = new AcceptThread(); mAcceptThread.start(); } setState(STATE_LISTEN); } //取消CONNECTING和CONNECTED状态下的相关线程,然后运行新的mConnectThread线程 public synchronized void connect(BluetoothDevice device) { if (mState == STATE_CONNECTING) { if (mConnectThread != null) { mConnectThread.cancel(); mConnectThread = null; } } if (mConnectedThread != null) { mConnectedThread.cancel(); mConnectedThread = null; } mConnectThread = new ConnectThread(device); mConnectThread.start(); setState(STATE_CONNECTING); } //开启一个ConnectedThread来管理对应的当前连接。之前先取消任意现存的mConnectThread、 //mConnectedThread 、mAcceptThread 线程,然后开启新 mConnectedThread ,传入当前刚刚接受的 //socket连接 //最后通过Handler来通知UI连接 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; } mConnectedThread = new ConnectedThread(socket); mConnectedThread.start(); 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); } //停止所有相关线程,设当前状态为NONE public synchronized void stop() { if (mConnectThread != null) { mConnectThread.cancel(); mConnectThread = null; } if (mConnectedThread != null) { mConnectedThread.cancel(); mConnectedThread = null; } if (mAcceptThread != null) { mAcceptThread.cancel(); setState(STATE_NONE); } } //在 STATE_CONNECTED 状态下,调用 mConnectedThread 里的 write 方法,写入 byte public void write(byte[] out) { ConnectedThread r; synchronized (this) { if (mState != STATE_CONNECTED) return; r = mConnectedThread; } r.write(out); } //连接失败的时候处理,通知ui ,并设为STATE一LISTEN状态 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); } //当连接失去的时候,设为STATE_LISTEN状态并通知UI 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); } //创建监听线程,准备接受新连接。使用阻塞方式调用BluetoothServerSocket.accept() private class AcceptThread extends Thread { private final BluetoothServerSocket mmServerSocket; public AcceptThread() { BluetoothServerSocket tmp = null; try { tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID); } catch (IOException e) { } mmServerSocket = tmp; } public void run() { setName("AcceptThread"); BluetoothSocket socket = null; while (mState != STATE_CONNECTED) { try { socket = mmServerSocket.accept(); } catch (IOException e) { break; } if (socket != null) { synchronized (ChatService.this) { switch (mState) { case STATE_LISTEN: case STATE_CONNECTING: connected(socket, socket.getRemoteDevice()); break; case STATE_NONE: case STATE_CONNECTED: try { socket.close(); } catch (IOException e) { } break; } } } } } public void cancel() { try { mmServerSocket.close(); } catch (IOException e) { } } } // 连接进程,专门用来对外发出连接对方蓝牙的请求并进行处理 // 构造函数里,通过BluetoothDevice.createRfcommSocketToServiceRecord(), // 从待连接的device产生BluetoothSocket.然后在run方法中connect , // 成功后调用BluetoothChatSevice的connected() 方法。定义cancel()再关闭线程时能关闭相关Socket private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { mmDevice = device; BluetoothSocket tmp = null; try { tmp = device.createRfcommSocketToServiceRecord(MY_UUID); } catch (IOException e) { } mmSocket = tmp; } public void run() { setName("ConnectThread"); mAdapter.cancelDiscovery(); try { mmSocket.connect(); } catch (IOException e) { connectionFailed(); try { mmSocket.close(); } catch (IOException e2) { } ChatService.this.start(); return; synchronized (ChatService.this) { mConnectThread = null; } connected(mmSocket, mmDevice); } } public void cancel() { try { mmSocket.close(); } catch (IOException e) { } } } // 双方蓝牙连接后一直运行的线程。构造函数中设置输入输出流 // Run方法中使用阻塞模式的Inputstream.read()循环读取输入流, // 然后post到UI线程中更新聊天信息。也提供了write()将聊天信息写入输出流传输至对方,传输成功后回写入UI线程,最后cancle()关闭连接的socket private class ConnectedThread extends Thread { private final BluetoothSocket mmSocket; private final InputStream mmInStream; private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket) { mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { } mmInStream = tmpIn; mmOutStream = tmpOut; } public void run() { byte[] buffer = new byte[1024]; int bytes; while (true) { try { bytes = mmInStream.read(buffer); mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer).sendToTarget(); } catch (IOException e) { connectionLost(); break; } } } public void write(byte[] buffer) { try { mmOutStream.write(buffer); mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1, buffer).sendToTarget(); } catch (IOException e) { } } } public void cancel() { try { mmSocket.close(); } catch (IOException e) { } } }
DeviceList 用于显示蓝牙设备列表,并返回蓝牙设备信息。DeviceList.java 的代码如下:package introduction.android.BluetoothChat; import java.util.Set; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import android.view.Window; import android.view.View.OnClickListener; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import android.widget.AdapterView.OnItemClickListener; import introduction.android.mydbdemo.R; public class DeviceList extends Activity { public static String EXTRA_DEVICE_ADDRESS = "device_address"; private BluetoothAdapter mBtAdapter; private ArrayAdapter<String> mPairedDevicesArrayAdapter; private ArrayAdapter<String> mNewDevicesArrayAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setContentView(R.layout.device_list); setResult(Activity.RESULT_CANCELED); Button scanButton = (Button) findViewById(R.id.button_scan); scanButton.setOnClickListener(new OnClickListener() { public void onClick(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); ListView pairedListView = (ListView) findViewById(R.id.paired_devices); pairedListView.setAdapter(mPairedDevicesArrayAdapter); pairedListView.setOnItemClickListener(mDeviceClickListener); ListView newDevicesListView = (ListView) findViewById(R.id.new_devices); newDevicesListView.setAdapter(mNewDevicesArrayAdapter); newDevicesListView.setOnItemClickListener(mDeviceClickListener); IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); this.registerReceiver(mReceiver, filter); filter = new IntentFilter(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(); if (mBtAdapter != null) { mBtAdapter.cancelDiscovery(); } this.unregisterReceiver(mReceiver); } 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(); } private OnItemClickListener mDeviceClickListener = new OnItemClickListener() { public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) { mBtAdapter.cancelDiscovery(); String info = ((TextView) v).getText().toString(); String address = info.substring(info.length() - 17); Intent intent = new Intent(); intent.putExtra(EXTRA_DEVICE_ADDRESS, address); setResult(Activity.RESULT_OK, intent); finish(); } }; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(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) { String noDevices = getResources().getText( R.string.none_found).toString(); mNewDevicesArrayAdapter.add(noDevices); } } } }; }