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