PbapStateMachine.java revision c7ed3fe050d6393a34ec9fa4237b77a1290ebc0d
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 17package com.android.bluetooth.pbap; 18 19import android.annotation.NonNull; 20import android.app.Notification; 21import android.app.NotificationChannel; 22import android.app.NotificationManager; 23import android.app.PendingIntent; 24import android.bluetooth.BluetoothDevice; 25import android.bluetooth.BluetoothProfile; 26import android.bluetooth.BluetoothSocket; 27import android.content.Context; 28import android.content.Intent; 29import android.os.Handler; 30import android.os.Looper; 31import android.os.Message; 32import android.util.Log; 33 34import com.android.bluetooth.BluetoothObexTransport; 35import com.android.bluetooth.IObexConnectionHandler; 36import com.android.bluetooth.ObexRejectServer; 37import com.android.bluetooth.R; 38import com.android.internal.util.State; 39import com.android.internal.util.StateMachine; 40 41import java.io.IOException; 42 43import javax.obex.ResponseCodes; 44import javax.obex.ServerSession; 45 46/** 47 * Bluetooth PBAP StateMachine 48 * (New connection socket) 49 * WAITING FOR AUTH 50 * | 51 * | (request permission from Settings UI) 52 * | 53 * (Accept) / \ (Reject) 54 * / \ 55 * v v 56 * CONNECTED -----> FINISHED 57 * (OBEX Server done) 58 */ 59class PbapStateMachine extends StateMachine { 60 private static final String TAG = "PbapStateMachine"; 61 private static final boolean DEBUG = true; 62 private static final boolean VERBOSE = true; 63 64 private static final String PBAP_OBEX_NOTIFICATION_CHANNEL = "pbap_obex_notification_channel"; 65 private static final int NOTIFICATION_ID_AUTH = -1000002; 66 // TODO: set a notification channel for each sm 67 68 static final int AUTHORIZED = 1; 69 static final int REJECTED = 2; 70 static final int DISCONNECT = 3; 71 static final int REQUEST_PERMISSION = 4; 72 static final int CREATE_NOTIFICATION = 5; 73 static final int REMOVE_NOTIFICATION = 6; 74 static final int AUTH_KEY_INPUT = 7; 75 static final int AUTH_CANCELLED = 8; 76 77 BluetoothPbapService mService; 78 IObexConnectionHandler mIObexConnectionHandler; 79 80 private final WaitingForAuth mWaitingForAuth = new WaitingForAuth(); 81 private final Finished mFinished = new Finished(); 82 private final Connected mConnected = new Connected(); 83 private BluetoothDevice mRemoteDevice; 84 private Handler mServiceHandler; 85 private BluetoothSocket mConnSocket; 86 private BluetoothPbapObexServer mPbapServer; 87 private BluetoothPbapAuthenticator mObexAuth; 88 private ServerSession mServerSession; 89 90 private PbapStateMachine(@NonNull BluetoothPbapService service, Looper looper, 91 @NonNull BluetoothDevice device, @NonNull BluetoothSocket connSocket, 92 IObexConnectionHandler obexConnectionHandler, Handler pbapHandler) { 93 super(TAG, looper); 94 mService = service; 95 mIObexConnectionHandler = obexConnectionHandler; 96 mRemoteDevice = device; 97 mServiceHandler = pbapHandler; 98 mConnSocket = connSocket; 99 100 addState(mFinished); 101 addState(mWaitingForAuth); 102 addState(mConnected); 103 setInitialState(mWaitingForAuth); 104 } 105 106 static PbapStateMachine make(BluetoothPbapService service, Looper looper, 107 BluetoothDevice device, BluetoothSocket connSocket, 108 IObexConnectionHandler obexConnectionHandler, Handler pbapHandler) { 109 PbapStateMachine stateMachine = new PbapStateMachine(service, looper, device, connSocket, 110 obexConnectionHandler, pbapHandler); 111 stateMachine.start(); 112 return stateMachine; 113 } 114 115 BluetoothDevice getRemoteDevice() { 116 return mRemoteDevice; 117 } 118 119 private abstract class PbapStateBase extends State { 120 /** 121 * Get a state value from {@link BluetoothProfile} that represents the connection state of 122 * this headset state 123 * 124 * @return a value in {@link BluetoothProfile#STATE_DISCONNECTED}, 125 * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or 126 * {@link BluetoothProfile#STATE_DISCONNECTING} 127 */ 128 abstract int getConnectionStateInt(); 129 } 130 131 class WaitingForAuth extends PbapStateBase { 132 @Override 133 int getConnectionStateInt() { 134 return BluetoothProfile.STATE_CONNECTING; 135 } 136 137 @Override 138 public void enter() { 139 mService.checkOrGetPhonebookPermission(PbapStateMachine.this); 140 } 141 142 @Override 143 public boolean processMessage(Message message) { 144 switch (message.what) { 145 case AUTHORIZED: 146 transitionTo(mConnected); 147 break; 148 case REJECTED: 149 rejectConnection(); 150 transitionTo(mFinished); 151 break; 152 case DISCONNECT: 153 mServiceHandler.removeMessages(BluetoothPbapService.USER_TIMEOUT); 154 Message msg = mServiceHandler.obtainMessage( 155 BluetoothPbapService.USER_TIMEOUT); 156 msg.obj = PbapStateMachine.this; 157 msg.sendToTarget(); 158 transitionTo(mFinished); 159 break; 160 } 161 return HANDLED; 162 } 163 164 private void rejectConnection() { 165 mPbapServer = new BluetoothPbapObexServer(mServiceHandler, mService, 166 PbapStateMachine.this); 167 BluetoothObexTransport transport = new BluetoothObexTransport(mConnSocket); 168 ObexRejectServer server = new ObexRejectServer(ResponseCodes.OBEX_HTTP_UNAVAILABLE, 169 mConnSocket); 170 try { 171 mServerSession = new ServerSession(transport, server, null); 172 } catch (IOException ex) { 173 Log.e(TAG, "Caught exception starting OBEX reject server session" 174 + ex.toString()); 175 } 176 } 177 } 178 179 class Finished extends PbapStateBase { 180 @Override 181 int getConnectionStateInt() { 182 return BluetoothProfile.STATE_DISCONNECTED; 183 } 184 185 @Override 186 public void enter() { 187 // Close OBEX server session 188 if (mServerSession != null) { 189 mServerSession.close(); 190 mServerSession = null; 191 } 192 193 // Close connection socket 194 try { 195 mConnSocket.close(); 196 mConnSocket = null; 197 } catch (IOException e) { 198 Log.e(TAG, "Close Connection Socket error: " + e.toString()); 199 } 200 201 mServiceHandler.obtainMessage(BluetoothPbapService.MSG_STATE_MACHINE_DONE) 202 .sendToTarget(); 203 } 204 205 } 206 207 class Connected extends PbapStateBase { 208 @Override 209 int getConnectionStateInt() { 210 return BluetoothProfile.STATE_CONNECTED; 211 } 212 213 @Override 214 public void enter() { 215 try { 216 startObexServerSession(); 217 } catch (IOException ex) { 218 Log.e(TAG, "Caught exception starting OBEX server session" + ex.toString()); 219 } 220 } 221 222 @Override 223 public void exit() { 224 } 225 226 @Override 227 public boolean processMessage(Message message) { 228 switch (message.what) { 229 case DISCONNECT: 230 stopObexServerSession(); 231 break; 232 case CREATE_NOTIFICATION: 233 createPbapNotification(); 234 break; 235 case REMOVE_NOTIFICATION: 236 Intent i = new Intent(BluetoothPbapService.USER_CONFIRM_TIMEOUT_ACTION); 237 mService.sendBroadcast(i); 238 notifyAuthCancelled(); 239 removePbapNotification(NOTIFICATION_ID_AUTH); 240 break; 241 case AUTH_KEY_INPUT: 242 String key = (String) message.obj; 243 notifyAuthKeyInput(key); 244 break; 245 case AUTH_CANCELLED: 246 notifyAuthCancelled(); 247 break; 248 } 249 return HANDLED; 250 } 251 252 private void startObexServerSession() throws IOException { 253 if (VERBOSE) { 254 Log.v(TAG, "Pbap Service startObexServerSession"); 255 } 256 257 // acquire the wakeLock before start Obex transaction thread 258 mServiceHandler.sendMessage( 259 mServiceHandler.obtainMessage(BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK)); 260 261 mPbapServer = new BluetoothPbapObexServer(mServiceHandler, mService, 262 PbapStateMachine.this); 263 synchronized (this) { 264 mObexAuth = new BluetoothPbapAuthenticator(PbapStateMachine.this); 265 mObexAuth.setChallenged(false); 266 mObexAuth.setCancelled(false); 267 } 268 BluetoothObexTransport transport = new BluetoothObexTransport(mConnSocket); 269 mServerSession = new ServerSession(transport, mPbapServer, mObexAuth); 270 // It's ok to just use one wake lock 271 // Message MSG_ACQUIRE_WAKE_LOCK is always surrounded by RELEASE. safe. 272 } 273 274 private void stopObexServerSession() { 275 if (VERBOSE) { 276 Log.v(TAG, "Pbap Service stopObexServerSession"); 277 } 278 transitionTo(mFinished); 279 } 280 281 private void createPbapNotification() { 282 NotificationManager nm = 283 (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); 284 NotificationChannel notificationChannel = new NotificationChannel( 285 PBAP_OBEX_NOTIFICATION_CHANNEL, 286 mService.getString(R.string.pbap_notification_group), 287 NotificationManager.IMPORTANCE_HIGH); 288 nm.createNotificationChannel(notificationChannel); 289 290 // Create an intent triggered by clicking on the status icon. 291 Intent clickIntent = new Intent(); 292 clickIntent.setClass(mService, BluetoothPbapActivity.class); 293 clickIntent.putExtra(BluetoothPbapService.EXTRA_DEVICE, mRemoteDevice); 294 clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 295 clickIntent.setAction(BluetoothPbapService.AUTH_CHALL_ACTION); 296 297 // Create an intent triggered by clicking on the 298 // "Clear All Notifications" button 299 Intent deleteIntent = new Intent(); 300 deleteIntent.setClass(mService, BluetoothPbapService.class); 301 deleteIntent.setAction(BluetoothPbapService.AUTH_CANCELLED_ACTION); 302 303 String name = mRemoteDevice.getName(); 304 305 Notification notification = 306 new Notification.Builder(mService, PBAP_OBEX_NOTIFICATION_CHANNEL) 307 .setWhen(System.currentTimeMillis()) 308 .setContentTitle(mService.getString(R.string.auth_notif_title)) 309 .setContentText(mService.getString(R.string.auth_notif_message, name)) 310 .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) 311 .setTicker(mService.getString(R.string.auth_notif_ticker)) 312 .setColor(mService.getResources().getColor( 313 com.android.internal.R.color.system_notification_accent_color, 314 mService.getTheme())) 315 .setFlag(Notification.FLAG_AUTO_CANCEL, true) 316 .setFlag(Notification.FLAG_ONLY_ALERT_ONCE, true) 317 .setContentIntent(PendingIntent.getActivity(mService, 0, clickIntent, 318 0)) 319 .setDeleteIntent(PendingIntent.getBroadcast(mService, 0, deleteIntent, 320 0)) 321 .setLocalOnly(true) 322 .build(); 323 nm.notify(NOTIFICATION_ID_AUTH, notification); 324 } 325 326 private void removePbapNotification(int id) { 327 NotificationManager nm = 328 (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); 329 nm.cancel(id); 330 } 331 332 private void notifyAuthCancelled() { 333 synchronized (this) { 334 mObexAuth.setCancelled(true); 335 mObexAuth.notify(); 336 } 337 } 338 339 private void notifyAuthKeyInput(final String key) { 340 synchronized (this) { 341 if (key != null) { 342 mObexAuth.setSessionKey(key); 343 } 344 mObexAuth.setChallenged(true); 345 mObexAuth.notify(); 346 } 347 } 348 } 349 350 synchronized int getConnectionState() { 351 return ((PbapStateBase) getCurrentState()).getConnectionStateInt(); 352 } 353} 354