3. 经典蓝牙聊天

经典蓝牙聊天 学习卡片

任务说明

本案例实现两台Android设备之间通过经典蓝牙(Bluetooth Classic)进行文本消息互发的聊天应用。应用包含设备发现与配对、建立RFCOMM连接、消息收发三个核心环节。完成本案例后,用户可在两台真机上安装APK,一台作为服务端等待连接,另一台作为客户端发起连接,连接成功后即可双向发送文本消息。

实现方案

采用Android原生Bluetooth API,服务端通过BluetoothServerSocket监听连接,客户端通过BluetoothDevice.createRfcommSocketToServiceRecord()发起连接。消息传输使用独立线程管理输入/输出流,UI层通过Handler接收消息并刷新聊天界面。应用需要申请蓝牙权限(旧版本还需定位权限用于设备发现),并在运行时动态请求。

代码实现


// AndroidManifest.xml 关键权限
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

// ChatService.java - 蓝牙连接与消息收发核心
public class ChatService {
    private static final String APP_NAME = "BT_CHAT";
    private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
    private final BluetoothAdapter adapter;
    private final Handler handler;
    private AcceptThread acceptThread;
    private ConnectThread connectThread;
    private ConnectedThread connectedThread;
    private int state;

    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(Handler handler) {
        this.adapter = BluetoothAdapter.getDefaultAdapter();
        this.handler = handler;
        this.state = STATE_NONE;
    }

    // 开始监听(服务端)
    public synchronized void start() {
        if (connectThread != null) { connectThread.cancel(); connectThread = null; }
        if (connectedThread != null) { connectedThread.cancel(); connectedThread = null; }
        if (acceptThread == null) {
            acceptThread = new AcceptThread();
            acceptThread.start();
        }
        setState(STATE_LISTEN);
    }

    // 发起连接(客户端)
    public synchronized void connect(BluetoothDevice device) {
        if (state == STATE_CONNECTING && connectThread != null) {
            connectThread.cancel(); connectThread = null;
        }
        if (connectedThread != null) { connectedThread.cancel(); connectedThread = null; }
        connectThread = new ConnectThread(device);
        connectThread.start();
        setState(STATE_CONNECTING);
    }

    // 发送消息
    public void write(byte[] out) {
        ConnectedThread r = connectedThread;
        if (r != null) { r.write(out); }
    }

    private synchronized void setState(int state) {
        this.state = state;
        handler.obtainMessage(MainActivity.MESSAGE_STATE_CHANGE, state, -1).sendToTarget();
    }

    private synchronized void connectionSuccess(BluetoothSocket socket, BluetoothDevice device) {
        if (connectThread != null) { connectThread.cancel(); connectThread = null; }
        if (connectedThread != null) { connectedThread.cancel(); connectedThread = null; }
        if (acceptThread != null) { acceptThread.cancel(); acceptThread = null; }
        connectedThread = new ConnectedThread(socket);
        connectedThread.start();
        handler.obtainMessage(MainActivity.MESSAGE_DEVICE_NAME, -1, -1, device.getName()).sendToTarget();
        setState(STATE_CONNECTED);
    }

    private void connectionFailed() {
        setState(STATE_LISTEN);
        handler.obtainMessage(MainActivity.MESSAGE_TOAST, -1, -1, "无法连接设备").sendToTarget();
    }

    // 服务端监听线程
    private class AcceptThread extends Thread {
        private final BluetoothServerSocket serverSocket;

        public AcceptThread() {
            BluetoothServerSocket tmp = null;
            try {
                tmp = adapter.listenUsingRfcommWithServiceRecord(APP_NAME, MY_UUID);
            } catch (IOException e) { }
            serverSocket = tmp;
        }

