1 /*
2  * Copyright 2018 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 /**
18  * Bluetooth HearingAid 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  *
24  *                        (Disconnected)
25  *                           |       ^
26  *                   CONNECT |       | DISCONNECTED
27  *                           V       |
28  *                 (Connecting)<--->(Disconnecting)
29  *                           |       ^
30  *                 CONNECTED |       | DISCONNECT
31  *                           V       |
32  *                          (Connected)
33  * NOTES:
34  *  - If state machine is in "Connecting" state and the remote device sends
35  *    DISCONNECT request, the state machine transitions to "Disconnecting" state.
36  *  - Similarly, if the state machine is in "Disconnecting" state and the remote device
37  *    sends CONNECT request, the state machine transitions to "Connecting" state.
38  *
39  *                    DISCONNECT
40  *    (Connecting) ---------------> (Disconnecting)
41  *                 <---------------
42  *                      CONNECT
43  *
44  */
45 
46 package com.android.bluetooth.hearingaid;
47 
48 import static android.Manifest.permission.BLUETOOTH_CONNECT;
49 
50 import android.bluetooth.BluetoothDevice;
51 import android.bluetooth.BluetoothHearingAid;
52 import android.bluetooth.BluetoothProfile;
53 import android.content.Intent;
54 import android.os.Looper;
55 import android.os.Message;
56 import android.util.Log;
57 
58 import com.android.bluetooth.Utils;
59 import com.android.bluetooth.btservice.ProfileService;
60 import com.android.bluetooth.statemachine.State;
61 import com.android.bluetooth.statemachine.StateMachine;
62 import com.android.internal.annotations.VisibleForTesting;
63 
64 import java.io.FileDescriptor;
65 import java.io.PrintWriter;
66 import java.io.StringWriter;
67 import java.util.Scanner;
68 
69 final class HearingAidStateMachine extends StateMachine {
70     private static final boolean DBG = false;
71     private static final String TAG = "HearingAidStateMachine";
72 
73     static final int CONNECT = 1;
74     static final int DISCONNECT = 2;
75     @VisibleForTesting
76     static final int STACK_EVENT = 101;
77     private static final int CONNECT_TIMEOUT = 201;
78 
79     // NOTE: the value is not "final" - it is modified in the unit tests
80     @VisibleForTesting
81     static int sConnectTimeoutMs = 30000;        // 30s
82 
83     private Disconnected mDisconnected;
84     private Connecting mConnecting;
85     private Disconnecting mDisconnecting;
86     private Connected mConnected;
87     private int mConnectionState = BluetoothProfile.STATE_DISCONNECTED;
88     private int mLastConnectionState = -1;
89 
90     private HearingAidService mService;
91     private HearingAidNativeInterface mNativeInterface;
92 
93     private final BluetoothDevice mDevice;
94 
HearingAidStateMachine(BluetoothDevice device, HearingAidService svc, HearingAidNativeInterface nativeInterface, Looper looper)95     HearingAidStateMachine(BluetoothDevice device, HearingAidService svc,
96             HearingAidNativeInterface nativeInterface, Looper looper) {
97         super(TAG, looper);
98         mDevice = device;
99         mService = svc;
100         mNativeInterface = nativeInterface;
101 
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     }
114 
make(BluetoothDevice device, HearingAidService svc, HearingAidNativeInterface nativeInterface, Looper looper)115     static HearingAidStateMachine make(BluetoothDevice device, HearingAidService svc,
116             HearingAidNativeInterface nativeInterface, Looper looper) {
117         Log.i(TAG, "make for device " + device);
118         HearingAidStateMachine HearingAidSm = new HearingAidStateMachine(device, svc,
119                 nativeInterface, looper);
120         HearingAidSm.start();
121         return HearingAidSm;
122     }
123 
doQuit()124     public void doQuit() {
125         log("doQuit for device " + mDevice);
126         quitNow();
127     }
128 
cleanup()129     public void cleanup() {
130         log("cleanup for device " + mDevice);
131     }
132 
133     @VisibleForTesting
134     class Disconnected extends State {
135         @Override
enter()136         public void enter() {
137             Log.i(TAG, "Enter Disconnected(" + mDevice + "): " + messageWhatToString(
138                     getCurrentMessage().what));
139             mConnectionState = BluetoothProfile.STATE_DISCONNECTED;
140 
141             removeDeferredMessages(DISCONNECT);
142 
143             if (mLastConnectionState != -1) {
144                 // Don't broadcast during startup
145                 broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTED,
146                         mLastConnectionState);
147             }
148         }
149 
150         @Override
exit()151         public void exit() {
152             log("Exit Disconnected(" + mDevice + "): " + messageWhatToString(
153                     getCurrentMessage().what));
154             mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED;
155         }
156 
157         @Override
processMessage(Message message)158         public boolean processMessage(Message message) {
159             log("Disconnected process message(" + mDevice + "): " + messageWhatToString(
160                     message.what));
161 
162             switch (message.what) {
163                 case CONNECT:
164                     log("Connecting to " + mDevice);
165                     if (!mNativeInterface.connectHearingAid(mDevice)) {
166                         Log.e(TAG, "Disconnected: error connecting to " + mDevice);
167                         break;
168                     }
169                     if (mService.okToConnect(mDevice)) {
170                         transitionTo(mConnecting);
171                     } else {
172                         // Reject the request and stay in Disconnected state
173                         Log.w(TAG, "Outgoing HearingAid Connecting request rejected: " + mDevice);
174                     }
175                     break;
176                 case DISCONNECT:
177                     Log.d(TAG, "Disconnected: DISCONNECT: call native disconnect for " + mDevice);
178                     mNativeInterface.disconnectHearingAid(mDevice);
179                     break;
180                 case STACK_EVENT:
181                     HearingAidStackEvent event = (HearingAidStackEvent) message.obj;
182                     if (DBG) {
183                         Log.d(TAG, "Disconnected: stack event: " + event);
184                     }
185                     if (!mDevice.equals(event.device)) {
186                         Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
187                     }
188                     switch (event.type) {
189                         case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
190                             processConnectionEvent(event.valueInt1);
191                             break;
192                         default:
193                             Log.e(TAG, "Disconnected: ignoring stack event: " + event);
194                             break;
195                     }
196                     break;
197                 default:
198                     return NOT_HANDLED;
199             }
200             return HANDLED;
201         }
202 
203         // in Disconnected state
processConnectionEvent(int state)204         private void processConnectionEvent(int state) {
205             switch (state) {
206                 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED:
207                     Log.w(TAG, "Ignore HearingAid DISCONNECTED event: " + mDevice);
208                     break;
209                 case HearingAidStackEvent.CONNECTION_STATE_CONNECTING:
210                     if (mService.okToConnect(mDevice)) {
211                         Log.i(TAG, "Incoming HearingAid Connecting request accepted: " + mDevice);
212                         transitionTo(mConnecting);
213                     } else {
214                         // Reject the connection and stay in Disconnected state itself
215                         Log.w(TAG, "Incoming HearingAid Connecting request rejected: " + mDevice);
216                         mNativeInterface.disconnectHearingAid(mDevice);
217                     }
218                     break;
219                 case HearingAidStackEvent.CONNECTION_STATE_CONNECTED:
220                     Log.w(TAG, "HearingAid Connected from Disconnected state: " + mDevice);
221                     if (mService.okToConnect(mDevice)) {
222                         Log.i(TAG, "Incoming HearingAid Connected request accepted: " + mDevice);
223                         transitionTo(mConnected);
224                     } else {
225                         // Reject the connection and stay in Disconnected state itself
226                         Log.w(TAG, "Incoming HearingAid Connected request rejected: " + mDevice);
227                         mNativeInterface.disconnectHearingAid(mDevice);
228                     }
229                     break;
230                 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING:
231                     Log.w(TAG, "Ignore HearingAid DISCONNECTING event: " + mDevice);
232                     break;
233                 default:
234                     Log.e(TAG, "Incorrect state: " + state + " device: " + mDevice);
235                     break;
236             }
237         }
238     }
239 
240     @VisibleForTesting
241     class Connecting extends State {
242         @Override
enter()243         public void enter() {
244             Log.i(TAG, "Enter Connecting(" + mDevice + "): "
245                     + messageWhatToString(getCurrentMessage().what));
246             sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
247             mConnectionState = BluetoothProfile.STATE_CONNECTING;
248             broadcastConnectionState(BluetoothProfile.STATE_CONNECTING, mLastConnectionState);
249         }
250 
251         @Override
exit()252         public void exit() {
253             log("Exit Connecting(" + mDevice + "): "
254                     + messageWhatToString(getCurrentMessage().what));
255             mLastConnectionState = BluetoothProfile.STATE_CONNECTING;
256             removeMessages(CONNECT_TIMEOUT);
257         }
258 
259         @Override
processMessage(Message message)260         public boolean processMessage(Message message) {
261             log("Connecting process message(" + mDevice + "): "
262                     + messageWhatToString(message.what));
263 
264             switch (message.what) {
265                 case CONNECT:
266                     deferMessage(message);
267                     break;
268                 case CONNECT_TIMEOUT:
269                     Log.w(TAG, "Connecting connection timeout: " + mDevice);
270                     mNativeInterface.disconnectHearingAid(mDevice);
271                     if (mService.isConnectedPeerDevices(mDevice)) {
272                         Log.w(TAG, "One side connection timeout: " + mDevice + ". Try acceptlist");
273                         mNativeInterface.addToAcceptlist(mDevice);
274                     }
275                     HearingAidStackEvent disconnectEvent =
276                             new HearingAidStackEvent(
277                                     HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
278                     disconnectEvent.device = mDevice;
279                     disconnectEvent.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED;
280                     sendMessage(STACK_EVENT, disconnectEvent);
281                     break;
282                 case DISCONNECT:
283                     log("Connecting: connection canceled to " + mDevice);
284                     mNativeInterface.disconnectHearingAid(mDevice);
285                     transitionTo(mDisconnected);
286                     break;
287                 case STACK_EVENT:
288                     HearingAidStackEvent event = (HearingAidStackEvent) message.obj;
289                     log("Connecting: stack event: " + event);
290                     if (!mDevice.equals(event.device)) {
291                         Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
292                     }
293                     switch (event.type) {
294                         case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
295                             processConnectionEvent(event.valueInt1);
296                             break;
297                         default:
298                             Log.e(TAG, "Connecting: ignoring stack event: " + event);
299                             break;
300                     }
301                     break;
302                 default:
303                     return NOT_HANDLED;
304             }
305             return HANDLED;
306         }
307 
308         // in Connecting state
processConnectionEvent(int state)309         private void processConnectionEvent(int state) {
310             switch (state) {
311                 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED:
312                     Log.w(TAG, "Connecting device disconnected: " + mDevice);
313                     transitionTo(mDisconnected);
314                     break;
315                 case HearingAidStackEvent.CONNECTION_STATE_CONNECTED:
316                     transitionTo(mConnected);
317                     break;
318                 case HearingAidStackEvent.CONNECTION_STATE_CONNECTING:
319                     break;
320                 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING:
321                     Log.w(TAG, "Connecting interrupted: device is disconnecting: " + mDevice);
322                     transitionTo(mDisconnecting);
323                     break;
324                 default:
325                     Log.e(TAG, "Incorrect state: " + state);
326                     break;
327             }
328         }
329     }
330 
331     @VisibleForTesting
332     class Disconnecting extends State {
333         @Override
enter()334         public void enter() {
335             Log.i(TAG, "Enter Disconnecting(" + mDevice + "): "
336                     + messageWhatToString(getCurrentMessage().what));
337             sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
338             mConnectionState = BluetoothProfile.STATE_DISCONNECTING;
339             broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTING, mLastConnectionState);
340         }
341 
342         @Override
exit()343         public void exit() {
344             log("Exit Disconnecting(" + mDevice + "): "
345                     + messageWhatToString(getCurrentMessage().what));
346             mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING;
347             removeMessages(CONNECT_TIMEOUT);
348         }
349 
350         @Override
processMessage(Message message)351         public boolean processMessage(Message message) {
352             log("Disconnecting process message(" + mDevice + "): "
353                     + messageWhatToString(message.what));
354 
355             switch (message.what) {
356                 case CONNECT:
357                     deferMessage(message);
358                     break;
359                 case CONNECT_TIMEOUT: {
360                     Log.w(TAG, "Disconnecting connection timeout: " + mDevice);
361                     mNativeInterface.disconnectHearingAid(mDevice);
362                     HearingAidStackEvent disconnectEvent =
363                             new HearingAidStackEvent(
364                                     HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
365                     disconnectEvent.device = mDevice;
366                     disconnectEvent.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED;
367                     sendMessage(STACK_EVENT, disconnectEvent);
368                     break;
369                 }
370                 case DISCONNECT:
371                     deferMessage(message);
372                     break;
373                 case STACK_EVENT:
374                     HearingAidStackEvent event = (HearingAidStackEvent) message.obj;
375                     log("Disconnecting: stack event: " + event);
376                     if (!mDevice.equals(event.device)) {
377                         Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
378                     }
379                     switch (event.type) {
380                         case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
381                             processConnectionEvent(event.valueInt1);
382                             break;
383                         default:
384                             Log.e(TAG, "Disconnecting: ignoring stack event: " + event);
385                             break;
386                     }
387                     break;
388                 default:
389                     return NOT_HANDLED;
390             }
391             return HANDLED;
392         }
393 
394         // in Disconnecting state
processConnectionEvent(int state)395         private void processConnectionEvent(int state) {
396             switch (state) {
397                 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED:
398                     Log.i(TAG, "Disconnected: " + mDevice);
399                     transitionTo(mDisconnected);
400                     break;
401                 case HearingAidStackEvent.CONNECTION_STATE_CONNECTED:
402                     if (mService.okToConnect(mDevice)) {
403                         Log.w(TAG, "Disconnecting interrupted: device is connected: " + mDevice);
404                         transitionTo(mConnected);
405                     } else {
406                         // Reject the connection and stay in Disconnecting state
407                         Log.w(TAG, "Incoming HearingAid Connected request rejected: " + mDevice);
408                         mNativeInterface.disconnectHearingAid(mDevice);
409                     }
410                     break;
411                 case HearingAidStackEvent.CONNECTION_STATE_CONNECTING:
412                     if (mService.okToConnect(mDevice)) {
413                         Log.i(TAG, "Disconnecting interrupted: try to reconnect: " + mDevice);
414                         transitionTo(mConnecting);
415                     } else {
416                         // Reject the connection and stay in Disconnecting state
417                         Log.w(TAG, "Incoming HearingAid Connecting request rejected: " + mDevice);
418                         mNativeInterface.disconnectHearingAid(mDevice);
419                     }
420                     break;
421                 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING:
422                     break;
423                 default:
424                     Log.e(TAG, "Incorrect state: " + state);
425                     break;
426             }
427         }
428     }
429 
430     @VisibleForTesting
431     class Connected extends State {
432         @Override
enter()433         public void enter() {
434             Log.i(TAG, "Enter Connected(" + mDevice + "): "
435                     + messageWhatToString(getCurrentMessage().what));
436             mConnectionState = BluetoothProfile.STATE_CONNECTED;
437             removeDeferredMessages(CONNECT);
438             broadcastConnectionState(BluetoothProfile.STATE_CONNECTED, mLastConnectionState);
439         }
440 
441         @Override
exit()442         public void exit() {
443             log("Exit Connected(" + mDevice + "): "
444                     + messageWhatToString(getCurrentMessage().what));
445             mLastConnectionState = BluetoothProfile.STATE_CONNECTED;
446         }
447 
448         @Override
processMessage(Message message)449         public boolean processMessage(Message message) {
450             log("Connected process message(" + mDevice + "): "
451                     + messageWhatToString(message.what));
452 
453             switch (message.what) {
454                 case CONNECT:
455                     Log.w(TAG, "Connected: CONNECT ignored: " + mDevice);
456                     break;
457                 case DISCONNECT:
458                     log("Disconnecting from " + mDevice);
459                     if (!mNativeInterface.disconnectHearingAid(mDevice)) {
460                         // If error in the native stack, transition directly to Disconnected state.
461                         Log.e(TAG, "Connected: error disconnecting from " + mDevice);
462                         transitionTo(mDisconnected);
463                         break;
464                     }
465                     transitionTo(mDisconnecting);
466                     break;
467                 case STACK_EVENT:
468                     HearingAidStackEvent event = (HearingAidStackEvent) message.obj;
469                     log("Connected: stack event: " + event);
470                     if (!mDevice.equals(event.device)) {
471                         Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
472                     }
473                     switch (event.type) {
474                         case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
475                             processConnectionEvent(event.valueInt1);
476                             break;
477                         default:
478                             Log.e(TAG, "Connected: ignoring stack event: " + event);
479                             break;
480                     }
481                     break;
482                 default:
483                     return NOT_HANDLED;
484             }
485             return HANDLED;
486         }
487 
488         // in Connected state
processConnectionEvent(int state)489         private void processConnectionEvent(int state) {
490             switch (state) {
491                 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED:
492                     Log.i(TAG, "Disconnected from " + mDevice + " but still in Acceptlist");
493                     transitionTo(mDisconnected);
494                     break;
495                 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING:
496                     Log.i(TAG, "Disconnecting from " + mDevice);
497                     transitionTo(mDisconnecting);
498                     break;
499                 default:
500                     Log.e(TAG, "Connection State Device: " + mDevice + " bad state: " + state);
501                     break;
502             }
503         }
504     }
505 
getConnectionState()506     int getConnectionState() {
507         return mConnectionState;
508     }
509 
getDevice()510     BluetoothDevice getDevice() {
511         return mDevice;
512     }
513 
isConnected()514     synchronized boolean isConnected() {
515         return (getConnectionState() == BluetoothProfile.STATE_CONNECTED);
516     }
517 
518     // This method does not check for error condition (newState == prevState)
broadcastConnectionState(int newState, int prevState)519     private void broadcastConnectionState(int newState, int prevState) {
520         log("Connection state " + mDevice + ": " + profileStateToString(prevState)
521                     + "->" + profileStateToString(newState));
522 
523         Intent intent = new Intent(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
524         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
525         intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
526         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
527         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
528                         | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
529         mService.sendBroadcast(intent, BLUETOOTH_CONNECT, Utils.getTempAllowlistBroadcastOptions());
530     }
531 
messageWhatToString(int what)532     private static String messageWhatToString(int what) {
533         switch (what) {
534             case CONNECT:
535                 return "CONNECT";
536             case DISCONNECT:
537                 return "DISCONNECT";
538             case STACK_EVENT:
539                 return "STACK_EVENT";
540             case CONNECT_TIMEOUT:
541                 return "CONNECT_TIMEOUT";
542             default:
543                 break;
544         }
545         return Integer.toString(what);
546     }
547 
profileStateToString(int state)548     private static String profileStateToString(int state) {
549         switch (state) {
550             case BluetoothProfile.STATE_DISCONNECTED:
551                 return "DISCONNECTED";
552             case BluetoothProfile.STATE_CONNECTING:
553                 return "CONNECTING";
554             case BluetoothProfile.STATE_CONNECTED:
555                 return "CONNECTED";
556             case BluetoothProfile.STATE_DISCONNECTING:
557                 return "DISCONNECTING";
558             default:
559                 break;
560         }
561         return Integer.toString(state);
562     }
563 
dump(StringBuilder sb)564     public void dump(StringBuilder sb) {
565         ProfileService.println(sb, "mDevice: " + mDevice);
566         ProfileService.println(sb, "  StateMachine: " + this);
567         // Dump the state machine logs
568         StringWriter stringWriter = new StringWriter();
569         PrintWriter printWriter = new PrintWriter(stringWriter);
570         super.dump(new FileDescriptor(), printWriter, new String[]{});
571         printWriter.flush();
572         stringWriter.flush();
573         ProfileService.println(sb, "  StateMachineLog:");
574         Scanner scanner = new Scanner(stringWriter.toString());
575         while (scanner.hasNextLine()) {
576             String line = scanner.nextLine();
577             ProfileService.println(sb, "    " + line);
578         }
579         scanner.close();
580     }
581 
582     @Override
log(String msg)583     protected void log(String msg) {
584         if (DBG) {
585             super.log(msg);
586         }
587     }
588 }
589