blob: 7a299e7569e1fcdb6c8419df06a9be19839d5c15 [file] [log] [blame]
/*
* 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);
}
}
}