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