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