Move the notification logic out to a listener
so there can be an Activity based version
of the listener as well for NFC VCARD imports.

Removed the now unused showImmediatley from
ImportRequest.

Change-Id: I37292676239444516bcc11486fc53e69b869dfa6
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0e28ae8..078bb2c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -599,17 +599,23 @@
             android:configChanges="orientation|screenSize|keyboardHidden"
             android:theme="@style/BackgroundOnly">
             <intent-filter>
-                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
-                <data android:mimeType="text/x-vcard" />
-                <data android:mimeType="text/x-vCard" />
-                <data android:mimeType="text/vcard" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-            <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <data android:mimeType="text/directory" />
                 <data android:mimeType="text/vcard" />
                 <data android:mimeType="text/x-vcard" />
+                <data android:mimeType="text/x-vCard" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".vcard.NfcImportVCardActivity"
+            android:configChanges="orientation|screenSize|keyboardHidden"
+            android:theme="@style/BackgroundOnly">
+            <intent-filter>
+                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
+                <data android:mimeType="text/vcard" />
+                <data android:mimeType="text/x-vcard" />
+                <data android:mimeType="text/x-vCard" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
diff --git a/src/com/android/contacts/vcard/CancelActivity.java b/src/com/android/contacts/vcard/CancelActivity.java
index b1428bb..c890607 100644
--- a/src/com/android/contacts/vcard/CancelActivity.java
+++ b/src/com/android/contacts/vcard/CancelActivity.java
@@ -72,7 +72,6 @@
     private int mJobId;
     private String mDisplayName;
     private int mType;
-    private Messenger mMessenger;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -121,29 +120,21 @@
     }
 
     @Override
-    public void onServiceConnected(ComponentName name, IBinder service) {
-        mMessenger = new Messenger(service);
+    public void onServiceConnected(ComponentName name, IBinder binder) {
+        VCardService service = ((VCardService.MyBinder) binder).getService();
 
-        boolean callFinish = false;
         try {
             final CancelRequest request = new CancelRequest(mJobId, mDisplayName);
-            mMessenger.send(Message.obtain(null, VCardService.MSG_CANCEL_REQUEST, request));
-            callFinish = true;
-        } catch (RemoteException e) {
-            Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
-            showDialog(R.id.dialog_cancel_failed);
-            // finish() should be called from the Dialog
+            service.handleCancelRequest(request, null);
         } finally {
             unbindService(this);
         }
 
-        if (callFinish) {
-            finish();
-        }
+        finish();
     }
 
     @Override
     public void onServiceDisconnected(ComponentName name) {
-        mMessenger = null;
+        // do nothing
     }
 }
diff --git a/src/com/android/contacts/vcard/ExportProcessor.java b/src/com/android/contacts/vcard/ExportProcessor.java
index 6dc2c34..b52839b 100644
--- a/src/com/android/contacts/vcard/ExportProcessor.java
+++ b/src/com/android/contacts/vcard/ExportProcessor.java
@@ -235,9 +235,11 @@
         final String tickerText =
                 mService.getString(R.string.exporting_contact_list_title);
         final Notification notification =
-                VCardService.constructProgressNotification(mService, VCardService.TYPE_EXPORT,
-                        description, tickerText, mJobId, displayName, totalCount, currentCount);
-        mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, notification);
+                NotificationImportExportListener.constructProgressNotification(mService,
+                        VCardService.TYPE_EXPORT, description, tickerText, mJobId, displayName,
+                        totalCount, currentCount);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                mJobId, notification);
     }
 
     private void doCancelNotification() {
@@ -245,16 +247,19 @@
         final String description = mService.getString(R.string.exporting_vcard_canceled_title,
                 mExportRequest.destUri.getLastPathSegment());
         final Notification notification =
-                VCardService.constructCancelNotification(mService, description);
-        mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, notification);
+                NotificationImportExportListener.constructCancelNotification(mService, description);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                mJobId, notification);
     }
 
     private void doFinishNotification(final String title, final String description) {
         if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
         final Intent intent = new Intent(mService, PeopleActivity.class);
         final Notification notification =
-                VCardService.constructFinishNotification(mService, title, description, intent);
-        mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, notification);
+                NotificationImportExportListener.constructFinishNotification(mService, title,
+                        description, intent);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                mJobId, notification);
     }
 
     @Override
diff --git a/src/com/android/contacts/vcard/ExportVCardActivity.java b/src/com/android/contacts/vcard/ExportVCardActivity.java
index 8bd9899..a0fd4d8 100644
--- a/src/com/android/contacts/vcard/ExportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ExportVCardActivity.java
@@ -110,7 +110,7 @@
      */
     private volatile boolean mProcessOngoing = true;
 
-    private Messenger mOutgoingMessenger;
+    private VCardService mService;
     private final Messenger mIncomingMessenger = new Messenger(new IncomingHandler());
 
     // Used temporarily when asking users to confirm the file name
@@ -138,10 +138,8 @@
                 }
                 final ExportRequest request = new ExportRequest(mDestinationUri);
                 // The connection object will call finish().
-                if (trySend(Message.obtain(null, VCardService.MSG_EXPORT_REQUEST, request))) {
-                    Log.i(LOG_TAG, "Successfully sent export request. Finish itself");
-                    unbindAndFinish();
-                }
+                mService.handleExportRequest(request, new NotificationImportExportListener(
+                        ExportVCardActivity.this));
             }
         }
     }
@@ -160,14 +158,16 @@
             return;
         }
 
