blob: de47a45e41d9c4513f43d6fcf1925c52b2f89435 [file] [log] [blame]
/*
* Copyright (C) 2014 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.
*/
package com.android.bluetooth.a2dpsink;
import android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAudioConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import android.media.AudioFormat;
import android.os.Message;
import android.util.Log;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.statemachine.State;
import com.android.bluetooth.statemachine.StateMachine;
public class A2dpSinkStateMachine extends StateMachine {
static final String TAG = "A2DPSinkStateMachine";
static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
//0->99 Events from Outside
public static final int CONNECT = 1;
public static final int DISCONNECT = 2;
//100->199 Internal Events
protected static final int CLEANUP = 100;
private static final int CONNECT_TIMEOUT = 101;
//200->299 Events from Native
static final int STACK_EVENT = 200;
static final int CONNECT_TIMEOUT_MS = 5000;
protected final BluetoothDevice mDevice;
protected final byte[] mDeviceAddress;
protected final A2dpSinkService mService;
protected final Disconnected mDisconnected;
protected final Connecting mConnecting;
protected final Connected mConnected;
protected final Disconnecting mDisconnecting;
protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
protected BluetoothAudioConfig mAudioConfig = null;
A2dpSinkStateMachine(BluetoothDevice device, A2dpSinkService service) {
super(TAG);
mDevice = device;
mDeviceAddress = Utils.getByteAddress(mDevice);
mService = service;
if (DBG) Log.d(TAG, device.toString());
mDisconnected = new Disconnected();
mConnecting = new Connecting();
mConnected = new Connected();
mDisconnecting = new Disconnecting();
addState(mDisconnected);
addState(mConnecting);
addState(mConnected);
addState(mDisconnecting);
setInitialState(mDisconnected);
}
protected String getConnectionStateChangedIntent() {
return BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED;
}
/**
* Get the current connection state
*
* @return current State
*/
public int getState() {
return mMostRecentState;
}
/**
* get current audio config
*/
BluetoothAudioConfig getAudioConfig() {
return mAudioConfig;
}
/**
* Get the underlying device tracked by this state machine
*
* @return device in focus
*/
public synchronized BluetoothDevice getDevice() {
return mDevice;
}
/**
* send the Connect command asynchronously
*/
public final void connect() {
sendMessage(CONNECT);
}
/**
* send the Disconnect command asynchronously
*/
public final void disconnect() {
sendMessage(DISCONNECT);
}
/**
* Dump the current State Machine to the string builder.
* @param sb output string
*/
public void dump(StringBuilder sb) {
ProfileService.println(sb, "mDevice: " + mDevice.getAddress() + "("
+ mDevice.getName() + ") " + this.toString());
}
@Override
protected void unhandledMessage(Message msg) {
Log.w(TAG, "unhandledMessage in state " + getCurrentState() + "msg.what=" + msg.what);
}
class Disconnected extends State {
@Override
public void enter() {
if (DBG) Log.d(TAG, "Enter Disconnected");
if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) {
sendMessage(CLEANUP);
}
onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED);
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case STACK_EVENT:
processStackEvent((StackEvent) message.obj);
return true;
case CONNECT:
if (DBG) Log.d(TAG, "Connect");
transitionTo(mConnecting);
return true;
case CLEANUP:
mService.removeStateMachine(A2dpSinkStateMachine.this);
return true;
}
return false;
}
void processStackEvent(StackEvent event) {
switch (event.mType) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
switch (event.mState) {
case StackEvent.CONNECTION_STATE_CONNECTING:
if (mService.getConnectionPolicy(mDevice)
== BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
Log.w(TAG, "Ignore incoming connection, profile is"
+ " turned off for " + mDevice);
mService.disconnectA2dpNative(mDeviceAddress);
} else {
transitionTo(mConnecting);
}
break;
case StackEvent.CONNECTION_STATE_CONNECTED:
transitionTo(mConnected);
break;
case StackEvent.CONNECTION_STATE_DISCONNECTED:
sendMessage(CLEANUP);
break;
}
}
}
}
class Connecting extends State {
boolean mIncommingConnection = false;
@Override
public void enter() {
if (DBG) Log.d(TAG, "Enter Connecting");
onConnectionStateChanged(BluetoothProfile.STATE_CONNECTING);
sendMessageDelayed(CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS);
if (!mIncommingConnection) {
mService.connectA2dpNative(mDeviceAddress);
}
super.enter();
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case STACK_EVENT:
processStackEvent((StackEvent) message.obj);
return true;
case CONNECT_TIMEOUT:
transitionTo(mDisconnected);
return true;
}
return false;
}
void processStackEvent(StackEvent event) {
switch (event.mType) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
switch (event.mState) {
case StackEvent.CONNECTION_STATE_CONNECTED:
transitionTo(mConnected);
break;
case StackEvent.CONNECTION_STATE_DISCONNECTED:
transitionTo(mDisconnected);
break;
}
}
}
@Override
public void exit() {
removeMessages(CONNECT_TIMEOUT);
}
}
class Connected extends State {
@Override
public void enter() {
if (DBG) Log.d(TAG, "Enter Connected");
onConnectionStateChanged(BluetoothProfile.STATE_CONNECTED);
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case DISCONNECT:
transitionTo(mDisconnecting);
mService.disconnectA2dpNative(mDeviceAddress);
return true;
case STACK_EVENT:
processStackEvent((StackEvent) message.obj);
return true;
}
return false;
}
void processStackEvent(StackEvent event) {
switch (event.mType) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
switch (event.mState) {
case StackEvent.CONNECTION_STATE_DISCONNECTING:
transitionTo(mDisconnecting);
break;
case StackEvent.CONNECTION_STATE_DISCONNECTED:
transitionTo(mDisconnected);
break;
}
break;
case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED:
mAudioConfig = new BluetoothAudioConfig(event.mSampleRate, event.mChannelCount,
AudioFormat.ENCODING_PCM_16BIT);
break;
}
}
}
protected class Disconnecting extends State {
@Override
public void enter() {
if (DBG) Log.d(TAG, "Enter Disconnecting");
onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING);
transitionTo(mDisconnected);
}
}
protected void onConnectionStateChanged(int currentState) {
if (mMostRecentState == currentState) {
return;
}
if (currentState == BluetoothProfile.STATE_CONNECTED) {
MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP_SINK);
}
if (DBG) {
Log.d(TAG, "Connection state " + mDevice + ": " + mMostRecentState + "->"
+ currentState);
}
Intent intent = new Intent(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mMostRecentState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, currentState);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mMostRecentState = currentState;
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
}