        public void run() {
            BluetoothSocket socket;
            while (state != STATE_CONNECTED) {
                try {
                    socket = serverSocket.accept();
                } catch (IOException e) { break; }
                if (socket != null) {
                    synchronized (ChatService.this) {
                        switch (state) {
                            case STATE_LISTEN:
                            case STATE_CONNECTING:
                                connectionSuccess(socket, socket.getRemoteDevice());
                                break;
                            case STATE_NONE:
                            case STATE_CONNECTED:
                                try { socket.close(); } catch (IOException e) { }
                                break;
                        }
                    }
                }
            }
        }

        public void cancel() {
            try { serverSocket.close(); } catch (IOException e) { }
        }
    }

    // 客户端连接线程
    private class ConnectThread extends Thread {
        private final BluetoothSocket socket;
        private final BluetoothDevice device;

        public ConnectThread(BluetoothDevice device) {
            this.device = device;
            BluetoothSocket tmp = null;
            try {
                tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
            } catch (IOException e) { }
            socket = tmp;
        }

        public void run() {
            adapter.cancelDiscovery();
            try {
                socket.connect();
                synchronized (ChatService.this) {
                    connectThread = null;
                    connectionSuccess(socket, device);
                }
            } catch (IOException e) {
                connectionFailed();
                try { socket.close(); } catch (IOException e2) { }
            }
        }

        public void cancel() {
            try { socket.close(); } catch (IOException e) { }
        }
    }

    // 已连接数据收发线程
    private class ConnectedThread extends Thread {
        private final BluetoothSocket socket;
        private final InputStream inStream;
        private final OutputStream outStream;

        public ConnectedThread(BluetoothSocket socket) {
            this.socket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;
            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) { }
            inStream = tmpIn;
            outStream = tmpOut;
        }

        public void run() {
            byte[] buffer = new byte[1024];
            int bytes;
            while (true) {
                try {
                    bytes = inStream.read(buffer);
                    handler.obtainMessage(MainActivity.MESSAGE_READ, bytes, -1, buffer).sendToTarget();
                } catch (IOException e) { break; }
            }
        }

        public void write(byte[] bytes) {
            try {
                outStream.write(bytes);
            } catch (IOException e) { }
        }

        public void cancel() {
            try { socket.close(); } catch (IOException e) { }
        }
    }
}

// MainActivity.java - UI与消息处理
public class MainActivity extends AppCompatActivity {
    public static final int MESSAGE_STATE_CHANGE = 1;
    public static final int MESSAGE_READ = 2;
    public static final int MESSAGE_DEVICE_NAME = 3;
    public static final int MESSAGE_TOAST = 4;
    private ChatService chatService;
    private BluetoothAdapter bluetoothAdapter;
    private ArrayAdapter<String> conversationAdapter;
    private EditText inputText;
    private String connectedDeviceName = null;

