BluetoothHeadsetHandover.java 16 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.nfc.handover;

import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.Toast;

35
import com.android.nfc.R;
36

37 38 39 40 41 42 43 44 45 46
/**
 * Connects / Disconnects from a Bluetooth headset (or any device that
 * might implement BT HSP, HFP or A2DP sink) when touched with NFC.
 *
 * This object is created on an NFC interaction, and determines what
 * sequence of Bluetooth actions to take, and executes them. It is not
 * designed to be re-used after the sequence has completed or timed out.
 * Subsequent NFC interactions should use new objects.
 *
 */
47
public class BluetoothHeadsetHandover implements BluetoothProfile.ServiceListener {
48 49 50
    static final String TAG = HandoverManager.TAG;
    static final boolean DBG = HandoverManager.DBG;

51 52 53
    static final String ACTION_ALLOW_CONNECT = "com.android.nfc.handover.action.ALLOW_CONNECT";
    static final String ACTION_DENY_CONNECT = "com.android.nfc.handover.action.DENY_CONNECT";

54 55 56
    static final int TIMEOUT_MS = 20000;

    static final int STATE_INIT = 0;
57 58 59 60 61 62 63
    static final int STATE_WAITING_FOR_PROXIES = 1;
    static final int STATE_INIT_COMPLETE = 2;
    static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 3;
    static final int STATE_BONDING = 4;
    static final int STATE_CONNECTING = 5;
    static final int STATE_DISCONNECTING = 6;
    static final int STATE_COMPLETE = 7;
64 65 66 67 68

    static final int RESULT_PENDING = 0;
    static final int RESULT_CONNECTED = 1;
    static final int RESULT_DISCONNECTED = 2;

69
    static final int ACTION_INIT = 0;
70 71 72
    static final int ACTION_DISCONNECT = 1;
    static final int ACTION_CONNECT = 2;

73
    static final int MSG_TIMEOUT = 1;
74
    static final int MSG_NEXT_STEP = 2;
75 76 77 78 79

    final Context mContext;
    final BluetoothDevice mDevice;
    final String mName;
    final Callback mCallback;
80 81 82
    final BluetoothAdapter mBluetoothAdapter;

    final Object mLock = new Object();
83

84
    // only used on main thread
85 86 87 88 89
    int mAction;
    int mState;
    int mHfpResult;  // used only in STATE_CONNECTING and STATE_DISCONNETING
    int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING

90 91 92 93
    // protected by mLock
    BluetoothA2dp mA2dp;
    BluetoothHeadset mHeadset;

94
    public interface Callback {
95
        public void onBluetoothHeadsetHandoverComplete(boolean connected);
96 97 98
    }

    public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name,
99
            Callback callback) {
100
        checkMainThread();  // mHandler must get get constructed on Main Thread for toasts to work
101 102 103 104
        mContext = context;
        mDevice = device;
        mName = name;
        mCallback = callback;
105 106
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

107 108 109
        mState = STATE_INIT;
    }

110 111 112 113
    public boolean hasStarted() {
        return mState != STATE_INIT;
    }

114 115
    /**
     * Main entry point. This method is usually called after construction,
116
     * to begin the BT sequence. Must be called on Main thread.
117
     */
118 119 120
    public void start() {
        checkMainThread();
        if (mState != STATE_INIT) return;
121
        if (mBluetoothAdapter == null) return;
122

123 124 125 126 127
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
        filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
128 129 130
        filter.addAction(ACTION_ALLOW_CONNECT);
        filter.addAction(ACTION_DENY_CONNECT);

131 132 133
        mContext.registerReceiver(mReceiver, filter);

        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
134
        mAction = ACTION_INIT;
135 136 137 138 139 140
        nextStep();
    }

    /**
     * Called to execute next step in state machine
     */
141
    void nextStep() {
142 143 144
        if (mAction == ACTION_INIT) {
            nextStepInit();
        } else if (mAction == ACTION_CONNECT) {
145 146 147 148 149 150
            nextStepConnect();
        } else {
            nextStepDisconnect();
        }
    }

