1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.nearby.fastpair.provider.bluetooth; 18 19 import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.ACCEPTING; 20 import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.CONNECTED; 21 import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.RESTARTING; 22 import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.STARTING; 23 import static android.nearby.fastpair.provider.bluetooth.RfcommServer.State.STOPPED; 24 25 import static java.nio.charset.StandardCharsets.UTF_8; 26 27 import android.bluetooth.BluetoothAdapter; 28 import android.bluetooth.BluetoothServerSocket; 29 import android.bluetooth.BluetoothSocket; 30 import android.nearby.fastpair.provider.EventStreamProtocol; 31 import android.nearby.fastpair.provider.utils.Logger; 32 33 import androidx.annotation.Nullable; 34 import androidx.annotation.VisibleForTesting; 35 36 import java.io.DataInputStream; 37 import java.io.DataOutputStream; 38 import java.io.IOException; 39 import java.util.UUID; 40 import java.util.concurrent.CountDownLatch; 41 import java.util.concurrent.ExecutorService; 42 import java.util.concurrent.Executors; 43 44 /** 45 * Listens for a rfcomm client to connect and supports both sending messages to the client and 46 * receiving messages from the client. 47 */ 48 public class RfcommServer { 49 private static final String TAG = "RfcommServer"; 50 private final Logger mLogger = new Logger(TAG); 51 52 private static final String FAST_PAIR_RFCOMM_SERVICE_NAME = "FastPairServer"; 53 public static final UUID FAST_PAIR_RFCOMM_UUID = 54 UUID.fromString("df21fe2c-2515-4fdb-8886-f12c4d67927c"); 55 56 /** A single thread executor where all state checks are performed. */ 57 private final ExecutorService mControllerExecutor = Executors.newSingleThreadExecutor(); 58 59 private final ExecutorService mSendMessageExecutor = Executors.newSingleThreadExecutor(); 60 private final ExecutorService mReceiveMessageExecutor = Executors.newSingleThreadExecutor(); 61 62 @Nullable 63 private BluetoothServerSocket mServerSocket; 64 @Nullable 65 private BluetoothSocket mSocket; 66 67 private State mState = STOPPED; 68 private boolean mIsStopRequested = false; 69 70 @Nullable 71 private RequestHandler mRequestHandler; 72 73 @Nullable 74 private CountDownLatch mCountDownLatch; 75 @Nullable 76 private StateMonitor mStateMonitor; 77 78 /** 79 * Manages RfcommServer status. 80 * 81 * <pre>{@code 82 * +------------------------------------------------+ 83 * +-------------------------------+ | 84 * v | | 85 * +---------+ +----------+ +-----+-----+ +-----+-----+ 86 * | STOPPED +--> | STARTING +--> | ACCEPTING +--> | CONNECTED | 87 * +---------+ +-----+----+ +-------+---+ +-----+-----+ 88 * ^ | ^ v | 89 * +---------------+ +---+--------+ | 90 * | RESTARTING | <-------+ 91 * +------------+ 92 * }</pre> 93 * 94 * If Stop action is not requested, the server will restart forever. Otherwise, go stopped. 95 */ 96 public enum State { 97 STOPPED, 98 STARTING, 99 RESTARTING, 100 ACCEPTING, 101 CONNECTED, 102 } 103 104 /** Starts the rfcomm server. */ start()105 public void start() { 106 runInControllerExecutor(this::startServer); 107 } 108 startServer()109 private void startServer() { 110 log("Start RfcommServer"); 111 112 if (!mState.equals(STOPPED)) { 113 log("Server is not stopped, skip start request."); 114 return; 115 } 116 updateState(STARTING); 117 mIsStopRequested = false; 118 119 startAccept(); 120 } 121 restartServer()122 private void restartServer() { 123 log("Restart RfcommServer"); 124 updateState(RESTARTING); 125 startAccept(); 126 } 127 startAccept()128 private void startAccept() { 129 try { 130 // Gets server socket in controller thread for stop() API. 131 mServerSocket = 132 BluetoothAdapter.getDefaultAdapter() 133 .listenUsingRfcommWithServiceRecord( 134 FAST_PAIR_RFCOMM_SERVICE_NAME, FAST_PAIR_RFCOMM_UUID); 135 } catch (IOException e) { 136 log("Create service record failed, stop server"); 137 stopServer(); 138 return; 139 } 140 141 updateState(ACCEPTING); 142 new Thread(() -> accept(mServerSocket)).start(); 143 } 144 accept(BluetoothServerSocket serverSocket)145 private void accept(BluetoothServerSocket serverSocket) { 146 triggerCountdownLatch(); 147 148 try { 149 BluetoothSocket socket = serverSocket.accept(); 150 serverSocket.close(); 151 152 runInControllerExecutor(() -> startListen(socket)); 153 } catch (IOException e) { 154 log("IOException when accepting new connection"); 155 runInControllerExecutor(() -> handleAcceptException(serverSocket)); 156 } 157 } 158 handleAcceptException(BluetoothServerSocket serverSocket)159 private void handleAcceptException(BluetoothServerSocket serverSocket) { 160 if (mIsStopRequested) { 161 stopServer(); 162 } else { 163 closeServerSocket(serverSocket); 164 restartServer(); 165 } 166 } 167 startListen(BluetoothSocket bluetoothSocket)168 private void startListen(BluetoothSocket bluetoothSocket) { 169 if (mIsStopRequested) { 170 closeSocket(bluetoothSocket); 171 stopServer(); 172 return; 173 } 174 175 updateState(CONNECTED); 176 // Sets method parameter to global socket for stop() API. 177 this.mSocket = bluetoothSocket; 178 new Thread(() -> listen(bluetoothSocket)).start(); 179 } 180 listen(BluetoothSocket bluetoothSocket)181 private void listen(BluetoothSocket bluetoothSocket) { 182 triggerCountdownLatch(); 183 184 try { 185 DataInputStream dataInputStream = new DataInputStream(bluetoothSocket.getInputStream()); 186 while (true) { 187 int eventGroup = dataInputStream.readUnsignedByte(); 188 int eventCode = dataInputStream.readUnsignedByte(); 189 int additionalLength = dataInputStream.readUnsignedShort(); 190 191 byte[] data = new byte[additionalLength]; 192 if (additionalLength > 0) { 193 int count = 0; 194 do { 195 count += dataInputStream.read(data, count, additionalLength - count); 196 } while (count < additionalLength); 197 } 198 199 if (mRequestHandler != null) { 200 // In order not to block listening thread, use different thread to dispatch 201 // message. 202 mReceiveMessageExecutor.execute( 203 () -> { 204 mRequestHandler.handleRequest(eventGroup, eventCode, data); 205 triggerCountdownLatch(); 206 }); 207 } 208 } 209 } catch (IOException e) { 210 log( 211 String.format( 212 "IOException when listening to %s", 213 bluetoothSocket.getRemoteDevice().getAddress())); 214 runInControllerExecutor(() -> handleListenException(bluetoothSocket)); 215 } 216 } 217 handleListenException(BluetoothSocket bluetoothSocket)218 private void handleListenException(BluetoothSocket bluetoothSocket) { 219 if (mIsStopRequested) { 220 stopServer(); 221 } else { 222 closeSocket(bluetoothSocket); 223 restartServer(); 224 } 225 } 226 sendFakeEventStreamMessage(EventStreamProtocol.EventGroup eventGroup)227 public void sendFakeEventStreamMessage(EventStreamProtocol.EventGroup eventGroup) { 228 switch (eventGroup) { 229 case BLUETOOTH: 230 send(EventStreamProtocol.EventGroup.BLUETOOTH_VALUE, 231 EventStreamProtocol.BluetoothEventCode.BLUETOOTH_ENABLE_SILENCE_MODE_VALUE, 232 new byte[0]); 233 break; 234 case LOGGING: 235 send(EventStreamProtocol.EventGroup.LOGGING_VALUE, 236 EventStreamProtocol.LoggingEventCode.LOG_FULL_VALUE, 237 new byte[0]); 238 break; 239 case DEVICE: 240 send(EventStreamProtocol.EventGroup.DEVICE_VALUE, 241 EventStreamProtocol.DeviceEventCode.DEVICE_BATTERY_INFO_VALUE, 242 new byte[]{0x11, 0x12, 0x13}); 243 break; 244 default: // fall out 245 } 246 } 247 sendFakeEventStreamLoggingMessage(@ullable String logContent)248 public void sendFakeEventStreamLoggingMessage(@Nullable String logContent) { 249 send(EventStreamProtocol.EventGroup.LOGGING_VALUE, 250 EventStreamProtocol.LoggingEventCode.LOG_SAVE_TO_BUFFER_VALUE, 251 logContent != null ? logContent.getBytes(UTF_8) : new byte[0]); 252 } 253 send(int eventGroup, int eventCode, byte[] data)254 public void send(int eventGroup, int eventCode, byte[] data) { 255 runInControllerExecutor( 256 () -> { 257 if (!CONNECTED.equals(mState)) { 258 log("Server is not in CONNECTED state, skip send request"); 259 return; 260 } 261 BluetoothSocket bluetoothSocket = this.mSocket; 262 mSendMessageExecutor.execute(() -> { 263 String address = bluetoothSocket.getRemoteDevice().getAddress(); 264 try { 265 DataOutputStream dataOutputStream = 266 new DataOutputStream(bluetoothSocket.getOutputStream()); 267 dataOutputStream.writeByte(eventGroup); 268 dataOutputStream.writeByte(eventCode); 269 dataOutputStream.writeShort(data.length); 270 if (data.length > 0) { 271 dataOutputStream.write(data); 272 } 273 dataOutputStream.flush(); 274 log( 275 String.format( 276 "Send message to %s: %s, %s, %s.", 277 address, eventGroup, eventCode, data.length)); 278 } catch (IOException e) { 279 log( 280 String.format( 281 "Failed to send message to %s: %s, %s, %s.", 282 address, eventGroup, eventCode, data.length), 283 e); 284 } 285 }); 286 }); 287 } 288 289 /** Stops the rfcomm server. */ stop()290 public void stop() { 291 runInControllerExecutor(() -> { 292 log("Stop RfcommServer"); 293 294 if (STOPPED.equals(mState)) { 295 log("Server is stopped, skip stop request."); 296 return; 297 } 298 299 if (mIsStopRequested) { 300 log("Stop is already requested, skip stop request."); 301 return; 302 } 303 mIsStopRequested = true; 304 305 if (ACCEPTING.equals(mState)) { 306 closeServerSocket(mServerSocket); 307 } 308 309 if (CONNECTED.equals(mState)) { 310 closeSocket(mSocket); 311 } 312 }); 313 } 314 stopServer()315 private void stopServer() { 316 updateState(STOPPED); 317 triggerCountdownLatch(); 318 } 319 updateState(State newState)320 private void updateState(State newState) { 321 log(String.format("Change state from %s to %s", mState, newState)); 322 if (mStateMonitor != null) { 323 mStateMonitor.onStateChanged(newState); 324 } 325 mState = newState; 326 } 327 closeServerSocket(BluetoothServerSocket serverSocket)328 private void closeServerSocket(BluetoothServerSocket serverSocket) { 329 try { 330 if (serverSocket != null) { 331 log(String.format("Close server socket: %s", serverSocket)); 332 serverSocket.close(); 333 } 334 } catch (IOException | NullPointerException e) { 335 // NullPointerException is used to skip robolectric test failure. 336 // In unit test, different virtual devices are set up in different threads, calling 337 // ServerSocket.close() in wrong thread will result in NullPointerException since there 338 // is no corresponding service record. 339 // TODO(hylo): Remove NullPointerException when the solution is submitted to test cases. 340 log("Failed to stop server", e); 341 } 342 } 343 closeSocket(BluetoothSocket socket)344 private void closeSocket(BluetoothSocket socket) { 345 try { 346 if (socket != null && socket.isConnected()) { 347 log(String.format("Close socket: %s", socket.getRemoteDevice().getAddress())); 348 socket.close(); 349 } 350 } catch (IOException e) { 351 log(String.format("IOException when close socket %s", 352 socket.getRemoteDevice().getAddress())); 353 } 354 } 355 runInControllerExecutor(Runnable runnable)356 private void runInControllerExecutor(Runnable runnable) { 357 mControllerExecutor.execute(runnable); 358 } 359 log(String message)360 private void log(String message) { 361 mLogger.log("Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message); 362 } 363 log(String message, Throwable e)364 private void log(String message, Throwable e) { 365 mLogger.log(e, "Server=%s, %s", FAST_PAIR_RFCOMM_SERVICE_NAME, message); 366 } 367 triggerCountdownLatch()368 private void triggerCountdownLatch() { 369 if (mCountDownLatch != null) { 370 mCountDownLatch.countDown(); 371 } 372 } 373 374 /** Interface to handle incoming request from clients. */ 375 public interface RequestHandler { handleRequest(int eventGroup, int eventCode, byte[] data)376 void handleRequest(int eventGroup, int eventCode, byte[] data); 377 } 378 setRequestHandler(@ullable RequestHandler requestHandler)379 public void setRequestHandler(@Nullable RequestHandler requestHandler) { 380 this.mRequestHandler = requestHandler; 381 } 382 383 /** A state monitor to send signal when state is changed. */ 384 public interface StateMonitor { onStateChanged(State state)385 void onStateChanged(State state); 386 } 387 setStateMonitor(@ullable StateMonitor stateMonitor)388 public void setStateMonitor(@Nullable StateMonitor stateMonitor) { 389 this.mStateMonitor = stateMonitor; 390 } 391 392 @VisibleForTesting setCountDownLatch(@ullable CountDownLatch countDownLatch)393 void setCountDownLatch(@Nullable CountDownLatch countDownLatch) { 394 this.mCountDownLatch = countDownLatch; 395 } 396 397 @VisibleForTesting setIsStopRequested(boolean isStopRequested)398 void setIsStopRequested(boolean isStopRequested) { 399 this.mIsStopRequested = isStopRequested; 400 } 401 402 @VisibleForTesting simulateAcceptIOException()403 void simulateAcceptIOException() { 404 runInControllerExecutor(() -> { 405 if (ACCEPTING.equals(mState)) { 406 closeServerSocket(mServerSocket); 407 } 408 }); 409 } 410 411 @VisibleForTesting simulateListenIOException()412 void simulateListenIOException() { 413 runInControllerExecutor(() -> { 414 if (CONNECTED.equals(mState)) { 415 closeSocket(mSocket); 416 } 417 }); 418 } 419 } 420