Home | History | Annotate | Download | only in hearingaid
      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 
     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 
    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 
    121     public void doQuit() {
    122         log("doQuit for device " + mDevice);
    123         quitNow();
    124     }
    125 
    126     public void cleanup() {
    127         log("cleanup for device " + mDevice);
    128     }
    129 
    130     @VisibleForTesting
    131     class Disconnected extends State {
    132         @Override
    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
    147         public void exit() {
    148             log("Exit Disconnected(" + mDevice + "): " + messageWhatToString(
    149                     getCurrentMessage().what));
    150             mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED;
    151         }
    152 
    153         @Override
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    433         public void exit() {
    434             log("Exit Connected(" + mDevice + "): "
    435                     + messageWhatToString(getCurrentMessage().what));
    436             mLastConnectionState = BluetoothProfile.STATE_CONNECTED;
    437         }
    438 
    439         @Override
    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
    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 
    497     int getConnectionState() {
    498         return mConnectionState;
    499     }
    500 
    501     BluetoothDevice getDevice() {
    502         return mDevice;
    503     }
    504 
    505     synchronized boolean isConnected() {
    506         return getCurrentState() == mConnected;
    507     }
    508 
    509     // This method does not check for error condition (newState == 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 
    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 
    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 
    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
    574     protected void log(String msg) {
    575         if (DBG) {
    576             super.log(msg);
    577         }
    578     }
    579 }
    580