1 /* 2 * Copyright 2017 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 package com.android.bluetooth.pbap; 18 19 import static android.Manifest.permission.BLUETOOTH_CONNECT; 20 21 import android.annotation.NonNull; 22 import android.app.Notification; 23 import android.app.NotificationChannel; 24 import android.app.NotificationManager; 25 import android.app.PendingIntent; 26 import android.bluetooth.BluetoothDevice; 27 import android.bluetooth.BluetoothPbap; 28 import android.bluetooth.BluetoothProfile; 29 import android.bluetooth.BluetoothProtoEnums; 30 import android.bluetooth.BluetoothSocket; 31 import android.content.Intent; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.os.Message; 35 import android.os.UserHandle; 36 import android.util.Log; 37 38 import com.android.bluetooth.BluetoothMetricsProto; 39 import com.android.bluetooth.BluetoothObexTransport; 40 import com.android.bluetooth.BluetoothStatsLog; 41 import com.android.bluetooth.ObexRejectServer; 42 import com.android.bluetooth.R; 43 import com.android.bluetooth.Utils; 44 import com.android.bluetooth.btservice.AdapterService; 45 import com.android.bluetooth.btservice.MetricsLogger; 46 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; 47 import com.android.internal.annotations.VisibleForTesting; 48 import com.android.internal.annotations.VisibleForTesting.Visibility; 49 import com.android.internal.util.State; 50 import com.android.internal.util.StateMachine; 51 import com.android.obex.ResponseCodes; 52 import com.android.obex.ServerSession; 53 54 import java.io.IOException; 55 56 /** 57 * Bluetooth PBAP StateMachine (New connection socket) WAITING FOR AUTH | | (request permission from 58 * Settings UI) | (Accept) / \ (Reject) / \ v v CONNECTED -----> FINISHED (OBEX Server done) 59 */ 60 // Next tag value for ContentProfileErrorReportUtils.report(): 3 61 @VisibleForTesting(visibility = Visibility.PACKAGE) 62 public class PbapStateMachine extends StateMachine { 63 private static final String TAG = "PbapStateMachine"; 64 private static final String PBAP_OBEX_NOTIFICATION_CHANNEL = "pbap_obex_notification_channel"; 65 66 static final int AUTHORIZED = 1; 67 static final int REJECTED = 2; 68 static final int DISCONNECT = 3; 69 static final int REQUEST_PERMISSION = 4; 70 static final int CREATE_NOTIFICATION = 5; 71 static final int REMOVE_NOTIFICATION = 6; 72 static final int AUTH_KEY_INPUT = 7; 73 static final int AUTH_CANCELLED = 8; 74 75 /** Used to limit PBAP OBEX maximum packet size in order to reduce transaction time. */ 76 private static final int PBAP_OBEX_MAXIMUM_PACKET_SIZE = 8192; 77 78 private BluetoothPbapService mService; 79 80 private final WaitingForAuth mWaitingForAuth = new WaitingForAuth(); 81 private final Finished mFinished = new Finished(); 82 private final Connected mConnected = new Connected(); 83 private PbapStateBase mPrevState; 84 private BluetoothDevice mRemoteDevice; 85 private Handler mServiceHandler; 86 private BluetoothSocket mConnSocket; 87 private BluetoothPbapObexServer mPbapServer; 88 private BluetoothPbapAuthenticator mObexAuth; 89 private ServerSession mServerSession; 90 private int mNotificationId; 91 PbapStateMachine( @onNull BluetoothPbapService service, Looper looper, @NonNull BluetoothDevice device, @NonNull BluetoothSocket connSocket, Handler pbapHandler, int notificationId)92 private PbapStateMachine( 93 @NonNull BluetoothPbapService service, 94 Looper looper, 95 @NonNull BluetoothDevice device, 96 @NonNull BluetoothSocket connSocket, 97 Handler pbapHandler, 98 int notificationId) { 99 super(TAG, looper); 100 101 // Let the logging framework enforce the log level. TAG is set above in the parent 102 // constructor. 103 setDbg(true); 104 105 mService = service; 106 mRemoteDevice = device; 107 mServiceHandler = pbapHandler; 108 mConnSocket = connSocket; 109 mNotificationId = notificationId; 110 111 addState(mFinished); 112 addState(mWaitingForAuth); 113 addState(mConnected); 114 setInitialState(mWaitingForAuth); 115 } 116 make( BluetoothPbapService service, Looper looper, BluetoothDevice device, BluetoothSocket connSocket, Handler pbapHandler, int notificationId)117 static PbapStateMachine make( 118 BluetoothPbapService service, 119 Looper looper, 120 BluetoothDevice device, 121 BluetoothSocket connSocket, 122 Handler pbapHandler, 123 int notificationId) { 124 PbapStateMachine stateMachine = 125 new PbapStateMachine( 126 service, looper, device, connSocket, pbapHandler, notificationId); 127 stateMachine.start(); 128 return stateMachine; 129 } 130 getRemoteDevice()131 BluetoothDevice getRemoteDevice() { 132 return mRemoteDevice; 133 } 134 135 private abstract class PbapStateBase extends State { 136 /** 137 * Get a state value from {@link BluetoothProfile} that represents the connection state of 138 * this headset state 139 * 140 * @return a value in {@link BluetoothProfile#STATE_DISCONNECTED}, {@link 141 * BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or 142 * {@link BluetoothProfile#STATE_DISCONNECTING} 143 */ getConnectionStateInt()144 abstract int getConnectionStateInt(); 145 146 @Override enter()147 public void enter() { 148 // Crash if mPrevState is null and state is not Disconnected 149 if (!(this instanceof WaitingForAuth) && mPrevState == null) { 150 throw new IllegalStateException("mPrevState is null on entering initial state"); 151 } 152 enforceValidConnectionStateTransition(); 153 } 154 155 @Override exit()156 public void exit() { 157 mPrevState = this; 158 } 159 160 // Should not be called from enter() method broadcastConnectionState(BluetoothDevice device, int fromState, int toState)161 private void broadcastConnectionState(BluetoothDevice device, int fromState, int toState) { 162 stateLogD("broadcastConnectionState " + device + ": " + fromState + "->" + toState); 163 AdapterService adapterService = AdapterService.getAdapterService(); 164 if (adapterService != null) { 165 adapterService.updateProfileConnectionAdapterProperties( 166 device, BluetoothProfile.PBAP, toState, fromState); 167 } 168 169 Intent intent = new Intent(BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED); 170 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, fromState); 171 intent.putExtra(BluetoothProfile.EXTRA_STATE, toState); 172 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 173 intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 174 mService.sendBroadcastAsUser( 175 intent, 176 UserHandle.ALL, 177 BLUETOOTH_CONNECT, 178 Utils.getTempBroadcastOptions().toBundle()); 179 } 180 181 /** Broadcast connection state change for this state machine */ broadcastStateTransitions()182 void broadcastStateTransitions() { 183 int prevStateInt = BluetoothProfile.STATE_DISCONNECTED; 184 if (mPrevState != null) { 185 prevStateInt = mPrevState.getConnectionStateInt(); 186 } 187 if (getConnectionStateInt() != prevStateInt) { 188 stateLogD( 189 "connection state changed: " 190 + mRemoteDevice 191 + ": " 192 + mPrevState 193 + " -> " 194 + this); 195 broadcastConnectionState(mRemoteDevice, prevStateInt, getConnectionStateInt()); 196 } 197 } 198 199 /** 200 * Verify if the current state transition is legal by design. This is called from enter() 201 * method and crash if the state transition is not expected by the state machine design. 202 * 203 * <p>Note: This method uses state objects to verify transition because these objects should 204 * be final and any other instances are invalid 205 */ enforceValidConnectionStateTransition()206 private void enforceValidConnectionStateTransition() { 207 boolean isValidTransition = false; 208 if (this == mWaitingForAuth) { 209 isValidTransition = mPrevState == null; 210 } else if (this == mFinished) { 211 isValidTransition = mPrevState == mConnected || mPrevState == mWaitingForAuth; 212 } else if (this == mConnected) { 213 isValidTransition = mPrevState == mFinished || mPrevState == mWaitingForAuth; 214 } 215 if (!isValidTransition) { 216 throw new IllegalStateException( 217 "Invalid state transition from " 218 + mPrevState 219 + " to " 220 + this 221 + " for device " 222 + mRemoteDevice); 223 } 224 } 225 stateLogD(String msg)226 void stateLogD(String msg) { 227 Log.d(TAG, getName() + ": currentDevice=" + mRemoteDevice + ", msg=" + msg); 228 } 229 } 230 231 class WaitingForAuth extends PbapStateBase { 232 @Override getConnectionStateInt()233 int getConnectionStateInt() { 234 return BluetoothProfile.STATE_CONNECTING; 235 } 236 237 @Override enter()238 public void enter() { 239 super.enter(); 240 broadcastStateTransitions(); 241 } 242 243 @Override processMessage(Message message)244 public boolean processMessage(Message message) { 245 switch (message.what) { 246 case REQUEST_PERMISSION: 247 mService.checkOrGetPhonebookPermission(PbapStateMachine.this); 248 break; 249 case AUTHORIZED: 250 transitionTo(mConnected); 251 break; 252 case REJECTED: 253 rejectConnection(); 254 transitionTo(mFinished); 255 break; 256 case DISCONNECT: 257 mServiceHandler.removeMessages( 258 BluetoothPbapService.USER_TIMEOUT, PbapStateMachine.this); 259 mServiceHandler 260 .obtainMessage(BluetoothPbapService.USER_TIMEOUT, PbapStateMachine.this) 261 .sendToTarget(); 262 transitionTo(mFinished); 263 break; 264 } 265 return HANDLED; 266 } 267 rejectConnection()268 private void rejectConnection() { 269 mPbapServer = 270 new BluetoothPbapObexServer(mServiceHandler, mService, PbapStateMachine.this); 271 BluetoothObexTransport transport = 272 new BluetoothObexTransport( 273 mConnSocket, 274 PBAP_OBEX_MAXIMUM_PACKET_SIZE, 275 BluetoothObexTransport.PACKET_SIZE_UNSPECIFIED); 276 ObexRejectServer server = 277 new ObexRejectServer(ResponseCodes.OBEX_HTTP_UNAVAILABLE, mConnSocket); 278 try { 279 mServerSession = new ServerSession(transport, server, null); 280 } catch (IOException ex) { 281 ContentProfileErrorReportUtils.report( 282 BluetoothProfile.PBAP, 283 BluetoothProtoEnums.BLUETOOTH_PBAP_STATE_MACHINE, 284 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 285 0); 286 Log.e(TAG, "Caught exception starting OBEX reject server session" + ex.toString()); 287 } 288 } 289 } 290 291 class Finished extends PbapStateBase { 292 @Override getConnectionStateInt()293 int getConnectionStateInt() { 294 return BluetoothProfile.STATE_DISCONNECTED; 295 } 296 297 @Override enter()298 public void enter() { 299 super.enter(); 300 // Close OBEX server session 301 if (mServerSession != null) { 302 mServerSession.close(); 303 mServerSession = null; 304 } 305 306 // Close connection socket 307 try { 308 mConnSocket.close(); 309 mConnSocket = null; 310 } catch (IOException e) { 311 ContentProfileErrorReportUtils.report( 312 BluetoothProfile.PBAP, 313 BluetoothProtoEnums.BLUETOOTH_PBAP_STATE_MACHINE, 314 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 315 1); 316 Log.e(TAG, "Close Connection Socket error: " + e.toString()); 317 } 318 319 mServiceHandler 320 .obtainMessage( 321 BluetoothPbapService.MSG_STATE_MACHINE_DONE, PbapStateMachine.this) 322 .sendToTarget(); 323 broadcastStateTransitions(); 324 } 325 } 326 327 class Connected extends PbapStateBase { 328 @Override getConnectionStateInt()329 int getConnectionStateInt() { 330 return BluetoothProfile.STATE_CONNECTED; 331 } 332 333 @Override enter()334 public void enter() { 335 try { 336 startObexServerSession(); 337 } catch (IOException ex) { 338 ContentProfileErrorReportUtils.report( 339 BluetoothProfile.PBAP, 340 BluetoothProtoEnums.BLUETOOTH_PBAP_STATE_MACHINE, 341 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 342 2); 343 Log.e(TAG, "Caught exception starting OBEX server session" + ex.toString()); 344 } 345 broadcastStateTransitions(); 346 MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.PBAP); 347 mService.setConnectionPolicy(mRemoteDevice, BluetoothProfile.CONNECTION_POLICY_ALLOWED); 348 } 349 350 @Override processMessage(Message message)351 public boolean processMessage(Message message) { 352 switch (message.what) { 353 case DISCONNECT: 354 stopObexServerSession(); 355 break; 356 case CREATE_NOTIFICATION: 357 createPbapNotification(); 358 break; 359 case REMOVE_NOTIFICATION: 360 Intent i = new Intent(BluetoothPbapService.USER_CONFIRM_TIMEOUT_ACTION); 361 mService.sendBroadcast(i); 362 notifyAuthCancelled(); 363 removePbapNotification(mNotificationId); 364 break; 365 case AUTH_KEY_INPUT: 366 String key = (String) message.obj; 367 notifyAuthKeyInput(key); 368 break; 369 case AUTH_CANCELLED: 370 notifyAuthCancelled(); 371 break; 372 } 373 return HANDLED; 374 } 375 startObexServerSession()376 private void startObexServerSession() throws IOException { 377 Log.v(TAG, "Pbap Service startObexServerSession"); 378 379 // acquire the wakeLock before start Obex transaction thread 380 mServiceHandler.sendMessage( 381 mServiceHandler.obtainMessage(BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK)); 382 383 mPbapServer = 384 new BluetoothPbapObexServer(mServiceHandler, mService, PbapStateMachine.this); 385 synchronized (this) { 386 mObexAuth = new BluetoothPbapAuthenticator(PbapStateMachine.this); 387 mObexAuth.setChallenged(false); 388 mObexAuth.setCancelled(false); 389 } 390 BluetoothObexTransport transport = 391 new BluetoothObexTransport( 392 mConnSocket, 393 PBAP_OBEX_MAXIMUM_PACKET_SIZE, 394 BluetoothObexTransport.PACKET_SIZE_UNSPECIFIED); 395 mServerSession = new ServerSession(transport, mPbapServer, mObexAuth); 396 // It's ok to just use one wake lock 397 // Message MSG_ACQUIRE_WAKE_LOCK is always surrounded by RELEASE. safe. 398 } 399 stopObexServerSession()400 private void stopObexServerSession() { 401 Log.v(TAG, "Pbap Service stopObexServerSession"); 402 transitionTo(mFinished); 403 } 404 createPbapNotification()405 private void createPbapNotification() { 406 NotificationManager nm = mService.getSystemService(NotificationManager.class); 407 NotificationChannel notificationChannel = 408 new NotificationChannel( 409 PBAP_OBEX_NOTIFICATION_CHANNEL, 410 mService.getString(R.string.pbap_notification_group), 411 NotificationManager.IMPORTANCE_HIGH); 412 nm.createNotificationChannel(notificationChannel); 413 414 // Create an intent triggered by clicking on the status icon. 415 Intent clickIntent = new Intent(); 416 clickIntent.setClass(mService, BluetoothPbapActivity.class); 417 clickIntent.putExtra(BluetoothPbapService.EXTRA_DEVICE, mRemoteDevice); 418 clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 419 clickIntent.setAction(BluetoothPbapService.AUTH_CHALL_ACTION); 420 421 // Create an intent triggered by clicking on the 422 // "Clear All Notifications" button 423 Intent deleteIntent = new Intent(); 424 deleteIntent.setClass(mService, BluetoothPbapService.class); 425 deleteIntent.setAction(BluetoothPbapService.AUTH_CANCELLED_ACTION); 426 427 String name = Utils.getName(mRemoteDevice); 428 429 Notification notification = 430 new Notification.Builder(mService, PBAP_OBEX_NOTIFICATION_CHANNEL) 431 .setWhen(System.currentTimeMillis()) 432 .setContentTitle(mService.getString(R.string.auth_notif_title)) 433 .setContentText(mService.getString(R.string.auth_notif_message, name)) 434 .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) 435 .setTicker(mService.getString(R.string.auth_notif_ticker)) 436 .setColor( 437 mService.getResources() 438 .getColor( 439 android.R.color 440 .system_notification_accent_color, 441 mService.getTheme())) 442 .setFlag(Notification.FLAG_AUTO_CANCEL, true) 443 .setFlag(Notification.FLAG_ONLY_ALERT_ONCE, true) 444 .setContentIntent( 445 PendingIntent.getActivity( 446 mService, 0, clickIntent, PendingIntent.FLAG_IMMUTABLE)) 447 .setDeleteIntent( 448 PendingIntent.getBroadcast( 449 mService, 450 0, 451 deleteIntent, 452 PendingIntent.FLAG_IMMUTABLE)) 453 .setLocalOnly(true) 454 .build(); 455 nm.notify(mNotificationId, notification); 456 } 457 removePbapNotification(int id)458 private void removePbapNotification(int id) { 459 NotificationManager nm = mService.getSystemService(NotificationManager.class); 460 nm.cancel(id); 461 } 462 notifyAuthCancelled()463 private synchronized void notifyAuthCancelled() { 464 mObexAuth.setCancelled(true); 465 } 466 notifyAuthKeyInput(final String key)467 private synchronized void notifyAuthKeyInput(final String key) { 468 if (key != null) { 469 mObexAuth.setSessionKey(key); 470 } 471 mObexAuth.setChallenged(true); 472 } 473 } 474 475 /** 476 * Get the current connection state of this state machine 477 * 478 * @return current connection state, one of {@link BluetoothProfile#STATE_DISCONNECTED}, {@link 479 * BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or {@link 480 * BluetoothProfile#STATE_DISCONNECTING} 481 */ getConnectionState()482 synchronized int getConnectionState() { 483 PbapStateBase state = (PbapStateBase) getCurrentState(); 484 if (state == null) { 485 return BluetoothProfile.STATE_DISCONNECTED; 486 } 487 return state.getConnectionStateInt(); 488 } 489 } 490