Home | History | Annotate | Download | only in handover
      1 /*
      2  * Copyright (C) 2012 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.nfc.handover;
     18 
     19 import android.bluetooth.BluetoothA2dp;
     20 import android.bluetooth.BluetoothAdapter;
     21 import android.bluetooth.BluetoothDevice;
     22 import android.bluetooth.BluetoothHeadset;
     23 import android.bluetooth.BluetoothInputDevice;
     24 import android.bluetooth.BluetoothProfile;
     25 import android.content.BroadcastReceiver;
     26 import android.content.ContentResolver;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.IntentFilter;
     30 import android.media.session.MediaSessionLegacyHelper;
     31 import android.os.Handler;
     32 import android.os.Looper;
     33 import android.os.Message;
     34 import android.os.ParcelUuid;
     35 import android.provider.Settings;
     36 import android.util.Log;
     37 import android.view.KeyEvent;
     38 import android.widget.Toast;
     39 
     40 import com.android.nfc.R;
     41 
     42 /**
     43  * Connects / Disconnects from a Bluetooth headset (or any device that
     44  * might implement BT HSP, HFP, A2DP, or HOGP sink) when touched with NFC.
     45  *
     46  * This object is created on an NFC interaction, and determines what
     47  * sequence of Bluetooth actions to take, and executes them. It is not
     48  * designed to be re-used after the sequence has completed or timed out.
     49  * Subsequent NFC interactions should use new objects.
     50  *
     51  */
     52 public class BluetoothPeripheralHandover implements BluetoothProfile.ServiceListener {
     53     static final String TAG = "BluetoothPeripheralHandover";
     54     static final boolean DBG = false;
     55 
     56     static final String ACTION_ALLOW_CONNECT = "com.android.nfc.handover.action.ALLOW_CONNECT";
     57     static final String ACTION_DENY_CONNECT = "com.android.nfc.handover.action.DENY_CONNECT";
     58 
     59     static final int TIMEOUT_MS = 20000;
     60 
     61     static final int STATE_INIT = 0;
     62     static final int STATE_WAITING_FOR_PROXIES = 1;
     63     static final int STATE_INIT_COMPLETE = 2;
     64     static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 3;
     65     static final int STATE_BONDING = 4;
     66     static final int STATE_CONNECTING = 5;
     67     static final int STATE_DISCONNECTING = 6;
     68     static final int STATE_COMPLETE = 7;
     69 
     70     static final int RESULT_PENDING = 0;
     71     static final int RESULT_CONNECTED = 1;
     72     static final int RESULT_DISCONNECTED = 2;
     73 
     74     static final int ACTION_INIT = 0;
     75     static final int ACTION_DISCONNECT = 1;
     76     static final int ACTION_CONNECT = 2;
     77 
     78     static final int MSG_TIMEOUT = 1;
     79     static final int MSG_NEXT_STEP = 2;
     80 
     81     final Context mContext;
     82     final BluetoothDevice mDevice;
     83     final String mName;
     84     final Callback mCallback;
     85     final BluetoothAdapter mBluetoothAdapter;
     86     final int mTransport;
     87     final boolean mProvisioning;
     88 
     89     final Object mLock = new Object();
     90 
     91     // only used on main thread
     92     int mAction;
     93     int mState;
     94     int mHfpResult;  // used only in STATE_CONNECTING and STATE_DISCONNETING
     95     int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING
     96     int mHidResult;
     97 
     98     // protected by mLock
     99     BluetoothA2dp mA2dp;
    100     BluetoothHeadset mHeadset;
    101     BluetoothInputDevice mInput;
    102 
    103     public interface Callback {
    104         public void onBluetoothPeripheralHandoverComplete(boolean connected);
    105     }
    106 
    107     public BluetoothPeripheralHandover(Context context, BluetoothDevice device, String name,
    108                                        int transport, Callback callback) {
    109         checkMainThread();  // mHandler must get get constructed on Main Thread for toasts to work
    110         mContext = context;
    111         mDevice = device;
    112         mName = name;
    113         mTransport = transport;
    114         mCallback = callback;
    115         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    116 
    117         ContentResolver contentResolver = mContext.getContentResolver();
    118         mProvisioning = Settings.Secure.getInt(contentResolver,
    119                 Settings.Global.DEVICE_PROVISIONED, 0) == 0;
    120 
    121         mState = STATE_INIT;
    122     }
    123 
    124     public boolean hasStarted() {
    125         return mState != STATE_INIT;
    126     }
    127 
    128     /**
    129      * Main entry point. This method is usually called after construction,
    130      * to begin the BT sequence. Must be called on Main thread.
    131      */
    132     public boolean start() {
    133         checkMainThread();
    134         if (mState != STATE_INIT || mBluetoothAdapter == null
    135                 || (mProvisioning && mTransport != BluetoothDevice.TRANSPORT_LE)) {
    136             return false;
    137         }
    138 
    139 
    140         IntentFilter filter = new IntentFilter();
    141         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
    142         filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
    143         filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
    144         filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
    145         filter.addAction(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED);
    146         filter.addAction(ACTION_ALLOW_CONNECT);
    147         filter.addAction(ACTION_DENY_CONNECT);
    148 
    149         mContext.registerReceiver(mReceiver, filter);
    150 
    151         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
    152 
    153         mAction = ACTION_INIT;
    154 
    155         nextStep();
    156 
    157         return true;
    158     }
    159 
    160     /**
    161      * Called to execute next step in state machine
    162      */
    163     void nextStep() {
    164         if (mAction == ACTION_INIT) {
    165             nextStepInit();
    166         } else if (mAction == ACTION_CONNECT) {
    167             nextStepConnect();
    168         } else {
    169             nextStepDisconnect();
    170         }
    171     }
    172 
    173     /*
    174      * Enables bluetooth and gets the profile proxies
    175      */
    176     void nextStepInit() {
    177         switch (mState) {
    178             case STATE_INIT:
    179                 if (mA2dp == null || mHeadset == null || mInput == null) {
    180                     mState = STATE_WAITING_FOR_PROXIES;
    181                     if (!getProfileProxys()) {
    182                         complete(false);
    183                     }
    184                     break;
    185                 }
    186                 // fall-through
    187             case STATE_WAITING_FOR_PROXIES:
    188                 mState = STATE_INIT_COMPLETE;
    189                 // Check connected devices and see if we need to disconnect
    190                 synchronized(mLock) {
    191                     if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    192                         if (mInput.getConnectedDevices().contains(mDevice)) {
    193                             Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName);
    194                             mAction = ACTION_DISCONNECT;
    195                         } else {
    196                             Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName);
    197                             mAction = ACTION_CONNECT;
    198                         }
    199                     } else {
    200                         if (mA2dp.getConnectedDevices().contains(mDevice) ||
    201                                 mHeadset.getConnectedDevices().contains(mDevice)) {
    202                             Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName);
    203                             mAction = ACTION_DISCONNECT;
    204                         } else {
    205                             Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName);
    206                             mAction = ACTION_CONNECT;
    207                         }
    208                     }
    209                 }
    210                 nextStep();
    211         }
    212 
    213     }
    214 
    215     void nextStepDisconnect() {
    216         switch (mState) {
    217             case STATE_INIT_COMPLETE:
    218                 mState = STATE_DISCONNECTING;
    219                 synchronized (mLock) {
    220                     if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    221                         if (mInput.getConnectionState(mDevice)
    222                                 != BluetoothProfile.STATE_DISCONNECTED) {
    223                             mHidResult = RESULT_PENDING;
    224                             mInput.disconnect(mDevice);
    225                             toast(getToastString(R.string.disconnecting_peripheral));
    226                             break;
    227                         } else {
    228                             mHidResult = RESULT_DISCONNECTED;
    229                         }
    230                     } else {
    231                         if (mHeadset.getConnectionState(mDevice)
    232                                 != BluetoothProfile.STATE_DISCONNECTED) {
    233                             mHfpResult = RESULT_PENDING;
    234                             mHeadset.disconnect(mDevice);
    235                         } else {
    236                             mHfpResult = RESULT_DISCONNECTED;
    237                         }
    238                         if (mA2dp.getConnectionState(mDevice)
    239                                 != BluetoothProfile.STATE_DISCONNECTED) {
    240                             mA2dpResult = RESULT_PENDING;
    241                             mA2dp.disconnect(mDevice);
    242                         } else {
    243                             mA2dpResult = RESULT_DISCONNECTED;
    244                         }
    245                         if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    246                             toast(getToastString(R.string.disconnecting_peripheral));
    247                             break;
    248                         }
    249                     }
    250                 }
    251                 // fall-through
    252             case STATE_DISCONNECTING:
    253                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    254                     if (mHidResult == RESULT_DISCONNECTED) {
    255                         toast(getToastString(R.string.disconnected_peripheral));
    256                         complete(false);
    257                     }
    258 
    259                     break;
    260                 } else {
    261                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    262                         // still disconnecting
    263                         break;
    264                     }
    265                     if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) {
    266                         toast(getToastString(R.string.disconnected_peripheral));
    267                     }
    268                     complete(false);
    269                     break;
    270                 }
    271 
    272         }
    273 
    274     }
    275 
    276     private String getToastString(int resid) {
    277         return mContext.getString(resid, mName != null ? mName : R.string.device);
    278     }
    279 
    280     boolean getProfileProxys() {
    281 
    282         if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    283             if (!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.INPUT_DEVICE))
    284                 return false;
    285         } else {
    286             if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET))
    287                 return false;
    288 
    289             if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP))
    290                 return false;
    291         }
    292 
    293         return true;
    294     }
    295 
    296     void nextStepConnect() {
    297         switch (mState) {
    298             case STATE_INIT_COMPLETE:
    299 
    300                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
    301                     requestPairConfirmation();
    302                     mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
    303                     break;
    304                 }
    305 
    306                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    307                     if (mDevice.getBondState() != BluetoothDevice.BOND_NONE) {
    308                         mDevice.removeBond();
    309                         requestPairConfirmation();
    310                         mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
    311                         break;
    312                     }
    313                 }
    314                 // fall-through
    315             case STATE_WAITING_FOR_BOND_CONFIRMATION:
    316                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
    317                     startBonding();
    318                     break;
    319                 }
    320                 // fall-through
    321             case STATE_BONDING:
    322                 // Bluetooth Profile service will correctly serialize
    323                 // HFP then A2DP connect
    324                 mState = STATE_CONNECTING;
    325                 synchronized (mLock) {
    326                     if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    327                         if (mInput.getConnectionState(mDevice)
    328                                 != BluetoothProfile.STATE_CONNECTED) {
    329                             mHidResult = RESULT_PENDING;
    330                             mInput.connect(mDevice);
    331                             toast(getToastString(R.string.connecting_peripheral));
    332                             break;
    333                         } else {
    334                             mHidResult = RESULT_CONNECTED;
    335                         }
    336                     } else {
    337                         if (mHeadset.getConnectionState(mDevice) !=
    338                                 BluetoothProfile.STATE_CONNECTED) {
    339                             mHfpResult = RESULT_PENDING;
    340                             mHeadset.connect(mDevice);
    341                         } else {
    342                             mHfpResult = RESULT_CONNECTED;
    343                         }
    344                         if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
    345                             mA2dpResult = RESULT_PENDING;
    346                             mA2dp.connect(mDevice);
    347                         } else {
    348                             mA2dpResult = RESULT_CONNECTED;
    349                         }
    350                         if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    351                             toast(getToastString(R.string.connecting_peripheral));
    352                             break;
    353                         }
    354                     }
    355                 }
    356                 // fall-through
    357             case STATE_CONNECTING:
    358                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    359                     if (mHidResult == RESULT_PENDING) {
    360                         break;
    361                     } else if (mHidResult == RESULT_CONNECTED) {
    362                         toast(getToastString(R.string.connected_peripheral));
    363                         mDevice.setAlias(mName);
    364                         complete(true);
    365                     } else {
    366                         toast (getToastString(R.string.connect_peripheral_failed));
    367                         complete(false);
    368                     }
    369                 } else {
    370                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    371                         // another connection type still pending
    372                         break;
    373                     }
    374                     if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) {
    375                         // we'll take either as success
    376                         toast(getToastString(R.string.connected_peripheral));
    377                         if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
    378                         mDevice.setAlias(mName);
    379                         complete(true);
    380                     } else {
    381                         toast (getToastString(R.string.connect_peripheral_failed));
    382                         complete(false);
    383                     }
    384                 }
    385                 break;
    386         }
    387     }
    388 
    389     void startBonding() {
    390         mState = STATE_BONDING;
    391         toast(getToastString(R.string.pairing_peripheral));
    392         if (!mDevice.createBond(mTransport)) {
    393             toast(getToastString(R.string.pairing_peripheral_failed));
    394             complete(false);
    395         }
    396     }
    397 
    398     void handleIntent(Intent intent) {
    399         String action = intent.getAction();
    400         // Everything requires the device to match...
    401         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    402         if (!mDevice.equals(device)) return;
    403 
    404         if (ACTION_ALLOW_CONNECT.equals(action)) {
    405             nextStepConnect();
    406         } else if (ACTION_DENY_CONNECT.equals(action)) {
    407             complete(false);
    408         } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)
    409                 && mState == STATE_BONDING) {
    410             int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
    411                     BluetoothAdapter.ERROR);
    412             if (bond == BluetoothDevice.BOND_BONDED) {
    413                 nextStepConnect();
    414             } else if (bond == BluetoothDevice.BOND_NONE) {
    415                 toast(getToastString(R.string.pairing_peripheral_failed));
    416                 complete(false);
    417             }
    418         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
    419                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
    420             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
    421             if (state == BluetoothProfile.STATE_CONNECTED) {
    422                 mHfpResult = RESULT_CONNECTED;
    423                 nextStep();
    424             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
    425                 mHfpResult = RESULT_DISCONNECTED;
    426                 nextStep();
    427             }
    428         } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
    429                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
    430             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
    431             if (state == BluetoothProfile.STATE_CONNECTED) {
    432                 mA2dpResult = RESULT_CONNECTED;
    433                 nextStep();
    434             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
    435                 mA2dpResult = RESULT_DISCONNECTED;
    436                 nextStep();
    437             }
    438         } else if (BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
    439                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
    440             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
    441             if (state == BluetoothProfile.STATE_CONNECTED) {
    442                 mHidResult = RESULT_CONNECTED;
    443                 nextStep();
    444             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
    445                 mHidResult = RESULT_DISCONNECTED;
    446                 nextStep();
    447             }
    448         }
    449     }
    450 
    451     void complete(boolean connected) {
    452         if (DBG) Log.d(TAG, "complete()");
    453         mState = STATE_COMPLETE;
    454         mContext.unregisterReceiver(mReceiver);
    455         mHandler.removeMessages(MSG_TIMEOUT);
    456         synchronized (mLock) {
    457             if (mA2dp != null) {
    458                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp);
    459             }
    460             if (mHeadset != null) {
    461                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset);
    462             }
    463 
    464             if (mInput != null) {
    465                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.INPUT_DEVICE, mInput);
    466             }
    467 
    468             mA2dp = null;
    469             mHeadset = null;
    470             mInput = null;
    471         }
    472         mCallback.onBluetoothPeripheralHandoverComplete(connected);
    473     }
    474 
    475     void toast(CharSequence text) {
    476         Toast.makeText(mContext,  text, Toast.LENGTH_SHORT).show();
    477     }
    478 
    479     void startTheMusic() {
    480         MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext);
    481         if (helper != null) {
    482             KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
    483             helper.sendMediaButtonEvent(keyEvent, false);
    484             keyEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY);
    485             helper.sendMediaButtonEvent(keyEvent, false);
    486         } else {
    487             Log.w(TAG, "Unable to send media key event");
    488         }
    489     }
    490 
    491     void requestPairConfirmation() {
    492         Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
    493         dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    494         dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
    495 
    496         mContext.startActivity(dialogIntent);
    497     }
    498 
    499     final Handler mHandler = new Handler() {
    500         @Override
    501         public void handleMessage(Message msg) {
    502             switch (msg.what) {
    503                 case MSG_TIMEOUT:
    504                     if (mState == STATE_COMPLETE) return;
    505                     Log.i(TAG, "Timeout completing BT handover");
    506                     complete(false);
    507                     break;
    508                 case MSG_NEXT_STEP:
    509                     nextStep();
    510                     break;
    511             }
    512         }
    513     };
    514 
    515     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    516         @Override
    517         public void onReceive(Context context, Intent intent) {
    518             handleIntent(intent);
    519         }
    520     };
    521 
    522     static void checkMainThread() {
    523         if (Looper.myLooper() != Looper.getMainLooper()) {
    524             throw new IllegalThreadStateException("must be called on main thread");
    525         }
    526     }
    527 
    528     @Override
    529     public void onServiceConnected(int profile, BluetoothProfile proxy) {
    530         synchronized (mLock) {
    531             switch (profile) {
    532                 case BluetoothProfile.HEADSET:
    533                     mHeadset = (BluetoothHeadset) proxy;
    534                     if (mA2dp != null) {
    535                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
    536                     }
    537                     break;
    538                 case BluetoothProfile.A2DP:
    539                     mA2dp = (BluetoothA2dp) proxy;
    540                     if (mHeadset != null) {
    541                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
    542                     }
    543                     break;
    544                 case BluetoothProfile.INPUT_DEVICE:
    545                     mInput = (BluetoothInputDevice) proxy;
    546                     if (mInput != null) {
    547                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
    548                     }
    549                     break;
    550             }
    551         }
    552     }
    553 
    554     @Override
    555     public void onServiceDisconnected(int profile) {
    556         // We can ignore these
    557     }
    558 }
    559