安卓蓝牙聊天室(一)
单点聊天(两台设备聊天)
打算做一个真正意义上的聊天室,现在网上找的蓝牙博客基本都是点对点的。
关于蓝牙的基本知识:http://developer.android.com/guide/topics/connectivity/bluetooth.html
相关知识自己可以去网上找,相关操作本文结尾处也有几篇博客可以看一下
基本流程:
目录结构:
相关代码:
1.获取权限
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.rain.bluetoothchat"> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/android:Theme.Light"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- 用于显示蓝牙设备列表的Activity --> <activity android:name=".DeviceListActivity" android:label="@string/select_device" android:theme="@android:style/Theme.Holo.Dialog" android:configChanges="orientation|keyboardHidden" /> </application> </manifest>
main函数
2.custom_title.xml文件,该布局将title设置为一个相对布局RelativeLayout,其中包含了两个TextView,一个在左边一个在右边,分别用于显示应用程序的标题title和当前的蓝牙配对链接名称
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_vertical" > <TextView android:id="@+id/title_left_text" android:layout_alignParentLeft="true" android:ellipsize="end" android:maxLines="1" style="?android:attr/windowTitleStyle" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_weight="1" /> <TextView android:id="@+id/title_right_text" android:layout_alignParentRight="true" android:ellipsize="end" android:maxLines="1" android:layout_width="wrap_content" android:layout_height="match_parent" android:textColor="#fff" android:layout_weight="1" /> </RelativeLayout>
3.activity_main.xml主布局,整个界面的布局将是一个线性布局LinearLayout,其中包含了另一个ListView(用于显示聊天的对话信息)和另外一个线性布局来实现一个发送信息的窗口,发送消息发送框有一个输入框和一个发送按钮构成。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" > <!-- 显示设备列表 --> <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:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" > <!-- 显示发送消息的编辑框 --> <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" /> <!-- 发送按钮 --> <Button android:id="@+id/button_send" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="send" /> </LinearLayout> </LinearLayout>
4.message.xml,很简单,就包含了一个TextView用来显示对话内容即可,这里设置了文字标签的尺寸大小textSize和padding属性
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="18sp" android:padding="5dp" />
5.option_menu.xml菜单文件
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/scan" android:showAsAction="ifRoom" android:title="scan" /> <item android:id="@+id/discoverable" android:showAsAction="ifRoom" android:title="discoverable" /> </menu>
6.MainActivity.java,一些解释代码里面已经有了,
package com.example.rain.bluetoothchat; 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.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; 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 MainActivity extends Activity { // Debugging private static final String TAG = "BluetoothChat"; private static final boolean D = true; //从BluetoothChatService Handler发送的消息类型 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; // 从BluetoothChatService Handler接收消息时使用的键名(键-值模型) public static final String DEVICE_NAME = "device_name"; public static final String TOAST = "toast"; // Intent请求代码(请求链接,请求可见) private static final int REQUEST_CONNECT_DEVICE = 1; private static final int REQUEST_ENABLE_BT = 2; // Layout Views private TextView mTitle; private ListView mConversationView; private EditText mOutEditText; private Button mSendButton; // 链接的设备的名称 private String mConnectedDeviceName = null; // Array adapter for the conversation thread private ArrayAdapter mConversationArrayAdapter; // 将要发送出去的字符串 private StringBuffer mOutStringBuffer; // 本地蓝牙适配器 private BluetoothAdapter mBluetoothAdapter = null; // 聊天服务的对象 private BluetoothChatService mChatService = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); setContentView(R.layout.activity_main); getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.custom_title); init(); // /* 显示App icon左侧的back键 */ // ActionBar actionBar = getActionBar(); // actionBar.setDisplayHomeAsUpEnabled(true); } /* 初始化 */ private void init(){ // 设置自定义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(); // 如果适配器为null,则不支持蓝牙 if (mBluetoothAdapter == null) { Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show(); finish(); return; } } /* 检测蓝牙是否被打开,如果没有打开,则请求打开,否则就可以设置一些聊天信息的准备工作 */ @Override public void onStart() { super.onStart(); if(D) Log.e(TAG, "++ ON START ++"); // 如果蓝牙没有打开,则请求打开 // setupChat() will then be called during onActivityResult if (!mBluetoothAdapter.isEnabled()) { Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableIntent, REQUEST_ENABLE_BT); } else {// 否则,设置聊天会话 if (mChatService == null) setupChat(); } } /* 回调函数 */ //@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode){ case REQUEST_ENABLE_BT: //在请求打开蓝牙时返回的代码 if (resultCode == Activity.RESULT_OK) { // 蓝牙已经打开,所以设置一个聊天会话 setupChat(); } else { // 请求打开蓝牙出错 Log.d(TAG, "BT not enabled"); Toast.makeText(this, R.string.bt_not_enabled_leaving, Toast.LENGTH_SHORT).show(); finish(); } break; case REQUEST_CONNECT_DEVICE: // 当DeviceListActivity返回设备连接 if (resultCode == Activity.RESULT_OK) { // 从Intent中得到设备的MAC地址 String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);//String address = data.getExtras() .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS); // 得到蓝牙设备对象 BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); // 尝试连接这个设备 mChatService.connect(device); } break; } } /* 设置蓝牙聊天相关信息 */ private void setupChat() { Log.d(TAG, "setupChat()"); // 初始化对话进程 mConversationArrayAdapter = new ArrayAdapter(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 View.OnClickListener() { public void onClick(View v) { // 取得TextView中的内容来发送消息 TextView view = (TextView) findViewById(R.id.edit_text_out); String message = view.getText().toString(); sendMessage(message); } }); // 初始化BluetoothChatService并执行蓝牙连接 mChatService = new BluetoothChatService(this, mHandler); // 初始化将要发出的消息的字符串 mOutStringBuffer = new StringBuffer(""); } // The action listener for the EditText widget, to listen for the return key 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); } if(D) Log.i(TAG, "END onEditorAction"); return true; } }; //创建一个菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.option_menu, menu); return true; //return super.onCreateOptionsMenu(menu); } //处理菜单事件 @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.scan: // 启动DeviceListActivity查看设备并扫描 Intent serverIntent = new Intent(this, DeviceListActivity.class);//serverIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE); return true; case R.id.discoverable: // 确保设备处于可见状态 ensureDiscoverable(); return true; } return false; } /* 用于使设备处于可见状态 */ private void ensureDiscoverable() { if (D) Log.d(TAG, "ensure discoverable"); //判断扫描模式是否为既可被发现又可以被连接 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); } } /* 在链接某个设备之前,我们需要开启一个端口监听 */ @Override public synchronized void onResume() { super.onResume(); if(D) Log.e(TAG, "+ ON RESUME +"); // Performing this check in onResume() covers the case in which BT was // not enabled during onStart(), so we were paused to enable it... // onResume() will be called when ACTION_REQUEST_ENABLE activity returns. if (mChatService != null) { // 如果当前状态为STATE_NONE,则需要开启蓝牙聊天服务 if (mChatService.getState() == BluetoothChatService.STATE_NONE) { // 开始一个蓝牙聊天服务 mChatService.start(); } } } private final Handler mHandler=new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch(msg.what){ case MESSAGE_STATE_CHANGE: if(D) Log.i(TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1); switch (msg.arg1) { case BluetoothChatService.STATE_CONNECTED: //设置状态为已经链接 mTitle.setText(R.string.title_connected_to); //添加设备名称 mTitle.append(mConnectedDeviceName); //清理聊天记录 mConversationArrayAdapter.clear(); break; case BluetoothChatService.STATE_CONNECTING: //设置正在链接 mTitle.setText(R.string.title_connecting); break; case BluetoothChatService.STATE_LISTEN: case BluetoothChatService.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("Me: " + writeMessage); break; case MESSAGE_READ: byte[] readBuf = (byte[]) msg.obj; // 取得内容并添加到聊天对话列表中 String readMessage = new String(readBuf, 0, msg.arg1); mConversationArrayAdapter.add(mConnectedDeviceName+": " + readMessage); break; case MESSAGE_DEVICE_NAME: // 保存链接的设备名称,并显示一个toast提示 mConnectedDeviceName = msg.getData().getString(DEVICE_NAME); Toast.makeText(getApplicationContext(), "Connected to " + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); break; case MESSAGE_TOAST: //处理链接(发送)失败的消息 Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST), Toast.LENGTH_SHORT).show(); break; } } }; private void sendMessage(String message) { // 检查是否处于连接状态 if (mChatService.getState() != BluetoothChatService.STATE_CONNECTED) { Toast.makeText(this, R.string.not_connected, Toast.LENGTH_SHORT).show(); return; } // 如果输入的消息不为空才发送,否则不发送 if (message.length() > 0) { // Get the message bytes and tell the BluetoothChatService to write byte[] send = message.getBytes(); mChatService.write(send); // Reset out string buffer to zero and clear the edit text field mOutStringBuffer.setLength(0); mOutEditText.setText(mOutStringBuffer); } } }
设备扫描DeviceListActivity
7.device_list.xml该布局整体由一个线性布局LinearLayout组成,其中包含了两个textview中来显示已经配对的设备和信扫描出来的设备(还没有经过配对)和两个ListView分别用于显示已经配对和没有配对的设备的相关信息。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:descendantFocusability="blocksDescendants"> <!-- 已经配对的设备 --> <TextView android:id="@+id/title_paired_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="已配对" android:visibility="gone" android:background="#666" android:textColor="#fff" android:padding="5dp" /> <!-- 已经配对的设备信息 --> <ListView android:id="@+id/paired_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:stackFromBottom="true" android:layout_weight="1" /> <!-- 扫描出来没有经过配对的设备 --> <TextView android:id="@+id/title_new_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="未配对" android:visibility="gone" android:background="#666" android:textColor="#fff" android:paddingLeft="5dp" /> <!-- 扫描出来没有经过配对的设备信息 --> <ListView android:id="@+id/new_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:stackFromBottom="true" android:layout_weight="2"/> <!-- 扫描按钮 --> <Button android:id="@+id/button_scan" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="scan" /> </LinearLayout>
8.device_name.xml
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="18sp" android:clickable="false" android:focusableInTouchMode="false" android:padding="5dp" />
9.DeviceListActivity.java
package com.example.rain.bluetoothchat; 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.util.Log; import android.view.View; import android.view.Window; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import java.util.Set; /** * Created by rain on 2017/2/28. */ public class DeviceListActivity extends Activity { //debugging private static final String TAG="DeviceListActivity"; private static final boolean D=true; //return intrnt extra 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 View.OnClickListener() { public void onClick(View v) { doDiscovery(); v.setVisibility(View.GONE); } }); //初始化ArrayAdapter,一个是已经配对的设备,一个是新发现的设备 mPairedDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name); mNewDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name); // 检测并设置已配对的设备ListView ListView pairedListView = (ListView) findViewById(R.id.paired_devices); pairedListView.setAdapter(mPairedDevicesArrayAdapter); pairedListView.setOnItemClickListener(mDeviceClickListener); // 检查并设置行发现的蓝牙设备ListView ListView newDevicesListView = (ListView) findViewById(R.id.new_devices); newDevicesListView.setAdapter(mNewDevicesArrayAdapter); newDevicesListView.setOnItemClickListener(mDeviceClickListener); // 当一个设备被发现时,需要注册一个广播 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(); // 得到一个已经匹配到本地适配器的BluetoothDevice类的对象集合 Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices(); // 如果有配对成功的设备则添加到ArrayAdapter if (pairedDevices.size() > 0) { findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE); for (BluetoothDevice device : pairedDevices) { mPairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } } else { //否则添加一个没有被配对的字符串 String noDevices = getResources().getText(R.string.none_paired).toString(); mPairedDevicesArrayAdapter.add(noDevices); } } /* 要包括蓝牙适配器和广播的注销操作 */ @Override protected void onDestroy() { super.onDestroy(); // 确保我们没有发现,检测设备 if (mBtAdapter != null) { mBtAdapter.cancelDiscovery(); } // 卸载所注册的广播 this.unregisterReceiver(mReceiver); } /** *请求能被发现的设备 */ private void doDiscovery() { if (D) Log.d(TAG, "doDiscovery()"); // 设置显示进度条 setProgressBarIndeterminateVisibility(true); // 设置title为扫描状态 setTitle(R.string.scanning); // 显示新设备的子标题 findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE); // 如果已经在请求现实了,那么就先停止 if (mBtAdapter.isDiscovering()) { mBtAdapter.cancelDiscovery(); } // 请求从蓝牙适配器得到能够被发现的设备 mBtAdapter.startDiscovery(); } //监听扫描蓝牙设备 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // 当发现一个设备时 if (BluetoothDevice.ACTION_FOUND.equals(action)) { // 从Intent得到蓝牙设备对象 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); // 如果已经配对,则跳过,因为他已经在设备列表中了 if (device.getBondState() != BluetoothDevice.BOND_BONDED) { //否则添加到设备列表 mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } // 当扫描完成之后改变Activity的title } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { //设置进度条不显示 setProgressBarIndeterminateVisibility(false); //设置title setTitle(R.string.select_device); //如果计数为0,则表示没有发现蓝牙 if (mNewDevicesArrayAdapter.getCount() == 0) { String noDevices = getResources().getText(R.string.none_found).toString(); mNewDevicesArrayAdapter.add(noDevices); } } } }; // ListViews中所有设备的点击事件监听 private AdapterView.OnItemClickListener mDeviceClickListener = new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) { // 取消检测扫描发现设备的过程,因为内非常耗费资源 mBtAdapter.cancelDiscovery(); // 得到mac地址 String info = ((TextView) v).getText().toString(); String address = info.substring(info.length() - 17); // 创建一个包括Mac地址的Intent请求 //Intent intent = new Intent(DeviceListActivity.this, MainActivity.class); Intent intent = new Intent(); intent.putExtra(EXTRA_DEVICE_ADDRESS, address ); // 设置result并结束Activity setResult(Activity.RESULT_OK, intent);//这个常量的值是-1,导致onActivityResult没有被调用。 //setResult(Activity.RESULT_FIRST_USER, intent); //startActivityForResult(intent, 1); finish(); } }; }
BluetoothChatService
10.BluetoothChatService.java对于设备的监听,连接管理都将由REQUEST_CONNECT_DEVICE来实现,其中又包括三个主要部分,三个进程分贝是:请求连接的监听线程(AcceptThread)、连接一个设备的进程(ConnectThread)、连接之后的管理进程(ConnectedThread)。
package com.example.rain.bluetoothchat; 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 android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.UUID; /** * Created by rain on 2017/2/28. */ public class BluetoothChatService { // Debugging private static final String TAG = "BluetoothChatService"; private static final boolean D = true; //当创建socket服务时的SDP名称 private static final String NAME = "BluetoothChat"; // 应用程序的唯一UUID private static final UUID MY_UUID = UUID.fromString("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 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 BluetoothChatService(Context context, Handler handler) { //得到本地蓝牙适配器 mAdapter = BluetoothAdapter.getDefaultAdapter(); //设置状态 mState = STATE_NONE; //设置Handler mHandler = handler; } /* 状态同步锁,同一时刻只有一个线程执行状态相关代码 */ private synchronized void setState(int state) { if (D) Log.d(TAG, "setState() " + mState + " -> " + state); mState = state; // 状态更新之后UI Activity也需要更新(what,arg1,arg2) mHandler.obtainMessage(MainActivity.MESSAGE_STATE_CHANGE, state, -1).sendToTarget(); } public synchronized int getState() { return mState; } public synchronized void start() { if (D) Log.d(TAG, "start"); // 取消任何线程视图建立一个连接 if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} // 取消任何正在运行的链接 if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} // 启动AcceptThread线程来监听BluetoothServerSocket if (mAcceptThread == null) { mAcceptThread = new AcceptThread(); mAcceptThread.start(); } //设置状态为监听,,等待链接 setState(STATE_LISTEN); } //请求链接的监听线程 private class AcceptThread extends Thread { // 本地socket服务 private final BluetoothServerSocket mmServerSocket; public AcceptThread() { BluetoothServerSocket tmp = null; // 创建一个新的socket服务监听 try { tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID); } catch (IOException e) { Log.e(TAG, "listen() failed", e); } mmServerSocket = tmp; } public void run() { if (D) Log.d(TAG, "BEGIN mAcceptThread" + this); setName("AcceptThread"); BluetoothSocket socket = null; // 如果当前没有链接则一直监听socket服务 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: case STATE_CONNECTING: // 如果状态为监听或者正在链接中,,则调用connected来链接 connected(socket, socket.getRemoteDevice()); break; case STATE_NONE: case STATE_CONNECTED: // 如果为没有准备或者已经链接,这终止该socket try { socket.close(); } catch (IOException e) { Log.e(TAG, "Could not close unwanted socket", e); } break; } } } } if (D) Log.i(TAG, "END mAcceptThread"); } //关闭BluetoothServerSocket public void cancel() { if (D) Log.d(TAG, "cancel " + this); try { mmServerSocket.close(); } catch (IOException e) { Log.e(TAG, "close() of server failed", e); } } } public synchronized void connect(BluetoothDevice device) { if (D) Log.d(TAG, "connect to: " + 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); } //链接一个设备的线程 private class ConnectThread extends Thread { //蓝牙Socket 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, "BEGIN mConnectThread"); setName("ConnectThread"); // 取消可见状态,将会进行链接 mAdapter.cancelDiscovery(); // 创建一个BluetoothSocket链接 try { //同样是一个阻塞调用,返回成功和异常 mmSocket.connect(); } catch (IOException e) { //链接失败 connectionFailed(); // 如果异常则关闭socket try { mmSocket.close(); } catch (IOException e2) { Log.e(TAG, "unable to close() socket during connection failure", e2); } // 重新启动监听服务状态 BluetoothChatService.this.start(); return; } // 完成则重置ConnectThread synchronized (BluetoothChatService.this) { mConnectThread = null; } // 开启ConnectedThread(正在运行中...)线程 connected(mmSocket, mmDevice); } //取消链接线程ConnectThread public void cancel() { try { mmSocket.close(); } catch (IOException e) { Log.e(TAG, "close() of connect socket failed", e); } } } //连接失败 private void connectionFailed() { setState(STATE_LISTEN); // 发送链接失败的消息到UI界面 Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_TOAST); Bundle bundle = new Bundle(); bundle.putString(MainActivity.TOAST, "Unable to connect device"); msg.setData(bundle); mHandler.sendMessage(msg); } public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) { if (D) Log.d(TAG, "connected"); // 取消ConnectThread链接线程 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(); // 发送链接的设备名称到UI Activity界面 Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_DEVICE_NAME); Bundle bundle = new Bundle(); bundle.putString(MainActivity.DEVICE_NAME, device.getName()); msg.setData(bundle); mHandler.sendMessage(msg); //状态变为已经链接,即正在运行中 setState(STATE_CONNECTED); } //已经链接之后的管理线程 private class ConnectedThread extends Thread { //BluetoothSocket private final BluetoothSocket mmSocket; //输入输出流 private final InputStream mmInStream; private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket) { Log.d(TAG, "create ConnectedThread"); mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; // 得到BluetoothSocket的输入输出流 try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { Log.e(TAG, "temp sockets not created", e); } mmInStream = tmpIn; mmOutStream = tmpOut; } public void run() { Log.i(TAG, "BEGIN mConnectedThread"); byte[] buffer = new byte[1024]; int bytes; // 监听输入流 while (true) { try { // 从输入流中读取数据 bytes = mmInStream.read(buffer); // 发送一个消息到UI线程进行更新 mHandler.obtainMessage(MainActivity.MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } catch (IOException e) { //出现异常,则链接丢失 Log.e(TAG, "disconnected", e); connectionLost(); break; } } } /** * 写入要发送的消息 * @param buffer The bytes to write */ public void write(byte[] buffer) { try { mmOutStream.write(buffer); // 将写的消息同时传递给UI界面 mHandler.obtainMessage(MainActivity.MESSAGE_WRITE, -1, -1, buffer) .sendToTarget(); } catch (IOException e) { Log.e(TAG, "Exception during write", e); } } //取消ConnectedThread链接管理线程 public void cancel() { try { mmSocket.close(); } catch (IOException e) { Log.e(TAG, "close() of connect socket failed", e); } } } //失去连接 private void connectionLost() { setState(STATE_LISTEN); // 发送失败消息到UI界面 Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_TOAST); Bundle bundle = new Bundle(); bundle.putString(MainActivity.TOAST, "Device connection was lost"); msg.setData(bundle); mHandler.sendMessage(msg); } //写入自己要发送出来的消息 public void write(byte[] out) { // Create temporary object ConnectedThread r; // Synchronize a copy of the ConnectedThread synchronized (this) { //判断是否处于已经链接状态 if (mState != STATE_CONNECTED) return; r = mConnectedThread; } // 执行写 r.write(out); } //停止所有的线程 public synchronized void stop() { if (D) Log.d(TAG, "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); } }
values
11.strings.xml
<resources> <string name="app_name">BluetoothChat</string> <string name="none_paired">没有配对设备</string> <string name="none_found">未找到</string> <string name="scanning">扫描中</string> <string name="select_device">选择设备</string> <string name="not_connected">未连接</string> <string name="title_connected_to">已连接</string> <string name="title_connecting">正在连接</string> <string name="title_not_connected">找不到设备</string> <string name="bt_not_enabled_leaving">不支持蓝牙设备</string> </resources>
运行结果:
http://blog.csdn.net/it1039871366/article/details/46533481代码基本来源于此,只是众多bug一步一步调出来了很痛苦
http://www.jizhuomi.com/android/course/282.html
http://www.2cto.com/kf/201608/537390.html
过程中遇到的问题以及解决办法
1. You cannot combine custom titles with other title feature
http://www.eoeandroid.com/thread-225717-1-1.html?_dsign=50e7d781
2. You need to use a Theme.AppCompat theme (or descendant) with this activity.
http://www.cnblogs.com/jshen/p/3996071.html
3. menu items should specify a title
http://stackoverflow.com/questions/27979692/menu-items-should-specify-a-title
4. data.getExtras() 报错
https://zhidao.baidu.com/question/292535079.html
5. "Cannot resolve constructor 'intent(anonymous android.view.View.OnClickListener, java.lang.Class(com.example.xxx.buttonexample.emergencyIntent))'.
http://www.dabu.info/android-cannot-resolve-constructor-intent.html
6. 关于onActivityResult方法不执行的问题汇总
http://blog.csdn.net/sbvfhp/article/details/26858441
https://zhidao.baidu.com/question/1386501104240487180.html