| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.os.cts.companiontestapp; |
| |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothServerSocket; |
| import android.bluetooth.BluetoothSocket; |
| 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.nio.ByteBuffer; |
| import java.util.Arrays; |
| import java.util.UUID; |
| |
| /** |
| * This class does all the work for setting up and managing Bluetooth connections with other |
| * devices. It has a thread that listens for incoming connections, a thread for connecting with a |
| * device, and a thread for performing data transmissions when connected. |
| * |
| */ |
| public class BluetoothCommunicationService { |
| private static final String TAG = "BluetoothCommunicationService"; |
| |
| // Constants that indicate the current connection state |
| public static final int STATE_NONE = 0; // we're doing nothing |
| public static final int STATE_LISTEN = 1; // now listening for incoming connections |
| public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection |
| public static final int STATE_CONNECTED = 3; // now connected to a remote device |
| |
| // Message types sent from the 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_CONNECTION_FAILED = 6; |
| public static final int MESSAGE_CONNECTION_LOST = 7; |
| |
| // Key names received from the Handler |
| public static final String DEVICE_NAME = "device_name"; |
| public static final String TOAST = "toast"; |
| |
| // Name for the SDP record when creating server socket |
| private static final String NAME_SECURE = "CDMPermissionsSyncBluetoothSecure"; |
| |
| // Unique UUID for this application |
| private static final UUID MY_UUID_SECURE = |
| UUID.fromString("7606c653-6dc3-4a61-9870-07652896cc1c"); |
| |
| private static final int HEADER_SIZE = 4; |
| |
| // Member fields |
| private final BluetoothAdapter adapter; |
| private final Handler handler; |
| private AcceptThread secureAcceptThread; |
| private ConnectThread connectThread; |
| private ConnectedThread connectedThread; |
| private int state; |
| |
| public BluetoothCommunicationService(BluetoothAdapter adapter, Handler handler) { |
| this.adapter = adapter; |
| this.state = STATE_NONE; |
| this.handler = handler; |
| } |
| |
| /** |
| * Set the current state of the connection |
| * |
| * @param state An integer defining the current connection state |
| */ |
| private synchronized void setState(int state) { |
| Log.i(TAG, "setState() " + this.state + " -> " + state); |
| this.state = state; |
| |
| // Give the new state to the Handler so the UI Activity can update |
| handler.obtainMessage(MESSAGE_STATE_CHANGE, state, -1).sendToTarget(); |
| } |
| |
| /** Return the current connection state. */ |
| public synchronized int getState() { |
| return state; |
| } |
| |
| /** |
| * Start the service. Specifically start AcceptThread to begin a session in listening |
| * (server) mode. Called by the Activity onResume() |
| */ |
| public synchronized void start() { |
| Log.i(TAG, "start"); |
| // Cancel any thread attempting to make a connection |
| if (connectThread != null) { |
| connectThread.cancel(); |
| connectThread = null; |
| } |
| // Cancel any thread currently running a connection |
| if (connectedThread != null) { |
| connectedThread.cancel(); |
| connectedThread = null; |
| } |
| setState(STATE_LISTEN); |
| |
| // Start the thread to listen on a BluetoothServerSocket |
| if (secureAcceptThread == null) { |
| secureAcceptThread = new AcceptThread(); |
| secureAcceptThread.start(); |
| } |
| } |
| |
| /** |
| * Start the ConnectThread to initiate a connection to a remote device. |
| * |
| * @param device The BluetoothDevice to connect |
| */ |
| public synchronized void connect(BluetoothDevice device) { |
| Log.i(TAG, "connect to: " + device); |
| // Cancel any thread attempting to make a connection |
| if (state == STATE_CONNECTING) { |
| if (connectThread != null) { |
| connectThread.cancel(); |
| connectThread = null; |
| } |
| } |
| // Cancel any thread currently running a connection |
| if (connectedThread != null) { |
| connectedThread.cancel(); |
| connectedThread = null; |
| } |
| // Start the thread to connect with the given device |
| connectThread = new ConnectThread(device); |
| connectThread.start(); |
| setState(STATE_CONNECTING); |
| } |
| |
| /** |
| * Start the ConnectedThread to begin managing a Bluetooth connection |
| * |
| * @param socket The BluetoothSocket on which the connection was made |
| * @param device The BluetoothDevice that has been connected |
| */ |
| public synchronized void connected( |
| BluetoothSocket socket, BluetoothDevice device, final String socketType) { |
| Log.i(TAG, "connected, Socket Type:" + socketType); |
| // Cancel the thread that completed the connection |
| if (connectThread != null) { |
| connectThread.cancel(); |
| connectThread = null; |
| } |
| // Cancel any thread currently running a connection |
| if (connectedThread != null) { |
| connectedThread.cancel(); |
| connectedThread = null; |
| } |
| // Cancel the accept thread because we only want to connect to one device |
| if (secureAcceptThread != null) { |
| secureAcceptThread.cancel(); |
| secureAcceptThread = null; |
| } |
| |
| // Start the thread to manage the connection and perform transmissions |
| connectedThread = new ConnectedThread(socket, socketType); |
| connectedThread.start(); |
| // Send the name of the connected device back to the UI Activity |
| Message msg = handler.obtainMessage(MESSAGE_DEVICE_NAME); |
| Bundle bundle = new Bundle(); |
| bundle.putString(DEVICE_NAME, device.getName()); |
| msg.setData(bundle); |
| handler.sendMessage(msg); |
| setState(STATE_CONNECTED); |
| } |
| |
| /** Stop all threads */ |
| public synchronized void stop() { |
| Log.i(TAG, "stop"); |
| if (connectThread != null) { |
| connectThread.cancel(); |
| connectThread = null; |
| } |
| if (connectedThread != null) { |
| connectedThread.cancel(); |
| connectedThread = null; |
| } |
| if (secureAcceptThread != null) { |
| secureAcceptThread.cancel(); |
| secureAcceptThread = null; |
| } |
| |
| setState(STATE_NONE); |
| } |
| |
| /** |
| * Write to the ConnectedThread in an unsynchronized manner |
| * |
| * @param out The bytes to write |
| * @see ConnectedThread#write(byte[]) |
| */ |
| public void write(byte[] out) { |
| // Create temporary object |
| ConnectedThread r; |
| // Synchronize a copy of the ConnectedThread |
| synchronized (this) { |
| if (state != STATE_CONNECTED) { |
| return; |
| } |
| r = connectedThread; |
| } |
| // Perform the write unsynchronized |
| r.write(out); |
| } |
| |
| /** Indicate that the connection attempt failed and notify the UI Activity. */ |
| private void connectionFailed() { |
| // Send a failure message back to the Activity |
| Message msg = handler.obtainMessage(MESSAGE_CONNECTION_FAILED); |
| Bundle bundle = new Bundle(); |
| bundle.putString(TOAST, "Unable to connect device"); |
| msg.setData(bundle); |
| handler.sendMessage(msg); |
| |
| // Start the service over to restart listening mode |
| BluetoothCommunicationService.this.start(); |
| } |
| |
| /** Indicate that the connection was lost and notify the UI Activity. */ |
| private void connectionLost() { |
| // Send a failure message back to the Activity |
| Message msg = handler.obtainMessage(MESSAGE_CONNECTION_LOST); |
| Bundle bundle = new Bundle(); |
| bundle.putString(TOAST, "Device connection was lost"); |
| msg.setData(bundle); |
| handler.sendMessage(msg); |
| |
| // Start the service over to restart listening mode |
| BluetoothCommunicationService.this.start(); |
| } |
| |
| /** |
| * This thread runs while listening for incoming connections. It behaves like a server-side |
| * client. It runs until a connection is accepted (or until cancelled). |
| */ |
| private class AcceptThread extends Thread { |
| // The local server socket |
| private final BluetoothServerSocket serverSocket; |
| private final String socketType = "Secure"; |
| |
| public AcceptThread() { |
| BluetoothServerSocket tmp = null; |
| // Create a new listening server socket |
| try { |
| tmp = adapter.listenUsingRfcommWithServiceRecord(NAME_SECURE, MY_UUID_SECURE); |
| } catch (IOException e) { |
| Log.e(TAG, "Socket Type: " + socketType + "listen() failed", e); |
| } |
| serverSocket = tmp; |
| } |
| |
| @Override |
| public void run() { |
| Log.i(TAG, "Socket Type: " + socketType + "BEGIN acceptThread" + this); |
| setName("AcceptThread" + socketType); |
| BluetoothSocket socket = null; |
| // Listen to the server socket if we're not connected |
| while (state != STATE_CONNECTED) { |
| try { |
| // This is a blocking call and will only return on a |
| // successful connection or an exception |
| socket = serverSocket.accept(); |
| } catch (IOException e) { |
| Log.e(TAG, "Socket Type: " + socketType + "accept() failed", e); |
| break; |
| } |
| // If a connection was accepted |
| if (socket != null) { |
| synchronized (BluetoothCommunicationService.this) { |
| switch (state) { |
| case STATE_LISTEN: |
| case STATE_CONNECTING: |
| // Situation normal. Start the connected thread. |
| connected(socket, socket.getRemoteDevice(), socketType); |
| break; |
| case STATE_NONE: |
| case STATE_CONNECTED: |
| // Either not ready or already connected. Terminate new socket. |
| try { |
| socket.close(); |
| } catch (IOException e) { |
| Log.e(TAG, "Could not close unwanted socket", e); |
| } |
| break; |
| default: |
| Log.e(TAG, "Unsupported state seen"); |
| } |
| } |
| } |
| } |
| Log.i(TAG, "END acceptThread, socket Type: " + socketType); |
| } |
| |
| public void cancel() { |
| Log.i(TAG, "Socket Type" + socketType + "cancel " + this); |
| try { |
| serverSocket.close(); |
| } catch (IOException e) { |
| Log.e(TAG, "Socket Type" + socketType + "close() of server failed", e); |
| } |
| } |
| } |
| |
| /** |
| * This thread runs while attempting to make an outgoing connection with a device. It runs |
| * straight through; the connection either succeeds or fails. |
| */ |
| private class ConnectThread extends Thread { |
| private final BluetoothSocket socket; |
| private final BluetoothDevice device; |
| private final String socketType = "Secure" ; |
| |
| public ConnectThread(BluetoothDevice device) { |
| this.device = device; |
| BluetoothSocket tmp = null; |
| // Get a BluetoothSocket for a connection with the given BluetoothDevice |
| try { |
| tmp = device.createRfcommSocketToServiceRecord(MY_UUID_SECURE); |
| } catch (IOException e) { |
| Log.e(TAG, "Socket Type: " + socketType + "create() failed", e); |
| } |
| socket = tmp; |
| } |
| |
| @Override |
| public void run() { |
| Log.i(TAG, "BEGIN connectThread SocketType:" + socketType); |
| setName("ConnectThread" + socketType); |
| |
| // Make a connection to the BluetoothSocket |
| try { |
| // This is a blocking call and will only return on a |
| // successful connection or an exception |
| socket.connect(); |
| } catch (IOException e) { |
| // Close the socket |
| try { |
| socket.close(); |
| } catch (IOException e2) { |
| Log.e(TAG, "unable to close() " + socketType + " socket during connection failure", e2); |
| } |
| Log.e(TAG, "connection failed - IOException" + e.getMessage()); |
| connectionFailed(); |
| return; |
| } |
| // Reset the ConnectThread because we're done |
| synchronized (BluetoothCommunicationService.this) { |
| connectThread = null; |
| } |
| // Start the connected thread |
| connected(socket, device, socketType); |
| } |
| |
| public void cancel() { |
| try { |
| socket.close(); |
| } catch (IOException e) { |
| Log.e(TAG, "close() of connect " + socketType + " socket failed", e); |
| } |
| } |
| } |
| |
| /** |
| * This thread runs during a connection with a remote device. It handles all incoming and outgoing |
| * transmissions. |
| */ |
| private class ConnectedThread extends Thread { |
| private final BluetoothSocket socket; |
| private final InputStream inStream; |
| private final OutputStream outStream; |
| |
| public ConnectedThread(BluetoothSocket socket, String socketType) { |
| Log.i(TAG, "create ConnectedThread: " + socketType); |
| this.socket = socket; |
| InputStream tmpIn = null; |
| OutputStream tmpOut = null; |
| // Get the BluetoothSocket input and output streams |
| try { |
| tmpIn = socket.getInputStream(); |
| tmpOut = socket.getOutputStream(); |
| } catch (IOException e) { |
| Log.e(TAG, "temp sockets not created", e); |
| } |
| inStream = tmpIn; |
| outStream = tmpOut; |
| } |
| |
| @Override |
| public void run() { |
| Log.i(TAG, "Begin connectedThread"); |
| |
| // Keep listening to the InputStream while connected |
| byte[] stitchedMessage = new byte[0]; |
| byte[] leftOver = new byte[0]; |
| int partialMessageSize = 0; |
| int messageSize = 0; |
| while (true) { |
| try { |
| byte[] buffer = new byte[0]; |
| |
| // Read from the InputStream |
| byte[] newBuffer = new byte[1024]; |
| int bytes = inStream.read(newBuffer); |
| |
| if (leftOver.length > 0) { |
| buffer = new byte[leftOver.length + bytes]; |
| System.arraycopy(leftOver, 0, buffer, 0, leftOver.length); |
| System.arraycopy(newBuffer, 0, buffer, leftOver.length, bytes); |
| |
| leftOver = new byte[0]; |
| } else { |
| buffer = Arrays.copyOf(newBuffer, bytes); |
| } |
| |
| if (partialMessageSize == 0) { |
| byte[] messageSizeBytes = Arrays.copyOf(buffer, HEADER_SIZE); |
| messageSize = ByteBuffer.wrap(messageSizeBytes).getInt(); |
| if (stitchedMessage.length < messageSize) { |
| stitchedMessage = new byte[messageSize]; |
| } |
| partialMessageSize = buffer.length - HEADER_SIZE; |
| System.arraycopy(buffer, HEADER_SIZE, stitchedMessage, 0, |
| buffer.length - HEADER_SIZE); |
| } else { |
| System.arraycopy(buffer, 0, stitchedMessage, partialMessageSize, |
| Math.min(stitchedMessage.length - partialMessageSize, |
| buffer.length)); |
| |
| if (stitchedMessage.length - partialMessageSize < buffer.length) { |
| // There are more messages in the stream |
| leftOver = Arrays.copyOfRange(buffer, |
| stitchedMessage.length - partialMessageSize, |
| buffer.length); |
| |
| // Send the obtained bytes to the UI Activity |
| handler.obtainMessage(MESSAGE_READ, messageSize, -1, |
| stitchedMessage).sendToTarget(); |
| stitchedMessage = new byte[0]; |
| partialMessageSize = 0; |
| messageSize = 0; |
| continue; |
| } else { |
| partialMessageSize += buffer.length; |
| } |
| } |
| |
| if (partialMessageSize > messageSize) { |
| Log.e(TAG, "Invalid message header received."); |
| partialMessageSize = 0; |
| messageSize = 0; |
| } |
| |
| if (partialMessageSize == messageSize) { |
| // Send the obtained bytes to the UI Activity |
| handler.obtainMessage(MESSAGE_READ, messageSize, -1, |
| stitchedMessage).sendToTarget(); |
| stitchedMessage = new byte[0]; |
| partialMessageSize = 0; |
| messageSize = 0; |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "disconnected", e); |
| connectionLost(); |
| // Start the service over to restart listening mode |
| BluetoothCommunicationService.this.start(); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Write to the connected OutStream. |
| * |
| * @param buffer The bytes to write |
| */ |
| public void write(byte[] buffer) { |
| try { |
| // Add 4 bytes message size at the beginning of the stream |
| byte[] messageSizeToBytes = ByteBuffer.allocate(HEADER_SIZE).putInt( |
| buffer.length).array(); |
| byte[] bufferWithHeader = new byte[HEADER_SIZE + buffer.length]; |
| System.arraycopy(messageSizeToBytes, 0, bufferWithHeader, 0, HEADER_SIZE); |
| System.arraycopy(buffer, 0, bufferWithHeader, HEADER_SIZE, buffer.length); |
| |
| outStream.write(bufferWithHeader); |
| // Share the sent message back to the UI Activity |
| // handler.obtainMessage(MESSAGE_WRITE, -1, -1, bufferWithHeader).sendToTarget(); |
| } catch (IOException e) { |
| Log.e(TAG, "Exception during write", e); |
| } |
| } |
| |
| public void cancel() { |
| try { |
| socket.close(); |
| } catch (IOException e) { |
| Log.e(TAG, "close() of connect socket failed", e); |
| } |
| } |
| } |
| } |