PbapClientStateMachine.java revision 16159961f4060636b4f778b39ef2d006807fc803
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.accounts.Account; 45import android.accounts.AccountManager; 46import android.bluetooth.BluetoothDevice; 47import android.bluetooth.BluetoothProfile; 48import android.bluetooth.BluetoothPbapClient; 49import android.content.Context; 50import android.content.Intent; 51import android.os.HandlerThread; 52import android.os.Message; 53import android.os.Process; 54import android.os.UserManager; 55import android.provider.CallLog; 56import android.util.Log; 57 58import com.android.bluetooth.btservice.ProfileService; 59import com.android.bluetooth.R; 60import com.android.internal.util.IState; 61import com.android.internal.util.State; 62import com.android.internal.util.StateMachine; 63 64import java.lang.IllegalStateException; 65import java.util.ArrayList; 66import java.util.List; 67 68final class PbapClientStateMachine extends StateMachine { 69 private static final boolean DBG = true; 70 private static final String TAG = "PbapClientStateMachine"; 71 72 // Messages for handling connect/disconnect requests. 73 private static final int MSG_CONNECT = 1; 74 private static final int MSG_DISCONNECT = 2; 75 76 // Messages for handling error conditions. 77 private static final int MSG_CONNECT_TIMEOUT = 3; 78 private static final int MSG_DISCONNECT_TIMEOUT = 4; 79 80 // Messages for feedback from ConnectionHandler. 81 static final int MSG_CONNECTION_COMPLETE = 5; 82 static final int MSG_CONNECTION_FAILED = 6; 83 static final int MSG_CONNECTION_CLOSED = 7; 84 static final int MSG_RESUME_DOWNLOAD = 8; 85 86 static final int CONNECT_TIMEOUT = 6000; 87 static final int DISCONNECT_TIMEOUT = 3000; 88 89 private final Object mLock; 90 private State mDisconnected; 91 private State mConnecting; 92 private State mConnected; 93 private State mDisconnecting; 94 95 // mCurrentDevice may only be changed in Disconnected State. 96 private BluetoothDevice mCurrentDevice = null; 97 private PbapClientService mService; 98 private Context mContext; 99 private PbapClientConnectionHandler mConnectionHandler; 100 private HandlerThread mHandlerThread = null; 101 private UserManager mUserManager = null; 102 103 // mMostRecentState maintains previous state for broadcasting transitions. 104 private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; 105 106 PbapClientStateMachine(PbapClientService svc, Context context) { 107 super(TAG); 108 109 mService = svc; 110 mContext = context; 111 mLock = new Object(); 112 mUserManager = UserManager.get(mContext); 113 mDisconnected = new Disconnected(); 114 mConnecting = new Connecting(); 115 mDisconnecting = new Disconnecting(); 116 mConnected = new Connected(); 117 removeUncleanAccounts(); 118 119 addState(mDisconnected); 120 addState(mConnecting); 121 addState(mDisconnecting); 122 addState(mConnected); 123 124 setInitialState(mDisconnected); 125 } 126 127 class Disconnected extends State { 128 @Override 129 public void enter() { 130 Log.d(TAG,"Enter Disconnected: " + getCurrentMessage().what); 131 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 132 BluetoothProfile.STATE_DISCONNECTED); 133 mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; 134 synchronized (mLock) { 135 mCurrentDevice = null; 136 } 137 138 } 139 140 @Override 141 public boolean processMessage(Message message) { 142 if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName()); 143 switch (message.what) { 144 case MSG_CONNECT: 145 if (message.obj instanceof BluetoothDevice) { 146 synchronized(mLock) { 147 mCurrentDevice = (BluetoothDevice) message.obj; 148 } 149 transitionTo(mConnecting); 150 } else { 151 Log.w(TAG,"Received CONNECT without valid device"); 152 throw new IllegalStateException("invalid device"); 153 } 154 break; 155 156 case MSG_DISCONNECT: 157 Log.w(TAG,"Received unexpected disconnect while disconnected."); 158 // It is possible if something crashed for others to think we are connected 159 // already, just remind them. 160 if (message.obj instanceof BluetoothDevice) { 161 onConnectionStateChanged((BluetoothDevice) message.obj, 162 BluetoothProfile.STATE_DISCONNECTED, 163 BluetoothProfile.STATE_DISCONNECTED); 164 } 165 break; 166 167 case MSG_RESUME_DOWNLOAD: 168 // Do nothing. 169 break; 170 171 default: 172 Log.w(TAG,"Received unexpected message while disconnected."); 173 return NOT_HANDLED; 174 } 175 return HANDLED; 176 } 177 } 178 179 class Connecting extends State { 180 @Override 181 public void enter() { 182 if (DBG) Log.d(TAG,"Enter Connecting: " + getCurrentMessage().what); 183 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 184 BluetoothProfile.STATE_CONNECTING); 185 mMostRecentState = BluetoothProfile.STATE_CONNECTING; 186 // Create a seperate handler instance and thread for performing 187 // connect/download/disconnect opperations as they may be timeconsuming and error prone. 188 mHandlerThread = new HandlerThread("PBAP PCE handler", 189 Process.THREAD_PRIORITY_BACKGROUND); 190 mHandlerThread.start(); 191 mConnectionHandler = new PbapClientConnectionHandler(mHandlerThread.getLooper(), 192 mContext, PbapClientStateMachine.this, mCurrentDevice); 193 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_CONNECT) 194 .sendToTarget(); 195 sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT); 196 } 197 198 @Override 199 public boolean processMessage(Message message) { 200 if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName()); 201 switch (message.what) { 202 case MSG_DISCONNECT: 203 if (message.obj instanceof BluetoothDevice && 204 ((BluetoothDevice) message.obj).equals(mCurrentDevice)) { 205 removeMessages(MSG_CONNECT_TIMEOUT); 206 transitionTo(mDisconnecting); 207 } 208 break; 209 210 case MSG_CONNECTION_COMPLETE: 211 removeMessages(MSG_CONNECT_TIMEOUT); 212 transitionTo(mConnected); 213 break; 214 215 case MSG_CONNECTION_FAILED: 216 case MSG_CONNECT_TIMEOUT: 217 removeMessages(MSG_CONNECT_TIMEOUT); 218 transitionTo(mDisconnecting); 219 break; 220 case MSG_CONNECT: 221 Log.w(TAG,"Connecting already in progress"); 222 break; 223 224 case MSG_RESUME_DOWNLOAD: 225 // Do nothing. 226 break; 227 228 default: 229 Log.w(TAG,"Received unexpected message while Connecting"); 230 return NOT_HANDLED; 231 } 232 return HANDLED; 233 } 234 } 235 236 class Disconnecting extends State { 237 @Override 238 public void enter() { 239 Log.d(TAG,"Enter Disconnecting: " + getCurrentMessage().what); 240 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 241 BluetoothProfile.STATE_DISCONNECTING); 242 mMostRecentState = BluetoothProfile.STATE_DISCONNECTING; 243 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT) 244 .sendToTarget(); 245 sendMessageDelayed(MSG_DISCONNECT_TIMEOUT,DISCONNECT_TIMEOUT); 246 } 247 248 @Override 249 public boolean processMessage(Message message) { 250 if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName()); 251 switch (message.what) { 252 case MSG_CONNECTION_CLOSED: 253 removeMessages(MSG_DISCONNECT_TIMEOUT); 254 mHandlerThread.quitSafely(); 255 transitionTo(mDisconnected); 256 break; 257 258 case MSG_CONNECT: 259 case MSG_DISCONNECT: 260 deferMessage(message); 261 break; 262 263 case MSG_DISCONNECT_TIMEOUT: 264 Log.w(TAG,"Disconnect Timeout, Forcing"); 265 mConnectionHandler.abort(); 266 break; 267 268 case MSG_RESUME_DOWNLOAD: 269 // Do nothing. 270 break; 271 272 default: 273 Log.w(TAG,"Received unexpected message while Disconnecting"); 274 return NOT_HANDLED; 275 } 276 return HANDLED; 277 } 278 } 279 280 class Connected extends State { 281 @Override 282 public void enter() { 283 Log.d(TAG,"Enter Connected: " + getCurrentMessage().what); 284 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 285 BluetoothProfile.STATE_CONNECTED); 286 mMostRecentState = BluetoothProfile.STATE_CONNECTED; 287 if (mUserManager.isUserUnlocked()) { 288 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD) 289 .sendToTarget(); 290 } 291 } 292 293 @Override 294 public boolean processMessage(Message message) { 295 if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName()); 296 switch (message.what) { 297 case MSG_CONNECT: 298 onConnectionStateChanged(mCurrentDevice, BluetoothProfile.STATE_CONNECTED, 299 BluetoothProfile.STATE_CONNECTED); 300 301 302 Log.w(TAG,"Received CONNECT while Connected, ignoring"); 303 break; 304 305 case MSG_DISCONNECT: 306 if ((message.obj instanceof BluetoothDevice) && 307 ((BluetoothDevice) message.obj).equals(mCurrentDevice)) { 308 transitionTo(mDisconnecting); 309 } 310 break; 311 312 case MSG_RESUME_DOWNLOAD: 313 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD) 314 .sendToTarget(); 315 break; 316 317 default: 318 Log.w(TAG,"Received unexpected message while Connected"); 319 return NOT_HANDLED; 320 } 321 return HANDLED; 322 } 323 } 324 325 private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) { 326 if (device == null) { 327 Log.w(TAG,"onConnectionStateChanged with invalid device"); 328 return; 329 } 330 Log.d(TAG,"Connection state " + device + ": " + prevState + "->" + state); 331 Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); 332 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 333 intent.putExtra(BluetoothProfile.EXTRA_STATE, state); 334 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 335 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 336 mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); 337 mService.notifyProfileConnectionStateChanged(device, BluetoothProfile.PBAP_CLIENT, state, 338 prevState); 339 } 340 341 public void connect(BluetoothDevice device) { 342 Log.d(TAG, "Connect Request " + device.getAddress()); 343 sendMessage(MSG_CONNECT, device); 344 } 345 346 public void disconnect(BluetoothDevice device) { 347 Log.d(TAG, "Disconnect Request " + device); 348 sendMessage(MSG_DISCONNECT, device); 349 } 350 351 public void resumeDownload() { 352 removeUncleanAccounts(); 353 sendMessage(MSG_RESUME_DOWNLOAD); 354 } 355 356 void doQuit() { 357 removeUncleanAccounts(); 358 if (mHandlerThread != null) { 359 mHandlerThread.quitSafely(); 360 } 361 quitNow(); 362 } 363 364 public int getConnectionState() { 365 IState currentState = getCurrentState(); 366 if (currentState instanceof Disconnected) { 367 return BluetoothProfile.STATE_DISCONNECTED; 368 } else if (currentState instanceof Connecting) { 369 return BluetoothProfile.STATE_CONNECTING; 370 } else if (currentState instanceof Connected) { 371 return BluetoothProfile.STATE_CONNECTED; 372 } else if (currentState instanceof Disconnecting) { 373 return BluetoothProfile.STATE_DISCONNECTING; 374 } 375 Log.w(TAG, "Unknown State"); 376 return BluetoothProfile.STATE_DISCONNECTED; 377 } 378 379 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 380 int clientState = -1; 381 BluetoothDevice currentDevice = null; 382 synchronized (mLock) { 383 clientState = getConnectionState(); 384 currentDevice = getDevice(); 385 } 386 List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(); 387 for (int state : states) { 388 if (clientState == state) { 389 if (currentDevice != null) { 390 deviceList.add(currentDevice); 391 } 392 } 393 } 394 return deviceList; 395 } 396 397 public int getConnectionState(BluetoothDevice device) { 398 if (device == null) { 399 return BluetoothProfile.STATE_DISCONNECTED; 400 } 401 synchronized (mLock) { 402 if (device.equals(mCurrentDevice)) { 403 return getConnectionState(); 404 } 405 } 406 return BluetoothProfile.STATE_DISCONNECTED; 407 } 408 409 410 public BluetoothDevice getDevice() { 411 /* 412 * Disconnected is the only state where device can change, and to prevent the race 413 * condition of reporting a valid device while disconnected fix the report here. Note that 414 * Synchronization of the state and device is not possible with current state machine 415 * desingn since the actual Transition happens sometime after the transitionTo method. 416 */ 417 if (getCurrentState() instanceof Disconnected) { 418 return null; 419 } 420 return mCurrentDevice; 421 } 422 423 Context getContext() { 424 return mContext; 425 } 426 427 private void removeUncleanAccounts() { 428 // Find all accounts that match the type "pbap" and delete them. 429 AccountManager accountManager = AccountManager.get(mContext); 430 Account[] accounts = accountManager.getAccountsByType( 431 mContext.getString(R.string.pbap_account_type)); 432 Log.w(TAG, "Found " + accounts.length + " unclean accounts"); 433 for (Account acc : accounts) { 434 Log.w(TAG, "Deleting " + acc); 435 // The device ID is the name of the account. 436 accountManager.removeAccountExplicitly(acc); 437 } 438 mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, null, null); 439 440 } 441 442 public void dump(StringBuilder sb) { 443 ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice); 444 ProfileService.println(sb, "StateMachine: " + this.toString()); 445 } 446} 447