diff options
author | Nick Pelly <npelly@google.com> | 2012-03-02 11:49:01 -0800 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2012-03-02 11:49:01 -0800 |
commit | 32c52031a4c594035a64d8770019ac4af60ab546 (patch) | |
tree | e506ed80819ba3e3867ace81e45ddfbbf0bbead7 /src/com/android/nfc | |
parent | 0c39284106d29e8852197d163dcc95c01da29f0d (diff) | |
parent | a2908a164eec02c34efc39db2e3ee0e38ebbfdb1 (diff) | |
download | packages_apps_nfc-32c52031a4c594035a64d8770019ac4af60ab546.zip packages_apps_nfc-32c52031a4c594035a64d8770019ac4af60ab546.tar.gz packages_apps_nfc-32c52031a4c594035a64d8770019ac4af60ab546.tar.bz2 |
Merge "Implement NFC-BT handover for Bluetooth headsets, such as Nokia BH-505."
Diffstat (limited to 'src/com/android/nfc')
-rw-r--r-- | src/com/android/nfc/NfcDispatcher.java | 8 | ||||
-rw-r--r-- | src/com/android/nfc/handover/BluetoothHeadsetHandover.java | 340 | ||||
-rw-r--r-- | src/com/android/nfc/handover/HandoverManager.java | 228 |
3 files changed, 576 insertions, 0 deletions
diff --git a/src/com/android/nfc/NfcDispatcher.java b/src/com/android/nfc/NfcDispatcher.java index 2f442e6..1c7f912 100644 --- a/src/com/android/nfc/NfcDispatcher.java +++ b/src/com/android/nfc/NfcDispatcher.java @@ -17,6 +17,7 @@ package com.android.nfc; import com.android.nfc.RegisteredComponentCache.ComponentInfo; +import com.android.nfc.handover.HandoverManager; import android.app.Activity; import android.app.ActivityManagerNative; @@ -59,6 +60,7 @@ public class NfcDispatcher { final RegisteredComponentCache mTechListFilters; final PackageManager mPackageManager; final ContentResolver mContentResolver; + final HandoverManager mHandoverManager; // Locked on this PendingIntent mOverrideIntent; @@ -72,6 +74,7 @@ public class NfcDispatcher { NfcAdapter.ACTION_TECH_DISCOVERED, NfcAdapter.ACTION_TECH_DISCOVERED); mPackageManager = context.getPackageManager(); mContentResolver = context.getContentResolver(); + mHandoverManager = new HandoverManager(context); } public synchronized void setForegroundDispatch(PendingIntent intent, @@ -199,6 +202,11 @@ public class NfcDispatcher { return true; } + if (mHandoverManager.tryHandover(message)) { + if (DBG) Log.i(TAG, "matched BT HANDOVER"); + return true; + } + if (tryNdef(dispatch, message)) { return true; } diff --git a/src/com/android/nfc/handover/BluetoothHeadsetHandover.java b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java new file mode 100644 index 0000000..9cca2d9 --- /dev/null +++ b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java @@ -0,0 +1,340 @@ +/* + * 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; + +/** + * 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. + * + * TODO: enable Bluetooth without causing auto-connection to *other* devices + * TOOD: disable Bluetooth when disconnecting if it was enabled for this device + * TODO: il8n / UI review + */ +public class BluetoothHeadsetHandover { + static final String TAG = HandoverManager.TAG; + static final boolean DBG = HandoverManager.DBG; + + static final int TIMEOUT_MS = 20000; + + static final int STATE_INIT = 0; + static final int STATE_TURNING_ON = 1; + static final int STATE_BONDING = 2; + static final int STATE_CONNECTING = 3; + static final int STATE_DISCONNECTING = 4; + static final int STATE_COMPLETE = 5; + + static final int RESULT_PENDING = 0; + static final int RESULT_CONNECTED = 1; + static final int RESULT_DISCONNECTED = 2; + + static final int ACTION_DISCONNECT = 1; + static final int ACTION_CONNECT = 2; + + static final int MSG_TOAST = 1; + static final int MSG_TIMEOUT = 2; + + final Context mContext; + final BluetoothDevice mDevice; + final String mName; + final BluetoothAdapter mAdapter; + final BluetoothA2dp mA2dp; + final BluetoothHeadset mHeadset; + final Callback mCallback; + + // synchronized on BluetoothHeadsetHandover.this + int mAction; + int mState; + int mHfpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING + int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING + + public interface Callback { + public void onBluetoothHeadsetHandoverComplete(); + } + + public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name, + BluetoothAdapter adapter, BluetoothA2dp a2dp, BluetoothHeadset headset, + Callback callback) { + mContext = context; + mDevice = device; + mName = name; + mAdapter = adapter; + mA2dp = a2dp; + mHeadset = headset; + mCallback = callback; + mState = STATE_INIT; + } + + /** + * Main entry point. This method is usually called after construction, + * to begin the BT sequence. + */ + public synchronized void start() { + 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); + mContext.registerReceiver(mReceiver, filter); + + 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; + } + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS); + nextStep(); + } + + /** + * Called to execute next step in state machine + */ + synchronized void nextStep() { + if (mAction == ACTION_CONNECT) { + nextStepConnect(); + } else { + nextStepDisconnect(); + } + } + + synchronized void nextStepDisconnect() { + switch (mState) { + case STATE_INIT: + mState = STATE_DISCONNECTING; + 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("Disconnecting " + mName + "..."); + break; + } + // fall-through + case STATE_DISCONNECTING: + if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { + // still disconnecting + break; + } + if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) { + toast("Disconnected " + mName); + } + complete(); + break; + } + } + + synchronized void nextStepConnect() { + switch (mState) { + case STATE_INIT: + if (!mAdapter.isEnabled()) { + startEnabling(); + break; + } + // fall-through + case STATE_TURNING_ON: + 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; + 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("Connecting " + mName + "..."); + break; + } + // 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 + toast("Connected " + mName); + if (mA2dpResult == RESULT_CONNECTED) startTheMusic(); + } else { + toast ("Failed to connect " + mName); + } + complete(); + break; + } + } + + synchronized void startEnabling() { + mState = STATE_TURNING_ON; + toast("Enabling Bluetooth..."); + if (!mAdapter.enable()) { + toast("Failed to enable Bluetooth"); + complete(); + } + } + + synchronized void startBonding() { + mState = STATE_BONDING; + toast("Pairing " + mName + "..."); + if (!mDevice.createBond()) { + toast("Failed to pair " + mName); + complete(); + } + } + + final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleIntent(intent); + } + }; + + synchronized void handleIntent(Intent intent) { + String action = intent.getAction(); + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action) && mState == STATE_TURNING_ON) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + if (state == BluetoothAdapter.STATE_ON) { + nextStepConnect(); + } else if (state == BluetoothAdapter.STATE_OFF) { + toast("Failed to enable Bluetooth"); + complete(); + } + return; + } + + // Everything else requires the device to match... + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (!mDevice.equals(device)) return; + + if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action) && mState == STATE_BONDING) { + int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothAdapter.ERROR); + if (bond == BluetoothDevice.BOND_BONDED) { + nextStepConnect(); + } else if (bond == BluetoothDevice.BOND_NONE) { + toast("Failed to pair " + mName); + complete(); + } + } 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(); + } + } + } + + synchronized void complete() { + if (DBG) Log.d(TAG, "complete()"); + mState = STATE_COMPLETE; + mContext.unregisterReceiver(mReceiver); + mHandler.removeMessages(MSG_TIMEOUT); + mCallback.onBluetoothHeadsetHandoverComplete(); + } + + void toast(CharSequence text) { + if (Looper.myLooper() == Looper.getMainLooper()) { + Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show(); // already on main thread + } else { + mHandler.obtainMessage(MSG_TOAST, text).sendToTarget(); // move to main thread + } + } + + final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_TOAST: + Toast.makeText(mContext, (CharSequence) msg.obj, Toast.LENGTH_SHORT).show(); + break; + case MSG_TIMEOUT: + synchronized (BluetoothHeadsetHandover.this) { + if (mState == STATE_COMPLETE) return; + Log.i(TAG, "Timeout completing BT handover"); + complete(); + } + break; + } + } + }; + + 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)); + mContext.sendOrderedBroadcast(intent, null); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_MEDIA_PLAY)); + mContext.sendOrderedBroadcast(intent, null); + } +} diff --git a/src/com/android/nfc/handover/HandoverManager.java b/src/com/android/nfc/handover/HandoverManager.java new file mode 100644 index 0000000..debda04 --- /dev/null +++ b/src/com/android/nfc/handover/HandoverManager.java @@ -0,0 +1,228 @@ +/* + * 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 java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; + +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.util.Log; + +/** + * Manages handover of NFC to other technologies. + */ +public class HandoverManager implements BluetoothProfile.ServiceListener, + BluetoothHeadsetHandover.Callback { + static final String TAG = "NfcHandover"; + static final boolean DBG = true; + + static final byte[] TYPE_NOKIA = "nokia.com:bt".getBytes(Charset.forName("US_ASCII")); + static final byte[] TYPE_BT_OOB = "application/vnd.bluetooth.ep.oob". + getBytes(Charset.forName("US_ASCII")); + + final Context mContext; + final BluetoothAdapter mBluetoothAdapter; + + // synchronized on HandoverManager.this + BluetoothHeadset mBluetoothHeadset; + BluetoothA2dp mBluetoothA2dp; + BluetoothHeadsetHandover mBluetoothHeadsetHandover; + + static class BluetoothHandoverData { + public boolean valid = false; + public BluetoothDevice device; + public String name; + } + + public HandoverManager(Context context) { + mContext = context; + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET); + mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP); + } + + public boolean tryHandover(NdefMessage m) { + if (m == null) return false; + if (DBG) Log.d(TAG, "tryHandover(): " + m.toString()); + + BluetoothHandoverData handover = parse(m); + if (handover == null) return false; + if (!handover.valid) return true; + + synchronized (HandoverManager.this) { + if (mBluetoothAdapter == null || + mBluetoothA2dp == null || + mBluetoothHeadset == null) { + if (DBG) Log.d(TAG, "BT handover, but BT not available"); + return true; + } + if (mBluetoothHeadsetHandover != null) { + if (DBG) Log.d(TAG, "BT handover already in progress, ignoring"); + return true; + } + mBluetoothHeadsetHandover = new BluetoothHeadsetHandover(mContext, handover.device, + handover.name, mBluetoothAdapter, mBluetoothA2dp, mBluetoothHeadset, this); + mBluetoothHeadsetHandover.start(); + } + return true; + } + + BluetoothHandoverData parse(NdefMessage m) { + NdefRecord r = m.getRecords()[0]; + short tnf = r.getTnf(); + byte[] type = r.getType(); + + // Check for BT OOB record + if (r.getTnf() == NdefRecord.TNF_MIME_MEDIA && Arrays.equals(r.getType(), TYPE_BT_OOB)) { + return parseBtOob(ByteBuffer.wrap(r.getPayload())); + } + + // Check for Handover Select, followed by a BT OOB record + if (tnf == NdefRecord.TNF_WELL_KNOWN && + Arrays.equals(type, NdefRecord.RTD_HANDOVER_SELECT)) { + for (NdefRecord oob : m.getRecords()) { + if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA && + Arrays.equals(oob.getType(), TYPE_BT_OOB)) { + return parseBtOob(ByteBuffer.wrap(oob.getPayload())); + } + } + } + + // Check for Nokia BT record, found on some Nokia BH-505 Headsets + if (tnf == NdefRecord.TNF_EXTERNAL_TYPE && Arrays.equals(type, TYPE_NOKIA)) { + return parseNokia(ByteBuffer.wrap(r.getPayload())); + } + + return null; + } + + BluetoothHandoverData parseNokia(ByteBuffer payload) { + BluetoothHandoverData result = new BluetoothHandoverData(); + result.valid = false; + + try { + payload.position(1); + byte[] address = new byte[6]; + payload.get(address); + result.device = mBluetoothAdapter.getRemoteDevice(address); + result.valid = true; + payload.position(14); + int nameLength = payload.get(); + byte[] nameBytes = new byte[nameLength]; + payload.get(nameBytes); + result.name = new String(nameBytes, Charset.forName("UTF-8")); + } catch (IllegalArgumentException e) { + Log.i(TAG, "nokia: invalid BT address"); + } catch (BufferUnderflowException e) { + Log.i(TAG, "nokia: payload shorter than expected"); + } + if (result.valid && result.name == null) result.name = ""; + return result; + } + + BluetoothHandoverData parseBtOob(ByteBuffer payload) { + BluetoothHandoverData result = new BluetoothHandoverData(); + result.valid = false; + + try { + payload.position(2); + byte[] address = new byte[6]; + payload.get(address); + // ByteBuffer.order(LITTLE_ENDIAN) doesn't work for + // ByteBuffer.get(byte[]), so manually swap order + for (int i = 0; i < 3; i++) { + byte temp = address[i]; + address[i] = address[5 - i]; + address[5 - i] = temp; + } + result.device = mBluetoothAdapter.getRemoteDevice(address); + result.valid = true; + + while (payload.remaining() > 0) { + byte[] nameBytes; + int len = payload.get(); + int type = payload.get(); + switch (type) { + case 0x08: // short local name + nameBytes = new byte[len - 1]; + payload.get(nameBytes); + result.name = new String(nameBytes, Charset.forName("UTF-8")); + break; + case 0x09: // long local name + if (result.name != null) break; // prefer short name + nameBytes = new byte[len - 1]; + payload.get(nameBytes); + result.name = new String(nameBytes, Charset.forName("UTF-8")); + break; + default: + payload.position(payload.position() + len - 1); + break; + } + } + } catch (IllegalArgumentException e) { + Log.i(TAG, "BT OOB: invalid BT address"); + } catch (BufferUnderflowException e) { + Log.i(TAG, "BT OOB: payload shorter than expected"); + } + if (result.valid && result.name == null) result.name = ""; + return result; + } + + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + synchronized (HandoverManager.this) { + switch (profile) { + case BluetoothProfile.HEADSET: + mBluetoothHeadset = (BluetoothHeadset) proxy; + break; + case BluetoothProfile.A2DP: + mBluetoothA2dp = (BluetoothA2dp) proxy; + break; + } + } + } + + @Override + public void onServiceDisconnected(int profile) { + synchronized (HandoverManager.this) { + switch (profile) { + case BluetoothProfile.HEADSET: + mBluetoothHeadset = null; + break; + case BluetoothProfile.A2DP: + mBluetoothA2dp = null; + break; + } + } + } + + @Override + public void onBluetoothHeadsetHandoverComplete() { + synchronized (HandoverManager.this) { + mBluetoothHeadsetHandover = null; + } + } +} |