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