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 mService.setConnectionPolicy( 300 mRemoteDevice, BluetoothProfile.CONNECTION_POLICY_ALLOWED); 301 } 302 303 @Override processMessage(Message message)304 public boolean processMessage(Message message) { 305 switch (message.what) { 306 case DISCONNECT: 307 stopObexServerSession(); 308 break; 309 case CREATE_NOTIFICATION: 310 createPbapNotification(); 311 break; 312 case REMOVE_NOTIFICATION: 313 Intent i = new Intent(BluetoothPbapService.USER_CONFIRM_TIMEOUT_ACTION); 314 mService.sendBroadcast(i); 315 notifyAuthCancelled(); 316 removePbapNotification(mNotificationId); 317 break; 318 case AUTH_KEY_INPUT: 319 String key = (String) message.obj; 320 notifyAuthKeyInput(key); 321 break; 322 case AUTH_CANCELLED: 323 notifyAuthCancelled(); 324 break; 325 } 326 return HANDLED; 327 } 328 startObexServerSession()329 private void startObexServerSession() throws IOException { 330 if (VERBOSE) { 331 Log.v(TAG, "Pbap Service startObexServerSession"); 332 } 333 334 // acquire the wakeLock before start Obex transaction thread 335 mServiceHandler.sendMessage( 336 mServiceHandler.obtainMessage(BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK)); 337 338 mPbapServer = 339 new BluetoothPbapObexServer(mServiceHandler, mService, PbapStateMachine.this); 340 synchronized (this) { 341 mObexAuth = new BluetoothPbapAuthenticator(PbapStateMachine.this); 342 mObexAuth.setChallenged(false); 343 mObexAuth.setCancelled(false); 344 } 345 BluetoothObexTransport transport = new BluetoothObexTransport(mConnSocket); 346 mServerSession = new ServerSession(transport, mPbapServer, mObexAuth); 347 // It's ok to just use one wake lock 348 // Message MSG_ACQUIRE_WAKE_LOCK is always surrounded by RELEASE. safe. 349 } 350 stopObexServerSession()351 private void stopObexServerSession() { 352 if (VERBOSE) { 353 Log.v(TAG, "Pbap Service stopObexServerSession"); 354 } 355 transitionTo(mFinished); 356 } 357 createPbapNotification()358 private void createPbapNotification() { 359 NotificationManager nm = 360 (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); 361 NotificationChannel notificationChannel = 362 new NotificationChannel(PBAP_OBEX_NOTIFICATION_CHANNEL, 363 mService.getString(R.string.pbap_notification_group), 364 NotificationManager.IMPORTANCE_HIGH); 365 nm.createNotificationChannel(notificationChannel); 366 367 // Create an intent triggered by clicking on the status icon. 368 Intent clickIntent = new Intent(); 369 clickIntent.setClass(mService, BluetoothPbapActivity.class); 370 clickIntent.putExtra(BluetoothPbapService.EXTRA_DEVICE, mRemoteDevice); 371 clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 372 clickIntent.setAction(BluetoothPbapService.AUTH_CHALL_ACTION); 373 374 // Create an intent triggered by clicking on the 375 // "Clear All Notifications" button 376 Intent deleteIntent = new Intent(); 377 deleteIntent.setClass(mService, BluetoothPbapService.class); 378 deleteIntent.setAction(BluetoothPbapService.AUTH_CANCELLED_ACTION); 379 380 String name = mRemoteDevice.getName(); 381 382 Notification notification = 383 new Notification.Builder(mService, PBAP_OBEX_NOTIFICATION_CHANNEL).setWhen( 384 System.currentTimeMillis()) 385 .setContentTitle(mService.getString(R.string.auth_notif_title)) 386 .setContentText(mService.getString(R.string.auth_notif_message, name)) 387 .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) 388 .setTicker(mService.getString(R.string.auth_notif_ticker)) 389 .setColor(mService.getResources() 390 .getColor( 391 com.android.internal.R.color 392 .system_notification_accent_color, 393 mService.getTheme())) 394 .setFlag(Notification.FLAG_AUTO_CANCEL, true) 395 .setFlag(Notification.FLAG_ONLY_ALERT_ONCE, true) 396 .setContentIntent( 397 PendingIntent.getActivity(mService, 0, clickIntent, 0)) 398 .setDeleteIntent( 399 PendingIntent.getBroadcast(mService, 0, deleteIntent, 0)) 400 .setLocalOnly(true) 401 .build(); 402 nm.notify(mNotificationId, notification); 403 } 404 removePbapNotification(int id)405 private void removePbapNotification(int id) { 406 NotificationManager nm = 407 (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); 408 nm.cancel(id); 409 } 410 notifyAuthCancelled()411 private synchronized void notifyAuthCancelled() { 412 mObexAuth.setCancelled(true); 413 } 414 notifyAuthKeyInput(final String key)415 private synchronized void notifyAuthKeyInput(final String key) { 416 if (key != null) { 417 mObexAuth.setSessionKey(key); 418 } 419 mObexAuth.setChallenged(true); 420 } 421 } 422 423 /** 424 * Get the current connection state of this state machine 425 * 426 * @return current connection state, one of {@link BluetoothProfile#STATE_DISCONNECTED}, 427 * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or 428 * {@link BluetoothProfile#STATE_DISCONNECTING} 429 */ getConnectionState()430 synchronized int getConnectionState() { 431 PbapStateBase state = (PbapStateBase) getCurrentState(); 432 if (state == null) { 433 return BluetoothProfile.STATE_DISCONNECTED; 434 } 435 return state.getConnectionStateInt(); 436 } 437 438 @Override log(String msg)439 protected void log(String msg) { 440 if (DEBUG) { 441 super.log(msg); 442 } 443 } 444 } 445