diff options
-rwxr-xr-x | res/values/strings.xml | 14 | ||||
-rw-r--r-- | src/com/android/nfc/P2pEventManager.java | 6 | ||||
-rwxr-xr-x | src/com/android/nfc/P2pLinkManager.java | 30 | ||||
-rw-r--r-- | src/com/android/nfc/handover/BluetoothHeadsetHandover.java | 26 | ||||
-rw-r--r-- | src/com/android/nfc/handover/BluetoothOppHandover.java | 55 | ||||
-rw-r--r-- | src/com/android/nfc/handover/HandoverManager.java | 289 |
6 files changed, 307 insertions, 113 deletions
diff --git a/res/values/strings.xml b/res/values/strings.xml index 903103a..21cfab8 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -18,4 +18,18 @@ <!-- Content description of the NFC enabled notification icon for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="accessibility_nfc_enabled">NFC enabled.</string> <string name="touch">Touch to beam</string> + + <string name="beam_progress">Beam in progress</string> + <string name="beam_complete">Beam complete</string> + <string name="beam_failed">Beam failed</string> + <string name="beam_touch_to_view">Touch to view</string> + + <string name="connecting_headset">Connecting</string> + <string name="connected_headset">Connected</string> + <string name="connect_headset_failed">Failed to connect</string> + <string name="disconnecting_headset">Disconnecting</string> + <string name="disconnected_headset">Disconnected</string> + <string name="pairing_headset">Pairing</string> + <string name="pairing_headset_failed">Failed to pair</string> + <string name="failed_to_enable_bt">Failed to enable Bluetooth</string> </resources> diff --git a/src/com/android/nfc/P2pEventManager.java b/src/com/android/nfc/P2pEventManager.java index 4a57deb..9050288 100644 --- a/src/com/android/nfc/P2pEventManager.java +++ b/src/com/android/nfc/P2pEventManager.java @@ -87,16 +87,16 @@ public class P2pEventManager implements P2pEventListener, SendUi.Callback { } @Override - public void onP2pReceiveComplete() { + public void onP2pReceiveComplete(boolean playSound) { mVibrator.vibrate(VIBRATION_PATTERN, -1); - mNfcService.playSound(NfcService.SOUND_END); + if (playSound) mNfcService.playSound(NfcService.SOUND_END); // TODO we still don't have a nice receive solution // The sanest solution right now is just to scale back up what we had // and start the new activity. It is not perfect, but at least it is // consistent behavior. All other variants involve making the old // activity screenshot disappear, and then removing the animation // window hoping the new activity has started by then. This just goes - // wrong too often and can looks weird. + // wrong too often and can look weird. mSendUi.finish(SendUi.FINISH_SCALE_UP); mNdefReceived = true; } diff --git a/src/com/android/nfc/P2pLinkManager.java b/src/com/android/nfc/P2pLinkManager.java index 89c3dc8..ecded75 100755 --- a/src/com/android/nfc/P2pLinkManager.java +++ b/src/com/android/nfc/P2pLinkManager.java @@ -79,7 +79,7 @@ interface P2pEventListener { /** * Called to indicate a receive was successful. */ - public void onP2pReceiveComplete(); + public void onP2pReceiveComplete(boolean playSound); /** * Indicates the P2P device went out of range. @@ -123,9 +123,10 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba static final int MSG_DEBOUNCE_TIMEOUT = 1; static final int MSG_RECEIVE_COMPLETE = 2; - static final int MSG_SEND_COMPLETE = 3; - static final int MSG_START_ECHOSERVER = 4; - static final int MSG_STOP_ECHOSERVER = 5; + static final int MSG_RECEIVE_HANDOVER = 3; + static final int MSG_SEND_COMPLETE = 4; + static final int MSG_START_ECHOSERVER = 5; + static final int MSG_STOP_ECHOSERVER = 6; // values for mLinkState static final int LINK_STATE_DOWN = 1; @@ -467,6 +468,7 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba NdefMessage response = mHandoverManager.tryHandoverRequest(msg); if (response != null) { + onReceiveHandover(); return SnepMessage.getSuccessResponse(response); } else { return SnepMessage.getMessage(SnepMessage.RESPONSE_NOT_FOUND); @@ -474,6 +476,10 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba } }; + void onReceiveHandover() { + mHandler.obtainMessage(MSG_RECEIVE_HANDOVER).sendToTarget(); + } + void onReceiveComplete(NdefMessage msg) { EventLogTags.writeNfcNdefReceived(getMessageSize(msg), getMessageTnf(msg), getMessageType(msg), getMessageAarPresent(msg)); @@ -513,6 +519,20 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba mEventListener.onP2pOutOfRange(); } break; + case MSG_RECEIVE_HANDOVER: + // We're going to do a handover request + synchronized (this) { + if (mLinkState == LINK_STATE_DOWN) { + break; + } + if (mSendState == SEND_STATE_SENDING) { + cancelSendNdefMessage(); + } + mSendState = SEND_STATE_NOTHING_TO_SEND; + if (DBG) Log.d(TAG, "onP2pReceiveComplete()"); + mEventListener.onP2pReceiveComplete(false); + } + break; case MSG_RECEIVE_COMPLETE: NdefMessage m = (NdefMessage) msg.obj; synchronized (this) { @@ -524,7 +544,7 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba } mSendState = SEND_STATE_NOTHING_TO_SEND; if (DBG) Log.d(TAG, "onP2pReceiveComplete()"); - mEventListener.onP2pReceiveComplete(); + mEventListener.onP2pReceiveComplete(true); NfcService.getInstance().sendMockNdefTag(m); } break; diff --git a/src/com/android/nfc/handover/BluetoothHeadsetHandover.java b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java index 0cf195d..7cb0424 100644 --- a/src/com/android/nfc/handover/BluetoothHeadsetHandover.java +++ b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java @@ -33,6 +33,7 @@ import android.view.KeyEvent; import android.widget.Toast; import com.android.nfc.handover.HandoverManager.HandoverPowerManager; +import com.android.nfc.R; /** * Connects / Disconnects from a Bluetooth headset (or any device that @@ -43,9 +44,7 @@ import com.android.nfc.handover.HandoverManager.HandoverPowerManager; * designed to be re-used after the sequence has completed or timed out. * Subsequent NFC interactions should use new objects. * - * TODO: prevent auto-connecting to other devices and other incoming a2dp/hsp - * connects. - * TODO: il8n / UI review + * TODO: UI review */ public class BluetoothHeadsetHandover { static final String TAG = HandoverManager.TAG; @@ -156,7 +155,8 @@ public class BluetoothHeadsetHandover { mA2dpResult = RESULT_DISCONNECTED; } if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { - toast("Disconnecting " + mName + "..."); + toast(mContext.getString(R.string.disconnecting_headset ) + " " + + mName + "..."); break; } // fall-through @@ -166,7 +166,7 @@ public class BluetoothHeadsetHandover { break; } if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) { - toast("Disconnected " + mName); + toast(mContext.getString(R.string.disconnected_headset) + " " + mName); } complete(false); break; @@ -181,7 +181,7 @@ public class BluetoothHeadsetHandover { // Bluetooth is being enabled mState = STATE_TURNING_ON; } else { - toast("Failed to enable Bluetooth"); + toast(mContext.getString(R.string.failed_to_enable_bt)); complete(false); } break; @@ -210,7 +210,7 @@ public class BluetoothHeadsetHandover { mA2dpResult = RESULT_CONNECTED; } if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { - toast("Connecting " + mName + "..."); + toast(mContext.getString(R.string.connecting_headset) + " " + mName + "..."); break; } // fall-through @@ -221,11 +221,11 @@ public class BluetoothHeadsetHandover { } if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) { // we'll take either as success - toast("Connected " + mName); + toast(mContext.getString(R.string.connected_headset) + " " + mName); if (mA2dpResult == RESULT_CONNECTED) startTheMusic(); complete(true); } else { - toast ("Failed to connect " + mName); + toast (mContext.getString(R.string.connect_headset_failed) + " " + mName); complete(false); } break; @@ -234,9 +234,9 @@ public class BluetoothHeadsetHandover { void startBonding() { mState = STATE_BONDING; - toast("Pairing " + mName + "..."); + toast(mContext.getString(R.string.pairing_headset) + " " + mName + "..."); if (!mDevice.createBond()) { - toast("Failed to pair " + mName); + toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName); complete(false); } } @@ -248,7 +248,7 @@ public class BluetoothHeadsetHandover { if (state == BluetoothAdapter.STATE_ON) { nextStepConnect(); } else if (state == BluetoothAdapter.STATE_OFF) { - toast("Failed to enable Bluetooth"); + toast(mContext.getString(R.string.failed_to_enable_bt)); complete(false); } return; @@ -264,7 +264,7 @@ public class BluetoothHeadsetHandover { if (bond == BluetoothDevice.BOND_BONDED) { nextStepConnect(); } else if (bond == BluetoothDevice.BOND_NONE) { - toast("Failed to pair " + mName); + toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName); complete(false); } } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) && diff --git a/src/com/android/nfc/handover/BluetoothOppHandover.java b/src/com/android/nfc/handover/BluetoothOppHandover.java index 3f3718f..325691d 100644 --- a/src/com/android/nfc/handover/BluetoothOppHandover.java +++ b/src/com/android/nfc/handover/BluetoothOppHandover.java @@ -8,40 +8,55 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; -import android.nfc.NfcAdapter; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; import android.util.Log; import android.webkit.MimeTypeMap; +import android.widget.Toast; import com.android.nfc.handover.HandoverManager.HandoverPowerManager; +import com.android.nfc.R; import java.util.ArrayList; import java.util.Arrays; -public class BluetoothOppHandover { +public class BluetoothOppHandover implements Handler.Callback { static final String TAG = "BluetoothOppHandover"; static final boolean D = true; static final int STATE_INIT = 0; static final int STATE_TURNING_ON = 1; - static final int STATE_COMPLETE = 2; + static final int STATE_WAITING = 2; // Need to wait for remote side turning on BT + static final int STATE_COMPLETE = 3; + + static final int MSG_START_SEND = 0; + + static final int REMOTE_BT_ENABLE_DELAY_MS = 3000; public static final String EXTRA_CONNECTION_HANDOVER = "com.android.intent.extra.CONNECTION_HANDOVER"; final Context mContext; final BluetoothDevice mDevice; + final Uri[] mUris; final HandoverPowerManager mHandoverPowerManager; + final boolean mRemoteActivating; + final Handler mHandler; int mState; + Long mStartTime; public BluetoothOppHandover(Context context, BluetoothDevice device, Uri[] uris, - HandoverPowerManager powerManager) { + HandoverPowerManager powerManager, boolean remoteActivating) { mContext = context; mDevice = device; mUris = uris; mHandoverPowerManager = powerManager; + mRemoteActivating = remoteActivating; + mHandler = new Handler(context.getMainLooper(),this); mState = STATE_INIT; } @@ -67,6 +82,8 @@ public class BluetoothOppHandover { * to begin the BT sequence. Must be called on Main thread. */ public void start() { + mStartTime = SystemClock.elapsedRealtime(); + IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); mContext.registerReceiver(mReceiver, filter); @@ -74,11 +91,18 @@ public class BluetoothOppHandover { if (mHandoverPowerManager.enableBluetooth()) { mState = STATE_TURNING_ON; } else { - // TODO deal with this: toast or tie in to Beam failure? + Toast.makeText(mContext, mContext.getString(R.string.beam_failed), + Toast.LENGTH_SHORT).show(); + complete(); } } else { // BT already enabled - sendIntent(); + if (mRemoteActivating) { + mHandler.sendEmptyMessageDelayed(MSG_START_SEND, REMOTE_BT_ENABLE_DELAY_MS); + } else { + // Remote BT enabled too, start send immediately + sendIntent(); + } } } @@ -101,7 +125,7 @@ public class BluetoothOppHandover { intent.setAction(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_STREAM, mUris[0]); } else { - ArrayList<Uri> uris = (ArrayList<Uri>)Arrays.asList(mUris); + ArrayList<Uri> uris = new ArrayList<Uri>(Arrays.asList(mUris)); intent.setAction(Intent.ACTION_SEND_MULTIPLE); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); } @@ -117,7 +141,14 @@ public class BluetoothOppHandover { 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) { - sendIntent(); + // Add additional delay if needed + Long timeElapsed = SystemClock.elapsedRealtime() - mStartTime; + if (mRemoteActivating && timeElapsed < REMOTE_BT_ENABLE_DELAY_MS) { + mHandler.sendEmptyMessageDelayed(MSG_START_SEND, + REMOTE_BT_ENABLE_DELAY_MS - timeElapsed); + } else { + sendIntent(); + } } else if (state == BluetoothAdapter.STATE_OFF) { complete(); } @@ -132,4 +163,12 @@ public class BluetoothOppHandover { } }; + @Override + public boolean handleMessage(Message msg) { + if (msg.what == MSG_START_SEND) { + sendIntent(); + return true; + } + return false; + } } diff --git a/src/com/android/nfc/handover/HandoverManager.java b/src/com/android/nfc/handover/HandoverManager.java index 997afa6..6ebfc2d 100644 --- a/src/com/android/nfc/handover/HandoverManager.java +++ b/src/com/android/nfc/handover/HandoverManager.java @@ -22,6 +22,8 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; import java.util.Random; import android.app.Notification; @@ -38,6 +40,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; +import android.nfc.FormatException; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.os.Handler; @@ -45,6 +48,11 @@ import android.os.Message; import android.os.SystemClock; import android.util.Log; import android.util.Pair; +import android.widget.Toast; + +import com.android.nfc.NfcService; +import com.android.nfc.R; + /** * Manages handover of NFC to other technologies. @@ -58,37 +66,37 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, static final byte[] TYPE_BT_OOB = "application/vnd.bluetooth.ep.oob". getBytes(Charset.forName("US_ASCII")); - public static final String ACTION_BT_OPP_TRANSFER_PROGRESS = + static final String ACTION_BT_OPP_TRANSFER_PROGRESS = "android.btopp.intent.action.BT_OPP_TRANSFER_PROGRESS"; - public static final String ACTION_BT_OPP_TRANSFER_DONE = + static final String ACTION_BT_OPP_TRANSFER_DONE = "android.btopp.intent.action.BT_OPP_TRANSFER_DONE"; - public static final String EXTRA_BT_OPP_TRANSFER_STATUS = + static final String EXTRA_BT_OPP_TRANSFER_STATUS = "android.btopp.intent.extra.BT_OPP_TRANSFER_STATUS"; - public static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0; + static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0; - public static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1; + static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1; - public static final String EXTRA_BT_OPP_TRANSFER_DIRECTION = + static final String EXTRA_BT_OPP_TRANSFER_DIRECTION = "android.btopp.intent.extra.BT_OPP_TRANSFER_DIRECTION"; - public static final int DIRECTION_BLUETOOTH_INCOMING = 0; + static final int DIRECTION_BLUETOOTH_INCOMING = 0; - public static final int DIRECTION_BLUETOOTH_OUTGOING = 1; + static final int DIRECTION_BLUETOOTH_OUTGOING = 1; - public static final String EXTRA_BT_OPP_TRANSFER_ID = + static final String EXTRA_BT_OPP_TRANSFER_ID = "android.btopp.intent.extra.BT_OPP_TRANSFER_ID"; - public static final String EXTRA_BT_OPP_TRANSFER_PROGRESS = + static final String EXTRA_BT_OPP_TRANSFER_PROGRESS = "android.btopp.intent.extra.BT_OPP_TRANSFER_PROGRESS"; - public static final String EXTRA_BT_OPP_TRANSFER_URI = + static final String EXTRA_BT_OPP_TRANSFER_URI = "android.btopp.intent.extra.BT_OPP_TRANSFER_URI"; // permission needed to be able to receive handover status requests - public static final String HANDOVER_STATUS_PERMISSION = + static final String HANDOVER_STATUS_PERMISSION = "com.android.permission.HANDOVER_STATUS"; static final int MSG_HANDOVER_POWER_CHECK = 0; @@ -96,13 +104,17 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, // We poll whether we can safely disable BT every POWER_CHECK_MS static final int POWER_CHECK_MS = 20000; - public static final String ACTION_WHITELIST_DEVICE = + static final String ACTION_WHITELIST_DEVICE = "android.btopp.intent.action.WHITELIST_DEVICE"; - public static final int SOURCE_BLUETOOTH_INCOMING = 0; + static final int SOURCE_BLUETOOTH_INCOMING = 0; - public static final int SOURCE_BLUETOOTH_OUTGOING = 1; + static final int SOURCE_BLUETOOTH_OUTGOING = 1; + static final int CARRIER_POWER_STATE_INACTIVE = 0; + static final int CARRIER_POWER_STATE_ACTIVE = 1; + static final int CARRIER_POWER_STATE_ACTIVATING = 2; + static final int CARRIER_POWER_STATE_UNKNOWN = 3; final Context mContext; final BluetoothAdapter mBluetoothAdapter; @@ -115,18 +127,18 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, BluetoothHeadsetHandover mBluetoothHeadsetHandover; boolean mBluetoothHeadsetConnected; + String mLocalBluetoothAddress; int mNotificationId; - // TODO regular cleanup of finished transfers. HashMap<Pair<Integer, Integer>, HandoverTransfer> mTransfers; static class BluetoothHandoverData { public boolean valid = false; public BluetoothDevice device; public String name; + public boolean carrierActivating = false; } class HandoverPowerManager implements Handler.Callback { - // TODO stop monitoring if BT is turned off by the user final Handler handler; final Context context; @@ -156,6 +168,10 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, return mBluetoothAdapter.isEnabled(); } + void stopMonitoring() { + handler.removeMessages(MSG_HANDOVER_POWER_CHECK); + } + @Override public boolean handleMessage(Message msg) { switch (msg.what) { @@ -252,15 +268,15 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, int progressInt = (int) (progress * 100); notBuilder.setAutoCancel(false); notBuilder.setSmallIcon(android.R.drawable.stat_sys_download); - notBuilder.setTicker("Beam incoming..."); - notBuilder.setContentTitle("Beam incoming..."); + notBuilder.setTicker(mContext.getString(R.string.beam_progress)); + notBuilder.setContentTitle(mContext.getString(R.string.beam_progress)); notBuilder.setProgress(100, progressInt, progress == -1); } else if (state == STATE_SUCCESS) { notBuilder.setAutoCancel(true); notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); - notBuilder.setTicker("Beam complete."); - notBuilder.setContentTitle("Beam complete"); - notBuilder.setContentText("Touch to view"); + notBuilder.setTicker(mContext.getString(R.string.beam_complete)); + notBuilder.setContentTitle(mContext.getString(R.string.beam_complete)); + notBuilder.setContentText(mContext.getString(R.string.beam_touch_to_view)); Intent notificationIntent = new Intent(Intent.ACTION_VIEW); String mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri); @@ -268,17 +284,19 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, notificationIntent, 0); notBuilder.setContentIntent(contentIntent); + + // Play Beam success sound + NfcService.getInstance().playSound(NfcService.SOUND_END); } else if (state == STATE_FAILED) { notBuilder.setAutoCancel(true); notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); - notBuilder.setTicker("Beam failed."); - notBuilder.setContentTitle("Beam failed"); - // TODO content text + notBuilder.setTicker(mContext.getString(R.string.beam_failed)); + notBuilder.setContentTitle(mContext.getString(R.string.beam_failed)); } else { return; } - mNotificationManager.notify(mNotificationId, notBuilder.getNotification()); + mNotificationManager.notify(mNotificationId, notBuilder.build()); } } @@ -311,24 +329,35 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, IntentFilter filter = new IntentFilter(ACTION_BT_OPP_TRANSFER_DONE); filter.addAction(ACTION_BT_OPP_TRANSFER_PROGRESS); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); mContext.registerReceiver(mReceiver, filter, HANDOVER_STATUS_PERMISSION, null); } + synchronized void cleanupTransfers() { + Iterator<Map.Entry<Pair<Integer, Integer>, HandoverTransfer>> it = mTransfers.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry<Pair<Integer, Integer>, HandoverTransfer> pair = it.next(); + HandoverTransfer transfer = pair.getValue(); + if (!transfer.isRunning()) { + it.remove(); + } + } + } + static NdefRecord createCollisionRecord() { byte[] random = new byte[2]; new Random().nextBytes(random); - return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, new byte[] {0x48, 0x72}, null, random); + return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null, random); } - NdefRecord createBluetoothAlternateCarrierRecord() { + NdefRecord createBluetoothAlternateCarrierRecord(boolean activating) { byte[] payload = new byte[4]; - //TODO: Encode 'activating' if BT is not on yet - payload[0] = 0x01; // Carrier Power State: Active + payload[0] = (byte) (activating ? CARRIER_POWER_STATE_ACTIVATING : + CARRIER_POWER_STATE_ACTIVE); // Carrier Power State: Activating or active payload[1] = 1; // length of carrier data reference payload[2] = 'b'; // carrier data reference: ID for Bluetooth OOB data record payload[3] = 0; // Auxiliary data reference count - // 0x61, 0x63 is "ac" - return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, new byte[] {0x61, 0x63}, null, payload); + return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_ALTERNATIVE_CARRIER, null, payload); } NdefRecord createBluetoothOobDataRecord() { @@ -336,10 +365,15 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, payload[0] = 0; payload[1] = (byte)payload.length; - //TODO: check getAddress() works with BT off - String address = mBluetoothAdapter.getAddress(); - byte[] addressBytes = addressToReverseBytes(address); - System.arraycopy(addressBytes, 0, payload, 2, 6); + synchronized (HandoverManager.this) { + if (mLocalBluetoothAddress == null) { + mLocalBluetoothAddress = mBluetoothAdapter.getAddress(); + } + + byte[] addressBytes = addressToReverseBytes(mLocalBluetoothAddress); + System.arraycopy(addressBytes, 0, payload, 2, 6); + } + return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, new byte[]{'b'}, payload); } @@ -347,23 +381,40 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, return new NdefMessage(createHandoverRequestRecord(), createBluetoothOobDataRecord()); } - NdefRecord createHandoverRequestRecord() { - ByteBuffer payload = ByteBuffer.allocate(100); //TODO figure out size + NdefMessage createHandoverSelectMessage(boolean activating) { + return new NdefMessage(createHandoverSelectRecord(activating), createBluetoothOobDataRecord()); + } + + NdefRecord createHandoverSelectRecord(boolean activating) { + NdefMessage nestedMessage = new NdefMessage(createBluetoothAlternateCarrierRecord(activating)); + byte[] nestedPayload = nestedMessage.toByteArray(); + + ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1); payload.put((byte)0x12); // connection handover v1.2 + payload.put(nestedPayload); + + byte[] payloadBytes = new byte[payload.position()]; + payload.position(0); + payload.get(payloadBytes); + return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null, + payloadBytes); + + } - //TODO: spec is not clear if we encode each nested NdefRecord as a - // a stand-alone message (with MB and ME flags set), or as a combined - // message (MB only set on first, ME only set on last). Current - // implementation assumes the later. + NdefRecord createHandoverRequestRecord() { NdefMessage nestedMessage = new NdefMessage(createCollisionRecord(), - createBluetoothAlternateCarrierRecord()); + createBluetoothAlternateCarrierRecord(false)); + byte[] nestedPayload = nestedMessage.toByteArray(); + + ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1); + payload.put((byte)0x12); // connection handover v1.2 payload.put(nestedMessage.toByteArray()); byte[] payloadBytes = new byte[payload.position()]; payload.position(0); payload.get(payloadBytes); - // 0x48, 0x72 is "Hr" - return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, new byte[]{0x48, 0x72}, null, payloadBytes); + return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null, + payloadBytes); } /** @@ -376,9 +427,9 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, NdefRecord r = m.getRecords()[0]; if (r.getTnf() != NdefRecord.TNF_WELL_KNOWN) return null; - if (!Arrays.equals(r.getType(), new byte[]{0x48, 0x72})) return null; + if (!Arrays.equals(r.getType(), NdefRecord.RTD_HANDOVER_REQUEST)) return null; - // we have a handover select, look for BT OOB record + // we have a handover request, look for BT OOB record BluetoothHandoverData bluetoothData = null; for (NdefRecord oob : m.getRecords()) { if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA && @@ -389,17 +440,21 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, } if (bluetoothData == null) return null; + boolean bluetoothActivating = false; + if (!mHandoverPowerManager.isBluetoothEnabled()) { - mHandoverPowerManager.enableBluetooth(); - // TODO determine how to deal with failure (toast?) + if (!mHandoverPowerManager.enableBluetooth()) { + toast(mContext.getString(R.string.beam_failed)); + return null; + } + bluetoothActivating = true; } // BT OOB found, whitelist it for incoming OPP data whitelistOppDevice(bluetoothData.device); // return BT OOB record so they can perform handover - //TODO: Should use full Carrier Select form with power state to handle BT enabling... - return new NdefMessage(createBluetoothOobDataRecord()); + return (createHandoverSelectMessage(bluetoothActivating)); } void whitelistOppDevice(BluetoothDevice device) { @@ -437,11 +492,63 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, // This starts sending an Uri over BT public void doHandoverUri(Uri[] uris, NdefMessage m) { - BluetoothHandoverData data = parse(m); - BluetoothOppHandover handover = new BluetoothOppHandover(mContext, data.device, - uris, mHandoverPowerManager); - handover.start(); + if (data != null && data.valid) { + BluetoothOppHandover handover = new BluetoothOppHandover(mContext, data.device, + uris, mHandoverPowerManager, data.carrierActivating); + handover.start(); + } + } + + boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) { + byte[] payload = handoverRec.getPayload(); + if (payload == null || payload.length <= 1) return false; + // Skip version + byte[] payloadNdef = new byte[payload.length - 1]; + System.arraycopy(payload, 1, payloadNdef, 0, payload.length - 1); + NdefMessage msg; + try { + msg = new NdefMessage(payloadNdef); + } catch (FormatException e) { + return false; + } + + for (NdefRecord alt : msg.getRecords()) { + byte[] acPayload = alt.getPayload(); + if (acPayload != null) { + ByteBuffer buf = ByteBuffer.wrap(acPayload); + int cps = buf.get() & 0x03; // Carrier Power State is in lower 2 bits + int carrierRefLength = buf.get() & 0xFF; + if (carrierRefLength != carrierId.length) return false; + + byte[] carrierRefId = new byte[carrierRefLength]; + buf.get(carrierRefId); + if (Arrays.equals(carrierRefId, carrierId)) { + // Found match, returning whether power state is activating + return (cps == CARRIER_POWER_STATE_ACTIVATING); + } + } + } + + return true; + } + + BluetoothHandoverData parseHandoverSelect(NdefMessage m) { + // TODO we could parse this a lot more strictly; right now + // we just search for a BT OOB record, and try to cross-reference + // the carrier state inside the 'hs' payload. + for (NdefRecord oob : m.getRecords()) { + if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA && + Arrays.equals(oob.getType(), TYPE_BT_OOB)) { + BluetoothHandoverData data = parseBtOob(ByteBuffer.wrap(oob.getPayload())); + if (data != null && isCarrierActivating(m.getRecords()[0], oob.getId())) { + data.carrierActivating = true; + } + return data; + } + } + + return null; } BluetoothHandoverData parse(NdefMessage m) { @@ -457,13 +564,9 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, // 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())); - } - } + return parseHandoverSelect(m); } + // 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())); @@ -592,35 +695,53 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, } } + void toast(CharSequence text) { + Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show(); + } + final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - int direction = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_DIRECTION, -1); - int id = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_ID, -1); - if (direction == -1 || id == -1) return; - int source = (direction == DIRECTION_BLUETOOTH_INCOMING) ? - SOURCE_BLUETOOTH_INCOMING : SOURCE_BLUETOOTH_OUTGOING; - HandoverTransfer transfer = getHandoverTransfer(source, id); - if (transfer == null) return; - - if (action.equals(ACTION_BT_OPP_TRANSFER_DONE)) { - int handoverStatus = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_STATUS, - HANDOVER_TRANSFER_STATUS_FAILURE); - - if (handoverStatus == HANDOVER_TRANSFER_STATUS_SUCCESS) { - String uriString = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_URI); - Uri uri = Uri.parse(uriString); - if (uri.getScheme() == null) { - uri = Uri.fromFile(new File(uri.getPath())); + + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + if (state == BluetoothAdapter.STATE_OFF) { + mHandoverPowerManager.stopMonitoring(); + } + + return; + } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS) || + action.equals(ACTION_BT_OPP_TRANSFER_DONE)) { + // Clean up old transfers in progress + cleanupTransfers(); + + int direction = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_DIRECTION, -1); + int id = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_ID, -1); + if (direction == -1 || id == -1) return; + int source = (direction == DIRECTION_BLUETOOTH_INCOMING) ? + SOURCE_BLUETOOTH_INCOMING : SOURCE_BLUETOOTH_OUTGOING; + HandoverTransfer transfer = getHandoverTransfer(source, id); + if (transfer == null) return; + + if (action.equals(ACTION_BT_OPP_TRANSFER_DONE)) { + int handoverStatus = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_STATUS, + HANDOVER_TRANSFER_STATUS_FAILURE); + + if (handoverStatus == HANDOVER_TRANSFER_STATUS_SUCCESS) { + String uriString = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_URI); + Uri uri = Uri.parse(uriString); + if (uri.getScheme() == null) { + uri = Uri.fromFile(new File(uri.getPath())); + } + transfer.finishTransfer(true, uri); + } else { + transfer.finishTransfer(false, null); } - transfer.finishTransfer(true, uri); - } else { - transfer.finishTransfer(false, null); + } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS)) { + float progress = intent.getFloatExtra(EXTRA_BT_OPP_TRANSFER_PROGRESS, 0.0f); + transfer.updateTransferProgress(progress); } - } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS)) { - float progress = intent.getFloatExtra(EXTRA_BT_OPP_TRANSFER_PROGRESS, 0.0f); - transfer.updateTransferProgress(progress); } } |