Snapshot b80adb2c263702442cf2f2d771168400e6ceb9f8

Change-Id: I391d8e1be1a61e68b01f0db371dbb4ed3e5b5933
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 710e2ee..7b41af2 100755
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -52,5 +52,8 @@
             android:noHistory="true"
 
         />
+        <service android:name=".handover.HandoverService"
+            android:process=":handover"
+        />
     </application>
 </manifest>
diff --git a/nci/jni/NativeNfcManager.cpp b/nci/jni/NativeNfcManager.cpp
index f15c92f..61fc052 100755
--- a/nci/jni/NativeNfcManager.cpp
+++ b/nci/jni/NativeNfcManager.cpp
@@ -1276,6 +1276,7 @@
 {
     ALOGD ("%s: enter", __FUNCTION__);
     bool stat = false;
+    bool bRestartDiscovery = false;
 
     if (! sIsSecElemSelected)
     {
@@ -1290,6 +1291,12 @@
         goto TheEnd;
     }
 
+    if (sRfEnabled) {
+        // Stop RF Discovery if we were polling
+        startRfDiscovery (false);
+        bRestartDiscovery = true;
+    }
+
     stat = SecureElement::getInstance().routeToDefault ();
     sIsSecElemSelected = false;
 
@@ -1299,6 +1306,9 @@
         SecureElement::getInstance().deactivate (0xABCDEF);
 
 TheEnd:
+    if (bRestartDiscovery)
+        startRfDiscovery (true);
+
     //if nothing is active after this, then tell the controller to power down
     if (! PowerSwitch::getInstance ().setModeOff (PowerSwitch::SE_ROUTING))
         PowerSwitch::getInstance ().setLevel (PowerSwitch::LOW_POWER);
diff --git a/nci/jni/PeerToPeer.cpp b/nci/jni/PeerToPeer.cpp
index 7994e2a..b40e366 100644
--- a/nci/jni/PeerToPeer.cpp
+++ b/nci/jni/PeerToPeer.cpp
@@ -1431,9 +1431,9 @@
         if ((pConn = sP2p.findConnection(eventData->disc.handle)) == NULL)
         {
             // If no connection, may be a client that is trying to connect
-            if ((pClient = sP2p.findClientCon ((tNFA_HANDLE)NFA_HANDLE_INVALID)) == NULL)
+            if ((pClient = sP2p.findClient (eventData->disc.handle)) == NULL)
             {
-                ALOGE ("%s: NFA_P2P_DISC_EVT: can't find conn for NFA handle: 0x%04x", fn, eventData->disc.handle);
+                ALOGE ("%s: NFA_P2P_DISC_EVT: can't find client for NFA handle: 0x%04x", fn, eventData->disc.handle);
                 return;
             }
             // Unblock createDataLinkConn()
diff --git a/nci/jni/SecureElement.cpp b/nci/jni/SecureElement.cpp
index 3c9256b..a4c4d57 100755
--- a/nci/jni/SecureElement.cpp
+++ b/nci/jni/SecureElement.cpp
@@ -156,12 +156,12 @@
 
     if (GetNumValue("NFA_HCI_DEFAULT_DEST_GATE", &num, sizeof(num)))
         mDestinationGate = num;
-    ALOGD ("%s: Default destination gate: %d", fn, mDestinationGate);
+    ALOGD ("%s: Default destination gate: 0x%X", fn, mDestinationGate);
 
     // active SE, if not set active all SEs
     if (GetNumValue("ACTIVE_SE", &num, sizeof(num)))
         mActiveSeOverride = num;
-    ALOGD ("%s: Active SE override: %d", fn, mActiveSeOverride);
+    ALOGD ("%s: Active SE override: 0x%X", fn, mActiveSeOverride);
 
     if (GetNumValue("OBERTHUR_WARM_RESET_COMMAND", &num, sizeof(num)))
     {
@@ -442,7 +442,7 @@
 ** Function:        activate
 **
 ** Description:     Turn on the secure element.
-**                  seID: ID of secure element.
+**                  seID: ID of secure element; 0xF3 or 0xF4.
 **
 ** Returns:         True if ok.
 **
@@ -474,24 +474,16 @@
         return false;
     }
 
-    mActiveEeHandle = getDefaultEeHandle();
-    ALOGD ("%s: active ee h=0x%X, override se=0x%X", fn, mActiveEeHandle, mActiveSeOverride);
-    if (mActiveEeHandle == NFA_HANDLE_INVALID)
-    {
-        ALOGE ("%s: ee not found", fn);
-        return false;
-    }
-
-    UINT16 override_se = 0;
+    UINT16 overrideEeHandle = 0;
     if (mActiveSeOverride)
-        override_se = NFA_HANDLE_GROUP_EE | mActiveSeOverride;
+        overrideEeHandle = NFA_HANDLE_GROUP_EE | mActiveSeOverride;
 
     if (mRfFieldIsOn) {
         ALOGE("%s: RF field indication still on, resetting", fn);
         mRfFieldIsOn = false;
     }
 
-    ALOGD ("%s: override seid=0x%X", fn, override_se );
+    ALOGD ("%s: override ee h=0x%X", fn, overrideEeHandle );
     //activate every discovered secure element
     for (int index=0; index < mActualNumEe; index++)
     {
@@ -499,7 +491,7 @@
 
         if ((eeItem.ee_handle == EE_HANDLE_0xF3) || (eeItem.ee_handle == EE_HANDLE_0xF4))
         {
-            if (override_se && (override_se != eeItem.ee_handle) )
+            if (overrideEeHandle && (overrideEeHandle != eeItem.ee_handle) )
                 continue;   // do not enable all SEs; only the override one
 
             if (eeItem.ee_status != NFC_NFCEE_STATUS_INACTIVE)
@@ -524,18 +516,11 @@
         }
     } //for
 
-    for (UINT8 xx = 0; xx < mActualNumEe; xx++)
-    {
-        if ((mEeInfo[xx].num_interface != 0) && (mEeInfo[xx].ee_interface[0] != NCI_NFCEE_INTERFACE_HCI_ACCESS) &&
-            (mEeInfo[xx].ee_status != NFC_NFCEE_STATUS_INACTIVE))
-        {
-            mActiveEeHandle = mEeInfo[xx].ee_handle;
-            break;
-        }
-    }
-
-    ALOGD ("%s: exit; ok=%u", fn, numActivatedEe > 0);
-    return numActivatedEe > 0;
+    mActiveEeHandle = getDefaultEeHandle();
+    if (mActiveEeHandle == NFA_HANDLE_INVALID)
+        ALOGE ("%s: ee handle not found", fn);
+    ALOGD ("%s: exit; active ee h=0x%X", fn, mActiveEeHandle);
+    return mActiveEeHandle != NFA_HANDLE_INVALID;
 }
 
 
@@ -544,7 +529,7 @@
 ** Function:        deactivate
 **
 ** Description:     Turn off the secure element.
-**                  seID: ID of secure element.
+**                  seID: ID of secure element; 0xF3 or 0xF4.
 **
 ** Returns:         True if ok.
 **
@@ -1417,7 +1402,7 @@
     {
         tNFA_HANDLE eeHandle = NFA_EE_HANDLE_DH;
         if (routeSelection == SecElemRoute)
-            eeHandle = getDefaultEeHandle ();
+            eeHandle = mActiveEeHandle;
         ALOGD ("%s: route to default EE h=0x%X", fn, eeHandle);
         SyncEventGuard guard (mRoutingEvent);
         nfaStat = NFA_EeSetDefaultProtoRouting (eeHandle, protoMask, 0, 0);
@@ -1573,7 +1558,7 @@
     {
         tNFA_HANDLE eeHandle = NFA_EE_HANDLE_DH;
         if (routeSelection == SecElemRoute)
-            eeHandle = getDefaultEeHandle ();
+            eeHandle = mActiveEeHandle;
         ALOGD ("%s: route to default EE h=0x%X", fn, eeHandle);
         SyncEventGuard guard (mRoutingEvent);
         nfaStat = NFA_EeSetDefaultTechRouting (eeHandle, techMask, 0, 0);
@@ -1975,26 +1960,31 @@
 *******************************************************************************/
 tNFA_HANDLE SecureElement::getDefaultEeHandle ()
 {
+    UINT16 overrideEeHandle = NFA_HANDLE_GROUP_EE | mActiveSeOverride;
     // Find the first EE that is not the HCI Access i/f.
     for (UINT8 xx = 0; xx < mActualNumEe; xx++)
     {
-        if ((mEeInfo[xx].num_interface != 0) && (mEeInfo[xx].ee_interface[0] != NCI_NFCEE_INTERFACE_HCI_ACCESS) )
+        if (mActiveSeOverride && (overrideEeHandle != mEeInfo[xx].ee_handle))
+            continue; //skip all the EE's that are ignored
+        if ((mEeInfo[xx].num_interface != 0) &&
+            (mEeInfo[xx].ee_interface[0] != NCI_NFCEE_INTERFACE_HCI_ACCESS) &&
+            (mEeInfo[xx].ee_status != NFC_NFCEE_STATUS_INACTIVE))
             return (mEeInfo[xx].ee_handle);
     }
     return NFA_HANDLE_INVALID;
 }
 
 
-    /*******************************************************************************
-    **
-    ** Function:        findUiccByHandle
-    **
-    ** Description:     Find information about an execution environment.
-    **                  eeHandle: Handle of the execution environment.
-    **
-    ** Returns:         Information about the execution environment.
-    **
-    *******************************************************************************/
+/*******************************************************************************
+**
+** Function:        findUiccByHandle
+**
+** Description:     Find information about an execution environment.
+**                  eeHandle: Handle of the execution environment.
+**
+** Returns:         Information about the execution environment.
+**
+*******************************************************************************/
 tNFA_EE_DISCOVER_INFO *SecureElement::findUiccByHandle (tNFA_HANDLE eeHandle)
 {
     for (UINT8 index = 0; index < mUiccInfo.num_ee; index++)
diff --git a/src/com/android/nfc/NfcApplication.java b/src/com/android/nfc/NfcApplication.java
index 867b8bb..3e7194d 100644
--- a/src/com/android/nfc/NfcApplication.java
+++ b/src/com/android/nfc/NfcApplication.java
@@ -1,12 +1,18 @@
 package com.android.nfc;
 
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
 import android.app.Application;
+import android.os.Process;
 import android.os.UserHandle;
-import android.util.Log;
+import java.util.Iterator;
+import java.util.List;
 
 public class NfcApplication extends Application {
 
-    public static final String TAG = "NfcApplication";
+    static final String TAG = "NfcApplication";
+    static final String NFC_PROCESS = "com.android.nfc";
+
     NfcService mNfcService;
 
     public NfcApplication() {
@@ -17,7 +23,23 @@
     public void onCreate() {
         super.onCreate();
 
-        if (UserHandle.myUserId() == 0) {
+        boolean isMainProcess = false;
+        // We start a service in a separate process to do
+        // handover transfer. We don't want to instantiate an NfcService
+        // object in those cases, hence check the name of the process
+        // to determine whether we're the main NFC service, or the
+        // handover process
+        ActivityManager am = (ActivityManager)this.getSystemService(ACTIVITY_SERVICE);
+        List processes = am.getRunningAppProcesses();
+        Iterator i = processes.iterator();
+        while (i.hasNext()) {
+            RunningAppProcessInfo appInfo = (RunningAppProcessInfo)(i.next());
+            if (appInfo.pid == Process.myPid()) {
+                isMainProcess =  (NFC_PROCESS.equals(appInfo.processName));
+                break;
+            }
+        }
+        if (UserHandle.myUserId() == 0 && isMainProcess) {
             mNfcService = new NfcService(this);
         }
     }
diff --git a/src/com/android/nfc/RegisteredComponentCache.java b/src/com/android/nfc/RegisteredComponentCache.java
index 5da2cd4..8d73317 100644
--- a/src/com/android/nfc/RegisteredComponentCache.java
+++ b/src/com/android/nfc/RegisteredComponentCache.java
@@ -43,6 +43,7 @@
  */
 public class RegisteredComponentCache {
     private static final String TAG = "RegisteredComponentCache";
+    private static final boolean DEBUG = false;
 
     final Context mContext;
     final String mAction;
@@ -165,7 +166,9 @@
             }
         }
 
-        dump(components);
+        if (DEBUG) {
+            dump(components);
+        }
 
         synchronized (this) {
             mComponents = components;
diff --git a/src/com/android/nfc/handover/BluetoothHeadsetHandover.java b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java
index 1377160..c845f89 100644
--- a/src/com/android/nfc/handover/BluetoothHeadsetHandover.java
+++ b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java
@@ -16,7 +16,6 @@
 
 package com.android.nfc.handover;
 
-import android.app.ActivityManager;
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -29,12 +28,10 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.os.UserHandle;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.widget.Toast;
 
-import com.android.nfc.handover.HandoverManager.HandoverPowerManager;
 import com.android.nfc.R;
 
 /**
@@ -57,14 +54,13 @@
     static final int TIMEOUT_MS = 20000;
 
     static final int STATE_INIT = 0;
-    static final int STATE_TURNING_ON = 1;
-    static final int STATE_WAITING_FOR_PROXIES = 2;
-    static final int STATE_INIT_COMPLETE = 3;
-    static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 4;
-    static final int STATE_BONDING = 5;
-    static final int STATE_CONNECTING = 6;
-    static final int STATE_DISCONNECTING = 7;
-    static final int STATE_COMPLETE = 8;
+    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;
 
     static final int RESULT_PENDING = 0;
     static final int RESULT_CONNECTED = 1;
@@ -80,7 +76,6 @@
     final Context mContext;
     final BluetoothDevice mDevice;
     final String mName;
-    final HandoverPowerManager mHandoverPowerManager;
     final Callback mCallback;
     final BluetoothAdapter mBluetoothAdapter;
 
@@ -101,18 +96,21 @@
     }
 
     public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name,
-            HandoverPowerManager powerManager, Callback callback) {
+            Callback callback) {
         checkMainThread();  // mHandler must get get constructed on Main Thread for toasts to work
         mContext = context;
         mDevice = device;
         mName = name;
-        mHandoverPowerManager = powerManager;
         mCallback = callback;
         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 
         mState = STATE_INIT;
     }
 
+    public boolean hasStarted() {
+        return mState != STATE_INIT;
+    }
+
     /**
      * Main entry point. This method is usually called after construction,
      * to begin the BT sequence. Must be called on Main thread.
@@ -156,18 +154,6 @@
     void nextStepInit() {
         switch (mState) {
             case STATE_INIT:
-                if (!mHandoverPowerManager.isBluetoothEnabled()) {
-                    if (mHandoverPowerManager.enableBluetooth()) {
-                        // Bluetooth is being enabled
-                        mState = STATE_TURNING_ON;
-                    } else {
-                        toast(mContext.getString(R.string.failed_to_enable_bt));
-                        complete(false);
-                    }
-                    break;
-                }
-                // fall-through
-            case STATE_TURNING_ON:
                 if (mA2dp == null || mHeadset == null) {
                     mState = STATE_WAITING_FOR_PROXIES;
                     if (!getProfileProxys()) {
@@ -310,18 +296,7 @@
 
     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) {
-                nextStep();
-            } else if (state == BluetoothAdapter.STATE_OFF) {
-                toast(mContext.getString(R.string.failed_to_enable_bt));
-                complete(false);
-            }
-            return;
-        }
-
-        // Everything else requires the device to match...
+        // Everything requires the device to match...
         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
         if (!mDevice.equals(device)) return;
 
@@ -387,19 +362,18 @@
         Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
         intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
                 KeyEvent.KEYCODE_MEDIA_PLAY));
-        mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null, null, null, 0, null, null);
+        mContext.sendOrderedBroadcast(intent, null);
         intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP,
                 KeyEvent.KEYCODE_MEDIA_PLAY));
-        mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null, null, null, 0, null, null);
+        mContext.sendOrderedBroadcast(intent, null);
     }
 
     void requestPairConfirmation() {
         Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
         dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
         dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
 
-        mContext.startActivityAsUser(dialogIntent, new UserHandle(UserHandle.USER_CURRENT));
+        mContext.startActivity(dialogIntent);
     }
 
     final Handler mHandler = new Handler() {
diff --git a/src/com/android/nfc/handover/BluetoothOppHandover.java b/src/com/android/nfc/handover/BluetoothOppHandover.java
index ceb3c62..fdb5eff 100644
--- a/src/com/android/nfc/handover/BluetoothOppHandover.java
+++ b/src/com/android/nfc/handover/BluetoothOppHandover.java
@@ -16,25 +16,17 @@
 
 package com.android.nfc.handover;
 
-import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
-import android.content.ActivityNotFoundException;
-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.os.Handler;
 import android.os.Message;
 import android.os.SystemClock;
+import android.os.UserHandle;
 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;
 
@@ -61,20 +53,19 @@
     final BluetoothDevice mDevice;
 
     final Uri[] mUris;
-    final HandoverPowerManager mHandoverPowerManager;
     final boolean mRemoteActivating;
     final Handler mHandler;
+    final Long mCreateTime;
 
     int mState;
-    Long mStartTime;
 
     public BluetoothOppHandover(Context context, BluetoothDevice device, Uri[] uris,
-            HandoverPowerManager powerManager, boolean remoteActivating) {
+            boolean remoteActivating) {
         mContext = context;
         mDevice = device;
         mUris = uris;
-        mHandoverPowerManager = powerManager;
         mRemoteActivating = remoteActivating;
+        mCreateTime = SystemClock.elapsedRealtime();
 
         mHandler = new Handler(context.getMainLooper(),this);
         mState = STATE_INIT;
@@ -104,33 +95,24 @@
      * 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);
-
-        if (!mHandoverPowerManager.isBluetoothEnabled()) {
-           if (mHandoverPowerManager.enableBluetooth()) {
-               mState = STATE_TURNING_ON;
-           } else {
-               Toast.makeText(mContext, mContext.getString(R.string.beam_failed),
-                       Toast.LENGTH_SHORT).show();
-               complete();
-           }
-        } else {
-            // BT already enabled
-            if (mRemoteActivating) {
-                mHandler.sendEmptyMessageDelayed(MSG_START_SEND, REMOTE_BT_ENABLE_DELAY_MS);
+        if (mRemoteActivating) {
+            Long timeElapsed = SystemClock.elapsedRealtime() - mCreateTime;
+            if (timeElapsed < REMOTE_BT_ENABLE_DELAY_MS) {
+                mHandler.sendEmptyMessageDelayed(MSG_START_SEND,
+                        REMOTE_BT_ENABLE_DELAY_MS - timeElapsed);
             } else {
-                // Remote BT enabled too, start send immediately
+                // Already waited long enough for BT to come up
+                // - start send.
                 sendIntent();
             }
+        } else {
+            // Remote BT enabled already, start send immediately
+            sendIntent();
         }
     }
 
     void complete() {
         mState = STATE_COMPLETE;
-        mContext.unregisterReceiver(mReceiver);
     }
 
     void sendIntent() {
@@ -153,32 +135,6 @@
         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) {
-                // 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();
-            }
-            return;
-        }
-    }
-
-    final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            handleIntent(intent);
-        }
-    };
 
     @Override
     public boolean handleMessage(Message msg) {
diff --git a/src/com/android/nfc/handover/HandoverManager.java b/src/com/android/nfc/handover/HandoverManager.java
index e7e807d..6d2271a 100644
--- a/src/com/android/nfc/handover/HandoverManager.java
+++ b/src/com/android/nfc/handover/HandoverManager.java
@@ -16,134 +16,71 @@
 
 package com.android.nfc.handover;
 
-import java.io.File;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
 import java.util.Random;
 
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Notification.Builder;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.media.MediaScannerConnection;
+import android.content.ServiceConnection;
 import android.net.Uri;
 import android.nfc.FormatException;
 import android.nfc.NdefMessage;
 import android.nfc.NdefRecord;
-import android.os.Environment;
+import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Message;
-import android.os.SystemClock;
+import android.os.Messenger;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.Log;
-import android.util.Pair;
-
-import com.android.nfc.NfcService;
-import com.android.nfc.R;
-
 
 /**
  * Manages handover of NFC to other technologies.
  */
-public class HandoverManager implements BluetoothHeadsetHandover.Callback {
+public class HandoverManager {
     static final String TAG = "NfcHandover";
     static final boolean DBG = true;
 
+    static final String ACTION_WHITELIST_DEVICE =
+            "android.btopp.intent.action.WHITELIST_DEVICE";
+
     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"));
 
     static final byte[] RTD_COLLISION_RESOLUTION = {0x63, 0x72}; // "cr";
 
-    static final String ACTION_BT_OPP_TRANSFER_PROGRESS =
-            "android.btopp.intent.action.BT_OPP_TRANSFER_PROGRESS";
-
-    static final String ACTION_BT_OPP_TRANSFER_DONE =
-            "android.btopp.intent.action.BT_OPP_TRANSFER_DONE";
-
-    static final String EXTRA_BT_OPP_TRANSFER_STATUS =
-            "android.btopp.intent.extra.BT_OPP_TRANSFER_STATUS";
-
-    static final String EXTRA_BT_OPP_TRANSFER_MIMETYPE =
-            "android.btopp.intent.extra.BT_OPP_TRANSFER_MIMETYPE";
-
-    static final String EXTRA_BT_OPP_ADDRESS =
-            "android.btopp.intent.extra.BT_OPP_ADDRESS";
-
-    static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0;
-
-    static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1;
-
-    static final String EXTRA_BT_OPP_TRANSFER_DIRECTION =
-            "android.btopp.intent.extra.BT_OPP_TRANSFER_DIRECTION";
-
-    static final int DIRECTION_BLUETOOTH_INCOMING = 0;
-
-    static final int DIRECTION_BLUETOOTH_OUTGOING = 1;
-
-    static final String EXTRA_BT_OPP_TRANSFER_ID =
-            "android.btopp.intent.extra.BT_OPP_TRANSFER_ID";
-
-    static final String EXTRA_BT_OPP_TRANSFER_PROGRESS =
-            "android.btopp.intent.extra.BT_OPP_TRANSFER_PROGRESS";
-
-    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
-    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;
-
-    static final String ACTION_WHITELIST_DEVICE =
-            "android.btopp.intent.action.WHITELIST_DEVICE";
-
-    static final String ACTION_CANCEL_HANDOVER_TRANSFER =
-            "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER";
-    static final String EXTRA_SOURCE_ADDRESS =
-            "com.android.nfc.handover.extra.SOURCE_ADDRESS";
-
-    static final int SOURCE_BLUETOOTH_INCOMING = 0;
-
-    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;
 
+    static final int MSG_HANDOVER_COMPLETE = 0;
+    static final int MSG_HEADSET_CONNECTED = 1;
+    static final int MSG_HEADSET_NOT_CONNECTED = 2;
+
     final Context mContext;
     final BluetoothAdapter mBluetoothAdapter;
-    final NotificationManager mNotificationManager;
-    final HandoverPowerManager mHandoverPowerManager;
+    final Messenger mMessenger = new Messenger (new MessageHandler());
 
-    // Variables below synchronized on HandoverManager.this
-    final HashMap<Pair<String, Boolean>, HandoverTransfer> mTransfers;
-
-    BluetoothHeadsetHandover mBluetoothHeadsetHandover;
+    final Object mLock = new Object();
+    // Variables below synchronized on mLock
+    HashMap<Integer, PendingHandoverTransfer> mPendingTransfers;
     boolean mBluetoothHeadsetConnected;
-
+    int mHandoverTransferId;
+    Messenger mService = null;
+    boolean mBound;
     String mLocalBluetoothAddress;
-    int mNotificationId;
 
     static class BluetoothHandoverData {
         public boolean valid = false;
@@ -152,487 +89,93 @@
         public boolean carrierActivating = false;
     }
 
-    class HandoverPowerManager implements Handler.Callback {
-        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.
-         */
-        synchronized boolean enableBluetooth() {
-            // Enable BT
-            boolean result = mBluetoothAdapter.enableNoAutoConnect();
-
-            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;
-        }
-
-        synchronized boolean isBluetoothEnabled() {
-            return mBluetoothAdapter.isEnabled();
-        }
-
-        synchronized void resetTimer() {
-            if (handler.hasMessages(MSG_HANDOVER_POWER_CHECK)) {
-                handler.removeMessages(MSG_HANDOVER_POWER_CHECK);
-                handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS);
+    class MessageHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            synchronized (mLock) {
+                switch (msg.what) {
+                    case MSG_HANDOVER_COMPLETE:
+                        int transferId = msg.arg1;
+                        Log.d(TAG, "Completed transfer id: " + Integer.toString(transferId));
+                        if (mPendingTransfers.containsKey(transferId)) {
+                            mPendingTransfers.remove(transferId);
+                        } else {
+                            Log.e(TAG, "Could not find completed transfer id: " + Integer.toString(transferId));
+                        }
+                        break;
+                    case MSG_HEADSET_CONNECTED:
+                        mBluetoothHeadsetConnected = true;
+                        break;
+                    case MSG_HEADSET_NOT_CONNECTED:
+                        mBluetoothHeadsetConnected = false;
+                        break;
+                    default:
+                        break;
+                }
             }
         }
+    };
 
-        void stopMonitoring() {
-            handler.removeMessages(MSG_HANDOVER_POWER_CHECK);
+    private ServiceConnection mConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            synchronized (mLock) {
+                mService = new Messenger(service);
+                mBound = true;
+                // Register this client
+                Message msg = Message.obtain(null, HandoverService.MSG_REGISTER_CLIENT);
+                msg.replyTo = mMessenger;
+                try {
+                    mService.send(msg);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Failed to register client");
+                }
+            }
         }
 
         @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);
-                        }
+        public void onServiceDisconnected(ComponentName name) {
+            synchronized (mLock) {
+                if (mBound) {
+                    try {
+                        Message msg = Message.obtain(null, HandoverService.MSG_DEREGISTER_CLIENT);
+                        msg.replyTo = mMessenger;
+                        mService.send(msg);
+                    } catch (RemoteException e) {
+                        // Service may have crashed - ignore
                     }
-                    return true;
-            }
-            return false;
-        }
-    }
-
-    /**
-     * A HandoverTransfer object represents a set of files
-     * that were received through NFC connection handover
-     * from the same source address.
-     *
-     * For Bluetooth, files are received through OPP, and
-     * we have no knowledge how many files will be transferred
-     * as part of a single transaction.
-     * Hence, a transfer has a notion of being "alive": if
-     * the last update to a transfer was within WAIT_FOR_NEXT_TRANSFER_MS
-     * milliseconds, we consider a new file transfer from the
-     * same source address as part of the same transfer.
-     * The corresponding URIs will be grouped in a single folder.
-     *
-     */
-    class HandoverTransfer implements Handler.Callback,
-            MediaScannerConnection.OnScanCompletedListener {
-        // In the states below we still accept new file transfer
-        static final int STATE_NEW = 0;
-        static final int STATE_IN_PROGRESS = 1;
-        static final int STATE_W4_NEXT_TRANSFER = 2;
-
-        // In the states below no new files are accepted.
-        static final int STATE_W4_MEDIA_SCANNER = 3;
-        static final int STATE_FAILED = 4;
-        static final int STATE_SUCCESS = 5;
-        static final int STATE_CANCELLED = 6;
-
-        static final int MSG_NEXT_TRANSFER_TIMER = 0;
-        static final int MSG_TRANSFER_TIMEOUT = 1;
-
-        // 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;
-
-        // The amount of time to wait for a new transfer
-        // once the current one completes.
-        static final int WAIT_FOR_NEXT_TRANSFER_MS = 4000;
-
-        static final String BEAM_DIR = "beam";
-
-        final BluetoothDevice device;
-        final String sourceAddress;
-        final boolean incoming;  // whether this is an incoming transfer
-        final int notificationId; // Unique ID of this transfer used for notifications
-        final Handler handler;
-        final PendingIntent cancelIntent;
-
-        int state;
-        Long lastUpdate; // Last time an event occurred for this transfer
-        float progress; // Progress in range [0..1]
-        ArrayList<Uri> btUris; // Received uris from Bluetooth OPP
-        ArrayList<String> btMimeTypes; // Mime-types received from Bluetooth OPP
-
-        ArrayList<String> paths; // Raw paths on the filesystem for Beam-stored files
-        HashMap<String, String> mimeTypes; // Mime-types associated with each path
-        HashMap<String, Uri> mediaUris; // URIs found by the media scanner for each path
-        int urisScanned;
-
-        public HandoverTransfer(String sourceAddress, boolean incoming) {
-            synchronized (HandoverManager.this) {
-                this.notificationId = mNotificationId++;
-            }
-            this.lastUpdate = SystemClock.elapsedRealtime();
-            this.progress = 0.0f;
-            this.state = STATE_NEW;
-            this.btUris = new ArrayList<Uri>();
-            this.btMimeTypes = new ArrayList<String>();
-            this.paths = new ArrayList<String>();
-            this.mimeTypes = new HashMap<String, String>();
-            this.mediaUris = new HashMap<String, Uri>();
-            this.sourceAddress = sourceAddress;
-            this.incoming = incoming;
-            this.handler = new Handler(mContext.getMainLooper(), this);
-            this.cancelIntent = buildCancelIntent();
-            this.urisScanned = 0;
-            this.device = mBluetoothAdapter.getRemoteDevice(sourceAddress);
-
-            handler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
-        }
-
-        public synchronized void updateFileProgress(float progress) {
-            if (!isRunning()) return; // Ignore when we're no longer running
-
-            handler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
-
-            this.progress = progress;
-
-            // We're still receiving data from this device - keep it in
-            // the whitelist for a while longer
-            if (incoming) whitelistOppDevice(device);
-
-            updateStateAndNotification(STATE_IN_PROGRESS);
-        }
-
-        public synchronized void finishTransfer(boolean success, Uri uri, String mimeType) {
-            if (!isRunning()) return; // Ignore when we're no longer running
-
-            if (success && uri != null) {
-                if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType);
-                this.progress = 1.0f;
-                if (mimeType == null) {
-                    mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri);
                 }
-                if (mimeType != null) {
-                    btUris.add(uri);
-                    btMimeTypes.add(mimeType);
-                } else {
-                    if (DBG) Log.d(TAG, "Could not get mimeType for file.");
-                }
-            } else {
-                Log.e(TAG, "Handover transfer failed");
-                // Do wait to see if there's another file coming.
-            }
-            handler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
-            handler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS);
-            updateStateAndNotification(STATE_W4_NEXT_TRANSFER);
-        }
-
-        public synchronized boolean isRunning() {
-            if (state != STATE_NEW && state != STATE_IN_PROGRESS && state != STATE_W4_NEXT_TRANSFER) {
-                return false;
-            } else {
-                return true;
+                mService = null;
+                mBound = false;
             }
         }
-
-        synchronized void cancel() {
-            if (!isRunning()) return;
-
-            // Delete all files received so far
-            for (Uri uri : btUris) {
-                File file = new File(uri.getPath());
-                if (file.exists()) file.delete();
-            }
-
-            updateStateAndNotification(STATE_CANCELLED);
-        }
-
-        synchronized void updateNotification() {
-            if (!incoming) return; // No notifications for outgoing transfers
-
-            Builder notBuilder = new Notification.Builder(mContext);
-
-            if (state == STATE_NEW || state == STATE_IN_PROGRESS ||
-                    state == STATE_W4_NEXT_TRANSFER || state == STATE_W4_MEDIA_SCANNER) {
-                notBuilder.setAutoCancel(false);
-                notBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
-                notBuilder.setTicker(mContext.getString(R.string.beam_progress));
-                notBuilder.setContentTitle(mContext.getString(R.string.beam_progress));
-                notBuilder.addAction(R.drawable.ic_menu_cancel_holo_dark,
-                        mContext.getString(R.string.cancel), cancelIntent);
-                notBuilder.setDeleteIntent(cancelIntent);
-                // We do have progress indication on a per-file basis, but in a multi-file
-                // transfer we don't know the total progress. So for now, just show an
-                // indeterminate progress bar.
-                notBuilder.setProgress(100, 0, true);
-            } else if (state == STATE_SUCCESS) {
-                notBuilder.setAutoCancel(true);
-                notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
-                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 viewIntent = buildViewIntent();
-                PendingIntent contentIntent = PendingIntent.getActivityAsUser(
-                        mContext, 0, viewIntent, 0, null, UserHandle.CURRENT);
-
-                notBuilder.setContentIntent(contentIntent);
-
-                // Play Beam success sound
-                NfcService.getInstance().playSound(NfcService.SOUND_END);
-            } else if (state == STATE_FAILED) {
-                notBuilder.setAutoCancel(false);
-                notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
-                notBuilder.setTicker(mContext.getString(R.string.beam_failed));
-                notBuilder.setContentTitle(mContext.getString(R.string.beam_failed));
-            } else if (state == STATE_CANCELLED) {
-                notBuilder.setAutoCancel(false);
-                notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
-                notBuilder.setTicker(mContext.getString(R.string.beam_canceled));
-                notBuilder.setContentTitle(mContext.getString(R.string.beam_canceled));
-            } else {
-                return;
-            }
-
-            mNotificationManager.notifyAsUser(null, mNotificationId, notBuilder.build(),
-                    UserHandle.CURRENT);
-        }
-
-        synchronized void updateStateAndNotification(int newState) {
-            this.state = newState;
-            this.lastUpdate = SystemClock.elapsedRealtime();
-
-            if (handler.hasMessages(MSG_TRANSFER_TIMEOUT)) {
-                // Update timeout timer
-                handler.removeMessages(MSG_TRANSFER_TIMEOUT);
-                handler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
-            }
-            updateNotification();
-        }
-
-        synchronized void processFiles() {
-            // Check the amount of files we received in this transfer;
-            // If more than one, create a separate directory for it.
-            String extRoot = Environment.getExternalStorageDirectory().getPath();
-            File beamPath = new File(extRoot + "/" + BEAM_DIR);
-
-            if (!checkMediaStorage(beamPath) || btUris.size() == 0) {
-                Log.e(TAG, "Media storage not valid or no uris received.");
-                updateStateAndNotification(STATE_FAILED);
-                return;
-            }
-
-            if (btUris.size() > 1) {
-                beamPath = generateMultiplePath(extRoot + "/" + BEAM_DIR + "/");
-                if (!beamPath.isDirectory() && !beamPath.mkdir()) {
-                    Log.e(TAG, "Failed to create multiple path " + beamPath.toString());
-                    updateStateAndNotification(STATE_FAILED);
-                    return;
-                }
-            }
-
-            for (int i = 0; i < btUris.size(); i++) {
-                Uri uri = btUris.get(i);
-                String mimeType = btMimeTypes.get(i);
-
-                File srcFile = new File(uri.getPath());
-
-                File dstFile = generateUniqueDestination(beamPath.getAbsolutePath(),
-                        uri.getLastPathSegment());
-                if (!srcFile.renameTo(dstFile)) {
-                    if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile);
-                    srcFile.delete();
-                    return;
-                } else {
-                    paths.add(dstFile.getAbsolutePath());
-                    mimeTypes.put(dstFile.getAbsolutePath(), mimeType);
-                    if (DBG) Log.d(TAG, "Did successful rename from " + srcFile + " to " + dstFile);
-                }
-            }
-
-            // We can either add files to the media provider, or provide an ACTION_VIEW
-            // intent to the file directly. We base this decision on the mime type
-            // of the first file; if it's media the platform can deal with,
-            // use the media provider, if it's something else, just launch an ACTION_VIEW
-            // on the file.
-            String mimeType = mimeTypes.get(paths.get(0));
-            if (mimeType.startsWith("image/") || mimeType.startsWith("video/") ||
-                    mimeType.startsWith("audio/")) {
-                String[] arrayPaths = new String[paths.size()];
-                MediaScannerConnection.scanFile(mContext, paths.toArray(arrayPaths), null, this);
-                updateStateAndNotification(STATE_W4_MEDIA_SCANNER);
-            } else {
-                // We're done.
-                updateStateAndNotification(STATE_SUCCESS);
-            }
-
-        }
-
-        public boolean handleMessage(Message msg) {
-            if (msg.what == MSG_NEXT_TRANSFER_TIMER) {
-                // We didn't receive a new transfer in time, finalize this one
-                if (incoming) {
-                    processFiles();
-                } else {
-                    updateStateAndNotification(STATE_SUCCESS);
-                }
-                return true;
-            } else if (msg.what == MSG_TRANSFER_TIMEOUT) {
-                // No update on this transfer for a while, check
-                // to see if it's still running, and fail it if it is.
-                if (isRunning()) {
-                    updateStateAndNotification(STATE_FAILED);
-                }
-            }
-            return false;
-        }
-
-        public synchronized void onScanCompleted(String path, Uri uri) {
-            if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri);
-            if (uri != null) {
-                mediaUris.put(path, uri);
-            }
-            urisScanned++;
-            if (urisScanned == paths.size()) {
-                // We're done
-                updateStateAndNotification(STATE_SUCCESS);
-            }
-        }
-
-        boolean checkMediaStorage(File path) {
-            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
-                if (!path.isDirectory() && !path.mkdir()) {
-                    Log.e(TAG, "Not dir or not mkdir " + path.getAbsolutePath());
-                    return false;
-                }
-                return true;
-            } else {
-                Log.e(TAG, "External storage not mounted, can't store file.");
-                return false;
-            }
-        }
-
-        synchronized Intent buildViewIntent() {
-            if (paths.size() == 0) return null;
-
-            Intent viewIntent = new Intent(Intent.ACTION_VIEW);
-
-            String filePath = paths.get(0);
-            Uri mediaUri = mediaUris.get(filePath);
-            Uri uri =  mediaUri != null ? mediaUri :
-                Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath);
-            viewIntent.setDataAndTypeAndNormalize(uri, mimeTypes.get(filePath));
-            viewIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            return viewIntent;
-        }
-
-        PendingIntent buildCancelIntent() {
-            Intent intent = new Intent(ACTION_CANCEL_HANDOVER_TRANSFER);
-            intent.putExtra(EXTRA_SOURCE_ADDRESS, sourceAddress);
-            PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
-
-            return pi;
-        }
-
-        synchronized File generateUniqueDestination(String path, String fileName) {
-            int dotIndex = fileName.lastIndexOf(".");
-            String extension = null;
-            String fileNameWithoutExtension = null;
-            if (dotIndex < 0) {
-                extension = "";
-                fileNameWithoutExtension = fileName;
-            } else {
-                extension = fileName.substring(dotIndex);
-                fileNameWithoutExtension = fileName.substring(0, dotIndex);
-            }
-            File dstFile = new File(path + File.separator + fileName);
-            int count = 0;
-            while (dstFile.exists()) {
-                dstFile = new File(path + File.separator + fileNameWithoutExtension + "-" +
-                        Integer.toString(count) + extension);
-                count++;
-            }
-            return dstFile;
-        }
-
-        synchronized File generateMultiplePath(String beamRoot) {
-            // Generate a unique directory with the date
-            String format = "yyyy-MM-dd";
-            SimpleDateFormat sdf = new SimpleDateFormat(format);
-            String newPath = beamRoot + "beam-" + sdf.format(new Date());
-            File newFile = new File(newPath);
-            int count = 0;
-            while (newFile.exists()) {
-                newPath = beamRoot + "beam-" + sdf.format(new Date()) + "-" +
-                        Integer.toString(count);
-                newFile = new File(newPath);
-                count++;
-            }
-
-            return newFile;
-        }
-    }
-
-    synchronized HandoverTransfer getOrCreateHandoverTransfer(String sourceAddress, boolean incoming,
-            boolean create) {
-        Pair<String, Boolean> key = new Pair<String, Boolean>(sourceAddress, incoming);
-        if (mTransfers.containsKey(key)) {
-            HandoverTransfer transfer = mTransfers.get(key);
-            if (transfer.isRunning()) {
-                return transfer;
-            } else {
-                if (create) mTransfers.remove(key); // new one created below
-            }
-        }
-        if (create) {
-            HandoverTransfer transfer = new HandoverTransfer(sourceAddress, incoming);
-            mTransfers.put(key, transfer);
-
-            return transfer;
-        } else {
-            return null;
-        }
-    }
+    };
 
     public HandoverManager(Context context) {
         mContext = context;
         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 
-        mNotificationManager = (NotificationManager) mContext.getSystemService(
-                Context.NOTIFICATION_SERVICE);
+        mPendingTransfers = new HashMap<Integer, PendingHandoverTransfer>();
 
-        mTransfers = new HashMap<Pair<String, Boolean>, HandoverTransfer>();
-        mHandoverPowerManager = new HandoverPowerManager(context);
+        IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
+        mContext.registerReceiver(mReceiver, filter, null, null);
 
-        IntentFilter filter = new IntentFilter(ACTION_BT_OPP_TRANSFER_DONE);
-        filter.addAction(ACTION_BT_OPP_TRANSFER_PROGRESS);
-        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
-        filter.addAction(ACTION_CANCEL_HANDOVER_TRANSFER);
-        mContext.registerReceiver(mReceiver, filter, HANDOVER_STATUS_PERMISSION, null);
+        mContext.bindService(new Intent(mContext, HandoverService.class), mConnection,
+                Context.BIND_AUTO_CREATE, UserHandle.USER_CURRENT);
     }
 
