Home | History | Annotate | Download | only in pbapclient
      1 /*
      2  * Copyright (C) 2016 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 /*
     18  * Bluetooth Pbap PCE StateMachine
     19  *                      (Disconnected)
     20  *                           |    ^
     21  *                   CONNECT |    | DISCONNECTED
     22  *                           V    |
     23  *                 (Connecting) (Disconnecting)
     24  *                           |    ^
     25  *                 CONNECTED |    | DISCONNECT
     26  *                           V    |
     27  *                        (Connected)
     28  *
     29  * Valid Transitions:
     30  * State + Event -> Transition:
     31  *
     32  * Disconnected + CONNECT -> Connecting
     33  * Connecting + CONNECTED -> Connected
     34  * Connecting + TIMEOUT -> Disconnecting
     35  * Connecting + DISCONNECT -> Disconnecting
     36  * Connected + DISCONNECT -> Disconnecting
     37  * Disconnecting + DISCONNECTED -> (Safe) Disconnected
     38  * Disconnecting + TIMEOUT -> (Force) Disconnected
     39  * Disconnecting + CONNECT : Defer Message
     40  *
     41  */
     42 package com.android.bluetooth.pbapclient;
     43 
     44 import android.bluetooth.BluetoothDevice;
     45 import android.bluetooth.BluetoothPbapClient;
     46 import android.bluetooth.BluetoothProfile;
     47 import android.bluetooth.BluetoothUuid;
     48 import android.content.BroadcastReceiver;
     49 import android.content.Context;
     50 import android.content.Intent;
     51 import android.content.IntentFilter;
     52 import android.os.HandlerThread;
     53 import android.os.Message;
     54 import android.os.ParcelUuid;
     55 import android.os.Process;
     56 import android.os.UserManager;
     57 import android.util.Log;
     58 
     59 import com.android.bluetooth.BluetoothMetricsProto;
     60 import com.android.bluetooth.btservice.MetricsLogger;
     61 import com.android.bluetooth.btservice.ProfileService;
     62 import com.android.internal.util.IState;
     63 import com.android.internal.util.State;
     64 import com.android.internal.util.StateMachine;
     65 
     66 import java.util.ArrayList;
     67 import java.util.List;
     68 
     69 final class PbapClientStateMachine extends StateMachine {
     70     private static final boolean DBG = true;
     71     private static final String TAG = "PbapClientStateMachine";
     72 
     73     // Messages for handling connect/disconnect requests.
     74     private static final int MSG_DISCONNECT = 2;
     75     private static final int MSG_SDP_COMPLETE = 9;
     76 
     77     // Messages for handling error conditions.
     78     private static final int MSG_CONNECT_TIMEOUT = 3;
     79     private static final int MSG_DISCONNECT_TIMEOUT = 4;
     80 
     81     // Messages for feedback from ConnectionHandler.
     82     static final int MSG_CONNECTION_COMPLETE = 5;
     83     static final int MSG_CONNECTION_FAILED = 6;
     84     static final int MSG_CONNECTION_CLOSED = 7;
     85     static final int MSG_RESUME_DOWNLOAD = 8;
     86 
     87     static final int CONNECT_TIMEOUT = 10000;
     88     static final int DISCONNECT_TIMEOUT = 3000;
     89 
     90     private final Object mLock;
     91     private State mDisconnected;
     92     private State mConnecting;
     93     private State mConnected;
     94     private State mDisconnecting;
     95 
     96     // mCurrentDevice may only be changed in Disconnected State.
     97     private final BluetoothDevice mCurrentDevice;
     98     private PbapClientService mService;
     99     private PbapClientConnectionHandler mConnectionHandler;
    100     private HandlerThread mHandlerThread = null;
    101     private UserManager mUserManager = null;
    102 
    103     // mMostRecentState maintains previous state for broadcasting transitions.
    104     private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
    105 
    106     PbapClientStateMachine(PbapClientService svc, BluetoothDevice device) {
    107         super(TAG);
    108 
    109         mService = svc;
    110         mCurrentDevice = device;
    111         mLock = new Object();
    112         mUserManager = UserManager.get(mService);
    113         mDisconnected = new Disconnected();
    114         mConnecting = new Connecting();
    115         mDisconnecting = new Disconnecting();
    116         mConnected = new Connected();
    117 
    118         addState(mDisconnected);
    119         addState(mConnecting);
    120         addState(mDisconnecting);
    121         addState(mConnected);
    122 
    123         setInitialState(mConnecting);
    124     }
    125 
    126     class Disconnected extends State {
    127         @Override
    128         public void enter() {
    129             Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what);
    130             onConnectionStateChanged(mCurrentDevice, mMostRecentState,
    131                     BluetoothProfile.STATE_DISCONNECTED);
    132             mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
    133             quit();
    134         }
    135     }
    136 
    137     class Connecting extends State {
    138         private SDPBroadcastReceiver mSdpReceiver;
    139 
    140         @Override
    141         public void enter() {
    142             if (DBG) {
    143                 Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what);
    144             }
    145             onConnectionStateChanged(mCurrentDevice, mMostRecentState,
    146                     BluetoothProfile.STATE_CONNECTING);
    147             mSdpReceiver = new SDPBroadcastReceiver();
    148             mSdpReceiver.register();
    149             mCurrentDevice.sdpSearch(BluetoothUuid.PBAP_PSE);
    150             mMostRecentState = BluetoothProfile.STATE_CONNECTING;
    151 
    152             // Create a separate handler instance and thread for performing
    153             // connect/download/disconnect operations as they may be time consuming and error prone.
    154             mHandlerThread =
    155                     new HandlerThread("PBAP PCE handler", Process.THREAD_PRIORITY_BACKGROUND);
    156             mHandlerThread.start();
    157             mConnectionHandler =
    158                     new PbapClientConnectionHandler.Builder().setLooper(mHandlerThread.getLooper())
    159                             .setContext(mService)
    160                             .setClientSM(PbapClientStateMachine.this)
    161                             .setRemoteDevice(mCurrentDevice)
    162                             .build();
    163 
    164             sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT);
    165         }
    166 
    167         @Override
    168         public boolean processMessage(Message message) {
    169             if (DBG) {
    170                 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
    171             }
    172             switch (message.what) {
    173                 case MSG_DISCONNECT:
    174                     if (message.obj instanceof BluetoothDevice && message.obj.equals(
    175                             mCurrentDevice)) {
    176                         removeMessages(MSG_CONNECT_TIMEOUT);
    177                         transitionTo(mDisconnecting);
    178                     }
    179                     break;
    180 
    181                 case MSG_CONNECTION_COMPLETE:
    182                     removeMessages(MSG_CONNECT_TIMEOUT);
    183                     transitionTo(mConnected);
    184                     break;
    185 
    186                 case MSG_CONNECTION_FAILED:
    187                 case MSG_CONNECT_TIMEOUT:
    188                     removeMessages(MSG_CONNECT_TIMEOUT);
    189                     transitionTo(mDisconnecting);
    190                     break;
    191 
    192                 case MSG_SDP_COMPLETE:
    193                     mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_CONNECT,
    194                             message.obj).sendToTarget();
    195                     break;
    196 
    197                 default:
    198                     Log.w(TAG, "Received unexpected message while Connecting");
    199                     return NOT_HANDLED;
    200             }
    201             return HANDLED;
    202         }
    203 
    204         @Override
    205         public void exit() {
    206             mSdpReceiver.unregister();
    207             mSdpReceiver = null;
    208         }
    209 
    210         private class SDPBroadcastReceiver extends BroadcastReceiver {
    211             @Override
    212             public void onReceive(Context context, Intent intent) {
    213                 String action = intent.getAction();
    214                 if (DBG) {
    215                     Log.v(TAG, "onReceive" + action);
    216                 }
    217                 if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) {
    218                     BluetoothDevice device =
    219                             intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    220                     if (!device.equals(getDevice())) {
    221                         Log.w(TAG, "SDP Record fetched for different device - Ignore");
    222                         return;
    223                     }
    224                     ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
    225                     if (DBG) {
    226                         Log.v(TAG, "Received UUID: " + uuid.toString());
    227                     }
    228                     if (DBG) {
    229                         Log.v(TAG, "expected UUID: " + BluetoothUuid.PBAP_PSE.toString());
    230                     }
    231                     if (uuid.equals(BluetoothUuid.PBAP_PSE)) {
    232                         sendMessage(MSG_SDP_COMPLETE,
    233                                 intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD));
    234                     }
    235                 }
    236             }
    237 
    238             public void register() {
    239                 IntentFilter filter = new IntentFilter();
    240                 filter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
    241                 mService.registerReceiver(this, filter);
    242             }
    243 
    244             public void unregister() {
    245                 mService.unregisterReceiver(this);
    246             }
    247         }
    248     }
    249 
    250     class Disconnecting extends State {
    251         @Override
    252         public void enter() {
    253             Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what);
    254             onConnectionStateChanged(mCurrentDevice, mMostRecentState,
    255                     BluetoothProfile.STATE_DISCONNECTING);
    256             mMostRecentState = BluetoothProfile.STATE_DISCONNECTING;
    257             mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT)
    258                     .sendToTarget();
    259             sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT);
    260         }
    261 
    262         @Override
    263         public boolean processMessage(Message message) {
    264             if (DBG) {
    265                 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
    266             }
    267             switch (message.what) {
    268                 case MSG_CONNECTION_CLOSED:
    269                     removeMessages(MSG_DISCONNECT_TIMEOUT);
    270                     mHandlerThread.quitSafely();
    271                     transitionTo(mDisconnected);
    272                     break;
    273 
    274                 case MSG_DISCONNECT:
    275                     deferMessage(message);
    276                     break;
    277 
    278                 case MSG_DISCONNECT_TIMEOUT:
    279                     Log.w(TAG, "Disconnect Timeout, Forcing");
    280                     mConnectionHandler.abort();
    281                     break;
    282 
    283                 case MSG_RESUME_DOWNLOAD:
    284                     // Do nothing.
    285                     break;
    286 
    287                 default:
    288                     Log.w(TAG, "Received unexpected message while Disconnecting");
    289                     return NOT_HANDLED;
    290             }
    291             return HANDLED;
    292         }
    293     }
    294 
    295     class Connected extends State {
    296         @Override
    297         public void enter() {
    298             Log.d(TAG, "Enter Connected: " + getCurrentMessage().what);
    299             onConnectionStateChanged(mCurrentDevice, mMostRecentState,
    300                     BluetoothProfile.STATE_CONNECTED);
    301             mMostRecentState = BluetoothProfile.STATE_CONNECTED;
    302             if (mUserManager.isUserUnlocked()) {
    303                 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
    304                         .sendToTarget();
    305             }
    306         }
    307 
    308         @Override
    309         public boolean processMessage(Message message) {
    310             if (DBG) {
    311                 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
    312             }
    313             switch (message.what) {
    314                 case MSG_DISCONNECT:
    315                     if ((message.obj instanceof BluetoothDevice)
    316                             && ((BluetoothDevice) message.obj).equals(mCurrentDevice)) {
    317                         transitionTo(mDisconnecting);
    318                     }
    319                     break;
    320 
    321                 case MSG_RESUME_DOWNLOAD:
    322                     mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
    323                             .sendToTarget();
    324                     break;
    325 
    326                 default:
    327                     Log.w(TAG, "Received unexpected message while Connected");
    328                     return NOT_HANDLED;
    329             }
    330             return HANDLED;
    331         }
    332     }
    333 
    334     private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
    335         if (device == null) {
    336             Log.w(TAG, "onConnectionStateChanged with invalid device");
    337             return;
    338         }
    339         if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) {
    340             MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.PBAP_CLIENT);
    341         }
    342         Log.d(TAG, "Connection state " + device + ": " + prevState + "->" + state);
    343         Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
    344         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
    345         intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
    346         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
    347         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
    348         mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
    349     }
    350 
    351     public void disconnect(BluetoothDevice device) {
    352         Log.d(TAG, "Disconnect Request " + device);
    353         sendMessage(MSG_DISCONNECT, device);
    354     }
    355 
    356     public void resumeDownload() {
    357         sendMessage(MSG_RESUME_DOWNLOAD);
    358     }
    359 
    360     void doQuit() {
    361         if (mHandlerThread != null) {
    362             mHandlerThread.quitSafely();
    363         }
    364         quitNow();
    365     }
    366 
    367     @Override
    368     protected void onQuitting() {
    369         mService.cleanupDevice(mCurrentDevice);
    370     }
    371 
    372     public int getConnectionState() {
    373         IState currentState = getCurrentState();
    374         if (currentState instanceof Disconnected) {
    375             return BluetoothProfile.STATE_DISCONNECTED;
    376         } else if (currentState instanceof Connecting) {
    377             return BluetoothProfile.STATE_CONNECTING;
    378         } else if (currentState instanceof Connected) {
    379             return BluetoothProfile.STATE_CONNECTED;
    380         } else if (currentState instanceof Disconnecting) {
    381             return BluetoothProfile.STATE_DISCONNECTING;
    382         }
    383         Log.w(TAG, "Unknown State");
    384         return BluetoothProfile.STATE_DISCONNECTED;
    385     }
    386 
    387     public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
    388         int clientState;
    389         BluetoothDevice currentDevice;
    390         synchronized (mLock) {
    391             clientState = getConnectionState();
    392             currentDevice = getDevice();
    393         }
    394         List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();
    395         for (int state : states) {
    396             if (clientState == state) {
    397                 if (currentDevice != null) {
    398                     deviceList.add(currentDevice);
    399                 }
    400             }
    401         }
    402         return deviceList;
    403     }
    404 
    405     public int getConnectionState(BluetoothDevice device) {
    406         if (device == null) {
    407             return BluetoothProfile.STATE_DISCONNECTED;
    408         }
    409         synchronized (mLock) {
    410             if (device.equals(mCurrentDevice)) {
    411                 return getConnectionState();
    412             }
    413         }
    414         return BluetoothProfile.STATE_DISCONNECTED;
    415     }
    416 
    417 
    418     public BluetoothDevice getDevice() {
    419         /*
    420          * Disconnected is the only state where device can change, and to prevent the race
    421          * condition of reporting a valid device while disconnected fix the report here.  Note that
    422          * Synchronization of the state and device is not possible with current state machine
    423          * desingn since the actual Transition happens sometime after the transitionTo method.
    424          */
    425         if (getCurrentState() instanceof Disconnected) {
    426             return null;
    427         }
    428         return mCurrentDevice;
    429     }
    430 
    431     Context getContext() {
    432         return mService;
    433     }
    434 
    435     public void dump(StringBuilder sb) {
    436         ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice);
    437         ProfileService.println(sb, "StateMachine: " + this.toString());
    438     }
    439 }
    440