diff options
Diffstat (limited to 'src/com')
3 files changed, 388 insertions, 39 deletions
diff --git a/src/com/android/nfc/handover/BluetoothHeadsetHandover.java b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java index 135baf8..0cf195d 100644 --- a/src/com/android/nfc/handover/BluetoothHeadsetHandover.java +++ b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java @@ -32,6 +32,8 @@ import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; +import com.android.nfc.handover.HandoverManager.HandoverPowerManager; + /** * Connects / Disconnects from a Bluetooth headset (or any device that * might implement BT HSP, HFP or A2DP sink) when touched with NFC. @@ -41,8 +43,8 @@ import android.widget.Toast; * 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: prevent auto-connecting to other devices and other incoming a2dp/hsp + * connects. * TODO: il8n / UI review */ public class BluetoothHeadsetHandover { @@ -70,7 +72,7 @@ public class BluetoothHeadsetHandover { final Context mContext; final BluetoothDevice mDevice; final String mName; - final BluetoothAdapter mAdapter; + final HandoverPowerManager mHandoverPowerManager; final BluetoothA2dp mA2dp; final BluetoothHeadset mHeadset; final Callback mCallback; @@ -82,17 +84,17 @@ public class BluetoothHeadsetHandover { int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING public interface Callback { - public void onBluetoothHeadsetHandoverComplete(); + public void onBluetoothHeadsetHandoverComplete(boolean connected); } public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name, - BluetoothAdapter adapter, BluetoothA2dp a2dp, BluetoothHeadset headset, + HandoverPowerManager powerManager, BluetoothA2dp a2dp, BluetoothHeadset headset, Callback callback) { checkMainThread(); // mHandler must get get constructed on Main Thread for toasts to work mContext = context; mDevice = device; mName = name; - mAdapter = adapter; + mHandoverPowerManager = powerManager; mA2dp = a2dp; mHeadset = headset; mCallback = callback; @@ -166,7 +168,7 @@ public class BluetoothHeadsetHandover { if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) { toast("Disconnected " + mName); } - complete(); + complete(false); break; } } @@ -174,8 +176,14 @@ public class BluetoothHeadsetHandover { void nextStepConnect() { switch (mState) { case STATE_INIT: - if (!mAdapter.isEnabled()) { - startEnabling(); + if (!mHandoverPowerManager.isBluetoothEnabled()) { + if (mHandoverPowerManager.enableBluetooth()) { + // Bluetooth is being enabled + mState = STATE_TURNING_ON; + } else { + toast("Failed to enable Bluetooth"); + complete(false); + } break; } // fall-through @@ -215,29 +223,21 @@ public class BluetoothHeadsetHandover { // we'll take either as success toast("Connected " + mName); if (mA2dpResult == RESULT_CONNECTED) startTheMusic(); + complete(true); } else { toast ("Failed to connect " + mName); + complete(false); } - complete(); break; } } - void startEnabling() { - mState = STATE_TURNING_ON; - toast("Enabling Bluetooth..."); - if (!mAdapter.enable()) { - toast("Failed to enable Bluetooth"); - complete(); - } - } - void startBonding() { mState = STATE_BONDING; toast("Pairing " + mName + "..."); if (!mDevice.createBond()) { toast("Failed to pair " + mName); - complete(); + complete(false); } } @@ -249,7 +249,7 @@ public class BluetoothHeadsetHandover { nextStepConnect(); } else if (state == BluetoothAdapter.STATE_OFF) { toast("Failed to enable Bluetooth"); - complete(); + complete(false); } return; } @@ -265,7 +265,7 @@ public class BluetoothHeadsetHandover { nextStepConnect(); } else if (bond == BluetoothDevice.BOND_NONE) { toast("Failed to pair " + mName); - complete(); + complete(false); } } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) && (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) { @@ -290,12 +290,12 @@ public class BluetoothHeadsetHandover { } } - void complete() { + void complete(boolean connected) { if (DBG) Log.d(TAG, "complete()"); mState = STATE_COMPLETE; mContext.unregisterReceiver(mReceiver); mHandler.removeMessages(MSG_TIMEOUT); - mCallback.onBluetoothHeadsetHandoverComplete(); + mCallback.onBluetoothHeadsetHandoverComplete(connected); } void toast(CharSequence text) { @@ -319,7 +319,7 @@ public class BluetoothHeadsetHandover { case MSG_TIMEOUT: if (mState == STATE_COMPLETE) return; Log.i(TAG, "Timeout completing BT handover"); - complete(); + complete(false); break; } } diff --git a/src/com/android/nfc/handover/BluetoothOppHandover.java b/src/com/android/nfc/handover/BluetoothOppHandover.java index 9c1a60d..3f3718f 100644 --- a/src/com/android/nfc/handover/BluetoothOppHandover.java +++ b/src/com/android/nfc/handover/BluetoothOppHandover.java @@ -1,13 +1,19 @@ package com.android.nfc.handover; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; +import android.nfc.NfcAdapter; import android.util.Log; import android.webkit.MimeTypeMap; +import com.android.nfc.handover.HandoverManager.HandoverPowerManager; + import java.util.ArrayList; import java.util.Arrays; @@ -15,27 +21,36 @@ public class BluetoothOppHandover { 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; + + 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; - public interface Callback { - public void onBluetoothOppHandoverComplete(); - } + int mState; - public BluetoothOppHandover(Context context, BluetoothDevice device, - Uri[] uris) { + public BluetoothOppHandover(Context context, BluetoothDevice device, Uri[] uris, + HandoverPowerManager powerManager) { mContext = context; mDevice = device; mUris = uris; + mHandoverPowerManager = powerManager; + + mState = STATE_INIT; } - public String getMimeTypeForUri(Uri uri) { + public static String getMimeTypeForUri(Context context, Uri uri) { if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - ContentResolver cr = mContext.getContentResolver(); + ContentResolver cr = context.getContentResolver(); return cr.getType(uri); } else if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { - String extension = MimeTypeMap.getFileExtensionFromUrl(uri.getPath()); + String extension = MimeTypeMap.getFileExtensionFromUrl(uri.getPath()).toLowerCase(); if (extension != null) { return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); } else { @@ -52,12 +67,33 @@ public class BluetoothOppHandover { * to begin the BT sequence. Must be called on Main thread. */ public void start() { + IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); + mContext.registerReceiver(mReceiver, filter); + + if (!mHandoverPowerManager.isBluetoothEnabled()) { + if (mHandoverPowerManager.enableBluetooth()) { + mState = STATE_TURNING_ON; + } else { + // TODO deal with this: toast or tie in to Beam failure? + } + } else { + // BT already enabled + sendIntent(); + } + } + + void complete() { + mState = STATE_COMPLETE; + mContext.unregisterReceiver(mReceiver); + } + + void sendIntent() { //TODO: either open up BluetoothOppLauncherActivity to all MIME types // or gracefully handle mime types that can't be sent Log.d(TAG, "Sending handover intent for " + mDevice.getAddress()); Intent intent = new Intent(); intent.setPackage("com.android.bluetooth"); - String mimeType = getMimeTypeForUri(mUris[0]); + String mimeType = getMimeTypeForUri(mContext, mUris[0]); Log.d(TAG, "Determined mime type as " + mimeType); intent.setType(mimeType); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); @@ -69,7 +105,31 @@ public class BluetoothOppHandover { intent.setAction(Intent.ACTION_SEND_MULTIPLE); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); } + intent.putExtra(EXTRA_CONNECTION_HANDOVER, true); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); + + complete(); + } + + 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) { + sendIntent(); + } else if (state == BluetoothAdapter.STATE_OFF) { + complete(); + } + return; + } } + + final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleIntent(intent); + } + }; + } diff --git a/src/com/android/nfc/handover/HandoverManager.java b/src/com/android/nfc/handover/HandoverManager.java index bbb42cc..7d406ec 100644 --- a/src/com/android/nfc/handover/HandoverManager.java +++ b/src/com/android/nfc/handover/HandoverManager.java @@ -16,23 +16,35 @@ package com.android.nfc.handover; +import java.io.File; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Arrays; +import java.util.HashMap; import java.util.Random; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Notification.Builder; 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.net.Uri; import android.nfc.NdefMessage; import android.nfc.NdefRecord; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; import android.util.Log; +import android.util.Pair; /** * Manages handover of NFC to other technologies. @@ -46,13 +58,66 @@ 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 = + "android.btopp.intent.action.BT_OPP_TRANSFER_PROGRESS"; + + public 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 = + "android.btopp.intent.extra.BT_OPP_TRANSFER_STATUS"; + + public static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0; + + public static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1; + + public static final String EXTRA_BT_OPP_TRANSFER_DIRECTION = + "android.btopp.intent.extra.BT_OPP_TRANSFER_DIRECTION"; + + public static final int DIRECTION_BLUETOOTH_INCOMING = 0; + + public static final int DIRECTION_BLUETOOTH_OUTGOING = 1; + + public 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 = + "android.btopp.intent.extra.BT_OPP_TRANSFER_PROGRESS"; + + public 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 = + "com.android.permission.HANDOVER_STATUS"; + + static final int MSG_HANDOVER_POWER_CHECK = 0; + + // 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 = + "android.btopp.intent.action.WHITELIST_DEVICE"; + + public static final int SOURCE_BLUETOOTH_INCOMING = 0; + + public static final int SOURCE_BLUETOOTH_OUTGOING = 1; + + final Context mContext; final BluetoothAdapter mBluetoothAdapter; + final NotificationManager mNotificationManager; + final HandoverPowerManager mHandoverPowerManager; // synchronized on HandoverManager.this BluetoothHeadset mBluetoothHeadset; BluetoothA2dp mBluetoothA2dp; BluetoothHeadsetHandover mBluetoothHeadsetHandover; + boolean mBluetoothHeadsetConnected; + + int mNotificationId; + // TODO regular cleanup of finished transfers. + HashMap<Pair<Integer, Integer>, HandoverTransfer> mTransfers; static class BluetoothHandoverData { public boolean valid = false; @@ -60,11 +125,193 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, public String name; } + class HandoverPowerManager implements Handler.Callback { + // TODO stop monitoring if BT is turned off by the user + final Handler handler; + final Context context; + + public HandoverPowerManager(Context context) { + this.handler = new Handler(this); + this.context = context; + } + + /** + * Enables Bluetooth and will automatically disable it + * when there is no Bluetooth activity intitiated by NFC + * anymore. + */ + boolean enableBluetooth() { + // Enable BT + boolean result = mBluetoothAdapter.enable(); + + if (result) { + // Start polling for BT activity to make sure we eventually disable + // it again. + handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS); + } + return result; + } + + boolean isBluetoothEnabled() { + return mBluetoothAdapter.isEnabled(); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_HANDOVER_POWER_CHECK: + // Check for any alive transfers + boolean transferAlive = false; + synchronized (HandoverManager.this) { + for (HandoverTransfer transfer : mTransfers.values()) { + if (transfer.isRunning()) { + transferAlive = true; + } + } + + if (!transferAlive && !mBluetoothHeadsetConnected) { + mBluetoothAdapter.disable(); + handler.removeMessages(MSG_HANDOVER_POWER_CHECK); + } else { + handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS); + } + } + return true; + } + return false; + } + } + + class HandoverTransfer { + static final int STATE_NEW = 0; + static final int STATE_IN_PROGRESS = 1; + static final int STATE_FAILED = 2; + static final int STATE_SUCCESS = 3; + + // We need to receive an update within this time period + // to still consider this transfer to be "alive" (ie + // a reason to keep the handover transport enabled). + static final int ALIVE_CHECK_MS = 20000; + + int notificationId; // Unique ID of this transfer used for notifications + Long lastUpdate; // Last time an event occurred for this transfer + float progress; // Progress in range [0..1] + int state; + Uri uri; + boolean incoming; // whether this is an incoming transfer + + public HandoverTransfer(boolean incoming) { + synchronized (HandoverManager.this) { + this.notificationId = mNotificationId++; + } + this.lastUpdate = SystemClock.elapsedRealtime(); + this.progress = 0.0f; + this.state = STATE_NEW; + this.uri = null; + this.incoming = incoming; + } + + synchronized void updateTransferProgress(float progress) { + this.state = STATE_IN_PROGRESS; + this.progress = progress; + this.lastUpdate = SystemClock.elapsedRealtime(); + + updateNotification(); + } + + synchronized void finishTransfer(boolean success, Uri uri) { + if (success && uri != null) { + this.state = STATE_SUCCESS; + this.uri = uri; + } else { + this.state = STATE_FAILED; + } + this.lastUpdate = SystemClock.elapsedRealtime(); + + updateNotification(); + } + + synchronized boolean isRunning() { + if (state != STATE_IN_PROGRESS) return false; + + // Check that we've made progress + Long currentTime = SystemClock.elapsedRealtime(); + if (currentTime - lastUpdate > ALIVE_CHECK_MS) { + return false; + } else { + return true; + } + } + + synchronized void updateNotification() { + if (!incoming) return; // No notifications for outgoing transfers + + Builder notBuilder = new Notification.Builder(mContext); + + if (state == STATE_IN_PROGRESS) { + 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.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"); + + Intent notificationIntent = new Intent(Intent.ACTION_VIEW); + String mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri); + notificationIntent.setDataAndType(uri, mimeType); + PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, notificationIntent, 0); + + notBuilder.setContentIntent(contentIntent); + } 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 + } else { + return; + } + + mNotificationManager.notify(mNotificationId, notBuilder.getNotification()); + } + } + + synchronized HandoverTransfer getHandoverTransfer(int source, int id) { + Pair<Integer, Integer> key = new Pair<Integer, Integer>(source,id); + if (!mTransfers.containsKey(key)) { + boolean incoming = false; + if (source == SOURCE_BLUETOOTH_INCOMING) { + incoming = true; + } + HandoverTransfer transfer = new HandoverTransfer(incoming); + mTransfers.put(key, transfer); + return transfer; + } else { + return mTransfers.get(key); + } + } + public HandoverManager(Context context) { mContext = context; mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET); mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP); + + mNotificationManager = (NotificationManager) mContext.getSystemService( + Context.NOTIFICATION_SERVICE); + + mTransfers = new HashMap<Pair<Integer, Integer>, HandoverTransfer>(); + mHandoverPowerManager = new HandoverPowerManager(context); + + IntentFilter filter = new IntentFilter(ACTION_BT_OPP_TRANSFER_DONE); + filter.addAction(ACTION_BT_OPP_TRANSFER_PROGRESS); + mContext.registerReceiver(mReceiver, filter, HANDOVER_STATUS_PERMISSION, null); } static NdefRecord createCollisionRecord() { @@ -142,6 +389,11 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, } if (bluetoothData == null) return null; + if (!mHandoverPowerManager.isBluetoothEnabled()) { + mHandoverPowerManager.enableBluetooth(); + // TODO determine how to deal with failure (toast?) + } + // BT OOB found, whitelist it for incoming OPP data whitelistOppDevice(bluetoothData.device); @@ -152,7 +404,7 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, void whitelistOppDevice(BluetoothDevice device) { if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP"); - Intent intent = new Intent("todo-whitelist"); + Intent intent = new Intent(ACTION_WHITELIST_DEVICE); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); mContext.sendBroadcast(intent); } @@ -177,7 +429,7 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, return true; } mBluetoothHeadsetHandover = new BluetoothHeadsetHandover(mContext, handover.device, - handover.name, mBluetoothAdapter, mBluetoothA2dp, mBluetoothHeadset, this); + handover.name, mHandoverPowerManager, mBluetoothA2dp, mBluetoothHeadset, this); mBluetoothHeadsetHandover.start(); } return true; @@ -185,9 +437,10 @@ 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); + uris, mHandoverPowerManager); handover.start(); } @@ -211,7 +464,6 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, } } } - // 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())); @@ -333,9 +585,46 @@ public class HandoverManager implements BluetoothProfile.ServiceListener, } @Override - public void onBluetoothHeadsetHandoverComplete() { + public void onBluetoothHeadsetHandoverComplete(boolean connected) { synchronized (HandoverManager.this) { mBluetoothHeadsetHandover = null; + mBluetoothHeadsetConnected = connected; } } + + 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())); + } + 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); + } + } + + }; + + } |