blob: bacf10c8574ecc79625ac2ec1f220a2abb54c336 [file] [log] [blame]
/*
* 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);
}
}
}
}