• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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