| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /** |
| * Bluetooth HearingAid StateMachine. There is one instance per remote device. |
| * - "Disconnected" and "Connected" are steady states. |
| * - "Connecting" and "Disconnecting" are transient states until the |
| * connection / disconnection is completed. |
| * |
| * |
| * (Disconnected) |
| * | ^ |
| * CONNECT | | DISCONNECTED |
| * V | |
| * (Connecting)<--->(Disconnecting) |
| * | ^ |
| * CONNECTED | | DISCONNECT |
| * V | |
| * (Connected) |
| * NOTES: |
| * - If state machine is in "Connecting" state and the remote device sends |
| * DISCONNECT request, the state machine transitions to "Disconnecting" state. |
| * - Similarly, if the state machine is in "Disconnecting" state and the remote device |
| * sends CONNECT request, the state machine transitions to "Connecting" state. |
| * |
| * DISCONNECT |
| * (Connecting) ---------------> (Disconnecting) |
| * <--------------- |
| * CONNECT |
| * |
| */ |
| |
| package com.android.bluetooth.hearingaid; |
| |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHearingAid; |
| import android.bluetooth.BluetoothProfile; |
| import android.content.Intent; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.util.Log; |
| |
| import com.android.bluetooth.btservice.ProfileService; |
| import com.android.bluetooth.statemachine.State; |
| import com.android.bluetooth.statemachine.StateMachine; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.util.Scanner; |
| |
| final class HearingAidStateMachine extends StateMachine { |
| private static final boolean DBG = false; |
| private static final String TAG = "HearingAidStateMachine"; |
| |
| static final int CONNECT = 1; |
| static final int DISCONNECT = 2; |
| @VisibleForTesting |
| static final int STACK_EVENT = 101; |
| private static final int CONNECT_TIMEOUT = 201; |
| |
| // NOTE: the value is not "final" - it is modified in the unit tests |
| @VisibleForTesting |
| static int sConnectTimeoutMs = 30000; // 30s |
| |
| private Disconnected mDisconnected; |
| private Connecting mConnecting; |
| private Disconnecting mDisconnecting; |
| private Connected mConnected; |
| private int mConnectionState = BluetoothProfile.STATE_DISCONNECTED; |
| private int mLastConnectionState = -1; |
| |
| private HearingAidService mService; |
| private HearingAidNativeInterface mNativeInterface; |
| |
| private final BluetoothDevice mDevice; |
| |
| HearingAidStateMachine(BluetoothDevice device, HearingAidService svc, |
| HearingAidNativeInterface nativeInterface, Looper looper) { |
| super(TAG, looper); |
| mDevice = device; |
| mService = svc; |
| mNativeInterface = nativeInterface; |
| |
| mDisconnected = new Disconnected(); |
| mConnecting = new Connecting(); |
| mDisconnecting = new Disconnecting(); |
| mConnected = new Connected(); |
| |
| addState(mDisconnected); |
| addState(mConnecting); |
| addState(mDisconnecting); |
| addState(mConnected); |
| |
| setInitialState(mDisconnected); |
| } |
| |
| static HearingAidStateMachine make(BluetoothDevice device, HearingAidService svc, |
| HearingAidNativeInterface nativeInterface, Looper looper) { |
| Log.i(TAG, "make for device " + device); |
| HearingAidStateMachine HearingAidSm = new HearingAidStateMachine(device, svc, |
| nativeInterface, looper); |
| HearingAidSm.start(); |
| return HearingAidSm; |
| } |
| |
| public void doQuit() { |
| log("doQuit for device " + mDevice); |
| quitNow(); |
| } |
| |
| public void cleanup() { |
| log("cleanup for device " + mDevice); |
| } |
| |
| @VisibleForTesting |
| class Disconnected extends State { |
| @Override |
| public void enter() { |
| Log.i(TAG, "Enter Disconnected(" + mDevice + "): " + messageWhatToString( |
| getCurrentMessage().what)); |
| mConnectionState = BluetoothProfile.STATE_DISCONNECTED; |
| |
| removeDeferredMessages(DISCONNECT); |
| |
| if (mLastConnectionState != -1) { |
| // Don't broadcast during startup |
| broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTED, |
| mLastConnectionState); |
| } |
| } |
| |
| @Override |
| public void exit() { |
| log("Exit Disconnected(" + mDevice + "): " + messageWhatToString( |
| getCurrentMessage().what)); |
| mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED; |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("Disconnected process message(" + mDevice + "): " + messageWhatToString( |
| message.what)); |
| |
| switch (message.what) { |
| case CONNECT: |
| log("Connecting to " + mDevice); |
| if (!mNativeInterface.connectHearingAid(mDevice)) { |
| Log.e(TAG, "Disconnected: error connecting to " + mDevice); |
| break; |
| } |
| if (mService.okToConnect(mDevice)) { |
| transitionTo(mConnecting); |
| } else { |
| // Reject the request and stay in Disconnected state |
| Log.w(TAG, "Outgoing HearingAid Connecting request rejected: " + mDevice); |
| } |
| break; |
| case DISCONNECT: |
| Log.d(TAG, "Disconnected: DISCONNECT: call native disconnect for " + mDevice); |
| mNativeInterface.disconnectHearingAid(mDevice); |
| break; |
| case STACK_EVENT: |
| HearingAidStackEvent event = (HearingAidStackEvent) message.obj; |
| if (DBG) { |
| Log.d(TAG, "Disconnected: stack event: " + event); |
| } |
| if (!mDevice.equals(event.device)) { |
| Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event); |
| } |
| switch (event.type) { |
| case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: |
| processConnectionEvent(event.valueInt1); |
| break; |
| default: |
| Log.e(TAG, "Disconnected: ignoring stack event: " + event); |
| break; |
| } |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| |
| // in Disconnected state |
| private void processConnectionEvent(int state) { |
| switch (state) { |
| case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED: |
| Log.w(TAG, "Ignore HearingAid DISCONNECTED event: " + mDevice); |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_CONNECTING: |
| if (mService.okToConnect(mDevice)) { |
| Log.i(TAG, "Incoming HearingAid Connecting request accepted: " + mDevice); |
| transitionTo(mConnecting); |
| } else { |
| // Reject the connection and stay in Disconnected state itself |
| Log.w(TAG, "Incoming HearingAid Connecting request rejected: " + mDevice); |
| mNativeInterface.disconnectHearingAid(mDevice); |
| } |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_CONNECTED: |
| Log.w(TAG, "HearingAid Connected from Disconnected state: " + mDevice); |
| if (mService.okToConnect(mDevice)) { |
| Log.i(TAG, "Incoming HearingAid Connected request accepted: " + mDevice); |
| transitionTo(mConnected); |
| } else { |
| // Reject the connection and stay in Disconnected state itself |
| Log.w(TAG, "Incoming HearingAid Connected request rejected: " + mDevice); |
| mNativeInterface.disconnectHearingAid(mDevice); |
| } |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING: |
| Log.w(TAG, "Ignore HearingAid DISCONNECTING event: " + mDevice); |
| break; |
| default: |
| Log.e(TAG, "Incorrect state: " + state + " device: " + mDevice); |
| break; |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| class Connecting extends State { |
| @Override |
| public void enter() { |
| Log.i(TAG, "Enter Connecting(" + mDevice + "): " |
| + messageWhatToString(getCurrentMessage().what)); |
| sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs); |
| mConnectionState = BluetoothProfile.STATE_CONNECTING; |
| broadcastConnectionState(BluetoothProfile.STATE_CONNECTING, mLastConnectionState); |
| } |
| |
| @Override |
| public void exit() { |
| log("Exit Connecting(" + mDevice + "): " |
| + messageWhatToString(getCurrentMessage().what)); |
| mLastConnectionState = BluetoothProfile.STATE_CONNECTING; |
| removeMessages(CONNECT_TIMEOUT); |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("Connecting process message(" + mDevice + "): " |
| + messageWhatToString(message.what)); |
| |
| switch (message.what) { |
| case CONNECT: |
| deferMessage(message); |
| break; |
| case CONNECT_TIMEOUT: |
| Log.w(TAG, "Connecting connection timeout: " + mDevice); |
| mNativeInterface.disconnectHearingAid(mDevice); |
| if (mService.isConnectedPeerDevices(mDevice)) { |
| Log.w(TAG, "One side connection timeout: " + mDevice + ". Try acceptlist"); |
| mNativeInterface.addToAcceptlist(mDevice); |
| } |
| HearingAidStackEvent disconnectEvent = |
| new HearingAidStackEvent( |
| HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); |
| disconnectEvent.device = mDevice; |
| disconnectEvent.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED; |
| sendMessage(STACK_EVENT, disconnectEvent); |
| break; |
| case DISCONNECT: |
| log("Connecting: connection canceled to " + mDevice); |
| mNativeInterface.disconnectHearingAid(mDevice); |
| transitionTo(mDisconnected); |
| break; |
| case STACK_EVENT: |
| HearingAidStackEvent event = (HearingAidStackEvent) message.obj; |
| log("Connecting: stack event: " + event); |
| if (!mDevice.equals(event.device)) { |
| Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event); |
| } |
| switch (event.type) { |
| case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: |
| processConnectionEvent(event.valueInt1); |
| break; |
| default: |
| Log.e(TAG, "Connecting: ignoring stack event: " + event); |
| break; |
| } |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| |
| // in Connecting state |
| private void processConnectionEvent(int state) { |
| switch (state) { |
| case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED: |
| Log.w(TAG, "Connecting device disconnected: " + mDevice); |
| transitionTo(mDisconnected); |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_CONNECTED: |
| transitionTo(mConnected); |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_CONNECTING: |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING: |
| Log.w(TAG, "Connecting interrupted: device is disconnecting: " + mDevice); |
| transitionTo(mDisconnecting); |
| break; |
| default: |
| Log.e(TAG, "Incorrect state: " + state); |
| break; |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| class Disconnecting extends State { |
| @Override |
| public void enter() { |
| Log.i(TAG, "Enter Disconnecting(" + mDevice + "): " |
| + messageWhatToString(getCurrentMessage().what)); |
| sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs); |
| mConnectionState = BluetoothProfile.STATE_DISCONNECTING; |
| broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTING, mLastConnectionState); |
| } |
| |
| @Override |
| public void exit() { |
| log("Exit Disconnecting(" + mDevice + "): " |
| + messageWhatToString(getCurrentMessage().what)); |
| mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING; |
| removeMessages(CONNECT_TIMEOUT); |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("Disconnecting process message(" + mDevice + "): " |
| + messageWhatToString(message.what)); |
| |
| switch (message.what) { |
| case CONNECT: |
| deferMessage(message); |
| break; |
| case CONNECT_TIMEOUT: { |
| Log.w(TAG, "Disconnecting connection timeout: " + mDevice); |
| mNativeInterface.disconnectHearingAid(mDevice); |
| HearingAidStackEvent disconnectEvent = |
| new HearingAidStackEvent( |
| HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); |
| disconnectEvent.device = mDevice; |
| disconnectEvent.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED; |
| sendMessage(STACK_EVENT, disconnectEvent); |
| break; |
| } |
| case DISCONNECT: |
| deferMessage(message); |
| break; |
| case STACK_EVENT: |
| HearingAidStackEvent event = (HearingAidStackEvent) message.obj; |
| log("Disconnecting: stack event: " + event); |
| if (!mDevice.equals(event.device)) { |
| Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event); |
| } |
| switch (event.type) { |
| case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: |
| processConnectionEvent(event.valueInt1); |
| break; |
| default: |
| Log.e(TAG, "Disconnecting: ignoring stack event: " + event); |
| break; |
| } |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| |
| // in Disconnecting state |
| private void processConnectionEvent(int state) { |
| switch (state) { |
| case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED: |
| Log.i(TAG, "Disconnected: " + mDevice); |
| transitionTo(mDisconnected); |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_CONNECTED: |
| if (mService.okToConnect(mDevice)) { |
| Log.w(TAG, "Disconnecting interrupted: device is connected: " + mDevice); |
| transitionTo(mConnected); |
| } else { |
| // Reject the connection and stay in Disconnecting state |
| Log.w(TAG, "Incoming HearingAid Connected request rejected: " + mDevice); |
| mNativeInterface.disconnectHearingAid(mDevice); |
| } |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_CONNECTING: |
| if (mService.okToConnect(mDevice)) { |
| Log.i(TAG, "Disconnecting interrupted: try to reconnect: " + mDevice); |
| transitionTo(mConnecting); |
| } else { |
| // Reject the connection and stay in Disconnecting state |
| Log.w(TAG, "Incoming HearingAid Connecting request rejected: " + mDevice); |
| mNativeInterface.disconnectHearingAid(mDevice); |
| } |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING: |
| break; |
| default: |
| Log.e(TAG, "Incorrect state: " + state); |
| break; |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| class Connected extends State { |
| @Override |
| public void enter() { |
| Log.i(TAG, "Enter Connected(" + mDevice + "): " |
| + messageWhatToString(getCurrentMessage().what)); |
| mConnectionState = BluetoothProfile.STATE_CONNECTED; |
| removeDeferredMessages(CONNECT); |
| broadcastConnectionState(BluetoothProfile.STATE_CONNECTED, mLastConnectionState); |
| } |
| |
| @Override |
| public void exit() { |
| log("Exit Connected(" + mDevice + "): " |
| + messageWhatToString(getCurrentMessage().what)); |
| mLastConnectionState = BluetoothProfile.STATE_CONNECTED; |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("Connected process message(" + mDevice + "): " |
| + messageWhatToString(message.what)); |
| |
| switch (message.what) { |
| case CONNECT: |
| Log.w(TAG, "Connected: CONNECT ignored: " + mDevice); |
| break; |
| case DISCONNECT: |
| log("Disconnecting from " + mDevice); |
| if (!mNativeInterface.disconnectHearingAid(mDevice)) { |
| // If error in the native stack, transition directly to Disconnected state. |
| Log.e(TAG, "Connected: error disconnecting from " + mDevice); |
| transitionTo(mDisconnected); |
| break; |
| } |
| transitionTo(mDisconnecting); |
| break; |
| case STACK_EVENT: |
| HearingAidStackEvent event = (HearingAidStackEvent) message.obj; |
| log("Connected: stack event: " + event); |
| if (!mDevice.equals(event.device)) { |
| Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event); |
| } |
| switch (event.type) { |
| case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: |
| processConnectionEvent(event.valueInt1); |
| break; |
| default: |
| Log.e(TAG, "Connected: ignoring stack event: " + event); |
| break; |
| } |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| |
| // in Connected state |
| private void processConnectionEvent(int state) { |
| switch (state) { |
| case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED: |
| Log.i(TAG, "Disconnected from " + mDevice + " but still in Acceptlist"); |
| transitionTo(mDisconnected); |
| break; |
| case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING: |
| Log.i(TAG, "Disconnecting from " + mDevice); |
| transitionTo(mDisconnecting); |
| break; |
| default: |
| Log.e(TAG, "Connection State Device: " + mDevice + " bad state: " + state); |
| break; |
| } |
| } |
| } |
| |
| int getConnectionState() { |
| return mConnectionState; |
| } |
| |
| BluetoothDevice getDevice() { |
| return mDevice; |
| } |
| |
| synchronized boolean isConnected() { |
| return (getConnectionState() == BluetoothProfile.STATE_CONNECTED); |
| } |
| |
| // This method does not check for error condition (newState == prevState) |
| private void broadcastConnectionState(int newState, int prevState) { |
| log("Connection state " + mDevice + ": " + profileStateToString(prevState) |
| + "->" + profileStateToString(newState)); |
| |
| Intent intent = new Intent(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); |
| intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); |
| intent.putExtra(BluetoothProfile.EXTRA_STATE, newState); |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); |
| intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT |
| | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); |
| mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); |
| } |
| |
| private static String messageWhatToString(int what) { |
| switch (what) { |
| case CONNECT: |
| return "CONNECT"; |
| case DISCONNECT: |
| return "DISCONNECT"; |
| case STACK_EVENT: |
| return "STACK_EVENT"; |
| case CONNECT_TIMEOUT: |
| return "CONNECT_TIMEOUT"; |
| default: |
| break; |
| } |
| return Integer.toString(what); |
| } |
| |
| private static String profileStateToString(int state) { |
| switch (state) { |
| case BluetoothProfile.STATE_DISCONNECTED: |
| return "DISCONNECTED"; |
| case BluetoothProfile.STATE_CONNECTING: |
| return "CONNECTING"; |
| case BluetoothProfile.STATE_CONNECTED: |
| return "CONNECTED"; |
| case BluetoothProfile.STATE_DISCONNECTING: |
| return "DISCONNECTING"; |
| default: |
| break; |
| } |
| return Integer.toString(state); |
| } |
| |
| public void dump(StringBuilder sb) { |
| ProfileService.println(sb, "mDevice: " + mDevice); |
| ProfileService.println(sb, " StateMachine: " + this); |
| // Dump the state machine logs |
| StringWriter stringWriter = new StringWriter(); |
| PrintWriter printWriter = new PrintWriter(stringWriter); |
| super.dump(new FileDescriptor(), printWriter, new String[]{}); |
| printWriter.flush(); |
| stringWriter.flush(); |
| ProfileService.println(sb, " StateMachineLog:"); |
| Scanner scanner = new Scanner(stringWriter.toString()); |
| while (scanner.hasNextLine()) { |
| String line = scanner.nextLine(); |
| ProfileService.println(sb, " " + line); |
| } |
| scanner.close(); |
| } |
| |
| @Override |
| protected void log(String msg) { |
| if (DBG) { |
| super.log(msg); |
| } |
| } |
| } |