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 47 import android.bluetooth.BluetoothDevice; 48 import android.bluetooth.BluetoothPbapClient; 49 import android.bluetooth.BluetoothProfile; 50 import android.bluetooth.BluetoothUuid; 51 import android.content.BroadcastReceiver; 52 import android.content.Context; 53 import android.content.Intent; 54 import android.content.IntentFilter; 55 import android.os.HandlerThread; 56 import android.os.Message; 57 import android.os.ParcelUuid; 58 import android.os.Process; 59 import android.os.UserManager; 60 import android.util.Log; 61 62 import com.android.bluetooth.BluetoothMetricsProto; 63 import com.android.bluetooth.Utils; 64 import com.android.bluetooth.btservice.MetricsLogger; 65 import com.android.bluetooth.btservice.ProfileService; 66 import com.android.internal.annotations.VisibleForTesting; 67 import com.android.internal.util.IState; 68 import com.android.internal.util.State; 69 import com.android.internal.util.StateMachine; 70 71 import java.util.ArrayList; 72 import java.util.List; 73 74 class PbapClientStateMachine extends StateMachine { 75 private static final String TAG = "PbapClientStateMachine"; 76 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 77 78 // Messages for handling connect/disconnect requests. 79 private static final int MSG_DISCONNECT = 2; 80 private static final int MSG_SDP_COMPLETE = 9; 81 82 // Messages for handling error conditions. 83 private static final int MSG_CONNECT_TIMEOUT = 3; 84 private static final int MSG_DISCONNECT_TIMEOUT = 4; 85 86 // Messages for feedback from ConnectionHandler. 87 static final int MSG_CONNECTION_COMPLETE = 5; 88 static final int MSG_CONNECTION_FAILED = 6; 89 static final int MSG_CONNECTION_CLOSED = 7; 90 static final int MSG_RESUME_DOWNLOAD = 8; 91 92 static final int CONNECT_TIMEOUT = 10000; 93 static final int DISCONNECT_TIMEOUT = 3000; 94 95 private final Object mLock; 96 private State mDisconnected; 97 private State mConnecting; 98 private State mConnected; 99 private State mDisconnecting; 100 101 // mCurrentDevice may only be changed in Disconnected State. 102 private final BluetoothDevice mCurrentDevice; 103 private PbapClientService mService; 104 private PbapClientConnectionHandler mConnectionHandler; 105 private HandlerThread mHandlerThread = null; 106 private UserManager mUserManager = null; 107 108 // mMostRecentState maintains previous state for broadcasting transitions. 109 private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; 110 PbapClientStateMachine(PbapClientService svc, BluetoothDevice device)111 PbapClientStateMachine(PbapClientService svc, BluetoothDevice device) { 112 this(svc, device, null); 113 } 114 115 @VisibleForTesting PbapClientStateMachine(PbapClientService svc, BluetoothDevice device, PbapClientConnectionHandler connectionHandler)116 PbapClientStateMachine(PbapClientService svc, BluetoothDevice device, 117 PbapClientConnectionHandler connectionHandler) { 118 super(TAG); 119 120 mService = svc; 121 mCurrentDevice = device; 122 mConnectionHandler = connectionHandler; 123 mLock = new Object(); 124 mUserManager = mService.getSystemService(UserManager.class); 125 mDisconnected = new Disconnected(); 126 mConnecting = new Connecting(); 127 mDisconnecting = new Disconnecting(); 128 mConnected = new Connected(); 129 130 addState(mDisconnected); 131 addState(mConnecting); 132 addState(mDisconnecting); 133 addState(mConnected); 134 135 setInitialState(mConnecting); 136 } 137 138 class Disconnected extends State { 139 @Override enter()140 public void enter() { 141 if (DBG) Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what); 142 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 143 BluetoothProfile.STATE_DISCONNECTED); 144 mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; 145 quit(); 146 } 147 } 148 149 class Connecting extends State { 150 private SDPBroadcastReceiver mSdpReceiver; 151 152 @Override enter()153 public void enter() { 154 if (DBG) { 155 Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what); 156 } 157 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 158 BluetoothProfile.STATE_CONNECTING); 159 mSdpReceiver = new SDPBroadcastReceiver(); 160 mSdpReceiver.register(); 161 mCurrentDevice.sdpSearch(BluetoothUuid.PBAP_PSE); 162 mMostRecentState = BluetoothProfile.STATE_CONNECTING; 163 164 // Create a separate handler instance and thread for performing 165 // connect/download/disconnect operations as they may be time consuming and error prone. 166 mHandlerThread = 167 new HandlerThread("PBAP PCE handler", Process.THREAD_PRIORITY_BACKGROUND); 168 mHandlerThread.start(); 169 170 // Keeps mock handler from being overwritten in tests 171 if (mConnectionHandler == null) { 172 mConnectionHandler = 173 new PbapClientConnectionHandler.Builder().setLooper(mHandlerThread.getLooper()) 174 .setContext(mService) 175 .setClientSM(PbapClientStateMachine.this) 176 .setRemoteDevice(mCurrentDevice) 177 .build(); 178 } 179 180 sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT); 181 } 182 183 @Override processMessage(Message message)184 public boolean processMessage(Message message) { 185 if (DBG) { 186 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); 187 } 188 switch (message.what) { 189 case MSG_DISCONNECT: 190 if (message.obj instanceof BluetoothDevice && message.obj.equals( 191 mCurrentDevice)) { 192 removeMessages(MSG_CONNECT_TIMEOUT); 193 transitionTo(mDisconnecting); 194 } 195 break; 196 197 case MSG_CONNECTION_COMPLETE: 198 removeMessages(MSG_CONNECT_TIMEOUT); 199 transitionTo(mConnected); 200 break; 201 202 case MSG_CONNECTION_FAILED: 203 case MSG_CONNECT_TIMEOUT: 204 removeMessages(MSG_CONNECT_TIMEOUT); 205 transitionTo(mDisconnecting); 206 break; 207 208 case MSG_SDP_COMPLETE: 209 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_CONNECT, 210 message.obj).sendToTarget(); 211 break; 212 213 default: 214 Log.w(TAG, "Received unexpected message while Connecting"); 215 return NOT_HANDLED; 216 } 217 return HANDLED; 218 } 219 220 @Override exit()221 public void exit() { 222 mSdpReceiver.unregister(); 223 mSdpReceiver = null; 224 } 225 226 private class SDPBroadcastReceiver extends BroadcastReceiver { 227 @Override onReceive(Context context, Intent intent)228 public void onReceive(Context context, Intent intent) { 229 String action = intent.getAction(); 230 if (DBG) { 231 Log.v(TAG, "onReceive" + action); 232 } 233 if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { 234 BluetoothDevice device = 235 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 236 if (!device.equals(getDevice())) { 237 Log.w(TAG, "SDP Record fetched for different device - Ignore"); 238 return; 239 } 240 ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); 241 if (DBG) { 242 Log.v(TAG, "Received UUID: " + uuid.toString()); 243 Log.v(TAG, "expected UUID: " + BluetoothUuid.PBAP_PSE.toString()); 244 } 245 if (uuid.equals(BluetoothUuid.PBAP_PSE)) { 246 sendMessage(MSG_SDP_COMPLETE, 247 intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD)); 248 } 249 } 250 } 251 register()252 public void register() { 253 IntentFilter filter = new IntentFilter(); 254 filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); 255 filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); 256 mService.registerReceiver(this, filter); 257 } 258 unregister()259 public void unregister() { 260 mService.unregisterReceiver(this); 261 } 262 } 263 } 264 265 class Disconnecting extends State { 266 @Override enter()267 public void enter() { 268 if (DBG) Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what); 269 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 270 BluetoothProfile.STATE_DISCONNECTING); 271 mMostRecentState = BluetoothProfile.STATE_DISCONNECTING; 272 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT) 273 .sendToTarget(); 274 sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT); 275 } 276 277 @Override processMessage(Message message)278 public boolean processMessage(Message message) { 279 if (DBG) { 280 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); 281 } 282 switch (message.what) { 283 case MSG_CONNECTION_CLOSED: 284 removeMessages(MSG_DISCONNECT_TIMEOUT); 285 mHandlerThread.quitSafely(); 286 transitionTo(mDisconnected); 287 break; 288 289 case MSG_DISCONNECT: 290 deferMessage(message); 291 break; 292 293 case MSG_DISCONNECT_TIMEOUT: 294 Log.w(TAG, "Disconnect Timeout, Forcing"); 295 mConnectionHandler.abort(); 296 mHandlerThread.quitSafely(); 297 transitionTo(mDisconnected); 298 break; 299 300 case MSG_RESUME_DOWNLOAD: 301 // Do nothing. 302 break; 303 304 default: 305 Log.w(TAG, "Received unexpected message while Disconnecting"); 306 return NOT_HANDLED; 307 } 308 return HANDLED; 309 } 310 } 311 312 class Connected extends State { 313 @Override enter()314 public void enter() { 315 if (DBG) Log.d(TAG, "Enter Connected: " + getCurrentMessage().what); 316 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 317 BluetoothProfile.STATE_CONNECTED); 318 mMostRecentState = BluetoothProfile.STATE_CONNECTED; 319 downloadIfReady(); 320 } 321 322 @Override processMessage(Message message)323 public boolean processMessage(Message message) { 324 if (DBG) { 325 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); 326 } 327 switch (message.what) { 328 case MSG_DISCONNECT: 329 if ((message.obj instanceof BluetoothDevice) 330 && ((BluetoothDevice) message.obj).equals(mCurrentDevice)) { 331 transitionTo(mDisconnecting); 332 } 333 break; 334 335 case MSG_RESUME_DOWNLOAD: 336 downloadIfReady(); 337 break; 338 339 default: 340 Log.w(TAG, "Received unexpected message while Connected"); 341 return NOT_HANDLED; 342 } 343 return HANDLED; 344 } 345 } 346 347 /** 348 * Trigger a contacts download if the user is unlocked and our accounts are available to us 349 */ downloadIfReady()350 private void downloadIfReady() { 351 boolean userReady = mUserManager.isUserUnlocked(); 352 boolean accountServiceReady = mService.isAuthenticationServiceReady(); 353 if (!userReady || !accountServiceReady) { 354 Log.w(TAG, "Cannot download contacts yet, userReady=" + userReady 355 + ", accountServiceReady=" + accountServiceReady); 356 return; 357 } 358 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD) 359 .sendToTarget(); 360 } 361 onConnectionStateChanged(BluetoothDevice device, int prevState, int state)362 private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) { 363 if (device == null) { 364 Log.w(TAG, "onConnectionStateChanged with invalid device"); 365 return; 366 } 367 if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) { 368 MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.PBAP_CLIENT); 369 } 370 Log.d(TAG, "Connection state " + device + ": " + prevState + "->" + state); 371 Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); 372 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 373 intent.putExtra(BluetoothProfile.EXTRA_STATE, state); 374 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 375 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 376 mService.sendBroadcastMultiplePermissions(intent, 377 new String[] {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}, 378 Utils.getTempBroadcastOptions()); 379 } 380 disconnect(BluetoothDevice device)381 public void disconnect(BluetoothDevice device) { 382 if (DBG) Log.d(TAG, "Disconnect Request " + device); 383 sendMessage(MSG_DISCONNECT, device); 384 } 385 tryDownloadIfConnected()386 public void tryDownloadIfConnected() { 387 sendMessage(MSG_RESUME_DOWNLOAD); 388 } 389 doQuit()390 void doQuit() { 391 if (mHandlerThread != null) { 392 mHandlerThread.quitSafely(); 393 } 394 quitNow(); 395 } 396 397 @Override onQuitting()398 protected void onQuitting() { 399 mService.cleanupDevice(mCurrentDevice); 400 } 401 getConnectionState()402 public int getConnectionState() { 403 IState currentState = getCurrentState(); 404 if (currentState instanceof Disconnected) { 405 return BluetoothProfile.STATE_DISCONNECTED; 406 } else if (currentState instanceof Connecting) { 407 return BluetoothProfile.STATE_CONNECTING; 408 } else if (currentState instanceof Connected) { 409 return BluetoothProfile.STATE_CONNECTED; 410 } else if (currentState instanceof Disconnecting) { 411 return BluetoothProfile.STATE_DISCONNECTING; 412 } 413 Log.w(TAG, "Unknown State"); 414 return BluetoothProfile.STATE_DISCONNECTED; 415 } 416 getDevicesMatchingConnectionStates(int[] states)417 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 418 int clientState; 419 BluetoothDevice currentDevice; 420 synchronized (mLock) { 421 clientState = getConnectionState(); 422 currentDevice = getDevice(); 423 } 424 List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(); 425 for (int state : states) { 426 if (clientState == state) { 427 if (currentDevice != null) { 428 deviceList.add(currentDevice); 429 } 430 } 431 } 432 return deviceList; 433 } 434 getConnectionState(BluetoothDevice device)435 public int getConnectionState(BluetoothDevice device) { 436 if (device == null) { 437 return BluetoothProfile.STATE_DISCONNECTED; 438 } 439 synchronized (mLock) { 440 if (device.equals(mCurrentDevice)) { 441 return getConnectionState(); 442 } 443 } 444 return BluetoothProfile.STATE_DISCONNECTED; 445 } 446 447 getDevice()448 public BluetoothDevice getDevice() { 449 /* 450 * Disconnected is the only state where device can change, and to prevent the race 451 * condition of reporting a valid device while disconnected fix the report here. Note that 452 * Synchronization of the state and device is not possible with current state machine 453 * desingn since the actual Transition happens sometime after the transitionTo method. 454 */ 455 if (getCurrentState() instanceof Disconnected) { 456 return null; 457 } 458 return mCurrentDevice; 459 } 460 getContext()461 Context getContext() { 462 return mService; 463 } 464 dump(StringBuilder sb)465 public void dump(StringBuilder sb) { 466 ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice.getAddress() + "(" 467 + Utils.getName(mCurrentDevice) + ") " + this.toString()); 468 } 469 } 470