-    synchronized void cleanupTransfers() {
-        Iterator<Map.Entry<Pair<String, Boolean>, HandoverTransfer>> it = mTransfers.entrySet().iterator();
-        while (it.hasNext()) {
-            Map.Entry<Pair<String, Boolean>, HandoverTransfer> pair = it.next();
-            HandoverTransfer transfer = pair.getValue();
-            if (!transfer.isRunning()) {
-                it.remove();
+    final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_USER_SWITCHED)) {
+                // Re-bind a service for the current user
+                mContext.unbindService(mConnection);
+                mContext.bindService(new Intent(mContext, HandoverService.class), mConnection,
+                        Context.BIND_AUTO_CREATE, UserHandle.USER_CURRENT);
             }
         }
-    }
+    };
 
     static NdefRecord createCollisionRecord() {
         byte[] random = new byte[2];
@@ -659,7 +202,7 @@
         payload[0] = (byte) (payload.length & 0xFF);
         payload[1] = (byte) ((payload.length >> 8) & 0xFF);
 
-        synchronized (HandoverManager.this) {
+        synchronized (mLock) {
             if (mLocalBluetoothAddress == null) {
                 mLocalBluetoothAddress = mBluetoothAdapter.getAddress();
             }
@@ -698,7 +241,6 @@
         payload.get(payloadBytes);
         return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null,
                 payloadBytes);
-
     }
 
     NdefRecord createHandoverRequestRecord() {
@@ -742,24 +284,32 @@
         }
         if (bluetoothData == null) return null;
 
-        boolean bluetoothActivating = false;
-
-        synchronized(HandoverManager.this) {
-            if (!mHandoverPowerManager.isBluetoothEnabled()) {
-                if (!mHandoverPowerManager.enableBluetooth()) {
-                    return null;
-                }
-                bluetoothActivating = true;
-            } else {
-                mHandoverPowerManager.resetTimer();
+        // Note: there could be a race where we conclude
+        // that Bluetooth is already enabled, and shortly
+        // after the user turns it off. That will cause
+        // the transfer to fail, but there's nothing
+        // much we can do about it anyway. It shouldn't
+        // be common for the user to be changing BT settings
+        // while waiting to receive a picture.
+        boolean bluetoothActivating = !mBluetoothAdapter.isEnabled();
+        synchronized (mLock) {
+            if (!mBound) {
+                Log.e(TAG, "Could not connect to handover service");
+                return null;
             }
-
-            // Create the initial transfer object
-            HandoverTransfer transfer = getOrCreateHandoverTransfer(
-                    bluetoothData.device.getAddress(), true, true);
-            transfer.updateNotification();
+            Message msg = Message.obtain(null, HandoverService.MSG_START_INCOMING_TRANSFER);
+            PendingHandoverTransfer transfer = registerInTransferLocked(bluetoothData.device);
+            Bundle transferData = new Bundle();
+            transferData.putParcelable(HandoverService.BUNDLE_TRANSFER, transfer);
+            msg.setData(transferData);
+            try {
+                mService.send(msg);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Could not connect to handover service");
+               removeTransferLocked(transfer.id);
+               return null;
+            }
         }
-
         // BT OOB found, whitelist it for incoming OPP data
         whitelistOppDevice(bluetoothData.device);
 
@@ -767,13 +317,6 @@
         return (createHandoverSelectMessage(bluetoothActivating));
     }
 
-    void whitelistOppDevice(BluetoothDevice device) {
-        if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP");
-        Intent intent = new Intent(ACTION_WHITELIST_DEVICE);
-        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
-        mContext.sendBroadcast(intent);
-    }
-
     public boolean tryHandover(NdefMessage m) {
         if (m == null) return false;
         if (mBluetoothAdapter == null) return false;
@@ -784,18 +327,26 @@
         if (handover == null) return false;
         if (!handover.valid) return true;
 
-        synchronized (HandoverManager.this) {
+        synchronized (mLock) {
             if (mBluetoothAdapter == 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;
+            if (!mBound) {
+                Log.e(TAG, "Could not connect to handover service");
+                return false;
             }
-            mBluetoothHeadsetHandover = new BluetoothHeadsetHandover(mContext, handover.device,
-                    handover.name, mHandoverPowerManager, this);
-            mBluetoothHeadsetHandover.start();
+
+            Message msg = Message.obtain(null, HandoverService.MSG_HEADSET_HANDOVER, 0, 0);
+            Bundle headsetData = new Bundle();
+            headsetData.putParcelable(HandoverService.EXTRA_HEADSET_DEVICE, handover.device);
+            headsetData.putString(HandoverService.EXTRA_HEADSET_NAME, handover.name);
+            msg.setData(headsetData);
+            try {
+                mService.send(msg);
+            } catch (RemoteException e) {
+                return false;
+            }
         }
         return true;
     }
@@ -807,13 +358,53 @@
         BluetoothHandoverData data = parse(m);
         if (data != null && data.valid) {
             // Register a new handover transfer object
-            getOrCreateHandoverTransfer(data.device.getAddress(), false, true);
-            BluetoothOppHandover handover = new BluetoothOppHandover(mContext, data.device,
-                uris, mHandoverPowerManager, data.carrierActivating);
-            handover.start();
+            synchronized (mLock) {
+                if (!mBound) {
+                    Log.e(TAG, "Could not connect to handover service");
+                    return;
+                }
+
+                Message msg = Message.obtain(null, HandoverService.MSG_START_OUTGOING_TRANSFER, 0, 0);
+                PendingHandoverTransfer transfer = registerOutTransferLocked(data, uris);
+                Bundle transferData = new Bundle();
+                transferData.putParcelable(HandoverService.BUNDLE_TRANSFER, transfer);
+                msg.setData(transferData);
+                try {
+                    mService.send(msg);
+                } catch (RemoteException e) {
+                    removeTransferLocked(transfer.id);
+                }
+            }
         }
     }
 
+    PendingHandoverTransfer registerInTransferLocked(BluetoothDevice remoteDevice) {
+        PendingHandoverTransfer transfer = new PendingHandoverTransfer(
+                mHandoverTransferId++, true, remoteDevice, false, null);
+        mPendingTransfers.put(transfer.id, transfer);
+
+        return transfer;
+    }
+
+    PendingHandoverTransfer registerOutTransferLocked(BluetoothHandoverData data,
+            Uri[] uris) {
+        PendingHandoverTransfer transfer = new PendingHandoverTransfer(
+                mHandoverTransferId++, false, data.device, data.carrierActivating, uris);
+        mPendingTransfers.put(transfer.id, transfer);
+        return transfer;
+    }
+
+    void removeTransferLocked(int id) {
+        mPendingTransfers.remove(id);
+    }
+
+    void whitelistOppDevice(BluetoothDevice device) {
+        if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP");
+        Intent intent = new Intent(ACTION_WHITELIST_DEVICE);
+        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+        mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
+    }
+
     boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) {
         byte[] payload = handoverRec.getPayload();
         if (payload == null || payload.length <= 1) return false;
@@ -972,79 +563,4 @@
 
         return result;
     }
-
-    @Override
-    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();
-
-            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_CANCEL_HANDOVER_TRANSFER)) {
-                String sourceAddress = intent.getStringExtra(EXTRA_SOURCE_ADDRESS);
-                HandoverTransfer transfer = getOrCreateHandoverTransfer(sourceAddress, true,
-                        false);
-                if (transfer != null) {
-                    transfer.cancel();
-                }
-            } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS) ||
-                    action.equals(ACTION_BT_OPP_TRANSFER_DONE)) {
-                // Clean up old transfers no longer in progress
-                cleanupTransfers();
-
-                int direction = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_DIRECTION, -1);
-                int id = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_ID, -1);
-                String sourceAddress = intent.getStringExtra(EXTRA_BT_OPP_ADDRESS);
-
-                if (direction == -1 || id == -1 || sourceAddress == null) return;
-                boolean incoming = (direction == DIRECTION_BLUETOOTH_INCOMING);
-
-                HandoverTransfer transfer = getOrCreateHandoverTransfer(sourceAddress, incoming,
-                        false);
-                if (transfer == null) {
-                    // There is no transfer running for this source address; most likely
-                    // the transfer was cancelled. We need to tell BT OPP to stop transferring
-                    // in case this was an incoming transfer
-                    Intent cancelIntent = new Intent("android.btopp.intent.action.STOP_HANDOVER_TRANSFER");
-                    cancelIntent.putExtra(EXTRA_BT_OPP_TRANSFER_ID, id);
-                    mContext.sendBroadcast(cancelIntent);
-                    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);
-                        String mimeType = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_MIMETYPE);
-                        Uri uri = Uri.parse(uriString);
-                        if (uri.getScheme() == null) {
-                            uri = Uri.fromFile(new File(uri.getPath()));
-                        }
-                        transfer.finishTransfer(true, uri, mimeType);
-                    } else {
-                        transfer.finishTransfer(false, null, null);
-                    }
-                } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS)) {
-                    float progress = intent.getFloatExtra(EXTRA_BT_OPP_TRANSFER_PROGRESS, 0.0f);
-                    transfer.updateFileProgress(progress);
-                }
-            }
-        }
-    };
-
 }
