Home | History | Annotate | Download | only in pbap
      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