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