diff --git a/src/com/android/nfc/handover/HandoverServer.java b/src/com/android/nfc/handover/HandoverServer.java
index e789387..093d1dd 100644
--- a/src/com/android/nfc/handover/HandoverServer.java
+++ b/src/com/android/nfc/handover/HandoverServer.java
@@ -210,7 +210,8 @@
                         }
                         // We're done
                         mCallback.onHandoverRequestReceived();
-                        break;
+                        // We can process another handover transfer
+                        byteStream = new ByteArrayOutputStream();
                     }
 
                     synchronized (HandoverServer.this) {
diff --git a/src/com/android/nfc/handover/HandoverService.java b/src/com/android/nfc/handover/HandoverService.java
new file mode 100644
index 0000000..261cbca
--- /dev/null
+++ b/src/com/android/nfc/handover/HandoverService.java
@@ -0,0 +1,403 @@
+package com.android.nfc.handover;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.SoundPool;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.nfc.R;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+
+public class HandoverService extends Service implements HandoverTransfer.Callback,
+        BluetoothHeadsetHandover.Callback {
+
+    static final String TAG = "HandoverService";
+
+    static final int MSG_REGISTER_CLIENT = 0;
+    static final int MSG_DEREGISTER_CLIENT = 1;
+    static final int MSG_START_INCOMING_TRANSFER = 2;
+    static final int MSG_START_OUTGOING_TRANSFER = 3;
+    static final int MSG_HEADSET_HANDOVER = 4;
+
+    static final String BUNDLE_TRANSFER = "transfer";
+
+    static final String EXTRA_HEADSET_DEVICE = "device";
+    static final String EXTRA_HEADSET_NAME = "headsetname";
+
+    static final String ACTION_CANCEL_HANDOVER_TRANSFER =
+            "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER";
+    static final String EXTRA_SOURCE_ADDRESS =
+            "com.android.nfc.handover.extra.SOURCE_ADDRESS";
+
+    static final String ACTION_BT_OPP_TRANSFER_PROGRESS =
+            "android.btopp.intent.action.BT_OPP_TRANSFER_PROGRESS";
+
+    static final String ACTION_BT_OPP_TRANSFER_DONE =
+            "android.btopp.intent.action.BT_OPP_TRANSFER_DONE";
+
+    static final String EXTRA_BT_OPP_TRANSFER_STATUS =
+            "android.btopp.intent.extra.BT_OPP_TRANSFER_STATUS";
+
+    static final String EXTRA_BT_OPP_TRANSFER_MIMETYPE =
+            "android.btopp.intent.extra.BT_OPP_TRANSFER_MIMETYPE";
+
+    static final String EXTRA_BT_OPP_ADDRESS =
+            "android.btopp.intent.extra.BT_OPP_ADDRESS";
+
+    static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0;
+
+    static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1;
+
+    static final String EXTRA_BT_OPP_TRANSFER_DIRECTION =
+            "android.btopp.intent.extra.BT_OPP_TRANSFER_DIRECTION";
+
+    static final int DIRECTION_BLUETOOTH_INCOMING = 0;
+
+    static final int DIRECTION_BLUETOOTH_OUTGOING = 1;
+
+    static final String EXTRA_BT_OPP_TRANSFER_ID =
+            "android.btopp.intent.extra.BT_OPP_TRANSFER_ID";
+
+    static final String EXTRA_BT_OPP_TRANSFER_PROGRESS =
+            "android.btopp.intent.extra.BT_OPP_TRANSFER_PROGRESS";
+
+    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
+    static final String HANDOVER_STATUS_PERMISSION =
+            "com.android.permission.HANDOVER_STATUS";
+
+    // Variables below only accessed on main thread
+    final Queue<BluetoothOppHandover> mPendingOutTransfers;
+    final HashMap<Pair<String, Boolean>, HandoverTransfer> mTransfers;
+    final Messenger mMessenger;
+
+    SoundPool mSoundPool;
+    int mSuccessSound;
+
+    BluetoothAdapter mBluetoothAdapter;
+    Messenger mClient;
+    Handler mHandler;
+    BluetoothHeadsetHandover mBluetoothHeadsetHandover;
+    boolean mBluetoothHeadsetConnected;
+    boolean mBluetoothEnabledByNfc;
+
+    public HandoverService() {
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        mPendingOutTransfers = new LinkedList<BluetoothOppHandover>();
+        mTransfers = new HashMap<Pair<String, Boolean>, HandoverTransfer>();
+        mHandler = new MessageHandler();
+        mMessenger = new Messenger(mHandler);
+        mBluetoothHeadsetConnected = false;
+        mBluetoothEnabledByNfc = false;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0);
+        mSuccessSound = mSoundPool.load(this, R.raw.end, 1);
+
+        IntentFilter filter = new IntentFilter(ACTION_BT_OPP_TRANSFER_DONE);
+        filter.addAction(ACTION_BT_OPP_TRANSFER_PROGRESS);
+        filter.addAction(ACTION_CANCEL_HANDOVER_TRANSFER);
+        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+        registerReceiver(mReceiver, filter, HANDOVER_STATUS_PERMISSION, mHandler);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mSoundPool != null) {
+            mSoundPool.release();
+        }
+        unregisterReceiver(mReceiver);
+    }
+
+    void doOutgoingTransfer(Message msg) {
+        Bundle msgData = msg.getData();
+
+        msgData.setClassLoader(getClassLoader());
+        PendingHandoverTransfer pendingTransfer = (PendingHandoverTransfer)
+                msgData.getParcelable(BUNDLE_TRANSFER);
+        createHandoverTransfer(pendingTransfer);
+
+        // Create the actual transfer
+        BluetoothOppHandover handover = new BluetoothOppHandover(HandoverService.this,
+                pendingTransfer.remoteDevice, pendingTransfer.uris,
+                pendingTransfer.remoteActivating);
+        if (mBluetoothAdapter.isEnabled()) {
+            // Start the transfer
+            handover.start();
+        } else {
+            if (!enableBluetooth()) {
+                Log.e(TAG, "Error enabling Bluetooth.");
+                notifyClientTransferComplete(pendingTransfer.id);
+                return;
+            }
+            mPendingOutTransfers.add(handover);
+            // Queue the transfer and enable Bluetooth - when it is enabled
+            // the transfer will be started.
+        }
+    }
+
+    void doIncomingTransfer(Message msg) {
+        Bundle msgData = msg.getData();
+
+        msgData.setClassLoader(getClassLoader());
+        PendingHandoverTransfer pendingTransfer = (PendingHandoverTransfer)
+                msgData.getParcelable(BUNDLE_TRANSFER);
+        if (!mBluetoothAdapter.isEnabled() && !enableBluetooth()) {
+            Log.e(TAG, "Error enabling Bluetooth.");
+            notifyClientTransferComplete(pendingTransfer.id);
+            return;
+        }
+        createHandoverTransfer(pendingTransfer);
+        // Remote device will connect and finish the transfer
+    }
+
+    void doHeadsetHandover(Message msg) {
+        Bundle msgData = msg.getData();
+        BluetoothDevice device = (BluetoothDevice) msgData.getParcelable(EXTRA_HEADSET_DEVICE);
+        String name = (String) msgData.getString(EXTRA_HEADSET_NAME);
+        mBluetoothHeadsetHandover = new BluetoothHeadsetHandover(HandoverService.this,
+                device, name, HandoverService.this);
+        if (mBluetoothAdapter.isEnabled()) {
+            mBluetoothHeadsetHandover.start();
+        } else {
+            // Once BT is enabled, the headset pairing will be started
+            if (!enableBluetooth()) {
+                Log.e(TAG, "Error enabling Bluetooth.");
+                mBluetoothHeadsetHandover = null;
+            }
+        }
+    }
+
+    void startPendingTransfers() {
+        while (!mPendingOutTransfers.isEmpty()) {
+             BluetoothOppHandover handover = mPendingOutTransfers.remove();
+             handover.start();
+        }
+    }
+
+    class MessageHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_REGISTER_CLIENT:
+                    mClient = msg.replyTo;
+                    break;
+                case MSG_DEREGISTER_CLIENT:
+                    mClient = null;
+                    break;
+                case MSG_START_INCOMING_TRANSFER:
+                    doIncomingTransfer(msg);
+                    break;
+                case MSG_START_OUTGOING_TRANSFER:
+                    doOutgoingTransfer(msg);
+                    break;
+                case MSG_HEADSET_HANDOVER:
+                    doHeadsetHandover(msg);
+                    break;
+            }
+        }
+    }
+
+    boolean enableBluetooth() {
+        if (!mBluetoothAdapter.isEnabled()) {
+            mBluetoothEnabledByNfc = true;
+            return mBluetoothAdapter.enableNoAutoConnect();
+        }
+        return true;
+    }
+
+    void disableBluetoothIfNeeded() {
+        if (!mBluetoothEnabledByNfc) return;
+
+        if (mTransfers.size() == 0 && !mBluetoothHeadsetConnected) {
+            mBluetoothAdapter.disable();
+            mBluetoothEnabledByNfc = false;
+        }
+    }
+
+    void createHandoverTransfer(PendingHandoverTransfer pendingTransfer) {
+        Pair<String, Boolean> key = new Pair<String, Boolean>(
+                pendingTransfer.remoteDevice.getAddress(), pendingTransfer.incoming);
+        if (mTransfers.containsKey(key)) {
+            HandoverTransfer transfer = mTransfers.get(key);
+            if (!transfer.isRunning()) {
+                mTransfers.remove(key); // new one created below
+            } else {
+                // There is already a transfer running to this
+                // device - it will automatically get combined
+                // with the existing transfer.
+                notifyClientTransferComplete(pendingTransfer.id);
+                return;
+            }
+        }
+
+        HandoverTransfer transfer = new HandoverTransfer(this, this, pendingTransfer);
+        mTransfers.put(key, transfer);
+        transfer.updateNotification();
+    }
+
+    HandoverTransfer findHandoverTransfer(String sourceAddress, boolean incoming) {
+        Pair<String, Boolean> key = new Pair<String, Boolean>(sourceAddress, incoming);
+        if (mTransfers.containsKey(key)) {
+            HandoverTransfer transfer = mTransfers.get(key);
+            if (transfer.isRunning()) {
+                return transfer;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+       return mMessenger.getBinder();
+    }
+
+    final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+                int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+                if (state == BluetoothAdapter.STATE_ON) {
+                    // If there is a pending headset pairing, start it
+                    if (mBluetoothHeadsetHandover != null &&
+                            !mBluetoothHeadsetHandover.hasStarted()) {
+                        mBluetoothHeadsetHandover.start();
+                    }
+
+                    // Start any pending transfers
+                    startPendingTransfers();
+                } else if (state == BluetoothAdapter.STATE_OFF) {
+                    mBluetoothEnabledByNfc = false;
+                    mBluetoothHeadsetConnected = false;
+                }
+            }
+            else if (action.equals(ACTION_CANCEL_HANDOVER_TRANSFER)) {
+                String sourceAddress = intent.getStringExtra(EXTRA_SOURCE_ADDRESS);
+                HandoverTransfer transfer = findHandoverTransfer(sourceAddress, true);
+                if (transfer != null) {
+                    transfer.cancel();
+                }
+            } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS) ||
+                    action.equals(ACTION_BT_OPP_TRANSFER_DONE)) {
+                int direction = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_DIRECTION, -1);
+                int id = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_ID, -1);
+                String sourceAddress = intent.getStringExtra(EXTRA_BT_OPP_ADDRESS);
+
+                if (direction == -1 || id == -1 || sourceAddress == null) return;
+                boolean incoming = (direction == DIRECTION_BLUETOOTH_INCOMING);
+
+                HandoverTransfer transfer = findHandoverTransfer(sourceAddress, incoming);
+                if (transfer == null) {
+                    // There is no transfer running for this source address; most likely
+                    // the transfer was cancelled. We need to tell BT OPP to stop transferring
+                    // in case this was an incoming transfer
+                    Intent cancelIntent = new Intent("android.btopp.intent.action.STOP_HANDOVER_TRANSFER");
+                    cancelIntent.putExtra(EXTRA_BT_OPP_TRANSFER_ID, id);
+                    sendBroadcast(cancelIntent);
+                    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);
+                        String mimeType = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_MIMETYPE);
+                        Uri uri = Uri.parse(uriString);
+                        if (uri.getScheme() == null) {
+                            uri = Uri.fromFile(new File(uri.getPath()));
+                        }
+                        transfer.finishTransfer(true, uri, mimeType);
+                    } else {
+                        transfer.finishTransfer(false, null, null);
+                    }
+                } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS)) {
+                    float progress = intent.getFloatExtra(EXTRA_BT_OPP_TRANSFER_PROGRESS, 0.0f);
+                    transfer.updateFileProgress(progress);
+                }
+            }
+        }
+    };
+
+    void notifyClientTransferComplete(int transferId) {
+        if (mClient != null) {
+            Message msg = Message.obtain(null, HandoverManager.MSG_HANDOVER_COMPLETE);
+            msg.arg1 = transferId;
+            try {
+                mClient.send(msg);
+            } catch (RemoteException e) {
+                // Ignore
+            }
+        }
+    }
+
+    @Override
+    public void onTransferComplete(HandoverTransfer transfer, boolean success) {
+        // Called on the main thread
+
+        // First, remove the transfer from our list
+        Iterator it = mTransfers.entrySet().iterator();
+        while (it.hasNext()) {
+            Map.Entry hashPair = (Map.Entry)it.next();
+            HandoverTransfer transferEntry = (HandoverTransfer) hashPair.getValue();
+            if (transferEntry == transfer) {
+                it.remove();
+            }
+        }
+
+        // Notify any clients of the service
+        notifyClientTransferComplete(transfer.getTransferId());
+
+        // Play success sound
+        if (success) {
+            mSoundPool.play(mSuccessSound, 1.0f, 1.0f, 0, 0, 1.0f);
+        }
+        disableBluetoothIfNeeded();
+    }
+
+    @Override
+    public void onBluetoothHeadsetHandoverComplete(boolean connected) {
+        // Called on the main thread
+        mBluetoothHeadsetHandover = null;
+        mBluetoothHeadsetConnected = connected;
+        if (mClient != null) {
+            Message msg = Message.obtain(null,
+                    connected ? HandoverManager.MSG_HEADSET_CONNECTED
+                              : HandoverManager.MSG_HEADSET_NOT_CONNECTED);
+            try {
+                mClient.send(msg);
+            } catch (RemoteException e) {
+                // Ignore
+            }
+        }
+        disableBluetoothIfNeeded();
+    }
+}
diff --git a/src/com/android/nfc/handover/HandoverTransfer.java b/src/com/android/nfc/handover/HandoverTransfer.java
new file mode 100644
index 0000000..98b59a6
--- /dev/null
+++ b/src/com/android/nfc/handover/HandoverTransfer.java
@@ -0,0 +1,423 @@
+package com.android.nfc.handover;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Notification.Builder;
+import android.bluetooth.BluetoothDevice;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.nfc.R;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+
+/**
+ * A HandoverTransfer object represents a set of files
+ * that were received through NFC connection handover
+ * from the same source address.
+ *
+ * For Bluetooth, files are received through OPP, and
+ * we have no knowledge how many files will be transferred
+ * as part of a single transaction.
+ * Hence, a transfer has a notion of being "alive": if
+ * the last update to a transfer was within WAIT_FOR_NEXT_TRANSFER_MS
+ * milliseconds, we consider a new file transfer from the
+ * same source address as part of the same transfer.
+ * The corresponding URIs will be grouped in a single folder.
+ *
+ */
+public class HandoverTransfer implements Handler.Callback,
+        MediaScannerConnection.OnScanCompletedListener {
+
+    interface Callback {
+        void onTransferComplete(HandoverTransfer transfer, boolean success);
+    };
+
+    static final String TAG = "HandoverTransfer";
+
+    static final Boolean DBG = true;
+
+    // In the states below we still accept new file transfer
+    static final int STATE_NEW = 0;
+    static final int STATE_IN_PROGRESS = 1;
+    static final int STATE_W4_NEXT_TRANSFER = 2;
+
+    // In the states below no new files are accepted.
+    static final int STATE_W4_MEDIA_SCANNER = 3;
+    static final int STATE_FAILED = 4;
+    static final int STATE_SUCCESS = 5;
+    static final int STATE_CANCELLED = 6;
+
+    static final int MSG_NEXT_TRANSFER_TIMER = 0;
+    static final int MSG_TRANSFER_TIMEOUT = 1;
+
+    // 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;
+
+    // The amount of time to wait for a new transfer
+    // once the current one completes.
+    static final int WAIT_FOR_NEXT_TRANSFER_MS = 4000;
+
+    static final String BEAM_DIR = "beam";
+
+    final boolean mIncoming;  // whether this is an incoming transfer
+    final int mTransferId; // Unique ID of this transfer used for notifications
+    final PendingIntent mCancelIntent;
+    final Context mContext;
+    final Handler mHandler;
+    final NotificationManager mNotificationManager;
+    final BluetoothDevice mRemoteDevice;
+    final Callback mCallback;
+
+    // Variables below are only accessed on the main thread
+    int mState;
+    boolean mCalledBack;
+    Long mLastUpdate; // Last time an event occurred for this transfer
+    float mProgress; // Progress in range [0..1]
+    ArrayList<Uri> mBtUris; // Received uris from Bluetooth OPP
+    ArrayList<String> mBtMimeTypes; // Mime-types received from Bluetooth OPP
+
+    ArrayList<String> mPaths; // Raw paths on the filesystem for Beam-stored files
+    HashMap<String, String> mMimeTypes; // Mime-types associated with each path
+    HashMap<String, Uri> mMediaUris; // URIs found by the media scanner for each path
+    int mUrisScanned;
+
+    public HandoverTransfer(Context context, Callback callback,
+            PendingHandoverTransfer pendingTransfer) {
+        mContext = context;
+        mCallback = callback;
+        mRemoteDevice = pendingTransfer.remoteDevice;
+        mIncoming = pendingTransfer.incoming;
+        mTransferId = pendingTransfer.id;
+        mLastUpdate = SystemClock.elapsedRealtime();
+        mProgress = 0.0f;
+        mState = STATE_NEW;
+        mBtUris = new ArrayList<Uri>();
+        mBtMimeTypes = new ArrayList<String>();
+        mPaths = new ArrayList<String>();
+        mMimeTypes = new HashMap<String, String>();
+        mMediaUris = new HashMap<String, Uri>();
+        mCancelIntent = buildCancelIntent();
+        mUrisScanned = 0;
+
+        mHandler = new Handler(Looper.getMainLooper(), this);
+        mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
+        mNotificationManager = (NotificationManager) mContext.getSystemService(
+                Context.NOTIFICATION_SERVICE);
+    }
+
+    void whitelistOppDevice(BluetoothDevice device) {
+        if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP");
+        Intent intent = new Intent(HandoverManager.ACTION_WHITELIST_DEVICE);
+        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+        mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
+    }
+
+    public void updateFileProgress(float progress) {
+        if (!isRunning()) return; // Ignore when we're no longer running
+
+        mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
+
+        this.mProgress = progress;
+
+        // We're still receiving data from this device - keep it in
+        // the whitelist for a while longer
+        if (mIncoming) whitelistOppDevice(mRemoteDevice);
+
+        updateStateAndNotification(STATE_IN_PROGRESS);
+    }
+
+    public void finishTransfer(boolean success, Uri uri, String mimeType) {
+        if (!isRunning()) return; // Ignore when we're no longer running
+
+        if (success && uri != null) {
+            if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType);
+            this.mProgress = 1.0f;
+            if (mimeType == null) {
+                mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri);
+            }
+            if (mimeType != null) {
+                mBtUris.add(uri);
+                mBtMimeTypes.add(mimeType);
+            } else {
+                if (DBG) Log.d(TAG, "Could not get mimeType for file.");
+            }
+        } else {
+            Log.e(TAG, "Handover transfer failed");
+            // Do wait to see if there's another file coming.
+        }
+        mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER);
+        mHandler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS);
+        updateStateAndNotification(STATE_W4_NEXT_TRANSFER);
+    }
+
+    public boolean isRunning() {
+        if (mState != STATE_NEW && mState != STATE_IN_PROGRESS && mState != STATE_W4_NEXT_TRANSFER) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    void cancel() {
+        if (!isRunning()) return;
+
+        // Delete all files received so far
+        for (Uri uri : mBtUris) {
+            File file = new File(uri.getPath());
+            if (file.exists()) file.delete();
+        }
+
+        updateStateAndNotification(STATE_CANCELLED);
+    }
+
+    void updateNotification() {
+        if (!mIncoming) return; // No notifications for outgoing transfers
+
+        Builder notBuilder = new Notification.Builder(mContext);
+
+        if (mState == STATE_NEW || mState == STATE_IN_PROGRESS ||
+                mState == STATE_W4_NEXT_TRANSFER || mState == STATE_W4_MEDIA_SCANNER) {
+            notBuilder.setAutoCancel(false);
+            notBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
+            notBuilder.setTicker(mContext.getString(R.string.beam_progress));
+            notBuilder.setContentTitle(mContext.getString(R.string.beam_progress));
+            notBuilder.addAction(R.drawable.ic_menu_cancel_holo_dark,
+                    mContext.getString(R.string.cancel), mCancelIntent);
+            notBuilder.setDeleteIntent(mCancelIntent);
+            // We do have progress indication on a per-file basis, but in a multi-file
+            // transfer we don't know the total progress. So for now, just show an
+            // indeterminate progress bar.
+            notBuilder.setProgress(100, 0, true);
+        } else if (mState == STATE_SUCCESS) {
+            notBuilder.setAutoCancel(true);
+            notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
+            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 viewIntent = buildViewIntent();
+            PendingIntent contentIntent = PendingIntent.getActivity(
+                    mContext, 0, viewIntent, 0, null);
+
+            notBuilder.setContentIntent(contentIntent);
+        } else if (mState == STATE_FAILED) {
+            notBuilder.setAutoCancel(false);
+            notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
+            notBuilder.setTicker(mContext.getString(R.string.beam_failed));
+            notBuilder.setContentTitle(mContext.getString(R.string.beam_failed));
+        } else if (mState == STATE_CANCELLED) {
+            notBuilder.setAutoCancel(false);
+            notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
+            notBuilder.setTicker(mContext.getString(R.string.beam_canceled));
+            notBuilder.setContentTitle(mContext.getString(R.string.beam_canceled));
+        } else {
+            return;
+        }
+
+        mNotificationManager.notify(null, mTransferId, notBuilder.build());
+    }
+
+    void updateStateAndNotification(int newState) {
+        this.mState = newState;
+        this.mLastUpdate = SystemClock.elapsedRealtime();
+
+        if (mHandler.hasMessages(MSG_TRANSFER_TIMEOUT)) {
+            // Update timeout timer
+            mHandler.removeMessages(MSG_TRANSFER_TIMEOUT);
+            mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS);
+        }
+
+        updateNotification();
+
+        if ((mState == STATE_SUCCESS || mState == STATE_FAILED || mState == STATE_CANCELLED)
+                && !mCalledBack) {
+            mCalledBack = true;
+            // Notify that we're done with this transfer
+            mCallback.onTransferComplete(this, mState == STATE_SUCCESS);
+        }
+    }
+
+    void processFiles() {
+        // Check the amount of files we received in this transfer;
+        // If more than one, create a separate directory for it.
+        String extRoot = Environment.getExternalStorageDirectory().getPath();
+        File beamPath = new File(extRoot + "/" + BEAM_DIR);
+
+        if (!checkMediaStorage(beamPath) || mBtUris.size() == 0) {
+            Log.e(TAG, "Media storage not valid or no uris received.");
+            updateStateAndNotification(STATE_FAILED);
+            return;
+        }
+
+        if (mBtUris.size() > 1) {
+            beamPath = generateMultiplePath(extRoot + "/" + BEAM_DIR + "/");
+            if (!beamPath.isDirectory() && !beamPath.mkdir()) {
+                Log.e(TAG, "Failed to create multiple path " + beamPath.toString());
+                updateStateAndNotification(STATE_FAILED);
+                return;
+            }
+        }
+
+        for (int i = 0; i < mBtUris.size(); i++) {
+            Uri uri = mBtUris.get(i);
+            String mimeType = mBtMimeTypes.get(i);
+
+            File srcFile = new File(uri.getPath());
+
+            File dstFile = generateUniqueDestination(beamPath.getAbsolutePath(),
+                    uri.getLastPathSegment());
+            if (!srcFile.renameTo(dstFile)) {
+                if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile);
+                srcFile.delete();
+                return;
+            } else {
+                mPaths.add(dstFile.getAbsolutePath());
+                mMimeTypes.put(dstFile.getAbsolutePath(), mimeType);
+                if (DBG) Log.d(TAG, "Did successful rename from " + srcFile + " to " + dstFile);
+            }
+        }
+
+        // We can either add files to the media provider, or provide an ACTION_VIEW
+        // intent to the file directly. We base this decision on the mime type
+        // of the first file; if it's media the platform can deal with,
+        // use the media provider, if it's something else, just launch an ACTION_VIEW
+        // on the file.
+        String mimeType = mMimeTypes.get(mPaths.get(0));
+        if (mimeType.startsWith("image/") || mimeType.startsWith("video/") ||
+                mimeType.startsWith("audio/")) {
+            String[] arrayPaths = new String[mPaths.size()];
+            MediaScannerConnection.scanFile(mContext, mPaths.toArray(arrayPaths), null, this);
+            updateStateAndNotification(STATE_W4_MEDIA_SCANNER);
+        } else {
+            // We're done.
+            updateStateAndNotification(STATE_SUCCESS);
+        }
+
+    }
+
+    public int getTransferId() {
+        return mTransferId;
+    }
+
+    public boolean handleMessage(Message msg) {
+        if (msg.what == MSG_NEXT_TRANSFER_TIMER) {
+            // We didn't receive a new transfer in time, finalize this one
+            if (mIncoming) {
+                processFiles();
+            } else {
+                updateStateAndNotification(STATE_SUCCESS);
+            }
+            return true;
+        } else if (msg.what == MSG_TRANSFER_TIMEOUT) {
+            // No update on this transfer for a while, check
+            // to see if it's still running, and fail it if it is.
+            if (isRunning()) {
+                updateStateAndNotification(STATE_FAILED);
+            }
+        }
+        return false;
+    }
+
+    public synchronized void onScanCompleted(String path, Uri uri) {
+        if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri);
+        if (uri != null) {
+            mMediaUris.put(path, uri);
+        }
+        mUrisScanned++;
+        if (mUrisScanned == mPaths.size()) {
+            // We're done
+            updateStateAndNotification(STATE_SUCCESS);
+        }
+    }
+
+    boolean checkMediaStorage(File path) {
+        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+            if (!path.isDirectory() && !path.mkdir()) {
+                Log.e(TAG, "Not dir or not mkdir " + path.getAbsolutePath());
+                return false;
+            }
+            return true;
+        } else {
+            Log.e(TAG, "External storage not mounted, can't store file.");
+            return false;
+        }
+    }
+
+    Intent buildViewIntent() {
+        if (mPaths.size() == 0) return null;
+
+        Intent viewIntent = new Intent(Intent.ACTION_VIEW);
+
+        String filePath = mPaths.get(0);
+        Uri mediaUri = mMediaUris.get(filePath);
+        Uri uri =  mediaUri != null ? mediaUri :
+            Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath);
+        viewIntent.setDataAndTypeAndNormalize(uri, mMimeTypes.get(filePath));
+        viewIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        return viewIntent;
+    }
+
+    PendingIntent buildCancelIntent() {
+        Intent intent = new Intent(HandoverService.ACTION_CANCEL_HANDOVER_TRANSFER);
+        intent.putExtra(HandoverService.EXTRA_SOURCE_ADDRESS, mRemoteDevice.getAddress());
+        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+
+        return pi;
+    }
+
+    File generateUniqueDestination(String path, String fileName) {
+        int dotIndex = fileName.lastIndexOf(".");
+        String extension = null;
+        String fileNameWithoutExtension = null;
+        if (dotIndex < 0) {
+            extension = "";
+            fileNameWithoutExtension = fileName;
+        } else {
+            extension = fileName.substring(dotIndex);
+            fileNameWithoutExtension = fileName.substring(0, dotIndex);
+        }
+        File dstFile = new File(path + File.separator + fileName);
+        int count = 0;
+        while (dstFile.exists()) {
+            dstFile = new File(path + File.separator + fileNameWithoutExtension + "-" +
+                    Integer.toString(count) + extension);
+            count++;
+        }
+        return dstFile;
+    }
+
+    File generateMultiplePath(String beamRoot) {
+        // Generate a unique directory with the date
+        String format = "yyyy-MM-dd";
+        SimpleDateFormat sdf = new SimpleDateFormat(format);
+        String newPath = beamRoot + "beam-" + sdf.format(new Date());
+        File newFile = new File(newPath);
+        int count = 0;
+        while (newFile.exists()) {
+            newPath = beamRoot + "beam-" + sdf.format(new Date()) + "-" +
+                    Integer.toString(count);
+            newFile = new File(newPath);
+            count++;
+        }
+        return newFile;
+    }
+}
+
diff --git a/src/com/android/nfc/handover/PendingHandoverTransfer.java b/src/com/android/nfc/handover/PendingHandoverTransfer.java
new file mode 100644
index 0000000..db5c68b
--- /dev/null
+++ b/src/com/android/nfc/handover/PendingHandoverTransfer.java
@@ -0,0 +1,63 @@
+package com.android.nfc.handover;
+
+import android.bluetooth.BluetoothDevice;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class PendingHandoverTransfer implements Parcelable {
+    public int id;
+    public boolean incoming;
+    public BluetoothDevice remoteDevice;
+    public boolean remoteActivating;
+    public Uri[] uris;
+
+    PendingHandoverTransfer(int id, boolean incoming, BluetoothDevice remoteDevice,
+            boolean remoteActivating, Uri[] uris) {
+        this.id = id;
+        this.incoming = incoming;
+        this.remoteDevice = remoteDevice;
+        this.remoteActivating = remoteActivating;
+        this.uris = uris;
+    }
+
+    public static final Parcelable.Creator<PendingHandoverTransfer> CREATOR
+            = new Parcelable.Creator<PendingHandoverTransfer>() {
+        public PendingHandoverTransfer createFromParcel(Parcel in) {
+            int id = in.readInt();
+            boolean incoming = (in.readInt() == 1) ? true : false;
+            BluetoothDevice remoteDevice = in.readParcelable(getClass().getClassLoader());
+            boolean remoteActivating = (in.readInt() == 1) ? true : false;
+            int numUris = in.readInt();
+            Uri[] uris = null;
+            if (numUris > 0) {
+                uris = new Uri[numUris];
+                in.readTypedArray(uris, Uri.CREATOR);
+            }
+            return new PendingHandoverTransfer(id, incoming, remoteDevice,
+                    remoteActivating, uris);
+        }
+
+        @Override
+        public PendingHandoverTransfer[] newArray(int size) {
+            return new PendingHandoverTransfer[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(id);
+        dest.writeInt(incoming ? 1 : 0);
+        dest.writeParcelable(remoteDevice, 0);
+        dest.writeInt(remoteActivating ? 1 : 0);
+        dest.writeInt(uris != null ? uris.length : 0);
+        if (uris != null && uris.length > 0) {
+            dest.writeTypedArray(uris, 0);
+        }
+    }
+}