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.BluetoothProfile;
     24 import android.content.BroadcastReceiver;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.IntentFilter;
     28 import android.os.Handler;
     29 import android.os.Looper;
     30 import android.os.Message;
     31 import android.util.Log;
     32 import android.view.KeyEvent;
     33 import android.widget.Toast;
     34 
     35 import com.android.nfc.handover.HandoverManager.HandoverPowerManager;
     36 import com.android.nfc.R;
     37 
     38 /**
     39  * Connects / Disconnects from a Bluetooth headset (or any device that
     40  * might implement BT HSP, HFP or A2DP sink) when touched with NFC.
     41  *
     42  * This object is created on an NFC interaction, and determines what
     43  * sequence of Bluetooth actions to take, and executes them. It is not
     44  * designed to be re-used after the sequence has completed or timed out.
     45  * Subsequent NFC interactions should use new objects.
     46  *
     47  * TODO: UI review
     48  */
     49 public class BluetoothHeadsetHandover {
     50     static final String TAG = HandoverManager.TAG;
     51     static final boolean DBG = HandoverManager.DBG;
     52 
     53     static final String ACTION_ALLOW_CONNECT = "com.android.nfc.handover.action.ALLOW_CONNECT";
     54     static final String ACTION_DENY_CONNECT = "com.android.nfc.handover.action.DENY_CONNECT";
     55 
     56     static final int TIMEOUT_MS = 20000;
     57 
     58     static final int STATE_INIT = 0;
     59     static final int STATE_TURNING_ON = 1;
     60     static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 2;
     61     static final int STATE_BONDING = 3;
     62     static final int STATE_CONNECTING = 4;
     63     static final int STATE_DISCONNECTING = 5;
     64     static final int STATE_COMPLETE = 6;
     65 
     66     static final int RESULT_PENDING = 0;
     67     static final int RESULT_CONNECTED = 1;
     68     static final int RESULT_DISCONNECTED = 2;
     69 
     70     static final int ACTION_DISCONNECT = 1;
     71     static final int ACTION_CONNECT = 2;
     72 
     73     static final int MSG_TIMEOUT = 1;
     74 
     75     final Context mContext;
     76     final BluetoothDevice mDevice;
     77     final String mName;
     78     final HandoverPowerManager mHandoverPowerManager;
     79     final BluetoothA2dp mA2dp;
     80     final BluetoothHeadset mHeadset;
     81     final Callback mCallback;
     82 
     83     // only used on main thread
     84     int mAction;
     85     int mState;
     86     int mHfpResult;  // used only in STATE_CONNECTING and STATE_DISCONNETING
     87     int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING
     88 
     89     public interface Callback {
     90         public void onBluetoothHeadsetHandoverComplete(boolean connected);
     91     }
     92 
     93     public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name,
     94             HandoverPowerManager powerManager, BluetoothA2dp a2dp, BluetoothHeadset headset,
     95             Callback callback) {
     96         checkMainThread();  // mHandler must get get constructed on Main Thread for toasts to work
     97         mContext = context;
     98         mDevice = device;
     99         mName = name;
    100         mHandoverPowerManager = powerManager;
    101         mA2dp = a2dp;
    102         mHeadset = headset;
    103         mCallback = callback;
    104         mState = STATE_INIT;
    105     }
    106 
    107     /**
    108      * Main entry point. This method is usually called after construction,
    109      * to begin the BT sequence. Must be called on Main thread.
    110      */
    111     public void start() {
    112         checkMainThread();
    113         if (mState != STATE_INIT) return;
    114 
    115         IntentFilter filter = new IntentFilter();
    116         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
    117         filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
    118         filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
    119         filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
    120         filter.addAction(ACTION_ALLOW_CONNECT);
    121         filter.addAction(ACTION_DENY_CONNECT);
    122 
    123         mContext.registerReceiver(mReceiver, filter);
    124 
    125         if (mA2dp.getConnectedDevices().contains(mDevice) ||
    126                 mHeadset.getConnectedDevices().contains(mDevice)) {
    127             Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName);
    128             mAction = ACTION_DISCONNECT;
    129         } else {
    130             Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName);
    131             mAction = ACTION_CONNECT;
    132         }
    133         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
    134         nextStep();
    135     }
    136 
    137     /**
    138      * Called to execute next step in state machine
    139      */
    140     void nextStep() {
    141         if (mAction == ACTION_CONNECT) {
    142             nextStepConnect();
    143         } else {
    144             nextStepDisconnect();
    145         }
    146     }
    147 
    148     void nextStepDisconnect() {
    149         switch (mState) {
    150             case STATE_INIT:
    151                 mState = STATE_DISCONNECTING;
    152                 if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) {
    153                     mHfpResult = RESULT_PENDING;
    154                     mHeadset.disconnect(mDevice);
    155                 } else {
    156                     mHfpResult = RESULT_DISCONNECTED;
    157                 }
    158                 if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) {
    159                     mA2dpResult = RESULT_PENDING;
    160                     mA2dp.disconnect(mDevice);
    161                 } else {
    162                     mA2dpResult = RESULT_DISCONNECTED;
    163                 }
    164                 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    165                     toast(mContext.getString(R.string.disconnecting_headset ) + " " +
    166                             mName + "...");
    167                     break;
    168                 }
    169                 // fall-through
    170             case STATE_DISCONNECTING:
    171                 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    172                     // still disconnecting
    173                     break;
    174                 }
    175                 if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) {
    176                     toast(mContext.getString(R.string.disconnected_headset) + " " + mName);
    177                 }
    178                 complete(false);
    179                 break;
    180         }
    181     }
    182 
    183     void nextStepConnect() {
    184         switch (mState) {
    185             case STATE_INIT:
    186                 if (!mHandoverPowerManager.isBluetoothEnabled()) {
    187                     if (mHandoverPowerManager.enableBluetooth()) {
    188                         // Bluetooth is being enabled
    189                         mState = STATE_TURNING_ON;
    190                     } else {
    191                         toast(mContext.getString(R.string.failed_to_enable_bt));
    192                         complete(false);
    193                     }
    194                     break;
    195                 }
    196                 // fall-through
    197             case STATE_TURNING_ON:
    198                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
    199                     requestPairConfirmation();
    200                     mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
    201                     break;
    202                 }
    203                 // fall-through
    204             case STATE_WAITING_FOR_BOND_CONFIRMATION:
    205                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
    206                     startBonding();
    207                     break;
    208                 }
    209                 // fall-through
    210             case STATE_BONDING:
    211                 // Bluetooth Profile service will correctly serialize
    212                 // HFP then A2DP connect
    213                 mState = STATE_CONNECTING;
    214                 if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
    215                     mHfpResult = RESULT_PENDING;
    216                     mHeadset.connect(mDevice);
    217                 } else {
    218                     mHfpResult = RESULT_CONNECTED;
    219                 }
    220                 if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
    221                     mA2dpResult = RESULT_PENDING;
    222                     mA2dp.connect(mDevice);
    223                 } else {
    224                     mA2dpResult = RESULT_CONNECTED;
    225                 }
    226                 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    227                     toast(mContext.getString(R.string.connecting_headset) + " " + mName + "...");
    228                     break;
    229                 }
    230                 // fall-through
    231             case STATE_CONNECTING:
    232                 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    233                     // another connection type still pending
    234                     break;
    235                 }
    236                 if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) {
    237                     // we'll take either as success
    238                     toast(mContext.getString(R.string.connected_headset) + " " + mName);
    239                     if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
    240                     complete(true);
    241                 } else {
    242                     toast (mContext.getString(R.string.connect_headset_failed) + " " + mName);
    243                     complete(false);
    244                 }
    245                 break;
    246         }
    247     }
    248 
    249     void startBonding() {
    250         mState = STATE_BONDING;
    251         toast(mContext.getString(R.string.pairing_headset) + " " + mName + "...");
    252         if (!mDevice.createBond()) {
    253             toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName);
    254             complete(false);
    255         }
    256     }
    257 
    258     void handleIntent(Intent intent) {
    259         String action = intent.getAction();
    260         if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action) && mState == STATE_TURNING_ON) {
    261             int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
    262             if (state == BluetoothAdapter.STATE_ON) {
    263                 nextStepConnect();
    264             } else if (state == BluetoothAdapter.STATE_OFF) {
    265                 toast(mContext.getString(R.string.failed_to_enable_bt));
    266                 complete(false);
    267             }
    268             return;
    269         }
    270 
    271         // Everything else requires the device to match...
    272         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    273         if (!mDevice.equals(device)) return;
    274 
    275         if (ACTION_ALLOW_CONNECT.equals(action)) {
    276             nextStepConnect();
    277         } else if (ACTION_DENY_CONNECT.equals(action)) {
    278             complete(false);
    279         } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action) && mState == STATE_BONDING) {
    280             int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
    281                     BluetoothAdapter.ERROR);
    282             if (bond == BluetoothDevice.BOND_BONDED) {
    283                 nextStepConnect();
    284             } else if (bond == BluetoothDevice.BOND_NONE) {
    285                 toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName);
    286                 complete(false);
    287             }
    288         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
    289                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
    290             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
    291             if (state == BluetoothProfile.STATE_CONNECTED) {
    292                 mHfpResult = RESULT_CONNECTED;
    293                 nextStep();
    294             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
    295                 mHfpResult = RESULT_DISCONNECTED;
    296                 nextStep();
    297             }
    298         } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
    299                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
    300             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
    301             if (state == BluetoothProfile.STATE_CONNECTED) {
    302                 mA2dpResult = RESULT_CONNECTED;
    303                 nextStep();
    304             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
    305                 mA2dpResult = RESULT_DISCONNECTED;
    306                 nextStep();
    307             }
    308         }
    309     }
    310 
    311     void complete(boolean connected) {
    312         if (DBG) Log.d(TAG, "complete()");
    313         mState = STATE_COMPLETE;
    314         mContext.unregisterReceiver(mReceiver);
    315         mHandler.removeMessages(MSG_TIMEOUT);
    316         mCallback.onBluetoothHeadsetHandoverComplete(connected);
    317     }
    318 
    319     void toast(CharSequence text) {
    320         Toast.makeText(mContext,  text, Toast.LENGTH_SHORT).show();
    321     }
    322 
    323     void startTheMusic() {
    324         Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    325         intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
    326                 KeyEvent.KEYCODE_MEDIA_PLAY));
    327         mContext.sendOrderedBroadcast(intent, null);
    328         intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP,
    329                 KeyEvent.KEYCODE_MEDIA_PLAY));
    330         mContext.sendOrderedBroadcast(intent, null);
    331     }
    332 
    333     void requestPairConfirmation() {
    334         Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
    335         dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    336 
    337         dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
    338 
    339         mContext.startActivity(dialogIntent);
    340     }
    341 
    342     final Handler mHandler = new Handler() {
    343         @Override
    344         public void handleMessage(Message msg) {
    345             switch (msg.what) {
    346                 case MSG_TIMEOUT:
    347                     if (mState == STATE_COMPLETE) return;
    348                     Log.i(TAG, "Timeout completing BT handover");
    349                     complete(false);
    350                     break;
    351             }
    352         }
    353     };
    354 
    355     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    356         @Override
    357         public void onReceive(Context context, Intent intent) {
    358             handleIntent(intent);
    359         }
    360     };
    361 
    362     static void checkMainThread() {
    363         if (Looper.myLooper() != Looper.getMainLooper()) {
    364             throw new IllegalThreadStateException("must be called on main thread");
    365         }
    366     }
    367 }
    368