HearingAidStateMachine.java revision 09e97f4cfcca3564c8daa864ede0eee172e9d5aa
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
46package com.android.bluetooth.hearingaid;
47
48import android.bluetooth.BluetoothDevice;
49import android.bluetooth.BluetoothHearingAid;
50import android.bluetooth.BluetoothProfile;
51import android.content.Intent;
52import android.os.Looper;
53import android.os.Message;
54import android.support.annotation.VisibleForTesting;
55import android.util.Log;
56
57import com.android.bluetooth.btservice.ProfileService;
58import com.android.internal.util.State;
59import com.android.internal.util.StateMachine;
60
61import java.io.FileDescriptor;
62import java.io.PrintWriter;
63import java.io.StringWriter;
64import java.util.Scanner;
65
66final 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