• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 HIMSA II K/S - www.himsa.com.
3  * Represented by EHIMA - www.ehima.com
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 // Bluetooth Hap Client StateMachine. There is one instance per remote device.
19 //  - "Disconnected" and "Connected" are steady states.
20 //  - "Connecting" and "Disconnecting" are transient states until the
21 //     connection / disconnection is completed.
22 
23 //                        (Disconnected)
24 //                           |       ^
25 //                   CONNECT |       | DISCONNECTED
26 //                           V       |
27 //                 (Connecting)<--->(Disconnecting)
28 //                           |       ^
29 //                 CONNECTED |       | DISCONNECT
30 //                           V       |
31 //                          (Connected)
32 // NOTES:
33 //  - If state machine is in "Connecting" state and the remote device sends
34 //    DISCONNECT request, the state machine transitions to "Disconnecting" state.
35 //  - Similarly, if the state machine is in "Disconnecting" state and the remote device
36 //    sends CONNECT request, the state machine transitions to "Connecting" state.
37 
38 //                    DISCONNECT
39 //    (Connecting) ---------------> (Disconnecting)
40 //                 <---------------
41 //                      CONNECT
42 package com.android.bluetooth.hap;
43 
44 import static android.Manifest.permission.BLUETOOTH_CONNECT;
45 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
46 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
47 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING;
48 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
49 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING;
50 import static android.bluetooth.BluetoothProfile.getConnectionStateName;
51 
52 import android.bluetooth.BluetoothDevice;
53 import android.bluetooth.BluetoothHapClient;
54 import android.bluetooth.BluetoothProfile;
55 import android.content.Intent;
56 import android.os.Looper;
57 import android.os.Message;
58 import android.util.Log;
59 
60 import com.android.bluetooth.btservice.ProfileService;
61 import com.android.internal.annotations.VisibleForTesting;
62 import com.android.internal.util.State;
63 import com.android.internal.util.StateMachine;
64 
65 import java.io.FileDescriptor;
66 import java.io.PrintWriter;
67 import java.io.StringWriter;
68 import java.time.Duration;
69 import java.util.Scanner;
70 
71 final class HapClientStateMachine extends StateMachine {
72     private static final String TAG = HapClientStateMachine.class.getSimpleName();
73 
74     static final int MESSAGE_CONNECT = 1;
75     static final int MESSAGE_DISCONNECT = 2;
76     static final int MESSAGE_STACK_EVENT = 101;
77     @VisibleForTesting static final int MESSAGE_CONNECT_TIMEOUT = 201;
78 
79     @VisibleForTesting static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(30);
80 
81     private final Disconnected mDisconnected;
82     private final Connecting mConnecting;
83     private final Disconnecting mDisconnecting;
84     private final Connected mConnected;
85 
86     private int mConnectionState = STATE_DISCONNECTED;
87     private int mLastConnectionState = -1;
88 
89     private final HapClientService mService;
90     private final HapClientNativeInterface mNativeInterface;
91     private final BluetoothDevice mDevice;
92 
HapClientStateMachine( HapClientService svc, BluetoothDevice device, HapClientNativeInterface gattInterface, Looper looper)93     HapClientStateMachine(
94             HapClientService svc,
95             BluetoothDevice device,
96             HapClientNativeInterface gattInterface,
97             Looper looper) {
98         super(TAG, looper);
99         mDevice = device;
100         mService = svc;
101         mNativeInterface = gattInterface;
102         mDisconnected = new Disconnected();
103         mConnecting = new Connecting();
104         mDisconnecting = new Disconnecting();
105         mConnected = new Connected();
106 
107         addState(mDisconnected);
108         addState(mConnecting);
109         addState(mDisconnecting);
110         addState(mConnected);
111 
112         setInitialState(mDisconnected);
113         start();
114     }
115 
messageWhatToString(int what)116     private static String messageWhatToString(int what) {
117         return switch (what) {
118             case MESSAGE_CONNECT -> "CONNECT";
119             case MESSAGE_DISCONNECT -> "DISCONNECT";
120             case MESSAGE_STACK_EVENT -> "STACK_EVENT";
121             case MESSAGE_CONNECT_TIMEOUT -> "CONNECT_TIMEOUT";
122             default -> Integer.toString(what);
123         };
124     }
125 
doQuit()126     public void doQuit() {
127         Log.d(TAG, "doQuit for " + mDevice);
128         quitNow();
129     }
130 
getConnectionState()131     int getConnectionState() {
132         return mConnectionState;
133     }
134 
getDevice()135     BluetoothDevice getDevice() {
136         return mDevice;
137     }
138 
isConnected()139     synchronized boolean isConnected() {
140         return (getConnectionState() == STATE_CONNECTED);
141     }
142 
broadcastConnectionState()143     private void broadcastConnectionState() {
144         Log.d(
145                 TAG,
146                 ("Connection state " + mDevice + ": ")
147                         + getConnectionStateName(mLastConnectionState)
148                         + "->"
149                         + getConnectionStateName(mConnectionState));
150 
151         mService.connectionStateChanged(mDevice, mLastConnectionState, mConnectionState);
152         Intent intent =
153                 new Intent(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED)
154                         .putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mLastConnectionState)
155                         .putExtra(BluetoothProfile.EXTRA_STATE, mConnectionState)
156                         .putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice)
157                         .addFlags(
158                                 Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
159                                         | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
160         mService.getBaseContext()
161                 .sendBroadcastWithMultiplePermissions(
162                         intent, new String[] {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED});
163     }
164 
dump(StringBuilder sb)165     public void dump(StringBuilder sb) {
166         ProfileService.println(sb, "mDevice: " + mDevice);
167         ProfileService.println(sb, "  StateMachine: " + this);
168         // Dump the state machine logs
169         StringWriter stringWriter = new StringWriter();
170         PrintWriter printWriter = new PrintWriter(stringWriter);
171         super.dump(new FileDescriptor(), printWriter, new String[] {});
172         printWriter.flush();
173         stringWriter.flush();
174         ProfileService.println(sb, "  StateMachineLog:");
175         Scanner scanner = new Scanner(stringWriter.toString());
176         while (scanner.hasNextLine()) {
177             String line = scanner.nextLine();
178             ProfileService.println(sb, "    " + line);
179         }
180         scanner.close();
181     }
182 
183     @VisibleForTesting
184     class Disconnected extends State {
185         private final String mStateLog = "Disconnected(" + mDevice + "): ";
186 
187         @Override
enter()188         public void enter() {
189             Log.i(TAG, "Enter " + mStateLog + messageWhatToString(getCurrentMessage().what));
190 
191             removeDeferredMessages(MESSAGE_DISCONNECT);
192 
193             mConnectionState = STATE_DISCONNECTED;
194             if (mLastConnectionState != -1) { // Don't broadcast during startup
195                 broadcastConnectionState();
196             }
197         }
198 
199         @Override
exit()200         public void exit() {
201             Log.d(TAG, "Exit " + mStateLog + messageWhatToString(getCurrentMessage().what));
202             mLastConnectionState = STATE_DISCONNECTED;
203         }
204 
205         @Override
processMessage(Message message)206         public boolean processMessage(Message message) {
207             Log.d(TAG, mStateLog + "processMessage: " + messageWhatToString(message.what));
208 
209             switch (message.what) {
210                 case MESSAGE_CONNECT -> {
211                     if (!mNativeInterface.connectHapClient(mDevice)) {
212                         Log.e(TAG, mStateLog + "native cannot connect");
213                         break;
214                     }
215                     if (mService.okToConnect(mDevice)) {
216                         transitionTo(mConnecting);
217                     } else {
218                         Log.w(TAG, mStateLog + "outgoing connect request rejected");
219                     }
220                 }
221                 case MESSAGE_DISCONNECT -> {
222                     mNativeInterface.disconnectHapClient(mDevice);
223                 }
224                 case MESSAGE_STACK_EVENT -> {
225                     HapClientStackEvent event = (HapClientStackEvent) message.obj;
226                     switch (event.type) {
227                         case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> {
228                             processConnectionEvent(event.valueInt1);
229                         }
230                         default -> Log.e(TAG, mStateLog + "ignoring stack event: " + event);
231                     }
232                 }
233                 default -> {
234                     Log.e(TAG, mStateLog + "not handled: " + messageWhatToString(message.what));
235                     return NOT_HANDLED;
236                 }
237             }
238             return HANDLED;
239         }
240 
241         // in Disconnected state
processConnectionEvent(int state)242         private void processConnectionEvent(int state) {
243             switch (state) {
244                 case STATE_CONNECTING -> {
245                     if (mService.okToConnect(mDevice)) {
246                         Log.i(TAG, mStateLog + "Incoming connecting request accepted");
247                         transitionTo(mConnecting);
248                     } else {
249                         Log.w(TAG, mStateLog + "Incoming connecting request rejected");
250                         mNativeInterface.disconnectHapClient(mDevice);
251                     }
252                 }
253                 case STATE_CONNECTED -> {
254                     Log.w(TAG, "HearingAccess Connected from Disconnected state: " + mDevice);
255                     if (mService.okToConnect(mDevice)) {
256                         Log.w(TAG, mStateLog + "Incoming connected transition accepted");
257                         transitionTo(mConnected);
258                     } else {
259                         Log.w(TAG, mStateLog + "Incoming connected transition rejected");
260                         mNativeInterface.disconnectHapClient(mDevice);
261                     }
262                 }
263                 default -> Log.e(TAG, mStateLog + "Incorrect state: " + state);
264             }
265         }
266     }
267 
268     @VisibleForTesting
269     class Connecting extends State {
270         private final String mStateLog = "Connecting(" + mDevice + "): ";
271 
272         @Override
enter()273         public void enter() {
274             Log.i(TAG, "Enter " + mStateLog + messageWhatToString(getCurrentMessage().what));
275             sendMessageDelayed(MESSAGE_CONNECT_TIMEOUT, CONNECT_TIMEOUT.toMillis());
276             mConnectionState = STATE_CONNECTING;
277             broadcastConnectionState();
278         }
279 
280         @Override
exit()281         public void exit() {
282             Log.d(TAG, "Exit " + mStateLog + messageWhatToString(getCurrentMessage().what));
283             mLastConnectionState = STATE_CONNECTING;
284             removeMessages(MESSAGE_CONNECT_TIMEOUT);
285         }
286 
287         @Override
processMessage(Message message)288         public boolean processMessage(Message message) {
289             Log.d(TAG, mStateLog + "processMessage: " + messageWhatToString(message.what));
290 
291             switch (message.what) {
292                 case MESSAGE_CONNECT -> deferMessage(message);
293                 case MESSAGE_CONNECT_TIMEOUT -> {
294                     Log.w(TAG, mStateLog + "connection timeout");
295                     mNativeInterface.disconnectHapClient(mDevice);
296                     HapClientStackEvent disconnectEvent =
297                             new HapClientStackEvent(
298                                     HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
299                     disconnectEvent.device = mDevice;
300                     disconnectEvent.valueInt1 = STATE_DISCONNECTED;
301                     sendMessage(MESSAGE_STACK_EVENT, disconnectEvent);
302                 }
303                 case MESSAGE_DISCONNECT -> {
304                     Log.d(TAG, mStateLog + "connection canceled");
305                     mNativeInterface.disconnectHapClient(mDevice);
306                     transitionTo(mDisconnected);
307                 }
308                 case MESSAGE_STACK_EVENT -> {
309                     HapClientStackEvent event = (HapClientStackEvent) message.obj;
310                     switch (event.type) {
311                         case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> {
312                             processConnectionEvent(event.valueInt1);
313                         }
314                         default -> Log.e(TAG, mStateLog + "ignoring stack event: " + event);
315                     }
316                 }
317                 default -> {
318                     Log.e(TAG, mStateLog + "not handled: " + messageWhatToString(message.what));
319                     return NOT_HANDLED;
320                 }
321             }
322             return HANDLED;
323         }
324 
325         // in Connecting state
processConnectionEvent(int state)326         private void processConnectionEvent(int state) {
327             switch (state) {
328                 case STATE_DISCONNECTED -> {
329                     Log.i(TAG, mStateLog + "device disconnected");
330                     transitionTo(mDisconnected);
331                 }
332                 case STATE_CONNECTED -> transitionTo(mConnected);
333                 case STATE_DISCONNECTING -> {
334                     Log.i(TAG, mStateLog + "device disconnecting");
335                     transitionTo(mDisconnecting);
336                 }
337                 default -> Log.e(TAG, mStateLog + "Incorrect state: " + state);
338             }
339         }
340     }
341 
342     @VisibleForTesting
343     class Disconnecting extends State {
344         private final String mStateLog = "Disconnecting(" + mDevice + "): ";
345 
346         @Override
enter()347         public void enter() {
348             Log.i(TAG, "Enter " + mStateLog + messageWhatToString(getCurrentMessage().what));
349             sendMessageDelayed(MESSAGE_CONNECT_TIMEOUT, CONNECT_TIMEOUT.toMillis());
350             mConnectionState = STATE_DISCONNECTING;
351             broadcastConnectionState();
352         }
353 
354         @Override
exit()355         public void exit() {
356             Log.d(TAG, "Exit " + mStateLog + messageWhatToString(getCurrentMessage().what));
357             mLastConnectionState = STATE_DISCONNECTING;
358             removeMessages(MESSAGE_CONNECT_TIMEOUT);
359         }
360 
361         @Override
processMessage(Message message)362         public boolean processMessage(Message message) {
363             Log.d(TAG, mStateLog + "processMessage: " + messageWhatToString(message.what));
364 
365             switch (message.what) {
366                 case MESSAGE_CONNECT, MESSAGE_DISCONNECT -> deferMessage(message);
367                 case MESSAGE_CONNECT_TIMEOUT -> {
368                     Log.w(TAG, mStateLog + "connection timeout");
369                     mNativeInterface.disconnectHapClient(mDevice);
370 
371                     HapClientStackEvent disconnectEvent =
372                             new HapClientStackEvent(
373                                     HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
374                     disconnectEvent.device = mDevice;
375                     disconnectEvent.valueInt1 = STATE_DISCONNECTED;
376                     sendMessage(MESSAGE_STACK_EVENT, disconnectEvent);
377                 }
378                 case MESSAGE_STACK_EVENT -> {
379                     HapClientStackEvent event = (HapClientStackEvent) message.obj;
380                     switch (event.type) {
381                         case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> {
382                             processConnectionEvent(event.valueInt1);
383                         }
384                         default -> Log.e(TAG, mStateLog + "ignoring stack event: " + event);
385                     }
386                 }
387                 default -> {
388                     Log.e(TAG, mStateLog + "not handled: " + messageWhatToString(message.what));
389                     return NOT_HANDLED;
390                 }
391             }
392             return HANDLED;
393         }
394 
395         // in Disconnecting state
processConnectionEvent(int state)396         private void processConnectionEvent(int state) {
397             switch (state) {
398                 case STATE_DISCONNECTED -> {
399                     Log.i(TAG, mStateLog + "Disconnected");
400                     transitionTo(mDisconnected);
401                 }
402                 case STATE_CONNECTED -> {
403                     if (mService.okToConnect(mDevice)) {
404                         Log.w(TAG, mStateLog + "interrupted: device is connected");
405                         transitionTo(mConnected);
406                     } else {
407                         // Reject the connection and stay in Disconnecting state
408                         Log.w(TAG, mStateLog + "Incoming connect request rejected");
409                         mNativeInterface.disconnectHapClient(mDevice);
410                     }
411                 }
412                 case STATE_CONNECTING -> {
413                     if (mService.okToConnect(mDevice)) {
414                         Log.i(TAG, mStateLog + "interrupted: device try to reconnect");
415                         transitionTo(mConnecting);
416                     } else {
417                         // Reject the connection and stay in Disconnecting state
418                         Log.w(TAG, mStateLog + "Incoming connecting request rejected");
419                         mNativeInterface.disconnectHapClient(mDevice);
420                     }
421                 }
422                 default -> Log.e(TAG, mStateLog + "Incorrect state: " + state);
423             }
424         }
425     }
426 
427     @VisibleForTesting
428     class Connected extends State {
429         private final String mStateLog = "Connected(" + mDevice + "): ";
430 
431         @Override
enter()432         public void enter() {
433             Log.i(TAG, "Enter " + mStateLog + messageWhatToString(getCurrentMessage().what));
434             removeDeferredMessages(MESSAGE_CONNECT);
435             mConnectionState = STATE_CONNECTED;
436             broadcastConnectionState();
437         }
438 
439         @Override
exit()440         public void exit() {
441             Log.d(TAG, "Exit " + mStateLog + messageWhatToString(getCurrentMessage().what));
442             mLastConnectionState = STATE_CONNECTED;
443         }
444 
445         @Override
processMessage(Message message)446         public boolean processMessage(Message message) {
447             Log.d(TAG, mStateLog + "processMessage: " + messageWhatToString(message.what));
448 
449             switch (message.what) {
450                 case MESSAGE_DISCONNECT -> {
451                     if (!mNativeInterface.disconnectHapClient(mDevice)) {
452                         // If error in the native stack, transition directly to Disconnected state.
453                         Log.e(TAG, mStateLog + "native cannot disconnect");
454                         transitionTo(mDisconnected);
455                         break;
456                     }
457                     transitionTo(mDisconnecting);
458                 }
459                 case MESSAGE_STACK_EVENT -> {
460                     HapClientStackEvent event = (HapClientStackEvent) message.obj;
461                     switch (event.type) {
462                         case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED ->
463                                 processConnectionEvent(event.valueInt1);
464                         default -> Log.e(TAG, mStateLog + "ignoring stack event: " + event);
465                     }
466                 }
467                 default -> {
468                     Log.e(TAG, mStateLog + "not handled: " + messageWhatToString(message.what));
469                     return NOT_HANDLED;
470                 }
471             }
472             return HANDLED;
473         }
474 
475         // in Connected state
processConnectionEvent(int state)476         private void processConnectionEvent(int state) {
477             switch (state) {
478                 case STATE_DISCONNECTED -> {
479                     Log.i(TAG, mStateLog + "Disconnected but still in allowlist");
480                     transitionTo(mDisconnected);
481                 }
482                 case STATE_DISCONNECTING -> {
483                     Log.i(TAG, mStateLog + "Disconnecting");
484                     transitionTo(mDisconnecting);
485                 }
486                 default -> Log.e(TAG, mStateLog + "Incorrect state: " + state);
487             }
488         }
489     }
490 }
491