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