-        if (startService(new Intent(this, VCardService.class)) == null) {
+        Intent intent = new Intent(this, VCardService.class);
+
+        if (startService(intent) == null) {
             Log.e(LOG_TAG, "Failed to start vCard service");
             mErrorReason = getString(R.string.fail_reason_unknown);
             showDialog(R.id.dialog_fail_to_export_with_reason);
             return;
         }
 
-        if (!bindService(new Intent(this, VCardService.class), this, Context.BIND_AUTO_CREATE)) {
+        if (!bindService(intent, this, Context.BIND_AUTO_CREATE)) {
             Log.e(LOG_TAG, "Failed to connect to vCard service.");
             mErrorReason = getString(R.string.fail_reason_unknown);
             showDialog(R.id.dialog_fail_to_export_with_reason);
@@ -176,14 +176,11 @@
     }
 
     @Override
-    public synchronized void onServiceConnected(ComponentName name, IBinder service) {
+    public synchronized void onServiceConnected(ComponentName name, IBinder binder) {
         if (DEBUG) Log.d(LOG_TAG, "connected to service, requesting a destination file name");
         mConnected = true;
-        mOutgoingMessenger = new Messenger(service);
-        final Message message =
-                Message.obtain(null, VCardService.MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION);
-        message.replyTo = mIncomingMessenger;
-        trySend(message);
+        mService = ((VCardService.MyBinder) binder).getService();
+        mService.handleRequestAvailableExportDestination(mIncomingMessenger);
         // Wait until MSG_SET_AVAILABLE_EXPORT_DESTINATION message is available.
     }
 
@@ -191,7 +188,7 @@
     @Override
     public synchronized void onServiceDisconnected(ComponentName name) {
         if (DEBUG) Log.d(LOG_TAG, "onServiceDisconnected()");
-        mOutgoingMessenger = null;
+        mService = null;
         mConnected = false;
         if (mProcessOngoing) {
             // Unexpected disconnect event.
@@ -267,19 +264,6 @@
         }
     }
 
-    private boolean trySend(Message message) {
-        try {
-            mOutgoingMessenger.send(message);
-            return true;
-        } catch (RemoteException e) {
-            Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
-            unbindService(this);
-            mErrorReason = getString(R.string.fail_reason_unknown);
-            showDialog(R.id.dialog_fail_to_export_with_reason);
-            return false;
-        }
-    }
-
     @Override
     public void onClick(DialogInterface dialog, int which) {
         if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onClick() is called");
diff --git a/src/com/android/contacts/vcard/ImportProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java
index 3092087..e8a9193 100644
--- a/src/com/android/contacts/vcard/ImportProcessor.java
+++ b/src/com/android/contacts/vcard/ImportProcessor.java
@@ -15,9 +15,10 @@
  */
 package com.android.contacts.vcard;
 
-import com.android.contacts.R;
+import com.android.vcard.VCardEntry;
 import com.android.vcard.VCardEntryCommitter;
 import com.android.vcard.VCardEntryConstructor;
+import com.android.vcard.VCardEntryHandler;
 import com.android.vcard.VCardInterpreter;
 import com.android.vcard.VCardParser;
 import com.android.vcard.VCardParser_V21;
@@ -28,14 +29,8 @@
 import com.android.vcard.exception.VCardVersionException;
 
 import android.accounts.Account;
-import android.app.Notification;
-import android.app.NotificationManager;
 import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
 import android.net.Uri;
-import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
 
 import java.io.ByteArrayInputStream;
@@ -48,17 +43,15 @@
  * Class for processing one import request from a user. Dropped after importing requested Uri(s).
  * {@link VCardService} will create another object when there is another import request.
  */
-public class ImportProcessor extends ProcessorBase {
+public class ImportProcessor extends ProcessorBase implements VCardEntryHandler {
     private static final String LOG_TAG = "VCardImport";
     private static final boolean DEBUG = VCardService.DEBUG;
 
     private final VCardService mService;
     private final ContentResolver mResolver;
-    private final NotificationManager mNotificationManager;
     private final ImportRequest mImportRequest;
     private final int mJobId;
-
-    private final ImportProgressNotifier mNotifier;
+    private final VCardImportExportListener mListener;
 
     // TODO: remove and show appropriate message instead.
     private final List<Uri> mFailedUris = new ArrayList<Uri>();
@@ -68,17 +61,35 @@
     private volatile boolean mCanceled;
     private volatile boolean mDone;
 
-    public ImportProcessor(final VCardService service, final ImportRequest request,
-            final int jobId) {
+    private int mCurrentCount = 0;
+    private int mTotalCount = 0;
+
+    public ImportProcessor(final VCardService service, final VCardImportExportListener listener,
+            final ImportRequest request, final int jobId) {
         mService = service;
         mResolver = mService.getContentResolver();
-        mNotificationManager = (NotificationManager)
-                mService.getSystemService(Context.NOTIFICATION_SERVICE);
+        mListener = listener;
 
         mImportRequest = request;
         mJobId = jobId;
-        mNotifier = new ImportProgressNotifier(service, mNotificationManager, jobId,
-                request.displayName);
+    }
+
+    @Override
+    public void onStart() {
+        // do nothing
+    }
+
+    @Override
+    public void onEnd() {
+        // do nothing
+    }
+
+    @Override
+    public void onEntryCreated(VCardEntry entry) {
+        mCurrentCount++;
+        if (mListener != null) {
+            mListener.onImportParsed(mImportRequest, mJobId, entry, mCurrentCount, mTotalCount);
+        }
     }
 
     @Override
@@ -92,8 +103,8 @@
         try {
             runInternal();
 
-            if (isCancelled()) {
-                doCancelNotification();
+            if (isCancelled() && mListener != null) {
+                mListener.onImportCanceled(mImportRequest, mJobId);
             }
         } catch (OutOfMemoryError e) {
             Log.e(LOG_TAG, "OutOfMemoryError thrown during import", e);
@@ -136,15 +147,13 @@
         final int estimatedVCardType = request.estimatedVCardType;
         final String estimatedCharset = request.estimatedCharset;
         final int entryCount = request.entryCount;
-        mNotifier.addTotalCount(entryCount);
+        mTotalCount += entryCount;
 
         final VCardEntryConstructor constructor =
                 new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
         final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
         constructor.addEntryHandler(committer);
-        if (!request.showImmediately) {
-            constructor.addEntryHandler(mNotifier);
-        }
+        constructor.addEntryHandler(this);
 
         InputStream is = null;
         boolean successful = false;
@@ -184,15 +193,17 @@
             } else {
                 Log.i(LOG_TAG, "Successfully finished importing one vCard file: " + uri);
                 List<Uri> uris = committer.getCreatedUris();
-                if (uris != null && uris.size() > 0) {
-                    // TODO: construct intent showing a list of imported contact list.
-                    doFinishNotification(uris.get(0));
-                } else {
-                    // Not critical, but suspicious.
-                    Log.w(LOG_TAG,
-                            "Created Uris is null or 0 length " +
-                            "though the creation itself is successful.");
-                    doFinishNotification(null);
+                if (mListener != null) {
+                    if (uris != null && uris.size() > 0) {
+                        // TODO: construct intent showing a list of imported contact list.
+                        mListener.onImportFinished(mImportRequest, mJobId, uris.get(0));
+                    } else {
+                        // Not critical, but suspicious.
+                        Log.w(LOG_TAG,
+                                "Created Uris is null or 0 length " +
+                                "though the creation itself is successful.");
+                        mListener.onImportFinished(mImportRequest, mJobId, null);
+                    }
                 }
             }
         } else {
@@ -201,39 +212,6 @@
         }
     }
 
-    private void doCancelNotification() {
-        final String description = mService.getString(R.string.importing_vcard_canceled_title,
-                mImportRequest.displayName);
-        final Notification notification =
-                VCardService.constructCancelNotification(mService, description);
-        mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, notification);
-    }
-
-    private void doFinishNotification(final Uri createdUri) {
-        final String description = mService.getString(R.string.importing_vcard_finished_title,
-                mImportRequest.displayName);
-        final Intent intent;
-        if (createdUri != null) {
-            final long rawContactId = ContentUris.parseId(createdUri);
-            final Uri contactUri = RawContacts.getContactLookupUri(
-                    mResolver, ContentUris.withAppendedId(
-                            RawContacts.CONTENT_URI, rawContactId));
-            intent = new Intent(Intent.ACTION_VIEW, contactUri);
-        } else {
-            intent = null;
-        }
-        if (mImportRequest.showImmediately && (intent != null)) {
-            mNotificationManager.cancel(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId);
-            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            mService.startActivity(intent);
-        } else {
-            final Notification notification = VCardService.constructFinishNotification(mService,
-                    description, null, intent);
-            mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId,
-                    notification);
-        }
-    }
-
     private boolean readOneVCard(InputStream is, int vcardType, String charset,
             final VCardInterpreter interpreter,
             final int[] possibleVCardVersions) {
diff --git a/src/com/android/contacts/vcard/ImportProgressNotifier.java b/src/com/android/contacts/vcard/ImportProgressNotifier.java
deleted file mode 100644
index 698487d..0000000
--- a/src/com/android/contacts/vcard/ImportProgressNotifier.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.contacts.vcard;
-
-import com.android.contacts.R;
-import com.android.vcard.VCardEntry;
-import com.android.vcard.VCardEntryHandler;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.content.Context;
-
-/**
- * {@link VCardEntryHandler} implementation letting the system update the current status of
- * vCard import.
- */
-public class ImportProgressNotifier implements VCardEntryHandler {
-    private static final String LOG_TAG = "VCardImport";
-
-    private final Context mContext;
-    private final NotificationManager mNotificationManager;
-    private final int mJobId;
-    private final String mDisplayName;
-
-    private int mCurrentCount;
-    private int mTotalCount;
-
-    public ImportProgressNotifier(
-            Context context, NotificationManager notificationManager,
-            int jobId, String displayName) {
-        mContext = context;
-        mNotificationManager = notificationManager;
-        mJobId = jobId;
-        mDisplayName = displayName;
-    }
-
-    public void onStart() {
-    }
-
-    public void onEntryCreated(VCardEntry contactStruct) {
-        mCurrentCount++;  // 1 origin.
-        if (contactStruct.isIgnorable()) {
-            return;
-        }
-
-        final String totalCountString;
-        synchronized (this) {
-            totalCountString = String.valueOf(mTotalCount);
-        }
-        final String tickerText =
-                mContext.getString(R.string.progress_notifier_message,
-                        String.valueOf(mCurrentCount),
-                        totalCountString,
-                        contactStruct.getDisplayName());
-        final String description = mContext.getString(R.string.importing_vcard_description,
-                contactStruct.getDisplayName());
-
-        final Notification notification = VCardService.constructProgressNotification(
-                mContext.getApplicationContext(), VCardService.TYPE_IMPORT, description, tickerText,
-                mJobId, mDisplayName, mTotalCount, mCurrentCount);
-        mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, notification);
-    }
-
-    public synchronized void addTotalCount(int additionalCount) {
-        mTotalCount += additionalCount;
-    }
-
-    public void onEnd() {
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ImportRequest.java b/src/com/android/contacts/vcard/ImportRequest.java
index 84fbb0e..68b5424 100644
--- a/src/com/android/contacts/vcard/ImportRequest.java
+++ b/src/com/android/contacts/vcard/ImportRequest.java
@@ -56,12 +56,6 @@
     public final String displayName;
 
     /**
-     * Whether to show the imported vcard immediately after the import is done.
-     * If set to false, just a notification will be shown.
-     */
-    public final boolean showImmediately;
-
-    /**
      * Can be {@link VCardSourceDetector#PARSE_TYPE_UNKNOWN}.
      */
     public final int estimatedVCardType;
@@ -103,7 +97,7 @@
 
     public ImportRequest(Account account,
             byte[] data, Uri uri, String displayName, int estimatedType, String estimatedCharset,
-            int vcardVersion, int entryCount, boolean showImmediately) {
+            int vcardVersion, int entryCount) {
         this.account = account;
         this.data = data;
         this.uri = uri;
@@ -112,6 +106,5 @@
         this.estimatedCharset = estimatedCharset;
         this.vcardVersion = vcardVersion;
         this.entryCount = entryCount;
-        this.showImmediately = showImmediately;
     }
 }
diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java
index 28b8540..2baa9cb 100644
--- a/src/com/android/contacts/vcard/ImportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ImportVCardActivity.java
@@ -128,6 +128,7 @@
 
     private VCardCacheThread mVCardCacheThread;
     private ImportRequestConnection mConnection;
+    /* package */ VCardImportExportListener mListener;
 
     private String mErrorMessage;
 
@@ -188,23 +189,16 @@
     private CancelListener mCancelListener = new CancelListener();
 
     private class ImportRequestConnection implements ServiceConnection {
-        private Messenger mMessenger;
+        private VCardService mService;
 
         public void sendImportRequest(final List<ImportRequest> requests) {
             Log.i(LOG_TAG, "Send an import request");
-            try {
-                mMessenger.send(Message.obtain(null,
-                        VCardService.MSG_IMPORT_REQUEST,
-                        requests));
-            } catch (RemoteException e) {
-                Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
-                runOnUiThread(new DialogDisplayer(getString(R.string.fail_reason_unknown)));
-            }
+            mService.handleImportRequest(requests, mListener);
         }
 
         @Override
-        public void onServiceConnected(ComponentName name, IBinder service) {
-            mMessenger = new Messenger(service);
+        public void onServiceConnected(ComponentName name, IBinder binder) {
+            mService = ((VCardService.MyBinder) binder).getService();
             Log.i(LOG_TAG,
                     String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
                             Arrays.toString(mVCardCacheThread.getSourceUris())));
@@ -233,7 +227,6 @@
         private final Uri[] mSourceUris;  // Given from a caller.
         private final byte[] mSource;
         private final String mDisplayName;
-        private final boolean mShowImmediately;
 
         public VCardCacheThread(final Uri[] sourceUris) {
             mSourceUris = sourceUris;
@@ -245,24 +238,6 @@
                     PowerManager.SCREEN_DIM_WAKE_LOCK |
                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
             mDisplayName = null;
-            // Showing immediately could make sense here if we restrict
-            // it to cases where we import a single vcard. For now disable
-            // this feature though.
-            mShowImmediately = false;
-        }
-
-        public VCardCacheThread(final byte[] data, String displayName,
-                final boolean showImmediately) {
-            mSource = data;
-            mSourceUris = null;
-            final Context context = ImportVCardActivity.this;
-            final PowerManager powerManager =
-                    (PowerManager)context.getSystemService(Context.POWER_SERVICE);
-            mWakeLock = powerManager.newWakeLock(
-                    PowerManager.SCREEN_DIM_WAKE_LOCK |
-                    PowerManager.ON_AFTER_RELEASE, LOG_TAG);
-            mDisplayName = displayName;
-            mShowImmediately = showImmediately;
         }
 
         @Override
@@ -301,8 +276,7 @@
                 ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
                 if (mSource != null) {
                     try {
-                        requests.add(constructImportRequest(mSource, null, mDisplayName,
-                                mShowImmediately));
+                        requests.add(constructImportRequest(mSource, null, mDisplayName));
                     } catch (VCardException e) {
                         Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
                         showFailureNotification(R.string.fail_reason_not_supported);
@@ -336,7 +310,7 @@
                         final ImportRequest request;
                         try {
                             request = constructImportRequest(null, localDataUri,
-                                    sourceUri.getLastPathSegment(), mShowImmediately);
+                                    sourceUri.getLastPathSegment());
                         } catch (VCardException e) {
                             Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
                             showFailureNotification(R.string.fail_reason_not_supported);
@@ -436,8 +410,7 @@
          * {@link ImportRequest#displayName}.
          */
         private ImportRequest constructImportRequest(final byte[] data,
-                final Uri localDataUri, final String displayName,
-                final boolean showImmediately)
+                final Uri localDataUri, final String displayName)
                 throws IOException, VCardException {
             final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
             VCardEntryCounter counter = null;
@@ -499,8 +472,7 @@
                     data, localDataUri, displayName,
                     detector.getEstimatedType(),
                     detector.getEstimatedCharset(),
-                    vcardVersion, counter.getCount(),
-                    showImmediately);
+                    vcardVersion, counter.getCount());
         }
 
         public Uri[] getSourceUris() {
@@ -760,19 +732,10 @@
 
     private void importVCard(final Uri[] uris) {
         runOnUiThread(new Runnable() {
+            @Override
             public void run() {
                 mVCardCacheThread = new VCardCacheThread(uris);
-                showDialog(R.id.dialog_cache_vcard);
-            }
-        });
-    }
-
-    private void importVCard(final byte[] data, final String displayName,
-            final boolean showImmediately) {
-        runOnUiThread(new Runnable() {
-            public void run() {
-                mVCardCacheThread = new VCardCacheThread(data, displayName,
-                        showImmediately);
+                mListener = new NotificationImportExportListener(ImportVCardActivity.this);
                 showDialog(R.id.dialog_cache_vcard);
             }
         });
@@ -888,32 +851,14 @@
 
     private void startImport() {
         Intent intent = getIntent();
-        if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
-            // Handle inbound NDEF
-            NdefMessage msg = (NdefMessage) intent.getParcelableArrayExtra(
-                    NfcAdapter.EXTRA_NDEF_MESSAGES)[0];
-            NdefRecord record = msg.getRecords()[0];
-            String type = new String(record.getType(), Charset.forName("UTF8"));
-            if (record.getTnf() != NdefRecord.TNF_MIME_MEDIA ||
-                    (!"text/x-vcard".equalsIgnoreCase(type) && !"text/vcard".equals(type))) {
-                Log.d(LOG_TAG, "Not a vcard");
-                showFailureNotification(R.string.fail_reason_not_supported);
-                finish();
-                return;
-            }
-            // For NFC imports, we always show the contact once import is
-            // complete.
-            importVCard(record.getPayload(), getString(R.string.nfc_vcard_file_name), true);
+        // Handle inbound files
+        Uri uri = intent.getData();
+        if (uri != null) {
+            Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
+            importVCard(uri);
         } else {
-            // Handle inbound files
-            Uri uri = intent.getData();
-            if (uri != null) {
-                Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
-                importVCard(uri);
-            } else {
-                Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
-                doScanExternalStorageAndImportVCard();
-            }
+            Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
+            doScanExternalStorageAndImportVCard();
         }
     }
 
@@ -975,13 +920,7 @@
                     mProgressDialogForCachingVCard.setMessage(message);
                     mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
                     mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread);
-                    mConnection = new ImportRequestConnection();
-
-                    Log.i(LOG_TAG, "Bind to VCardService.");
-                    // We don't want the service finishes itself just after this connection.
-                    startService(new Intent(this, VCardService.class));
-                    bindService(new Intent(this, VCardService.class),
-                            mConnection, Context.BIND_AUTO_CREATE);
+                    startVCardService();
                 }
                 return mProgressDialogForCachingVCard;
             }
@@ -1015,6 +954,17 @@
         return super.onCreateDialog(resId, bundle);
     }
 
+    /* package */ void startVCardService() {
+        mConnection = new ImportRequestConnection();
+
+        Log.i(LOG_TAG, "Bind to VCardService.");
+        // We don't want the service finishes itself just after this connection.
+        Intent intent = new Intent(this, VCardService.class);
+        startService(intent);
+        bindService(new Intent(this, VCardService.class),
+                mConnection, Context.BIND_AUTO_CREATE);
+    }
+
     @Override
     protected void onRestoreInstanceState(Bundle savedInstanceState) {
         super.onRestoreInstanceState(savedInstanceState);
@@ -1052,11 +1002,11 @@
         final NotificationManager notificationManager =
                 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
         final Notification notification =
-                VCardService.constructImportFailureNotification(
+                NotificationImportExportListener.constructImportFailureNotification(
                         ImportVCardActivity.this,
                         getString(reasonId));
-        notificationManager.notify(VCardService.FAILURE_NOTIFICATION_TAG, FAILURE_NOTIFICATION_ID,
-                notification);
+        notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
+                FAILURE_NOTIFICATION_ID, notification);
         mHandler.post(new Runnable() {
             @Override
             public void run() {
diff --git a/src/com/android/contacts/vcard/NfcImportVCardActivity.java b/src/com/android/contacts/vcard/NfcImportVCardActivity.java
new file mode 100644
index 0000000..d182cfd
--- /dev/null
+++ b/src/com/android/contacts/vcard/NfcImportVCardActivity.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.vcard;
+
+import com.android.contacts.R;
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.AccountWithDataSet;
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryCounter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.VCardSourceDetector;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+public class NfcImportVCardActivity extends Activity implements ServiceConnection,
+        VCardImportExportListener {
+    private static final String TAG = "NfcImportVCardActivity";
+
+    private static final int SELECT_ACCOUNT = 1;
+
+    private NdefRecord mRecord;
+    private AccountWithDataSet mAccount;
+
+    /* package */ class ImportTask extends AsyncTask<VCardService, Void, ImportRequest> {
+        @Override
+        public ImportRequest doInBackground(VCardService... services) {
+            ImportRequest request = createImportRequest();
+            if (request == null) {
+                return null;
+            }
+
+            ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
+            requests.add(request);
+            services[0].handleImportRequest(requests, NfcImportVCardActivity.this);
+            return request;
+        }
+
+        @Override
+        public void onCancelled() {
+            unbindService(NfcImportVCardActivity.this);
+        }
+
+        @Override
+        public void onPostExecute(ImportRequest request) {
+            unbindService(NfcImportVCardActivity.this);
+        }
+    }
+
+    /* package */ ImportRequest createImportRequest() {
+        VCardParser parser;
+        VCardEntryCounter counter = null;
+        VCardSourceDetector detector = null;
+        int vcardVersion = ImportVCardActivity.VCARD_VERSION_V21;
+        try {
+            ByteArrayInputStream is = new ByteArrayInputStream(mRecord.getPayload());
+            is.mark(0);
+            parser = new VCardParser_V21();
+            try {
+                counter = new VCardEntryCounter();
+                detector = new VCardSourceDetector();
+                parser.addInterpreter(counter);
+                parser.addInterpreter(detector);
+                parser.parse(is);
+            } catch (VCardVersionException e1) {
+                is.reset();
+                vcardVersion = ImportVCardActivity.VCARD_VERSION_V30;
+                parser = new VCardParser_V30();
+                try {
+                    counter = new VCardEntryCounter();
+                    detector = new VCardSourceDetector();
+                    parser.addInterpreter(counter);
+                    parser.addInterpreter(detector);
+                    parser.parse(is);
+                } catch (VCardVersionException e2) {
+                    return null;
+                }
+            } finally {
+                try {
+                    if (is != null) is.close();
+                } catch (IOException e) {
+                }
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Failed reading vcard data", e);
+            return null;
+        } catch (VCardNestedException e) {
+            Log.w(TAG, "Nested Exception is found (it may be false-positive).");
+            // Go through without throwing the Exception, as we may be able to detect the
+            // version before it
+        } catch (VCardException e) {
+            Log.e(TAG, "Error parsing vcard", e);
+            return null;
+        }
+
+        return new ImportRequest(mAccount, mRecord.getPayload(), null,
+                getString(R.string.nfc_vcard_file_name), detector.getEstimatedType(),
+                detector.getEstimatedCharset(), vcardVersion, counter.getCount());
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder binder) {
+        VCardService service = ((VCardService.MyBinder) binder).getService();
+        new ImportTask().execute(service);
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        // Do nothing
+    }
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        Intent intent = getIntent();
+        if (!NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
+            Log.w(TAG, "Unknowon intent " + intent);
+            finish();
+        }
+
+        NdefMessage msg = (NdefMessage) intent.getParcelableArrayExtra(
+                NfcAdapter.EXTRA_NDEF_MESSAGES)[0];
+        NdefRecord records[] = msg.getRecords();
+        if (records == null || records.length == 0) {
+            Log.w(TAG, "No records " + intent);
+            finish();
+        }
+
+        NdefRecord record = records[0];
+        String type = new String(record.getType(), Charset.forName("UTF8"));
+        if (record.getTnf() != NdefRecord.TNF_MIME_MEDIA ||
+                (!"text/x-vcard".equalsIgnoreCase(type) && !"text/vcard".equals(type))) {
+            Log.w(TAG, "Not a vcard");
+            //setStatus(getString(R.string.fail_reason_not_supported));
+            return;
+        }
+        mRecord = record;
+
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
+        final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
+        if (accountList.size() == 0) {
+            mAccount = null;
+        } else if (accountList.size() == 1) {
+            mAccount = accountList.get(0);
+        } else {
+            startActivityForResult(new Intent(this, SelectAccountActivity.class), SELECT_ACCOUNT);
+            return;
+        }
+
+        startImport();
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        if (requestCode == SELECT_ACCOUNT) {
+            if (resultCode == RESULT_OK) {
+                mAccount = new AccountWithDataSet(
+                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
+                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
+                        intent.getStringExtra(SelectAccountActivity.DATA_SET));
+                startImport();
+            } else {
+                finish();
+            }
+        }
+    }
+
+    private void startImport() {
+        // We don't want the service finishes itself just after this connection.
+        Intent intent = new Intent(this, VCardService.class);
+        startService(intent);
+        bindService(intent, this, Context.BIND_AUTO_CREATE);
+    }
+
+    @Override
+    public void onImportProcessed(ImportRequest request, int jobId, int sequence) {
+        // do nothing
+    }
+
+    @Override
+    public void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
+            int totalCount) {
+        // do nothing
+    }
+
+    @Override
+    public void onImportFinished(ImportRequest request, int jobId, Uri uri) {
+        if (isFinishing()) {
+            Log.i(TAG, "Late import -- ignoring");
+            return;
+        }
+
+        if (uri != null) {
+            Uri contactUri = RawContacts.getContactLookupUri(getContentResolver(), uri);
+            Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
+            startActivity(intent);
+            finish();
+        }
+    }
+
+    @Override
+    public void onImportFailed(ImportRequest request) {
+        if (isFinishing()) {
+            Log.i(TAG, "Late import failure -- ignoring");
+            return;
+        }
+        // TODO: report failure
+    }
+
+    @Override
+    public void onImportCanceled(ImportRequest request, int jobId) {
+        // do nothing
+    }
+
+    @Override
+    public void onExportProcessed(ExportRequest request, int jobId) {
+        // do nothing
+    }
+
+    @Override
+    public void onExportFailed(ExportRequest request) {
+        // do nothing
+    }
+
+    @Override
+    public void onCancelRequest(CancelRequest request, int type) {
+        // do nothing
+    }
+
+    @Override
+    public void onComplete() {
+        // do nothing
+    }
+}
diff --git a/src/com/android/contacts/vcard/NotificationImportExportListener.java b/src/com/android/contacts/vcard/NotificationImportExportListener.java
new file mode 100644
index 0000000..fb83cec
--- /dev/null
+++ b/src/com/android/contacts/vcard/NotificationImportExportListener.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *	    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.vcard;
+
+import com.android.contacts.R;
+import com.android.vcard.VCardEntry;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.ContactsContract.RawContacts;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+public class NotificationImportExportListener implements VCardImportExportListener,
+        Handler.Callback {
+    /** The tag used by vCard-related notifications. */
+    /* package */ static final String DEFAULT_NOTIFICATION_TAG = "VCardServiceProgress";
+    /**
+     * The tag used by vCard-related failure notifications.
+     * <p>
+     * Use a different tag from {@link #DEFAULT_NOTIFICATION_TAG} so that failures do not get
+     * replaced by other notifications and vice-versa.
+     */
+    /* package */ static final String FAILURE_NOTIFICATION_TAG = "VCardServiceFailure";
+
+    private final NotificationManager mNotificationManager;
+    private final Activity mContext;
+    private final Handler mHandler;
+
+    public NotificationImportExportListener(Activity activity) {
+        mContext = activity;
+        mNotificationManager = (NotificationManager) activity.getSystemService(
+                Context.NOTIFICATION_SERVICE);
+        mHandler = new Handler(this);
+    }
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        String text = (String) msg.obj;
+        Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
+        return true;
+    }
+
+    @Override
+    public void onImportProcessed(ImportRequest request, int jobId, int sequence) {
+        // Show a notification about the status
+        final String displayName;
+        final String message;
+        if (request.displayName != null) {
+            displayName = request.displayName;
+            message = mContext.getString(R.string.vcard_import_will_start_message, displayName);
+        } else {
+            displayName = mContext.getString(R.string.vcard_unknown_filename);
+            message = mContext.getString(
+                    R.string.vcard_import_will_start_message_with_default_name);
+        }
+
+        // We just want to show notification for the first vCard.
+        if (sequence == 0) {
+            // TODO: Ideally we should detect the current status of import/export and
+            // show "started" when we can import right now and show "will start" when
+            // we cannot.
+            mHandler.obtainMessage(0, message).sendToTarget();
+        }
+
+        final Notification notification = constructProgressNotification(mContext,
+                VCardService.TYPE_IMPORT, message, message, jobId, displayName, -1, 0);
+        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
+    }
+
+    @Override
+    public void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
+            int totalCount) {
+        if (entry.isIgnorable()) {
+            return;
+        }
+
+        final String totalCountString = String.valueOf(totalCount);
+        final String tickerText =
+                mContext.getString(R.string.progress_notifier_message,
+                        String.valueOf(currentCount),
+                        totalCountString,
+                        entry.getDisplayName());
+        final String description = mContext.getString(R.string.importing_vcard_description,
+                entry.getDisplayName());
+
+        final Notification notification = constructProgressNotification(
+                mContext.getApplicationContext(), VCardService.TYPE_IMPORT, description, tickerText,
+                jobId, request.displayName, totalCount, currentCount);
+        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
+    }
+
+    @Override
+    public void onImportFinished(ImportRequest request, int jobId, Uri createdUri) {
+        final String description = mContext.getString(R.string.importing_vcard_finished_title,
+                request.displayName);
+        final Intent intent;
+        if (createdUri != null) {
+            final long rawContactId = ContentUris.parseId(createdUri);
+            final Uri contactUri = RawContacts.getContactLookupUri(
+                    mContext.getContentResolver(), ContentUris.withAppendedId(
+                            RawContacts.CONTENT_URI, rawContactId));
+            intent = new Intent(Intent.ACTION_VIEW, contactUri);
+        } else {
+            intent = null;
+        }
+        final Notification notification =
+                NotificationImportExportListener.constructFinishNotification(mContext,
+                description, null, intent);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                jobId, notification);
+    }
+
+    @Override
+    public void onImportFailed(ImportRequest request) {
+        // TODO: a little unkind to show Toast in this case, which is shown just a moment.
+        // Ideally we should show some persistent something users can notice more easily.
+        mHandler.obtainMessage(0,
+                mContext.getString(R.string.vcard_import_request_rejected_message)).sendToTarget();
+    }
+
+    @Override
+    public void onImportCanceled(ImportRequest request, int jobId) {
+        final String description = mContext.getString(R.string.importing_vcard_canceled_title,
+                request.displayName);
+        final Notification notification =
+                NotificationImportExportListener.constructCancelNotification(mContext, description);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                jobId, notification);
+    }
+
+    @Override
+    public void onExportProcessed(ExportRequest request, int jobId) {
+        final String displayName = request.destUri.getLastPathSegment();
+        final String message = mContext.getString(R.string.vcard_export_will_start_message,
+                displayName);
+
+        mHandler.obtainMessage(0, message).sendToTarget();
+        final Notification notification =
+                NotificationImportExportListener.constructProgressNotification(mContext,
+                        VCardService.TYPE_EXPORT, message, message, jobId, displayName, -1, 0);
+        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
+    }
+
+    @Override
+    public void onExportFailed(ExportRequest request) {
+        mHandler.obtainMessage(0,
+                mContext.getString(R.string.vcard_export_request_rejected_message)).sendToTarget();
+    }
+
+    @Override
+    public void onCancelRequest(CancelRequest request, int type) {
+        final String description = type == VCardService.TYPE_IMPORT ?
+                mContext.getString(R.string.importing_vcard_canceled_title, request.displayName) :
+                mContext.getString(R.string.exporting_vcard_canceled_title, request.displayName);
+        final Notification notification = constructCancelNotification(mContext, description);
+        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, request.jobId, notification);
+    }
+
+    /**
+     * Constructs a {@link Notification} showing the current status of import/export.
+     * Users can cancel the process with the Notification.
+     *
+     * @param context
+     * @param type import/export
+     * @param description Content of the Notification.
+     * @param tickerText
+     * @param jobId
+     * @param displayName Name to be shown to the Notification (e.g. "finished importing XXXX").
+     * Typycally a file name.
+     * @param totalCount The number of vCard entries to be imported. Used to show progress bar.
+     * -1 lets the system show the progress bar with "indeterminate" state.
+     * @param currentCount The index of current vCard. Used to show progress bar.
+     */
+    /* package */ static Notification constructProgressNotification(
+            Context context, int type, String description, String tickerText,
+            int jobId, String displayName, int totalCount, int currentCount) {
+        final RemoteViews remoteViews =
+                new RemoteViews(context.getPackageName(),
+                        R.layout.status_bar_ongoing_event_progress_bar);
+        remoteViews.setTextViewText(R.id.status_description, description);
+        remoteViews.setProgressBar(R.id.status_progress_bar, totalCount, currentCount,
+                totalCount == -1);
+        final String percentage;
+        if (totalCount > 0) {
+            percentage = context.getString(R.string.percentage,
+                    String.valueOf(currentCount * 100/totalCount));
+        } else {
+            percentage = "";
+        }
+        remoteViews.setTextViewText(R.id.status_progress_text, percentage);
+        final int icon = (type == VCardService.TYPE_IMPORT ? android.R.drawable.stat_sys_download :
+                android.R.drawable.stat_sys_upload);
+        remoteViews.setImageViewResource(R.id.status_icon, icon);
+
+        final Notification notification = new Notification();
+        notification.icon = icon;
+        notification.tickerText = tickerText;
+        notification.contentView = remoteViews;
+        notification.flags |= Notification.FLAG_ONGOING_EVENT;
+
+        // Note: We cannot use extra values here (like setIntExtra()), as PendingIntent doesn't
+        // preserve them across multiple Notifications. PendingIntent preserves the first extras
+        // (when flag is not set), or update them when PendingIntent#getActivity() is called
+        // (See PendingIntent#FLAG_UPDATE_CURRENT). In either case, we cannot preserve extras as we
+        // expect (for each vCard import/export request).
+        //
+        // We use query parameter in Uri instead.
+        // Scheme and Authority is arbitorary, assuming CancelActivity never refers them.
+        final Intent intent = new Intent(context, CancelActivity.class);
+        final Uri uri = (new Uri.Builder())
+                .scheme("invalidscheme")
+                .authority("invalidauthority")
+                .appendQueryParameter(CancelActivity.JOB_ID, String.valueOf(jobId))
+                .appendQueryParameter(CancelActivity.DISPLAY_NAME, displayName)
+                .appendQueryParameter(CancelActivity.TYPE, String.valueOf(type)).build();
+        intent.setData(uri);
+
+        notification.contentIntent = PendingIntent.getActivity(context, 0, intent, 0);
+        return notification;
+    }
+
+    /**
+     * Constructs a Notification telling users the process is canceled.
+     *
+     * @param context
+     * @param description Content of the Notification
+     */
+    /* package */ static Notification constructCancelNotification(
+            Context context, String description) {
+        return new Notification.Builder(context)
+                .setAutoCancel(true)
+                .setSmallIcon(android.R.drawable.stat_notify_error)
+                .setContentTitle(description)
+                .setContentText(description)
+                .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0))
+                .getNotification();
+    }
+
+    /**
+     * Constructs a Notification telling users the process is finished.
+     *
+     * @param context
+     * @param description Content of the Notification
+     * @param intent Intent to be launched when the Notification is clicked. Can be null.
+     */
+    /* package */ static Notification constructFinishNotification(
+            Context context, String title, String description, Intent intent) {
+        return new Notification.Builder(context)
+                .setAutoCancel(true)
+                .setSmallIcon(android.R.drawable.stat_sys_download_done)
+                .setContentTitle(title)
+                .setContentText(description)
+                .setContentIntent(PendingIntent.getActivity(context, 0,
+                        (intent != null ? intent : new Intent()), 0))
+                .getNotification();
+    }
+
+    /**
+     * Constructs a Notification telling the vCard import has failed.
+     *
+     * @param context
+     * @param reason The reason why the import has failed. Shown in description field.
+     */
+    /* package */ static Notification constructImportFailureNotification(
+            Context context, String reason) {
+        return new Notification.Builder(context)
+                .setAutoCancel(true)
+                .setSmallIcon(android.R.drawable.stat_notify_error)
+                .setContentTitle(context.getString(R.string.vcard_import_failed))
+                .setContentText(reason)
+                .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0))
+                .getNotification();
+    }
+
+    @Override
+    public void onComplete() {
+        mContext.finish();
+    }
+}
diff --git a/src/com/android/contacts/vcard/VCardImportExportListener.java b/src/com/android/contacts/vcard/VCardImportExportListener.java
new file mode 100644
index 0000000..d3ef198
--- /dev/null
+++ b/src/com/android/contacts/vcard/VCardImportExportListener.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *	    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.vcard;
+
+import com.android.vcard.VCardEntry;
+
+import android.net.Uri;
+
+interface VCardImportExportListener {
+    void onImportProcessed(ImportRequest request, int jobId, int sequence);
+    void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
+            int totalCount);
+    void onImportFinished(ImportRequest request, int jobId, Uri uri);
+    void onImportFailed(ImportRequest request);
+    void onImportCanceled(ImportRequest request, int jobId);
+
+    void onExportProcessed(ExportRequest request, int jobId);
+    void onExportFailed(ExportRequest request);
+
+    void onCancelRequest(CancelRequest request, int type);
+    void onComplete();
+}
diff --git a/src/com/android/contacts/vcard/VCardService.java b/src/com/android/contacts/vcard/VCardService.java
index 0358e22..71b0214 100644
--- a/src/com/android/contacts/vcard/VCardService.java
+++ b/src/com/android/contacts/vcard/VCardService.java
@@ -17,16 +17,13 @@
 
 import com.android.contacts.R;
 
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.app.Service;
-import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
 import android.media.MediaScannerConnection;
 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
