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 */ 42 package com.android.bluetooth.pbapclient; 43 44 import static android.Manifest.permission.BLUETOOTH_CONNECT; 45 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; 46 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; 47 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; 48 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; 49 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING; 50 51 import static com.android.bluetooth.Utils.joinUninterruptibly; 52 53 import android.bluetooth.BluetoothDevice; 54 import android.bluetooth.BluetoothPbapClient; 55 import android.bluetooth.BluetoothProfile; 56 import android.bluetooth.BluetoothUuid; 57 import android.content.Context; 58 import android.content.Intent; 59 import android.os.HandlerThread; 60 import android.os.Looper; 61 import android.os.Message; 62 import android.os.Process; 63 import android.os.UserManager; 64 import android.util.Log; 65 66 import com.android.bluetooth.Utils; 67 import com.android.bluetooth.btservice.AdapterService; 68 import com.android.bluetooth.btservice.MetricsLogger; 69 import com.android.bluetooth.btservice.ProfileService; 70 import com.android.bluetooth.flags.Flags; 71 import com.android.internal.annotations.VisibleForTesting; 72 import com.android.internal.util.IState; 73 import com.android.internal.util.State; 74 import com.android.internal.util.StateMachine; 75 76 import java.util.ArrayList; 77 import java.util.List; 78 79 class PbapClientStateMachineOld extends StateMachine { 80 private static final String TAG = PbapClientStateMachineOld.class.getSimpleName(); 81 82 // Messages for handling connect/disconnect requests. 83 private static final int MSG_DISCONNECT = 2; 84 85 // Messages for handling error conditions. 86 private static final int MSG_CONNECT_TIMEOUT = 3; 87 private static final int MSG_DISCONNECT_TIMEOUT = 4; 88 89 // Messages for feedback from ConnectionHandler. 90 static final int MSG_CONNECTION_COMPLETE = 5; 91 static final int MSG_CONNECTION_FAILED = 6; 92 static final int MSG_CONNECTION_CLOSED = 7; 93 static final int MSG_RESUME_DOWNLOAD = 8; 94 static final int MSG_SDP_COMPLETE = 9; 95 static final int MSG_SDP_BUSY = 10; 96 static final int MSG_SDP_FAIL = 11; 97 98 // Constants for SDP. Note that these values come from the native stack, but no centralized 99 // constants exist for them as part of the various SDP APIs. 100 public static final int SDP_SUCCESS = 0; 101 public static final int SDP_FAILED = 1; 102 public static final int SDP_BUSY = 2; 103 104 // All times are in milliseconds 105 static final int CONNECT_TIMEOUT = 10000; 106 static final int DISCONNECT_TIMEOUT = 3000; 107 static final int SDP_BUSY_RETRY_DELAY = 20; 108 109 private static final int LOCAL_SUPPORTED_FEATURES = 110 PbapSdpRecord.FEATURE_DEFAULT_IMAGE_FORMAT | PbapSdpRecord.FEATURE_DOWNLOADING; 111 112 private final Object mLock; 113 private final State mDisconnected; 114 private final State mConnecting; 115 private final State mConnected; 116 private final State mDisconnecting; 117 118 // mCurrentDevice may only be changed in Disconnected State. 119 private final BluetoothDevice mCurrentDevice; 120 private final PbapClientService mService; 121 private PbapClientConnectionHandler mConnectionHandler; 122 private HandlerThread mHandlerThread = null; 123 private UserManager mUserManager = null; 124 private final HandlerThread mSmHandlerThread; 125 126 // mMostRecentState maintains previous state for broadcasting transitions. 127 private int mMostRecentState = STATE_DISCONNECTED; 128 PbapClientStateMachineOld( PbapClientService svc, BluetoothDevice device, HandlerThread handlerThread)129 PbapClientStateMachineOld( 130 PbapClientService svc, BluetoothDevice device, HandlerThread handlerThread) { 131 this(svc, device, null, handlerThread); 132 } 133 134 @VisibleForTesting PbapClientStateMachineOld( PbapClientService svc, BluetoothDevice device, PbapClientConnectionHandler connectionHandler, HandlerThread handlerThread)135 PbapClientStateMachineOld( 136 PbapClientService svc, 137 BluetoothDevice device, 138 PbapClientConnectionHandler connectionHandler, 139 HandlerThread handlerThread) { 140 super(TAG, handlerThread.getLooper()); 141 mSmHandlerThread = handlerThread; 142 143 if (Flags.pbapClientStorageRefactor()) { 144 Log.w(TAG, "This object is no longer used in this configuration"); 145 } 146 147 mService = svc; 148 mCurrentDevice = device; 149 mConnectionHandler = connectionHandler; 150 mLock = new Object(); 151 mUserManager = mService.getSystemService(UserManager.class); 152 mDisconnected = new Disconnected(); 153 mConnecting = new Connecting(); 154 mDisconnecting = new Disconnecting(); 155 mConnected = new Connected(); 156 157 addState(mDisconnected); 158 addState(mConnecting); 159 addState(mDisconnecting); 160 addState(mConnected); 161 162 setInitialState(mConnecting); 163 } 164 165 class Disconnected extends State { 166 @Override enter()167 public void enter() { 168 Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what); 169 onConnectionStateChanged(mCurrentDevice, mMostRecentState, STATE_DISCONNECTED); 170 mMostRecentState = STATE_DISCONNECTED; 171 quit(); 172 } 173 } 174 175 class Connecting extends State { 176 177 @Override enter()178 public void enter() { 179 Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what); 180 onConnectionStateChanged(mCurrentDevice, mMostRecentState, STATE_CONNECTING); 181 mCurrentDevice.sdpSearch(BluetoothUuid.PBAP_PSE); 182 mMostRecentState = STATE_CONNECTING; 183 184 // Create a separate handler instance and thread for performing 185 // connect/download/disconnect operations as they may be time consuming and error prone. 186 HandlerThread handlerThread = 187 new HandlerThread("PBAP PCE handler", Process.THREAD_PRIORITY_BACKGROUND); 188 handlerThread.start(); 189 Looper looper = handlerThread.getLooper(); 190 191 // Keeps mock handler from being overwritten in tests 192 if (mConnectionHandler == null && looper != null) { 193 mConnectionHandler = 194 new PbapClientConnectionHandler.Builder() 195 .setLooper(looper) 196 .setLocalSupportedFeatures(LOCAL_SUPPORTED_FEATURES) 197 .setService(mService) 198 .setClientSM(PbapClientStateMachineOld.this) 199 .setRemoteDevice(mCurrentDevice) 200 .build(); 201 } 202 mHandlerThread = handlerThread; 203 sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT); 204 } 205 206 @Override processMessage(Message message)207 public boolean processMessage(Message message) { 208 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); 209 switch (message.what) { 210 case MSG_DISCONNECT: 211 if (message.obj instanceof BluetoothDevice 212 && message.obj.equals(mCurrentDevice)) { 213 removeMessages(MSG_CONNECT_TIMEOUT); 214 transitionTo(mDisconnecting); 215 } 216 break; 217 218 case MSG_CONNECTION_COMPLETE: 219 removeMessages(MSG_CONNECT_TIMEOUT); 220 transitionTo(mConnected); 221 break; 222 223 case MSG_CONNECTION_FAILED: 224 case MSG_CONNECT_TIMEOUT: 225 removeMessages(MSG_CONNECT_TIMEOUT); 226 transitionTo(mDisconnecting); 227 break; 228 229 case MSG_SDP_COMPLETE: 230 removeMessages(MSG_SDP_BUSY); 231 PbapClientConnectionHandler connectionHandler = mConnectionHandler; 232 if (connectionHandler != null) { 233 if (message.obj == null) { 234 Log.w(TAG, "Received SDP response without valid PSE record "); 235 } 236 connectionHandler 237 .obtainMessage(PbapClientConnectionHandler.MSG_CONNECT, message.obj) 238 .sendToTarget(); 239 } else { 240 Log.w(TAG, "Received SDP complete without connection handler"); 241 } 242 break; 243 244 case MSG_SDP_BUSY: 245 removeMessages(MSG_SDP_BUSY); 246 Log.d(TAG, "Received SDP busy, try again"); 247 mCurrentDevice.sdpSearch(BluetoothUuid.PBAP_PSE); 248 break; 249 250 case MSG_SDP_FAIL: 251 removeMessages(MSG_SDP_BUSY); 252 int status = message.arg1; 253 Log.w(TAG, "SDP failed status:" + status + ", starting disconnect"); 254 transitionTo(mDisconnecting); 255 break; 256 257 case MSG_RESUME_DOWNLOAD: 258 Log.i( 259 TAG, 260 "Received request to download phonebook but still in state " 261 + this.getName()); 262 break; 263 264 default: 265 Log.w(TAG, "Received unexpected message while Connecting"); 266 return NOT_HANDLED; 267 } 268 return HANDLED; 269 } 270 } 271 272 class Disconnecting extends State { 273 @Override enter()274 public void enter() { 275 Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what); 276 onConnectionStateChanged(mCurrentDevice, mMostRecentState, STATE_DISCONNECTING); 277 mMostRecentState = STATE_DISCONNECTING; 278 PbapClientConnectionHandler connectionHandler = mConnectionHandler; 279 if (connectionHandler != null) { 280 connectionHandler 281 .obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT) 282 .sendToTarget(); 283 } 284 sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT); 285 } 286 287 @Override processMessage(Message message)288 public boolean processMessage(Message message) { 289 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); 290 PbapClientConnectionHandler connectionHandler = mConnectionHandler; 291 HandlerThread handlerThread = mHandlerThread; 292 293 switch (message.what) { 294 case MSG_CONNECTION_CLOSED: 295 removeMessages(MSG_DISCONNECT_TIMEOUT); 296 if (handlerThread != null) { 297 handlerThread.quitSafely(); 298 } 299 transitionTo(mDisconnected); 300 break; 301 302 case MSG_DISCONNECT: 303 deferMessage(message); 304 break; 305 306 case MSG_DISCONNECT_TIMEOUT: 307 Log.w(TAG, "Disconnect Timeout, Forcing"); 308 if (connectionHandler != null) { 309 connectionHandler.abort(); 310 } 311 if (handlerThread != null) { 312 handlerThread.quitSafely(); 313 } 314 transitionTo(mDisconnected); 315 break; 316 317 case MSG_RESUME_DOWNLOAD: 318 // Do nothing. 319 break; 320 321 default: 322 Log.w(TAG, "Received unexpected message while Disconnecting"); 323 return NOT_HANDLED; 324 } 325 return HANDLED; 326 } 327 } 328 329 class Connected extends State { 330 @Override enter()331 public void enter() { 332 Log.d(TAG, "Enter Connected: " + getCurrentMessage().what); 333 onConnectionStateChanged(mCurrentDevice, mMostRecentState, STATE_CONNECTED); 334 mMostRecentState = STATE_CONNECTED; 335 downloadIfReady(); 336 } 337 338 @Override processMessage(Message message)339 public boolean processMessage(Message message) { 340 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); 341 switch (message.what) { 342 case MSG_DISCONNECT: 343 if ((message.obj instanceof BluetoothDevice) 344 && ((BluetoothDevice) message.obj).equals(mCurrentDevice)) { 345 transitionTo(mDisconnecting); 346 } 347 break; 348 349 case MSG_RESUME_DOWNLOAD: 350 downloadIfReady(); 351 break; 352 353 default: 354 Log.w(TAG, "Received unexpected message while Connected"); 355 return NOT_HANDLED; 356 } 357 return HANDLED; 358 } 359 } 360 361 /** Notify of SDP completion. */ onSdpResultReceived(int status, PbapSdpRecord record)362 public void onSdpResultReceived(int status, PbapSdpRecord record) { 363 Log.d(TAG, "Received SDP Result, status=" + status + ", record=" + record); 364 switch (status) { 365 case SDP_SUCCESS: 366 sendMessage(PbapClientStateMachineOld.MSG_SDP_COMPLETE, record); 367 break; 368 369 case SDP_BUSY: 370 sendMessageDelayed(PbapClientStateMachineOld.MSG_SDP_BUSY, SDP_BUSY_RETRY_DELAY); 371 break; 372 373 default: 374 sendMessage(PbapClientStateMachineOld.MSG_SDP_FAIL); 375 break; 376 } 377 } 378 379 /** Trigger a contacts download if the user is unlocked and our accounts are available to us */ downloadIfReady()380 private void downloadIfReady() { 381 boolean userReady = mUserManager.isUserUnlocked(); 382 boolean accountTypeReady = mService.isAccountTypeReady(); 383 if (!userReady || !accountTypeReady) { 384 Log.w( 385 TAG, 386 "Cannot download contacts yet, userReady=" 387 + userReady 388 + ", accountTypeReady=" 389 + accountTypeReady); 390 return; 391 } 392 PbapClientConnectionHandler connectionHandler = mConnectionHandler; 393 if (connectionHandler != null) { 394 connectionHandler 395 .obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD) 396 .sendToTarget(); 397 } 398 } 399 onConnectionStateChanged(BluetoothDevice device, int prevState, int state)400 private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) { 401 if (device == null) { 402 Log.w(TAG, "onConnectionStateChanged with invalid device"); 403 return; 404 } 405 Log.d(TAG, "Connection state " + device + ": " + prevState + "->" + state); 406 AdapterService adapterService = AdapterService.getAdapterService(); 407 if (adapterService != null) { 408 adapterService.updateProfileConnectionAdapterProperties( 409 device, BluetoothProfile.PBAP_CLIENT, state, prevState); 410 } 411 Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); 412 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 413 intent.putExtra(BluetoothProfile.EXTRA_STATE, state); 414 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 415 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 416 mService.sendBroadcastMultiplePermissions( 417 intent, 418 new String[] {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}, 419 Utils.getTempBroadcastOptions()); 420 } 421 disconnect(BluetoothDevice device)422 public void disconnect(BluetoothDevice device) { 423 Log.d(TAG, "Disconnect Request " + device); 424 sendMessage(MSG_DISCONNECT, device); 425 } 426 tryDownloadIfConnected()427 public void tryDownloadIfConnected() { 428 sendMessage(MSG_RESUME_DOWNLOAD); 429 } 430 doQuit()431 void doQuit() { 432 PbapClientConnectionHandler connectionHandler = mConnectionHandler; 433 if (connectionHandler != null) { 434 connectionHandler.abort(); 435 mConnectionHandler = null; 436 } 437 438 HandlerThread handlerThread = mHandlerThread; 439 if (handlerThread != null) { 440 handlerThread.quitSafely(); 441 joinUninterruptibly(handlerThread); 442 mHandlerThread = null; 443 } 444 mSmHandlerThread.quitSafely(); 445 joinUninterruptibly(mSmHandlerThread); 446 quitNow(); 447 } 448 449 @Override onQuitting()450 protected void onQuitting() { 451 mService.cleanupDevice(mCurrentDevice); 452 } 453 getConnectionState()454 public int getConnectionState() { 455 IState currentState = getCurrentState(); 456 if (currentState instanceof Disconnected) { 457 return STATE_DISCONNECTED; 458 } else if (currentState instanceof Connecting) { 459 return STATE_CONNECTING; 460 } else if (currentState instanceof Connected) { 461 return STATE_CONNECTED; 462 } else if (currentState instanceof Disconnecting) { 463 return STATE_DISCONNECTING; 464 } 465 Log.w(TAG, "Unknown State"); 466 return STATE_DISCONNECTED; 467 } 468 getDevicesMatchingConnectionStates(int[] states)469 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 470 int clientState; 471 BluetoothDevice currentDevice; 472 synchronized (mLock) { 473 clientState = getConnectionState(); 474 currentDevice = getDevice(); 475 } 476 List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(); 477 for (int state : states) { 478 if (clientState == state) { 479 if (currentDevice != null) { 480 deviceList.add(currentDevice); 481 } 482 } 483 } 484 return deviceList; 485 } 486 getConnectionState(BluetoothDevice device)487 public int getConnectionState(BluetoothDevice device) { 488 if (device == null) { 489 return STATE_DISCONNECTED; 490 } 491 synchronized (mLock) { 492 if (device.equals(mCurrentDevice)) { 493 return getConnectionState(); 494 } 495 } 496 return STATE_DISCONNECTED; 497 } 498 getDevice()499 public BluetoothDevice getDevice() { 500 /* 501 * Disconnected is the only state where device can change, and to prevent the race 502 * condition of reporting a valid device while disconnected fix the report here. Note that 503 * Synchronization of the state and device is not possible with current state machine 504 * design since the actual Transition happens sometime after the transitionTo method. 505 */ 506 if (getCurrentState() instanceof Disconnected) { 507 return null; 508 } 509 return mCurrentDevice; 510 } 511 getContext()512 Context getContext() { 513 return mService; 514 } 515 dump(StringBuilder sb)516 public void dump(StringBuilder sb) { 517 ProfileService.println( 518 sb, 519 "mCurrentDevice: " 520 + mCurrentDevice.getAddress() 521 + "(" 522 + Utils.getName(mCurrentDevice) 523 + ") " 524 + this.toString()); 525 } 526 } 527