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 PbapStateMachine(@onNull BluetoothPbapService service, Looper looper, @NonNull BluetoothDevice device, @NonNull BluetoothSocket connSocket, IObexConnectionHandler obexConnectionHandler, Handler pbapHandler, int notificationId)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 make(BluetoothPbapService service, Looper looper, BluetoothDevice device, BluetoothSocket connSocket, IObexConnectionHandler obexConnectionHandler, Handler pbapHandler, int notificationId)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 getRemoteDevice()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 */ getConnectionStateInt()133 abstract int getConnectionStateInt(); 134 135 @Override enter()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 exit()145 public void exit() { 146 mPrevState = this; 147 } 148 149 // Should not be called from enter() method broadcastConnectionState(BluetoothDevice device, int fromState, int toState)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 */ broadcastStateTransitions()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 */ enforceValidConnectionStateTransition()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 stateLogD(String msg)200 void stateLogD(String msg) { 201 log(getName() + ": currentDevice=" + mRemoteDevice + ", msg=" + msg); 202 } 203 } 204 205 class WaitingForAuth extends PbapStateBase { 206 @Override getConnectionStateInt()207 int getConnectionStateInt() { 208 return BluetoothProfile.STATE_CONNECTING; 209 } 210 211 @Override enter()212 public void enter() { 213 super.enter(); 214 broadcastStateTransitions(); 215 } 216 217 @Override processMessage(Message message)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 rejectConnection()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 getConnectionStateInt()257 int getConnectionStateInt() { 258 return BluetoothProfile.STATE_DISCONNECTED; 259 } 260 261 @Override enter()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 getConnectionStateInt()286 int getConnectionStateInt() { 287 return BluetoothProfile.STATE_CONNECTED; 288 } 289 290 @Override enter()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 processMessage(Message message)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 startObexServerSession()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 stopObexServerSession()349 private void stopObexServerSession() { 350 if (VERBOSE) { 351 Log.v(TAG, "Pbap Service stopObexServerSession"); 352 } 353 transitionTo(mFinished); 354 } 355 createPbapNotification()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 removePbapNotification(int id)403 private void removePbapNotification(int id) { 404 NotificationManager nm = 405 (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); 406 nm.cancel(id); 407 } 408 notifyAuthCancelled()409 private synchronized void notifyAuthCancelled() { 410 mObexAuth.setCancelled(true); 411 } 412 notifyAuthKeyInput(final String key)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 */ getConnectionState()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 log(String msg)437 protected void log(String msg) { 438 if (DEBUG) { 439 super.log(msg); 440 } 441 } 442 } 443