@@ -34,8 +31,6 @@
 import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.Log;
-import android.widget.RemoteViews;
-import android.widget.Toast;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -60,16 +55,6 @@
 public class VCardService extends Service {
     private final static String LOG_TAG = "VCardService";
 
-    /** The tag used by vCard-related notifications. */
-    /* package */ static final String DEFAULT_NOTIFICATION_TAG = "VCardServiceProgress";
-    /**
-     * The tag used by vCard-related failure notifications.
-     * <p>
-     * Use a different tag from {@link #DEFAULT_NOTIFICATION_TAG} so that failures do not get
-     * replaced by other notifications and vice-versa.
-     */
-    /* package */ static final String FAILURE_NOTIFICATION_TAG = "VCardServiceFailure";
-
     /* package */ final static boolean DEBUG = false;
 
     /* package */ static final int MSG_IMPORT_REQUEST = 1;
@@ -79,7 +64,7 @@
     /* package */ static final int MSG_SET_AVAILABLE_EXPORT_DESTINATION = 5;
 
     /**
-     * Specifies the type of operation. Used when constructing a {@link Notification}, canceling
+     * Specifies the type of operation. Used when constructing a notification, canceling
      * some operation, etc.
      */
     /* package */ static final int TYPE_IMPORT = 1;
@@ -87,34 +72,6 @@
 
     /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_";
 
-    private final Messenger mMessenger = new Messenger(new Handler() {
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_IMPORT_REQUEST: {
-                    handleImportRequest((List<ImportRequest>)msg.obj);
-                    break;
-                }
-                case MSG_EXPORT_REQUEST: {
-                    handleExportRequest((ExportRequest)msg.obj);
-                    break;
-                }
-                case MSG_CANCEL_REQUEST: {
-                    handleCancelRequest((CancelRequest)msg.obj);
-                    break;
-                }
-                case MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION: {
-                    handleRequestAvailableExportDestination(msg);
-                    break;
-                }
-                // TODO: add cancel capability for export..
-                default: {
-                    Log.w(LOG_TAG, "Received unknown request, ignoring it.");
-                    super.hasMessages(msg.what);
-                }
-            }
-        }
-    });
 
     private class CustomMediaScannerConnectionClient implements MediaScannerConnectionClient {
         final MediaScannerConnection mConnection;
@@ -143,8 +100,6 @@
         }
     }
 
