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.bluetooth.statemachine.IState; 63 import com.android.bluetooth.statemachine.State; 64 import com.android.bluetooth.statemachine.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 = Utils.DBG; 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 PbapClientStateMachine(PbapClientService svc, BluetoothDevice device)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 enter()128 public void enter() { 129 if (DBG) 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 enter()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 processMessage(Message message)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 exit()205 public void exit() { 206 mSdpReceiver.unregister(); 207 mSdpReceiver = null; 208 } 209 210 private class SDPBroadcastReceiver extends BroadcastReceiver { 211 @Override onReceive(Context context, Intent intent)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 Log.v(TAG, "expected UUID: " + BluetoothUuid.PBAP_PSE.toString()); 228 } 229 if (uuid.equals(BluetoothUuid.PBAP_PSE)) { 230 sendMessage(MSG_SDP_COMPLETE, 231 intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD)); 232 } 233 } 234 } 235 register()236 public void register() { 237 IntentFilter filter = new IntentFilter(); 238 filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); 239 mService.registerReceiver(this, filter); 240 } 241 unregister()242 public void unregister() { 243 mService.unregisterReceiver(this); 244 } 245 } 246 } 247 248 class Disconnecting extends State { 249 @Override enter()250 public void enter() { 251 if (DBG) Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what); 252 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 253 BluetoothProfile.STATE_DISCONNECTING); 254 mMostRecentState = BluetoothProfile.STATE_DISCONNECTING; 255 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT) 256 .sendToTarget(); 257 sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT); 258 } 259 260 @Override processMessage(Message message)261 public boolean processMessage(Message message) { 262 if (DBG) { 263 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); 264 } 265 switch (message.what) { 266 case MSG_CONNECTION_CLOSED: 267 removeMessages(MSG_DISCONNECT_TIMEOUT); 268 mHandlerThread.quitSafely(); 269 transitionTo(mDisconnected); 270 break; 271 272 case MSG_DISCONNECT: 273 deferMessage(message); 274 break; 275 276 case MSG_DISCONNECT_TIMEOUT: 277 Log.w(TAG, "Disconnect Timeout, Forcing"); 278 mConnectionHandler.abort(); 279 break; 280 281 case MSG_RESUME_DOWNLOAD: 282 // Do nothing. 283 break; 284 285 default: 286 Log.w(TAG, "Received unexpected message while Disconnecting"); 287 return NOT_HANDLED; 288 } 289 return HANDLED; 290 } 291 } 292 293 class Connected extends State { 294 @Override enter()295 public void enter() { 296 if (DBG) Log.d(TAG, "Enter Connected: " + getCurrentMessage().what); 297 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 298 BluetoothProfile.STATE_CONNECTED); 299 mMostRecentState = BluetoothProfile.STATE_CONNECTED; 300 if (mUserManager.isUserUnlocked()) { 301 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD) 302 .sendToTarget(); 303 } 304 } 305 306 @Override processMessage(Message message)307 public boolean processMessage(Message message) { 308 if (DBG) { 309 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); 310 } 311 switch (message.what) { 312 case MSG_DISCONNECT: 313 if ((message.obj instanceof BluetoothDevice) 314 && ((BluetoothDevice) message.obj).equals(mCurrentDevice)) { 315 transitionTo(mDisconnecting); 316 } 317 break; 318 319 case MSG_RESUME_DOWNLOAD: 320 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD) 321 .sendToTarget(); 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 onConnectionStateChanged(BluetoothDevice device, int prevState, int state)332 private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) { 333 if (device == null) { 334 Log.w(TAG, "onConnectionStateChanged with invalid device"); 335 return; 336 } 337 if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) { 338 MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.PBAP_CLIENT); 339 } 340 Log.d(TAG, "Connection state " + device + ": " + prevState + "->" + state); 341 Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); 342 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 343 intent.putExtra(BluetoothProfile.EXTRA_STATE, state); 344 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 345 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 346 mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); 347 } 348 disconnect(BluetoothDevice device)349 public void disconnect(BluetoothDevice device) { 350 if (DBG) Log.d(TAG, "Disconnect Request " + device); 351 sendMessage(MSG_DISCONNECT, device); 352 } 353 resumeDownload()354 public void resumeDownload() { 355 sendMessage(MSG_RESUME_DOWNLOAD); 356 } 357 doQuit()358 void doQuit() { 359 if (mHandlerThread != null) { 360 mHandlerThread.quitSafely(); 361 } 362 quitNow(); 363 } 364 365 @Override onQuitting()366 protected void onQuitting() { 367 mService.cleanupDevice(mCurrentDevice); 368 } 369 getConnectionState()370 public int getConnectionState() { 371 IState currentState = getCurrentState(); 372 if (currentState instanceof Disconnected) { 373 return BluetoothProfile.STATE_DISCONNECTED; 374 } else if (currentState instanceof Connecting) { 375 return BluetoothProfile.STATE_CONNECTING; 376 } else if (currentState instanceof Connected) { 377 return BluetoothProfile.STATE_CONNECTED; 378 } else if (currentState instanceof Disconnecting) { 379 return BluetoothProfile.STATE_DISCONNECTING; 380 } 381 Log.w(TAG, "Unknown State"); 382 return BluetoothProfile.STATE_DISCONNECTED; 383 } 384 getDevicesMatchingConnectionStates(int[] states)385 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 386 int clientState; 387 BluetoothDevice currentDevice; 388 synchronized (mLock) { 389 clientState = getConnectionState(); 390 currentDevice = getDevice(); 391 } 392 List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(); 393 for (int state : states) { 394 if (clientState == state) { 395 if (currentDevice != null) { 396 deviceList.add(currentDevice); 397 } 398 } 399 } 400 return deviceList; 401 } 402 getConnectionState(BluetoothDevice device)403 public int getConnectionState(BluetoothDevice device) { 404 if (device == null) { 405 return BluetoothProfile.STATE_DISCONNECTED; 406 } 407 synchronized (mLock) { 408 if (device.equals(mCurrentDevice)) { 409 return getConnectionState(); 410 } 411 } 412 return BluetoothProfile.STATE_DISCONNECTED; 413 } 414 415 getDevice()416 public BluetoothDevice getDevice() { 417 /* 418 * Disconnected is the only state where device can change, and to prevent the race 419 * condition of reporting a valid device while disconnected fix the report here. Note that 420 * Synchronization of the state and device is not possible with current state machine 421 * desingn since the actual Transition happens sometime after the transitionTo method. 422 */ 423 if (getCurrentState() instanceof Disconnected) { 424 return null; 425 } 426 return mCurrentDevice; 427 } 428 getContext()429 Context getContext() { 430 return mService; 431 } 432 dump(StringBuilder sb)433 public void dump(StringBuilder sb) { 434 ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice.getAddress() + "(" 435 + mCurrentDevice.getName() + ") " + this.toString()); 436 } 437 } 438