PbapClientStateMachine.java revision e0a3b23b18192f409d90ba08f8c8bd1b0c321160
1/* 2 * Copyright (C) 2016 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 Pbap PCE StateMachine 19 * (Disconnected) 20 * | ^ 21 * CONNECT | | DISCONNECTED 22 * V | 23 * (Connecting) (Disconnecting) 24 * | ^ 25 * CONNECTED | | DISCONNECT 26 * V | 27 * (Connected) 28 * 29 * Valid Transitions: 30 * State + Event -> Transition: 31 * 32 * Disconnected + CONNECT -> Connecting 33 * Connecting + CONNECTED -> Connected 34 * Connecting + TIMEOUT -> Disconnecting 35 * Connecting + DISCONNECT -> Disconnecting 36 * Connected + DISCONNECT -> Disconnecting 37 * Disconnecting + DISCONNECTED -> (Safe) Disconnected 38 * Disconnecting + TIMEOUT -> (Force) Disconnected 39 * Disconnecting + CONNECT : Defer Message 40 * 41 */ 42package com.android.bluetooth.pbapclient; 43 44import android.bluetooth.BluetoothDevice; 45import android.bluetooth.BluetoothProfile; 46import android.bluetooth.BluetoothPbapClient; 47import android.content.Context; 48import android.content.Intent; 49import android.os.Message; 50import android.os.Process; 51import android.os.HandlerThread; 52import android.util.Log; 53 54import com.android.bluetooth.btservice.ProfileService; 55import com.android.internal.util.IState; 56import com.android.internal.util.State; 57import com.android.internal.util.StateMachine; 58 59import java.lang.IllegalStateException; 60import java.util.ArrayList; 61import java.util.List; 62 63final class PbapClientStateMachine extends StateMachine { 64 private static final boolean DBG = true; 65 private static final String TAG = "PbapClientStateMachine"; 66 67 // Messages for handling connect/disconnect requests. 68 private static final int MSG_CONNECT = 1; 69 private static final int MSG_DISCONNECT = 2; 70 71 // Messages for handling error conditions. 72 private static final int MSG_CONNECT_TIMEOUT = 3; 73 private static final int MSG_DISCONNECT_TIMEOUT = 4; 74 75 // Messages for feedback from ConnectionHandler. 76 static final int MSG_CONNECTION_COMPLETE = 5; 77 static final int MSG_CONNECTION_FAILED = 6; 78 static final int MSG_CONNECTION_CLOSED = 7; 79 80 static final int CONNECT_TIMEOUT = 6000; 81 static final int DISCONNECT_TIMEOUT = 3000; 82 83 private final Object mLock; 84 private State mDisconnected; 85 private State mConnecting; 86 private State mConnected; 87 private State mDisconnecting; 88 89 // mCurrentDevice may only be changed in Disconnected State. 90 private BluetoothDevice mCurrentDevice = null; 91 private Context mContext; 92 private PbapClientConnectionHandler mConnectionHandler; 93 private HandlerThread mHandlerThread = null; 94 95 // mMostRecentState maintains previous state for broadcasting transitions. 96 private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; 97 98 PbapClientStateMachine(Context context) { 99 super(TAG); 100 mContext = context; 101 mLock = new Object(); 102 mDisconnected = new Disconnected(); 103 mConnecting = new Connecting(); 104 mDisconnecting = new Disconnecting(); 105 mConnected = new Connected(); 106 107 addState(mDisconnected); 108 addState(mConnecting); 109 addState(mDisconnecting); 110 addState(mConnected); 111 112 setInitialState(mDisconnected); 113 } 114 115 class Disconnected extends State { 116 @Override 117 public void enter() { 118 Log.d(TAG,"Enter Disconnected: " + getCurrentMessage().what); 119 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 120 BluetoothProfile.STATE_DISCONNECTED); 121 mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; 122 synchronized (mLock) { 123 mCurrentDevice = null; 124 } 125 126 } 127 128 @Override 129 public boolean processMessage(Message message) { 130 if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName()); 131 switch (message.what) { 132 case MSG_CONNECT: 133 if (message.obj instanceof BluetoothDevice) { 134 synchronized(mLock) { 135 mCurrentDevice = (BluetoothDevice) message.obj; 136 } 137 transitionTo(mConnecting); 138 } else { 139 Log.w(TAG,"Received CONNECT without valid device"); 140 throw new IllegalStateException("invalid device"); 141 } 142 break; 143 144 case MSG_DISCONNECT: 145 if (message.obj instanceof BluetoothDevice) { 146 onConnectionStateChanged((BluetoothDevice) message.obj, 147 BluetoothProfile.STATE_DISCONNECTED, 148 BluetoothProfile.STATE_DISCONNECTED); 149 } 150 break; 151 152 default: 153 Log.w(TAG,"Received unexpected message while disconnected."); 154 return NOT_HANDLED; 155 } 156 return HANDLED; 157 } 158 } 159 160 class Connecting extends State { 161 private boolean mAccountCreated; 162 private boolean mObexAuthorized; 163 164 @Override 165 public void enter() { 166 if (DBG) Log.d(TAG,"Enter Connecting: " + getCurrentMessage().what); 167 168 mAccountCreated = false; 169 mObexAuthorized = false; 170 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 171 BluetoothProfile.STATE_CONNECTING); 172 mMostRecentState = BluetoothProfile.STATE_CONNECTING; 173 // Create a seperate handler instance and thread for performing 174 // connect/download/disconnect opperations as they may be timeconsuming and error prone. 175 mHandlerThread = new HandlerThread("PBAP PCE handler", 176 Process.THREAD_PRIORITY_BACKGROUND); 177 mHandlerThread.start(); 178 mConnectionHandler = new PbapClientConnectionHandler(mHandlerThread.getLooper(), 179 PbapClientStateMachine.this, mCurrentDevice); 180 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_CONNECT) 181 .sendToTarget(); 182 sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT); 183 // TODO: create account 184 } 185 186 @Override 187 public boolean processMessage(Message message) { 188 if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName()); 189 switch (message.what) { 190 case MSG_DISCONNECT: 191 removeMessages(MSG_CONNECT_TIMEOUT); 192 transitionTo(mDisconnecting); 193 break; 194 195 case MSG_CONNECTION_COMPLETE: 196 removeMessages(MSG_CONNECT_TIMEOUT); 197 transitionTo(mConnected); 198 break; 199 200 case MSG_CONNECTION_FAILED: 201 case MSG_CONNECT_TIMEOUT: 202 removeMessages(MSG_CONNECT_TIMEOUT); 203 transitionTo(mDisconnecting); 204 break; 205 case MSG_CONNECT: 206 Log.w(TAG,"Connecting already in progress"); 207 break; 208 209 default: 210 Log.w(TAG,"Received unexpected message while Connecting"); 211 return NOT_HANDLED; 212 } 213 return HANDLED; 214 } 215 } 216 217 class Disconnecting extends State { 218 @Override 219 public void enter() { 220 Log.d(TAG,"Enter Disconnecting: " + getCurrentMessage().what); 221 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 222 BluetoothProfile.STATE_DISCONNECTING); 223 mMostRecentState = BluetoothProfile.STATE_DISCONNECTING; 224 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT).sendToTarget(); 225 sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT); 226 } 227 228 @Override 229 public boolean processMessage(Message message) { 230 if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName()); 231 switch (message.what) { 232 case MSG_CONNECTION_CLOSED: 233 removeMessages(MSG_DISCONNECT_TIMEOUT); 234 mHandlerThread.quitSafely(); 235 transitionTo(mDisconnected); 236 break; 237 238 case MSG_CONNECT: 239 case MSG_DISCONNECT: 240 deferMessage(message); 241 break; 242 243 case MSG_DISCONNECT_TIMEOUT: 244 Log.w(TAG,"Disconnect Timeout, Forcing"); 245 mConnectionHandler.abort(); 246 break; 247 248 default: 249 Log.w(TAG,"Received unexpected message while Disconnecting"); 250 return NOT_HANDLED; 251 } 252 return HANDLED; 253 } 254 } 255 256 class Connected extends State { 257 @Override 258 public void enter() { 259 Log.d(TAG,"Enter Connected: " + getCurrentMessage().what); 260 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 261 BluetoothProfile.STATE_CONNECTED); 262 mMostRecentState = BluetoothProfile.STATE_CONNECTED; 263 // mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD) 264 // .sendToTarget(); 265 } 266 267 @Override 268 public boolean processMessage(Message message) { 269 if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName()); 270 switch (message.what) { 271 case MSG_CONNECT: 272 onConnectionStateChanged(mCurrentDevice, BluetoothProfile.STATE_CONNECTED, 273 BluetoothProfile.STATE_CONNECTED); 274 275 276 Log.w(TAG,"Received CONNECT while Connected, ignoring"); 277 break; 278 279 case MSG_DISCONNECT: 280 transitionTo(mDisconnecting); 281 break; 282 283 default: 284 Log.w(TAG,"Received unexpected message while Connected"); 285 return NOT_HANDLED; 286 } 287 return HANDLED; 288 } 289 } 290 291 private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) { 292 if (device == null) { 293 Log.w(TAG,"onConnectionStateChanged with invalid device"); 294 return; 295 } 296 Log.d(TAG,"Connection state " + device + ": " + prevState + "->" + state); 297 Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); 298 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 299 intent.putExtra(BluetoothProfile.EXTRA_STATE, state); 300 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 301 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 302 mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); 303 } 304 305 public void connect(BluetoothDevice device) { 306 Log.d(TAG, "Connect Request " + device.getAddress()); 307 sendMessage(MSG_CONNECT, device); 308 } 309 310 public void disconnect(BluetoothDevice device) { 311 Log.d(TAG, "Disconnect Request " + device); 312 sendMessage(MSG_DISCONNECT, device); 313 } 314 315 public int getConnectionState() { 316 IState currentState = getCurrentState(); 317 if (currentState instanceof Disconnected) { 318 return BluetoothProfile.STATE_DISCONNECTED; 319 } else if (currentState instanceof Connecting) { 320 return BluetoothProfile.STATE_CONNECTING; 321 } else if (currentState instanceof Connected) { 322 return BluetoothProfile.STATE_CONNECTED; 323 } else if (currentState instanceof Disconnecting) { 324 return BluetoothProfile.STATE_DISCONNECTING; 325 } 326 Log.w(TAG, "Unknown State"); 327 return BluetoothProfile.STATE_DISCONNECTED; 328 } 329 330 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 331 int clientState = -1; 332 BluetoothDevice currentDevice = null; 333 synchronized (mLock) { 334 clientState = getConnectionState(); 335 currentDevice = getDevice(); 336 } 337 List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(); 338 for (int state : states) { 339 if (clientState == state) { 340 if (currentDevice != null) { 341 deviceList.add(currentDevice); 342 } 343 } 344 } 345 return deviceList; 346 } 347 348 public int getConnectionState(BluetoothDevice device) { 349 if (device == null) { 350 return BluetoothProfile.STATE_DISCONNECTED; 351 } 352 synchronized (mLock) { 353 if (device.equals(mCurrentDevice)) { 354 return getConnectionState(); 355 } 356 } 357 return BluetoothProfile.STATE_DISCONNECTED; 358 } 359 360 361 public BluetoothDevice getDevice() { 362 /* 363 * Disconnected is the only state where device can change, and to prevent the race 364 * condition of reporting a valid device while disconnected fix the report here. Note that 365 * Synchronization of the state and device is not possible with current state machine 366 * desingn since the actual Transition happens sometime after the transitionTo method. 367 */ 368 if (getCurrentState() instanceof Disconnected) { 369 return null; 370 } 371 return mCurrentDevice; 372 } 373} 374