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