151 152 153 154
    /*
     * Enables bluetooth and gets the profile proxies
     */
    void nextStepInit() {
155 156
        switch (mState) {
            case STATE_INIT:
157 158 159 160 161
                if (mA2dp == null || mHeadset == null) {
                    mState = STATE_WAITING_FOR_PROXIES;
                    if (!getProfileProxys()) {
                        complete(false);
                    }
162 163 164
                    break;
                }
                // fall-through
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
            case STATE_WAITING_FOR_PROXIES:
                mState = STATE_INIT_COMPLETE;
                // Check connected devices and see if we need to disconnect
                synchronized(mLock) {
                    if (mA2dp.getConnectedDevices().contains(mDevice) ||
                            mHeadset.getConnectedDevices().contains(mDevice)) {
                        Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName);
                        mAction = ACTION_DISCONNECT;
                    } else {
                        Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName);
                        mAction = ACTION_CONNECT;
                    }
                }
                nextStep();
        }

    }

    void nextStepDisconnect() {
        switch (mState) {
            case STATE_INIT_COMPLETE:
                mState = STATE_DISCONNECTING;
                synchronized (mLock) {
                    if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) {
                        mHfpResult = RESULT_PENDING;
                        mHeadset.disconnect(mDevice);
                    } else {
                        mHfpResult = RESULT_DISCONNECTED;
                    }
                    if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) {
                        mA2dpResult = RESULT_PENDING;
                        mA2dp.disconnect(mDevice);
                    } else {
                        mA2dpResult = RESULT_DISCONNECTED;
                    }
                    if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
                        toast(mContext.getString(R.string.disconnecting_headset ) + " " +
                                mName + "...");
                        break;
                    }
                }
                // fall-through
207 208 209 210 211 212
            case STATE_DISCONNECTING:
                if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
                    // still disconnecting
                    break;
                }
                if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) {
213
                    toast(mContext.getString(R.string.disconnected_headset) + " " + mName);
214
                }
215
                complete(false);
216 217
                break;
        }
218 219 220 221 222 223 224 225 226 227 228

    }

    boolean getProfileProxys() {
        if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET))
            return false;

        if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP))
            return false;

        return true;
229 230
    }

231
    void nextStepConnect() {
232
        switch (mState) {
233
            case STATE_INIT_COMPLETE:
234 235 236
                if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
                    requestPairConfirmation();
                    mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
237

238 239 240 241
                    break;
                }
                // fall-through
            case STATE_WAITING_FOR_BOND_CONFIRMATION:
242 243 244 245 246 247 248 249 250
                if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
                    startBonding();
                    break;
                }
                // fall-through
            case STATE_BONDING:
                // Bluetooth Profile service will correctly serialize
                // HFP then A2DP connect
                mState = STATE_CONNECTING;
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
                synchronized (mLock) {
                    if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
                        mHfpResult = RESULT_PENDING;
                        mHeadset.connect(mDevice);
                    } else {
                        mHfpResult = RESULT_CONNECTED;
                    }
                    if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
                        mA2dpResult = RESULT_PENDING;
                        mA2dp.connect(mDevice);
                    } else {
                        mA2dpResult = RESULT_CONNECTED;
                    }
                    if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
                        toast(mContext.getString(R.string.connecting_headset) + " " + mName + "...");
                        break;
                    }
268 269 270 271 272 273 274 275 276
                }
                // fall-through
            case STATE_CONNECTING:
                if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
                    // another connection type still pending
                    break;
                }
                if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) {
                    // we'll take either as success
277
                    toast(mContext.getString(R.string.connected_headset) + " " + mName);
278
                    if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
279
                    complete(true);
280
                } else {
281
                    toast (mContext.getString(R.string.connect_headset_failed) + " " + mName);
282
                    complete(false);
283 284 285 286 287
                }
                break;
        }
    }