    private final Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_STATE_CHANGE:
                    updateState(msg.arg1);
                    return true;
                case MESSAGE_READ:
                    byte[] readBuf = (byte[]) msg.obj;
                    String readMessage = new String(readBuf, 0, msg.arg1);
                    conversationAdapter.add("对方: " + readMessage);
                    return true;
                case MESSAGE_DEVICE_NAME:
                    connectedDeviceName = msg.getData().getString("device_name");
                    Toast.makeText(MainActivity.this, "已连接 " + connectedDeviceName, Toast.LENGTH_SHORT).show();
                    return true;
                case MESSAGE_TOAST:
                    Toast.makeText(MainActivity.this, msg.getData().getString("toast"), Toast.LENGTH_SHORT).show();
                    return true;
            }
            return false;
        }
    });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        chatService = new ChatService(handler);
        conversationAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
        ListView listView = findViewById(R.id.conversation_list);
        listView.setAdapter(conversationAdapter);
        inputText = findViewById(R.id.input_text);
        Button sendButton = findViewById(R.id.send_button);
        sendButton.setOnClickListener(v -> sendMessage());
        Button discoverableButton = findViewById(R.id.discoverable_button);
        discoverableButton.setOnClickListener(v -> ensureDiscoverable());
        Button scanButton = findViewById(R.id.scan_button);
        scanButton.setOnClickListener(v -> scanDevices());
        checkPermissions();
    }

    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 31) {
            requestPermissions(new String[]{
                    Manifest.permission.BLUETOOTH_CONNECT,
                    Manifest.permission.BLUETOOTH_SCAN,
                    Manifest.permission.ACCESS_FINE_LOCATION}, 1);
        } else {
            requestPermissions(new String[]{
                    Manifest.permission.ACCESS_FINE_LOCATION}, 1);
        }
    }

    private void ensureDiscoverable() {
        if (bluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
            Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
            discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
            startActivity(discoverableIntent);
        }
        chatService.start();
    }

    private void scanDevices() {
        if (bluetoothAdapter.isDiscovering()) bluetoothAdapter.cancelDiscovery();
        bluetoothAdapter.startDiscovery();
        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
        registerReceiver(receiver, filter);
    }

    private final BroadcastReceiver receiver = new BroadcastReceiver() {
        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 != null) {
                    new AlertDialog.Builder(MainActivity.this)
                            .setTitle("发现设备")
                            .setMessage(device.getName() + "\n" + device.getAddress())
                            .setPositiveButton("连接", (dialog, which) -> {
                                chatService.connect(device);
                            })
                            .setNegativeButton("取消", null)
                            .show();
                }
            }
        }
    };

    private void sendMessage() {
        String message = inputText.getText().toString();
        if (message.length() > 0) {
            chatService.write(message.getBytes());
            conversationAdapter.add("我: " + message);
            inputText.setText("");
        }
    }

    private void updateState(int state) {
        switch (state) {
            case ChatService.STATE_LISTEN:
                setTitle("等待连接...");
                break;
            case ChatService.STATE_CONNECTING:
                setTitle("连接中...");
                break;
            case ChatService.STATE_CONNECTED:
                setTitle("已连接: " + connectedDeviceName);
                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (chatService != null) chatService.stop();
        unregisterReceiver(receiver);
    }
}

布局文件 activity_main.xml


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <ListView
        android:id="@+id/conversation_list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/input_text"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="输入消息" />

        <Button
            android:id="@+id/send_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发送" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/discoverable_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="作为服务端" />

        <Button
            android:id="@+id/scan_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="扫描设备" />
    </LinearLayout>
</LinearLayout>

连接流程与消息收发时序

设备A(服务端) 设备B(客户端) listenUsingRfcomm() 等待连接 createRfcommSocket() connect() 请求 accept() 接受 连接建立 发送: "你好" OutputStream.write() InputStream.read() UI刷新显示消息

落地与验证

运行方式:使用Android Studio构建APK,分别安装到两台Android 6.0以上真机(需支持蓝牙)。两台设备均需开启蓝牙并授予定位权限。

操作步骤:

  1. 设备A点击“作为服务端”,系统弹出可被发现请求,点击允许(持续300秒);
  2. 设备B点击“扫描设备”,在发现列表中选择设备A并点击“连接”;
  3. 连接成功后,两台设备标题栏显示“已连接: 设备名称”;
  4. 任一方在输入框输入文字并点击“发送”,对方列表即时显示消息。

验证判定标准:

  • 设备A标题栏从“等待连接”变为“已连接”,设备B标题栏从“连接中”变为“已连接”,表示RFCOMM链路建立成功;
  • 设备A发送“测试消息1”,设备B聊天列表出现“对方: 测试消息1”;设备B回复“收到”,设备A列表出现“对方: 收到”,证明双向文本传输正常;
  • 关闭一台设备蓝牙或远离超过10米,聊天界面不再更新,重新靠近并重连后可继续通信。

预期输出示例:当设备A发送“你好”且设备B发送“Hello”时,两台设备聊天列表均显示两行消息:我: 你好、对方: Hello(设备A视角)或我: Hello、对方: 你好(设备B视角)。