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