288
    void startBonding() {
289
        mState = STATE_BONDING;
290
        toast(mContext.getString(R.string.pairing_headset) + " " + mName + "...");
291
        if (!mDevice.createBond()) {
292
            toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName);
293
            complete(false);
294 295 296
        }
    }

297
    void handleIntent(Intent intent) {
298
        String action = intent.getAction();
299
        // Everything requires the device to match...
300 301 302
        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        if (!mDevice.equals(device)) return;

303 304 305 306 307
        if (ACTION_ALLOW_CONNECT.equals(action)) {
            nextStepConnect();
        } else if (ACTION_DENY_CONNECT.equals(action)) {
            complete(false);
        } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action) && mState == STATE_BONDING) {
308 309 310 311 312
            int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
                    BluetoothAdapter.ERROR);
            if (bond == BluetoothDevice.BOND_BONDED) {
                nextStepConnect();
            } else if (bond == BluetoothDevice.BOND_NONE) {
313
                toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName);
314
                complete(false);
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
            }
        } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
                (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
            if (state == BluetoothProfile.STATE_CONNECTED) {
                mHfpResult = RESULT_CONNECTED;
                nextStep();
            } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
                mHfpResult = RESULT_DISCONNECTED;
                nextStep();
            }
        } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
                (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
            if (state == BluetoothProfile.STATE_CONNECTED) {
                mA2dpResult = RESULT_CONNECTED;
                nextStep();
            } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
                mA2dpResult = RESULT_DISCONNECTED;
                nextStep();
            }
        }
    }

339
    void complete(boolean connected) {
340 341 342 343
        if (DBG) Log.d(TAG, "complete()");
        mState = STATE_COMPLETE;
        mContext.unregisterReceiver(mReceiver);
        mHandler.removeMessages(MSG_TIMEOUT);
344 345 346 347 348 349 350 351 352 353
        synchronized (mLock) {
            if (mA2dp != null) {
                mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp);
            }
            if (mHeadset != null) {
                mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset);
            }
            mA2dp = null;
            mHeadset = null;
        }
354
        mCallback.onBluetoothHeadsetHandoverComplete(connected);
355 356 357
    }

    void toast(CharSequence text) {
358 359 360 361 362 363 364
        Toast.makeText(mContext,  text, Toast.LENGTH_SHORT).show();
    }

    void startTheMusic() {
        Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_MEDIA_PLAY));
365
        mContext.sendOrderedBroadcast(intent, null);
366 367
        intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_MEDIA_PLAY));
368
        mContext.sendOrderedBroadcast(intent, null);
369 370
    }

371 372 373 374 375
    void requestPairConfirmation() {
        Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
        dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);

376
        mContext.startActivity(dialogIntent);
377 378
    }

379 380 381 382 383
    final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_TIMEOUT:
384 385
                    if (mState == STATE_COMPLETE) return;
                    Log.i(TAG, "Timeout completing BT handover");
386
                    complete(false);
387
                    break;
388 389 390
                case MSG_NEXT_STEP:
                    nextStep();
                    break;
391 392 393 394
            }
        }
    };

395 396 397 398 399 400 401 402 403 404 405
    final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            handleIntent(intent);
        }
    };

    static void checkMainThread() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw new IllegalThreadStateException("must be called on main thread");
        }
406
    }
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431

    @Override
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
        synchronized (mLock) {
            switch (profile) {
                case BluetoothProfile.HEADSET:
                    mHeadset = (BluetoothHeadset) proxy;
                    if (mA2dp != null) {
                        mHandler.sendEmptyMessage(MSG_NEXT_STEP);
                    }
                    break;
                case BluetoothProfile.A2DP:
                    mA2dp = (BluetoothA2dp) proxy;
                    if (mHeadset != null) {
                        mHandler.sendEmptyMessage(MSG_NEXT_STEP);
                    }
                    break;
            }
        }
    }

    @Override
    public void onServiceDisconnected(int profile) {
        // We can ignore these
    }
432
}