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