-    private NotificationManager mNotificationManager;
-
     // Should be single thread, as we don't want to simultaneously handle import and export
     // requests.
     private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
@@ -173,16 +128,23 @@
     private String mFileNameExtension;
     private Set<String> mExtensionsToConsider;
     private String mErrorReason;
+    private MyBinder mBinder;
 
     // File names currently reserved by some export job.
     private final Set<String> mReservedDestination = new HashSet<String>();
     /* ** end of vCard exporter params ** */
 
-    @Override
+    public class MyBinder extends Binder {
+        public VCardService getService() {
+            return VCardService.this;
+        }
+    }
+
+   @Override
     public void onCreate() {
         super.onCreate();
+        mBinder = new MyBinder();
         if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
-        mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
         initExporterParams();
     }
 
@@ -218,7 +180,7 @@
 
     @Override
     public IBinder onBind(Intent intent) {
-        return mMessenger.getBinder();
+        return mBinder;
     }
 
     @Override
@@ -229,7 +191,8 @@
         super.onDestroy();
     }
 
-    private synchronized void handleImportRequest(List<ImportRequest> requests) {
+    public synchronized void handleImportRequest(List<ImportRequest> requests,
+            VCardImportExportListener listener) {
         if (DEBUG) {
             final ArrayList<String> uris = new ArrayList<String>();
             final ArrayList<String> displayNames = new ArrayList<String>();
@@ -245,71 +208,44 @@
         for (int i = 0; i < size; i++) {
             ImportRequest request = requests.get(i);
 
-            if (tryExecute(new ImportProcessor(this, request, mCurrentJobId))) {
-                if (!request.showImmediately) {
-                    // Show a notification about the status
-                    final String displayName;
-                    final String message;
-                    if (request.displayName != null) {
-                        displayName = request.displayName;
-                        message = getString(R.string.vcard_import_will_start_message, displayName);
-                    } else {
-                        displayName = getString(R.string.vcard_unknown_filename);
-                        message = getString(
-                                R.string.vcard_import_will_start_message_with_default_name);
-                    }
-
-                    // We just want to show notification for the first vCard.
-                    if (i == 0) {
-                        // TODO: Ideally we should detect the current status of import/export and
-                        // show "started" when we can import right now and show "will start" when
-                        // we cannot.
-                        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
-                    }
-
-                    final Notification notification = constructProgressNotification(this,
-                            TYPE_IMPORT, message, message, mCurrentJobId, displayName, -1, 0);
-                    mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG,
-                            mCurrentJobId, notification);
+            if (tryExecute(new ImportProcessor(this, listener, request, mCurrentJobId))) {
+                if (listener != null) {
+                    listener.onImportProcessed(request, mCurrentJobId, i);
                 }
                 mCurrentJobId++;
             } else {
-                // TODO: a little unkind to show Toast in this case, which is shown just a moment.
-                // Ideally we should show some persistent something users can notice more easily.
-                Toast.makeText(this, getString(R.string.vcard_import_request_rejected_message),
-                        Toast.LENGTH_LONG).show();
+                if (listener != null) {
+                    listener.onImportFailed(request);
+                }
                 // A rejection means executor doesn't run any more. Exit.
                 break;
             }
         }
     }
 
-    private synchronized void handleExportRequest(ExportRequest request) {
+    public synchronized void handleExportRequest(ExportRequest request,
+            VCardImportExportListener listener) {
         if (tryExecute(new ExportProcessor(this, request, mCurrentJobId))) {
-            final String displayName = request.destUri.getLastPathSegment();
-            final String message = getString(R.string.vcard_export_will_start_message,
-                    displayName);
-
             final String path = request.destUri.getEncodedPath();
             if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path);
             if (!mReservedDestination.add(path)) {
                 Log.w(LOG_TAG,
                         String.format("The path %s is already reserved. Reject export request",
                                 path));
-                Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
-                        Toast.LENGTH_LONG).show();
+                if (listener != null) {
+                    listener.onExportFailed(request);
+                }
                 return;
             }
 
-            Toast.makeText(this, message, Toast.LENGTH_LONG).show();
-            final Notification notification =
-                    constructProgressNotification(this, TYPE_EXPORT, message, message,
-                            mCurrentJobId, displayName, -1, 0);
-            mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mCurrentJobId, notification);
+            if (listener != null) {
+                listener.onExportProcessed(request, mCurrentJobId);
+            }
             mCurrentJobId++;
         } else {
-            Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
-                    Toast.LENGTH_LONG).show();
+            if (listener != null) {
+                listener.onExportFailed(request);
+            }
         }
     }
 
