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 import static android.Manifest.permission.BLUETOOTH_CONNECT; 59 60 import com.android.bluetooth.BluetoothMetricsProto; 61 import com.android.bluetooth.Utils; 62 import com.android.bluetooth.btservice.MetricsLogger; 63 import com.android.bluetooth.btservice.ProfileService; 64 import com.android.bluetooth.statemachine.IState; 65 import com.android.bluetooth.statemachine.State; 66 import com.android.bluetooth.statemachine.StateMachine; 67 68 import java.util.ArrayList; 69 import java.util.List; 70 71 final class PbapClientStateMachine extends StateMachine { 72 private static final boolean DBG = false; //Utils.DBG; 73 private static final String TAG = "PbapClientStateMachine"; 74 75 // Messages for handling connect/disconnect requests. 76 private static final int MSG_DISCONNECT = 2; 77 private static final int MSG_SDP_COMPLETE = 9; 78 79 // Messages for handling error conditions. 80 private static final int MSG_CONNECT_TIMEOUT = 3; 81 private static final int MSG_DISCONNECT_TIMEOUT = 4; 82 83 // Messages for feedback from ConnectionHandler. 84 static final int MSG_CONNECTION_COMPLETE = 5; 85 static final int MSG_CONNECTION_FAILED = 6; 86 static final int MSG_CONNECTION_CLOSED = 7; 87 static final int MSG_RESUME_DOWNLOAD = 8; 88 89 static final int CONNECT_TIMEOUT = 10000; 90 static final int DISCONNECT_TIMEOUT = 3000; 91 92 private final Object mLock; 93 private State mDisconnected; 94 private State mConnecting; 95 private State mConnected; 96 private State mDisconnecting; 97 98 // mCurrentDevice may only be changed in Disconnected State. 99 private final BluetoothDevice mCurrentDevice; 100 private PbapClientService mService; 101 private PbapClientConnectionHandler mConnectionHandler; 102 private HandlerThread mHandlerThread = null; 103 private UserManager mUserManager = null; 104 105 // mMostRecentState maintains previous state for broadcasting transitions. 106 private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; 107 PbapClientStateMachine(PbapClientService svc, BluetoothDevice device)108 PbapClientStateMachine(PbapClientService svc, BluetoothDevice device) { 109 super(TAG); 110 111 mService = svc; 112 mCurrentDevice = device; 113 mLock = new Object(); 114 mUserManager = UserManager.get(mService); 115 mDisconnected = new Disconnected(); 116 mConnecting = new Connecting(); 117 mDisconnecting = new Disconnecting(); 118 mConnected = new Connected(); 119 120 addState(mDisconnected); 121 addState(mConnecting); 122 addState(mDisconnecting); 123 addState(mConnected); 124 125 setInitialState(mConnecting); 126 } 127 128 class Disconnected extends State { 129 @Override enter()130 public void enter() { 131 if (DBG) Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what); 132 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 133 BluetoothProfile.STATE_DISCONNECTED); 134 mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; 135 quit(); 136 } 137 } 138 139 class Connecting extends State { 140 private SDPBroadcastReceiver mSdpReceiver; 141 142 @Override enter()143 public void enter() { 144 if (DBG) { 145 Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what); 146 } 147 onConnectionStateChanged(mCurrentDevice, mMostRecentState, 148 BluetoothProfile.STATE_CONNECTING); 149 mSdpReceiver = new SDPBroadcastReceiver(); 150 mSdpReceiver.register(); 151 mCurrentDevice.sdpSearch(BluetoothUuid.PBAP_PSE); 152 mMostRecentState = BluetoothProfile.STATE_CONNECTING; 153 154 // Create a separate handler instance and thread for performing 155 // connect/download/disconnect operations as they may be time consuming and error prone. 156 mHandlerThread = 157 new HandlerThread("PBAP PCE handler", Process.THREAD_PRIORITY_BACKGROUND); 158 mHandlerThread.start(); 159 mConnectionHandler = 160 new PbapClientConnectionHandler.Builder().setLooper(mHandlerThread.getLooper()) 161 .setContext(mService) 162 .setClientSM(PbapClientStateMachine.this) 163 .setRemoteDevice(mCurrentDevice) 164 .build(); 165 166 sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT); 167 } 168 169 @Override processMessage(Message message)170 public boolean processMessage(Message message) { 171 if (DBG) { 172 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName()); 173 } 174 switch (message.what) { 175 case MSG_DISCONNECT: 176 if (message.obj instanceof BluetoothDevice && message.obj.equals( 177 mCurrentDevice)) { 178 removeMessages(MSG_CONNECT_TIMEOUT); 179 transitionTo(mDisconnecting); 180 } 181 break; 182 183 case MSG_CONNECTION_COMPLETE: 184 removeMessages(MSG_CONNECT_TIMEOUT); 185 transitionTo(mConnected); 186 break; 187 188 case MSG_CONNECTION_FAILED: 189 case MSG_CONNECT_TIMEOUT: 190 removeMessages(MSG_CONNECT_TIMEOUT); 191 transitionTo(mDisconnecting); 192 break; 193 194 case MSG_SDP_COMPLETE: 195 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_CONNECT, 196 message.obj).sendToTarget(); 197 break; 198 199 default: 200 Log.w(TAG, "Received unexpected message while Connecting"); 201 return NOT_HANDLED; 202 } 203 return HANDLED; 204 } 205 206 @Override exit()207 public void exit() { 208 mSdpReceiver.unregister(); 209 mSdpReceiver = null; 210 } 211 212 private class SDPBroadcastReceiver extends BroadcastReceiver { 213 @Override onReceive(Context context, Intent intent)214 public void onReceive(Context context, Intent intent) { 215 String action = intent.getAction(); 216 if (DBG) { 217 Log.v(TAG, "onReceive" + action); 218 } 219 if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { 220 BluetoothDevice device = 221 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 222 if (!device.equals(getDevice())) { 223 Log.w(TAG, "SDP Record fetched for different device - Ignore"); 224 return; 225 } 226 ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); 227 if (DBG) { 228 Log.v(TAG, "Received UUID: " + uuid.toString()); 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 register()238 public void register() { 239 IntentFilter filter = new IntentFilter(); 240 filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); 241 mService.registerReceiver(this, filter); 242 } 243 unregister()244 public void unregister() { 245 mService.unregisterReceiver(this); 246 } 247 } 248 } 249 250 class Disconnecting extends State { 251 @Override enter()252 public void enter() { 253 if (DBG) 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 processMessage(Message message)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 enter()297 public void enter() { 298 if (DBG) 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 processMessage(Message message)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 onConnectionStateChanged(BluetoothDevice device, int prevState, int state)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, BLUETOOTH_CONNECT, Utils.getTempAllowlistBroadcastOptions()); 349 } 350 disconnect(BluetoothDevice device)351 public void disconnect(BluetoothDevice device) { 352 if (DBG) Log.d(TAG, "Disconnect Request " + device); 353 sendMessage(MSG_DISCONNECT, device); 354 } 355 resumeDownload()356 public void resumeDownload() { 357 sendMessage(MSG_RESUME_DOWNLOAD); 358 } 359 doQuit()360 void doQuit() { 361 if (mHandlerThread != null) { 362 mHandlerThread.quitSafely(); 363 } 364 quitNow(); 365 } 366 367 @Override onQuitting()368 protected void onQuitting() { 369 mService.cleanupDevice(mCurrentDevice); 370 } 371 getConnectionState()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 getDevicesMatchingConnectionStates(int[] states)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 getConnectionState(BluetoothDevice device)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 getDevice()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 getContext()431 Context getContext() { 432 return mService; 433 } 434 dump(StringBuilder sb)435 public void dump(StringBuilder sb) { 436 ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice.getAddress() + "(" 437 + Utils.getName(mCurrentDevice) + ") " + this.toString()); 438 } 439 } 440