/* * Copyright (C) 2016 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 Pbap PCE StateMachine * (Disconnected) * | ^ * CONNECT | | DISCONNECTED * V | * (Connecting) (Disconnecting) * | ^ * CONNECTED | | DISCONNECT * V | * (Connected) * * Valid Transitions: * State + Event -> Transition: * * Disconnected + CONNECT -> Connecting * Connecting + CONNECTED -> Connected * Connecting + TIMEOUT -> Disconnecting * Connecting + DISCONNECT -> Disconnecting * Connected + DISCONNECT -> Disconnecting * Disconnecting + DISCONNECTED -> (Safe) Disconnected * Disconnecting + TIMEOUT -> (Force) Disconnected * Disconnecting + CONNECT : Defer Message * */ package com.android.bluetooth.pbapclient; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothPbapClient; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.HandlerThread; import android.os.Message; import android.os.ParcelUuid; import android.os.Process; import android.os.UserManager; import android.util.Log; import com.android.bluetooth.BluetoothMetricsProto; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.statemachine.IState; import com.android.bluetooth.statemachine.State; import com.android.bluetooth.statemachine.StateMachine; import java.util.ArrayList; import java.util.List; final class PbapClientStateMachine extends StateMachine { private static final boolean DBG = Utils.DBG; private static final String TAG = "PbapClientStateMachine"; // Messages for handling connect/disconnect requests. private static final int MSG_DISCONNECT = 2; private static final int MSG_SDP_COMPLETE = 9; // Messages for handling error conditions. private static final int MSG_CONNECT_TIMEOUT = 3; private static final int MSG_DISCONNECT_TIMEOUT = 4; // Messages for feedback from ConnectionHandler. static final int MSG_CONNECTION_COMPLETE = 5; static final int MSG_CONNECTION_FAILED = 6; static final int MSG_CONNECTION_CLOSED = 7; static final int MSG_RESUME_DOWNLOAD = 8; static final int CONNECT_TIMEOUT = 10000; static final int DISCONNECT_TIMEOUT = 3000; private final Object mLock; private State mDisconnected; private State mConnecting; private State mConnected; private State mDisconnecting; // mCurrentDevice may only be changed in Disconnected State. private final BluetoothDevice mCurrentDevice; private PbapClientService mService; private PbapClientConnectionHandler mConnectionHandler; private HandlerThread mHandlerThread = null; private UserManager mUserManager = null; // mMostRecentState maintains previous state for broadcasting transitions. private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; PbapClientStateMachine(PbapClientService svc, BluetoothDevice device) { super(TAG); mService = svc; mCurrentDevice = device; mLock = new Object(); mUserManager = UserManager.get(mService); mDisconnected = new Disconnected(); mConnecting = new Connecting(); mDisconnecting = new Disconnecting(); mConnected = new Connected(); addState(mDisconnected); addState(mConnecting); addState(mDisconnecting); addState(mConnected); setInitialState(mConnecting); } class Disconnected extends State { @Override public void enter() { if (DBG) Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what); onConnectionStateChanged(mCurrentDevice, mMostRecentState, BluetoothProfile.STATE_DISCONNECTED); mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; quit(); } } class Connecting extends State { private SDPBroadcastReceiver mSdpReceiver; @Override public void enter() { if (DBG) { Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what); } onConnectionStateChanged(mCurrentDevice, mMostRecentState, BluetoothProfile.STATE_CONNECTING); mSdpReceiver = new SDPBroadcastReceiver(); mSdpReceiver.register(); mCurrentDevice.sdpSearch(BluetoothUuid.PBAP_PSE); mMostRecentState = BluetoothProfile.STATE_CONNECTING; // Create a separate handler instance and thread for performing // connect/download/disconnect operations as they may be time consuming and error prone. mHandlerThread = new HandlerThread("PBAP PCE handler", Process.THREAD_PRIORITY_BACKGROUND); mHandlerThread.start(); mConnectionHandler = new PbapClientConnectionHandler.Builder().setLooper(mHandlerThread.getLooper()) .setContext(mService) .setClientSM(PbapClientStateMachine.this) .setRemoteDevice(mCurrentDevice) .build(); sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT); } @Override public boolean processMessage(Message message) { if (DBG) { Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); } switch (message.what) { case MSG_DISCONNECT: if (message.obj instanceof BluetoothDevice && message.obj.equals( mCurrentDevice)) { removeMessages(MSG_CONNECT_TIMEOUT); transitionTo(mDisconnecting); } break; case MSG_CONNECTION_COMPLETE: removeMessages(MSG_CONNECT_TIMEOUT); transitionTo(mConnected); break; case MSG_CONNECTION_FAILED: case MSG_CONNECT_TIMEOUT: removeMessages(MSG_CONNECT_TIMEOUT); transitionTo(mDisconnecting); break; case MSG_SDP_COMPLETE: mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_CONNECT, message.obj).sendToTarget(); break; default: Log.w(TAG, "Received unexpected message while Connecting"); return NOT_HANDLED; } return HANDLED; } @Override public void exit() { mSdpReceiver.unregister(); mSdpReceiver = null; } private class SDPBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (DBG) { Log.v(TAG, "onReceive" + action); } if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (!device.equals(getDevice())) { Log.w(TAG, "SDP Record fetched for different device - Ignore"); return; } ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); if (DBG) { Log.v(TAG, "Received UUID: " + uuid.toString()); Log.v(TAG, "expected UUID: " + BluetoothUuid.PBAP_PSE.toString()); } if (uuid.equals(BluetoothUuid.PBAP_PSE)) { sendMessage(MSG_SDP_COMPLETE, intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD)); } } } public void register() { IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); mService.registerReceiver(this, filter); } public void unregister() { mService.unregisterReceiver(this); } } } class Disconnecting extends State { @Override public void enter() { if (DBG) Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what); onConnectionStateChanged(mCurrentDevice, mMostRecentState, BluetoothProfile.STATE_DISCONNECTING); mMostRecentState = BluetoothProfile.STATE_DISCONNECTING; mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT) .sendToTarget(); sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT); } @Override public boolean processMessage(Message message) { if (DBG) { Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); } switch (message.what) { case MSG_CONNECTION_CLOSED: removeMessages(MSG_DISCONNECT_TIMEOUT); mHandlerThread.quitSafely(); transitionTo(mDisconnected); break; case MSG_DISCONNECT: deferMessage(message); break; case MSG_DISCONNECT_TIMEOUT: Log.w(TAG, "Disconnect Timeout, Forcing"); mConnectionHandler.abort(); break; case MSG_RESUME_DOWNLOAD: // Do nothing. break; default: Log.w(TAG, "Received unexpected message while Disconnecting"); return NOT_HANDLED; } return HANDLED; } } class Connected extends State { @Override public void enter() { if (DBG) Log.d(TAG, "Enter Connected: " + getCurrentMessage().what); onConnectionStateChanged(mCurrentDevice, mMostRecentState, BluetoothProfile.STATE_CONNECTED); mMostRecentState = BluetoothProfile.STATE_CONNECTED; if (mUserManager.isUserUnlocked()) { mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD) .sendToTarget(); } } @Override public boolean processMessage(Message message) { if (DBG) { Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); } switch (message.what) { case MSG_DISCONNECT: if ((message.obj instanceof BluetoothDevice) && ((BluetoothDevice) message.obj).equals(mCurrentDevice)) { transitionTo(mDisconnecting); } break; case MSG_RESUME_DOWNLOAD: mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD) .sendToTarget(); break; default: Log.w(TAG, "Received unexpected message while Connected"); return NOT_HANDLED; } return HANDLED; } } private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) { if (device == null) { Log.w(TAG, "onConnectionStateChanged with invalid device"); return; } if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) { MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.PBAP_CLIENT); } Log.d(TAG, "Connection state " + device + ": " + prevState + "->" + state); Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); intent.putExtra(BluetoothProfile.EXTRA_STATE, state); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); } public void disconnect(BluetoothDevice device) { if (DBG) Log.d(TAG, "Disconnect Request " + device); sendMessage(MSG_DISCONNECT, device); } public void resumeDownload() { sendMessage(MSG_RESUME_DOWNLOAD); } void doQuit() { if (mHandlerThread != null) { mHandlerThread.quitSafely(); } quitNow(); } @Override protected void onQuitting() { mService.cleanupDevice(mCurrentDevice); } public int getConnectionState() { IState currentState = getCurrentState(); if (currentState instanceof Disconnected) { return BluetoothProfile.STATE_DISCONNECTED; } else if (currentState instanceof Connecting) { return BluetoothProfile.STATE_CONNECTING; } else if (currentState instanceof Connected) { return BluetoothProfile.STATE_CONNECTED; } else if (currentState instanceof Disconnecting) { return BluetoothProfile.STATE_DISCONNECTING; } Log.w(TAG, "Unknown State"); return BluetoothProfile.STATE_DISCONNECTED; } public List getDevicesMatchingConnectionStates(int[] states) { int clientState; BluetoothDevice currentDevice; synchronized (mLock) { clientState = getConnectionState(); currentDevice = getDevice(); } List deviceList = new ArrayList(); for (int state : states) { if (clientState == state) { if (currentDevice != null) { deviceList.add(currentDevice); } } } return deviceList; } public int getConnectionState(BluetoothDevice device) { if (device == null) { return BluetoothProfile.STATE_DISCONNECTED; } synchronized (mLock) { if (device.equals(mCurrentDevice)) { return getConnectionState(); } } return BluetoothProfile.STATE_DISCONNECTED; } public BluetoothDevice getDevice() { /* * Disconnected is the only state where device can change, and to prevent the race * condition of reporting a valid device while disconnected fix the report here. Note that * Synchronization of the state and device is not possible with current state machine * desingn since the actual Transition happens sometime after the transitionTo method. */ if (getCurrentState() instanceof Disconnected) { return null; } return mCurrentDevice; } Context getContext() { return mService; } public void dump(StringBuilder sb) { ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice.getAddress() + "(" + mCurrentDevice.getName() + ") " + this.toString()); } }