@@ -332,19 +268,19 @@
         }
     }
 
-    private synchronized void handleCancelRequest(CancelRequest request) {
+    public synchronized void handleCancelRequest(CancelRequest request,
+            VCardImportExportListener listener) {
         final int jobId = request.jobId;
         if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
         final ProcessorBase processor = mRunningJobMap.remove(jobId);
 
         if (processor != null) {
             processor.cancel(true);
-            final String description = processor.getType() == TYPE_IMPORT ?
-                    getString(R.string.importing_vcard_canceled_title, request.displayName) :
-                            getString(R.string.exporting_vcard_canceled_title, request.displayName);
-            final Notification notification = constructCancelNotification(this, description);
-            mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, jobId, notification);
-            if (processor.getType() == TYPE_EXPORT) {
+            final int type = processor.getType();
+            if (listener != null) {
+                listener.onCancelRequest(request, type);
+            }
+            if (type == TYPE_EXPORT) {
                 final String path =
                         ((ExportProcessor)processor).getRequest().destUri.getEncodedPath();
                 Log.i(LOG_TAG,
@@ -359,9 +295,8 @@
         stopServiceIfAppropriate();
     }
 
-    private synchronized void handleRequestAvailableExportDestination(Message msg) {
+    public synchronized void handleRequestAvailableExportDestination(final Messenger messenger) {
         if (DEBUG) Log.d(LOG_TAG, "Received available export destination request.");
-        final Messenger messenger = msg.replyTo;
         final String path = getAppropriateDestination(mTargetDirectory);
         final Message message;
         if (path != null) {
@@ -494,122 +429,6 @@
     }
 
     /**
-     * Constructs a {@link Notification} showing the current status of import/export.
-     * Users can cancel the process with the Notification.
-     *
-     * @param context
-     * @param type import/export
-     * @param description Content of the Notification.
-     * @param tickerText
-     * @param jobId
-     * @param displayName Name to be shown to the Notification (e.g. "finished importing XXXX").
-     * Typycally a file name.
-     * @param totalCount The number of vCard entries to be imported. Used to show progress bar.
-     * -1 lets the system show the progress bar with "indeterminate" state.
-     * @param currentCount The index of current vCard. Used to show progress bar.
-     */
-    /* package */ static Notification constructProgressNotification(
-            Context context, int type, String description, String tickerText,
-            int jobId, String displayName, int totalCount, int currentCount) {
-        final RemoteViews remoteViews =
-                new RemoteViews(context.getPackageName(),
-                        R.layout.status_bar_ongoing_event_progress_bar);
-        remoteViews.setTextViewText(R.id.status_description, description);
-        remoteViews.setProgressBar(R.id.status_progress_bar, totalCount, currentCount,
-                totalCount == -1);
-        final String percentage;
-        if (totalCount > 0) {
-            percentage = context.getString(R.string.percentage,
-                    String.valueOf(currentCount * 100/totalCount));
-        } else {
-            percentage = "";
-        }
-        remoteViews.setTextViewText(R.id.status_progress_text, percentage);
-        final int icon = (type == TYPE_IMPORT ? android.R.drawable.stat_sys_download :
-                android.R.drawable.stat_sys_upload);
-        remoteViews.setImageViewResource(R.id.status_icon, icon);
-
-        final Notification notification = new Notification();
-        notification.icon = icon;
-        notification.tickerText = tickerText;
-        notification.contentView = remoteViews;
-        notification.flags |= Notification.FLAG_ONGOING_EVENT;
-
-        // Note: We cannot use extra values here (like setIntExtra()), as PendingIntent doesn't
-        // preserve them across multiple Notifications. PendingIntent preserves the first extras
-        // (when flag is not set), or update them when PendingIntent#getActivity() is called
-        // (See PendingIntent#FLAG_UPDATE_CURRENT). In either case, we cannot preserve extras as we
-        // expect (for each vCard import/export request).
-        //
-        // We use query parameter in Uri instead.
-        // Scheme and Authority is arbitorary, assuming CancelActivity never refers them.
-        final Intent intent = new Intent(context, CancelActivity.class);
-        final Uri uri = (new Uri.Builder())
-                .scheme("invalidscheme")
-                .authority("invalidauthority")
-                .appendQueryParameter(CancelActivity.JOB_ID, String.valueOf(jobId))
-                .appendQueryParameter(CancelActivity.DISPLAY_NAME, displayName)
-                .appendQueryParameter(CancelActivity.TYPE, String.valueOf(type)).build();
-        intent.setData(uri);
-
-        notification.contentIntent = PendingIntent.getActivity(context, 0, intent, 0);
-        return notification;
-    }
-
-    /**
-     * Constructs a Notification telling users the process is canceled.
-     *
-     * @param context
-     * @param description Content of the Notification
-     */
-    /* package */ static Notification constructCancelNotification(
-            Context context, String description) {
-        return new Notification.Builder(context)
-                .setAutoCancel(true)
-                .setSmallIcon(android.R.drawable.stat_notify_error)
-                .setContentTitle(description)
-                .setContentText(description)
-                .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0))
-                .getNotification();
-    }
-
-    /**
-     * Constructs a Notification telling users the process is finished.
-     *
-     * @param context
-     * @param description Content of the Notification
-     * @param intent Intent to be launched when the Notification is clicked. Can be null.
-     */
-    /* package */ static Notification constructFinishNotification(
-            Context context, String title, String description, Intent intent) {
-        return new Notification.Builder(context)
-                .setAutoCancel(true)
-                .setSmallIcon(android.R.drawable.stat_sys_download_done)
-                .setContentTitle(title)
-                .setContentText(description)
-                .setContentIntent(PendingIntent.getActivity(context, 0,
-                        (intent != null ? intent : new Intent()), 0))
-                .getNotification();
-    }
-
-    /**
-     * Constructs a Notification telling the vCard import has failed.
-     *
-     * @param context
-     * @param reason The reason why the import has failed. Shown in description field.
-     */
-    /* package */ static Notification constructImportFailureNotification(
-            Context context, String reason) {
-        return new Notification.Builder(context)
-                .setAutoCancel(true)
-                .setSmallIcon(android.R.drawable.stat_notify_error)
-                .setContentTitle(context.getString(R.string.vcard_import_failed))
-                .setContentText(reason)
-                .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0))
-                .getNotification();
-    }
-
-    /**
      * Returns an appropriate file name for vCard export. Returns null when impossible.
      *
      * @return destination path for a vCard file to be exported. null on error and mErrorReason