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