blob: 9ed95acf9eaed4f60cdf4e6426ce00fd0138b987 [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.nearby.fastpair.provider.bluetooth;
import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.ACCEPTING;
import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.CONNECTED;
import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.RESTARTING;
import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.STARTING;
import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.STOPPED;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.nearby.fastpair.provider.EventStreamProtocol;
import android.nearby.fastpair.provider.utils.Logger;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Listens for a rfcomm client to connect and supports both sending messages to the client and
* receiving messages from the client.
*/
public class RfcommServer {
private static final String TAG = "RfcommServer";
private final Logger mLogger = new Logger(TAG);
private static final String FAST_PAIR_RFCOMM_SERVICE_NAME = "FastPairServer";
public static final UUID FAST_PAIR_RFCOMM_UUID =
UUID.fromString("df21fe2c-2515-4fdb-8886-f12c4d67927c");
/** A single thread executor where all state checks are performed. */
private final ExecutorService mControllerExecutor = Executors.newSingleThreadExecutor();
private final ExecutorService mSendMessageExecutor = Executors.newSingleThreadExecutor();
private final ExecutorService mReceiveMessageExecutor = Executors.newSingleThreadExecutor();
@Nullable
private BluetoothServerSocket mServerSocket;
@Nullable
private BluetoothSocket mSocket;
private State mState = STOPPED;
private boolean mIsStopRequested = false;
@Nullable
private RequestHandler mRequestHandler;
@Nullable
private CountDownLatch mCountDownLatch;
@Nullable
private StateMonitor mStateMonitor;
/**
* Manages RfcommServer status.
*
* <pre>{@code
* +------------------------------------------------+
* +-------------------------------+ |
* v | |
* +---------+ +----------+ +-----+-----+ +-----+-----+
* | STOPPED +--> | STARTING +--> | ACCEPTING +--> | CONNECTED |
* +---------+ +-----+----+ +-------+---+ +-----+-----+
* ^ | ^ v |
* +---------------+ +---+--------+ |
* | RESTARTING | <-------+
* +------------+
* }</pre>
*
* If Stop action is not requested, the server will restart forever. Otherwise, go stopped.
*/
public enum State {
STOPPED,
STARTING,
RESTARTING,
ACCEPTING,
CONNECTED,
}
/** Starts the rfcomm server. */
public void start() {
runInControllerExecutor(this::startServer);
}
private void startServer() {
log("Start RfcommServer");
if (!mState.equals(STOPPED)) {
log("Server is not stopped, skip start request.");
return;
}
updateState(STARTING);
mIsStopRequested = false;
startAccept();
}
private void restartServer() {
log("Restart RfcommServer");
updateState(RESTARTING);
startAccept();
}
private void startAccept() {
try {
// Gets server socket in controller thread for stop() API.
mServerSocket =
BluetoothAdapter.getDefaultAdapter()
.listenUsingRfcommWithServiceRecord(
FAST_PAIR_RFCOMM_SERVICE_NAME, FAST_PAIR_RFCOMM_UUID);
} catch (IOException e) {
log("Create service record failed, stop server");
stopServer();
return;
}
updateState(ACCEPTING);
new Thread(() -> accept(mServerSocket)).start();
}
private void accept(BluetoothServerSocket serverSocket) {
triggerCountdownLatch();
try {
BluetoothSocket socket = serverSocket.accept();
serverSocket.close();
runInControllerExecutor(() -> startListen(socket));
} catch (IOException e) {
log("IOException when accepting new connection");
runInControllerExecutor(() -> handleAcceptException(serverSocket));
}
}
private void handleAcceptException(BluetoothServerSocket serverSocket) {
if (mIsStopRequested) {
stopServer();
} else {
closeServerSocket(serverSocket);
restartServer();
}
}
private void startListen(BluetoothSocket bluetoothSocket) {
if (mIsStopRequested) {
closeSocket(bluetoothSocket);
stopServer();
return;
}
updateState(CONNECTED);
// Sets method parameter to global socket for stop() API.
this.mSocket = bluetoothSocket;
new Thread(() -> listen(bluetoothSocket)).start();
}
private void listen(BluetoothSocket bluetoothSocket) {
triggerCountdownLatch();
try {
DataInputStream dataInputStream = new DataInputStream(bluetoothSocket.getInputStream());
while (true) {
int eventGroup = dataInputStream.readUnsignedByte();
int eventCode = dataInputStream.readUnsignedByte();
int additionalLength = dataInputStream.readUnsignedShort();
byte[] data = new byte[additionalLength];
if (additionalLength > 0) {
int count = 0;
do {
count += dataInputStream.read(data, count, additionalLength - count);
} while (count < additionalLength);
}
if (mRequestHandler != null) {
// In order not to block listening thread, use different thread to dispatch
// message.
mReceiveMessageExecutor.execute(
() -> {
mRequestHandler.handleRequest(eventGroup, eventCode, data);
triggerCountdownLatch();
});
}
}
} catch (IOException e) {
log(
String.format(
"IOException when listening to %s",
bluetoothSocket.getRemoteDevice().getAddress()));
runInControllerExecutor(() -> handleListenException(bluetoothSocket));
}
}
private void handleListenException(BluetoothSocket bluetoothSocket) {
if (mIsStopRequested) {
stopServer();
} else {
closeSocket(bluetoothSocket);
restartServer();
}
}
public void sendFakeEventStreamMessage(EventStreamProtocol.EventGroup eventGroup) {
switch (eventGroup) {
case BLUETOOTH:
send(EventStreamProtocol.EventGroup.BLUETOOTH_VALUE,
EventStreamProtocol.BluetoothEventCode.BLUETOOTH_ENABLE_SILENCE_MODE_VALUE,
new byte[0]);
break;
case LOGGING:
send(EventStreamProtocol.EventGroup.LOGGING_VALUE,
EventStreamProtocol.LoggingEventCode.LOG_FULL_VALUE,
new byte[0]);
break;
case DEVICE:
send(EventStreamProtocol.EventGroup.DEVICE_VALUE,
EventStreamProtocol.DeviceEventCode.DEVICE_BATTERY_INFO_VALUE,
new byte[]{0x11, 0x12, 0x13});
break;
default: // fall out
}
}
public void sendFakeEventStreamLoggingMessage(@Nullable String logContent) {
send(EventStreamProtocol.EventGroup.LOGGING_VALUE,
EventStreamProtocol.LoggingEventCode.LOG_SAVE_TO_BUFFER_VALUE,
logContent != null ? logContent.getBytes(UTF_8) : new byte[0]);
}
public void send(int eventGroup, int eventCode, byte[] data) {
runInControllerExecutor(
() -> {
if (!CONNECTED.equals(mState)) {
log("Server is not in CONNECTED state, skip send request");
return;
}
BluetoothSocket bluetoothSocket = this.mSocket;
mSendMessageExecutor.execute(() -> {
String address = bluetoothSocket.getRemoteDevice().getAddress();
try {
DataOutputStream dataOutputStream =
new DataOutputStream(bluetoothSocket.getOutputStream());
dataOutputStream.writeByte(eventGroup);
dataOutputStream.writeByte(eventCode);
dataOutputStream.writeShort(data.length);
if (data.length > 0) {
dataOutputStream.write(data);
}
dataOutputStream.flush();
log(
String.format(
"Send message to %s: %s, %s, %s.",
address, eventGroup, eventCode, data.length));
} catch (IOException e) {
log(
String.format(
"Failed to send message to %s: %s, %s, %s.",
address, eventGroup, eventCode, data.length),
e);
}
});
});
}
/** Stops the rfcomm server. */
public void stop() {
runInControllerExecutor(() -> {
log("Stop RfcommServer");
if (STOPPED.equals(mState)) {
log("Server is stopped, skip stop request.");
return;
}
if (mIsStopRequested) {
log("Stop is already requested, skip stop request.");
return;
}
mIsStopRequested = true;
if (ACCEPTING.equals(mState)) {
closeServerSocket(mServerSocket);
}
if (CONNECTED.equals(mState)) {
closeSocket(mSocket);
}
});
}
private void stopServer() {
updateState(STOPPED);
triggerCountdownLatch();
}
private void updateState(State newState) {
log(String.format("Change state from %s to %s", mState, newState));
if (mStateMonitor != null) {
mStateMonitor.onStateChanged(newState);
}
mState = newState;
}
private void closeServerSocket(BluetoothServerSocket serverSocket) {
try {
if (serverSocket != null) {
log(String.format("Close server socket: %s", serverSocket));
serverSocket.close();
}
} catch (IOException | NullPointerException e) {
// NullPointerException is used to skip robolectric test failure.
// In unit test, different virtual devices are set up in different threads, calling
// ServerSocket.close() in wrong thread will result in NullPointerException since there
// is no corresponding service record.
// TODO(hylo): Remove NullPointerException when the solution is submitted to test cases.
log("Failed to stop server", e);
}
}
private void closeSocket(BluetoothSocket socket) {
try {
if (socket != null && socket.isConnected()) {
log(String.format("Close socket: %s", socket.getRemoteDevice().getAddress()));
socket.close();
}
} catch (IOException e) {
log(String.format("IOException when close socket %s",
socket.getRemoteDevice().getAddress()));
}
}
private void runInControllerExecutor(Runnable runnable) {
mControllerExecutor.execute(runnable);
}
private void log(String message) {
mLogger.log("Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
}
private void log(String message, Throwable e) {
mLogger.log(e, "Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message);
}
private void triggerCountdownLatch() {
if (mCountDownLatch != null) {
mCountDownLatch.countDown();
}
}
/** Interface to handle incoming request from clients. */
public interface RequestHandler {
void handleRequest(int eventGroup, int eventCode, byte[] data);
}
public void setRequestHandler(@Nullable RequestHandler requestHandler) {
this.mRequestHandler = requestHandler;
}
/** A state monitor to send signal when state is changed. */
public interface StateMonitor {
void onStateChanged(State state);
}
public void setStateMonitor(@Nullable StateMonitor stateMonitor) {
this.mStateMonitor = stateMonitor;
}
@VisibleForTesting
void setCountDownLatch(@Nullable CountDownLatch countDownLatch) {
this.mCountDownLatch = countDownLatch;
}
@VisibleForTesting
void setIsStopRequested(boolean isStopRequested) {
this.mIsStopRequested = isStopRequested;
}
@VisibleForTesting
void simulateAcceptIOException() {
runInControllerExecutor(() -> {
if (ACCEPTING.equals(mState)) {
closeServerSocket(mServerSocket);
}
});
}
@VisibleForTesting
void simulateListenIOException() {
runInControllerExecutor(() -> {
if (CONNECTED.equals(mState)) {
closeSocket(mSocket);
}
});
}
}