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