Merge "Fixing twitter profile display"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 07ea96c..7437427 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -806,11 +806,17 @@
          [CHAR LIMIT=40] -->
     <string name="importing_vcard_canceled_title">Reading vCard data was canceled</string>
 
-    <!-- The message shown when vCard importer started running. -->
-    <string name="vcard_importer_start_message">vCard importer started.</string>
+    <!-- The message shown when vCard import request is accepted. The system may start that work soon, or do it later
+         when there are already other import/export requests. [CHAR LIMIT=30] -->
+    <string name="vcard_import_will_start_message">vCard importer will start shortly.</string>
+    <!-- The message shown when a given vCard import request is rejected by the system. [CHAR LIMIT=NONE] -->
+    <string name="vcard_import_request_rejected_message">vCard import request is rejected. Please try later.</string>
+    <!-- The message shown when vCard export request is accepted. The system may start that work soon, or do it later
+         when there are already other import/export requests. [CHAR LIMIT=30] -->
+    <string name="vcard_export_will_start_message">vCard exporter will start shortly.</string>
+    <!-- The message shown when a given vCard export request is rejected by the system. [CHAR LIMIT=NONE] -->
+    <string name="vcard_export_request_rejected_message">vCard export request is rejected. Please try later.</string>
 
-    <!-- The message shown when additional vCard to be imported is given during the import for others -->
-    <string name="vcard_importer_will_start_message">vCard importer will import the vCard after a while.</string>
 
     <!-- The percentage, used for expressing the progress of vCard import/export. -->
     <string name="percentage">%s%%</string>
@@ -844,9 +850,6 @@
          mention it here. -->
     <string name="fail_reason_too_long_filename">Required filename is too long (\"<xliff:g id="filename">%s</xliff:g>\")</string>
 
-    <!-- The message shown when vCard importer started running. -->
-    <string name="vcard_exporter_start_message">vCard exporter started.</string>
-
     <!-- The title shown when reading vCard is canceled (probably by a user) -->
     <string name="exporting_vcard_finished_title">Finished exporting vCard</string>
 
diff --git a/src/com/android/contacts/vcard/ExportProcessor.java b/src/com/android/contacts/vcard/ExportProcessor.java
index 2532cb2..4634b40 100644
--- a/src/com/android/contacts/vcard/ExportProcessor.java
+++ b/src/com/android/contacts/vcard/ExportProcessor.java
@@ -15,6 +15,11 @@
  */
 package com.android.contacts.vcard;
 
+import com.android.contacts.R;
+import com.android.contacts.activities.ContactBrowserActivity;
+import com.android.vcard.VCardComposer;
+import com.android.vcard.VCardConfig;
+
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -26,126 +31,78 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.contacts.R;
-import com.android.contacts.activities.ContactBrowserActivity;
-import com.android.vcard.VCardComposer;
-import com.android.vcard.VCardConfig;
-
 import java.io.FileNotFoundException;
 import java.io.OutputStream;
-import java.util.LinkedList;
-import java.util.Queue;
 
-public class ExportProcessor {
-    private static final String LOG_TAG = "ExportProcessor";
+/**
+ * Class for processing one export request from a user. Dropped after exporting requested Uri(s).
+ * {@link VCardService} will create another object when there is another export request.
+ */
+public class ExportProcessor implements Runnable {
+    private static final String LOG_TAG = "VCardExport";
 
-    private final Context mContext;
+    private final VCardService mService;
 
     private ContentResolver mResolver;
     private NotificationManager mNotificationManager;
 
-    boolean mCanceled;
+    private volatile boolean mCanceled;
 
-    boolean mReadyForRequest;
-    private final Queue<ExportRequest> mPendingRequests =
-        new LinkedList<ExportRequest>();
+    private ExportRequest mExportRequest; 
+    private int mJobId;
 
-    public ExportProcessor(Context context) {
-        mContext = context;
+    public ExportProcessor(VCardService service, ExportRequest exportRequest, int jobId) {
+        mService = service;
+        mResolver = service.getContentResolver();
+        mNotificationManager =
+                (NotificationManager)mService.getSystemService(Context.NOTIFICATION_SERVICE);
+        mExportRequest = exportRequest;
+        mJobId = jobId;
     }
 
-    /* package */ ThreadStarter mThreadStarter = new ThreadStarter() {
-        public void start() {
-            final Thread thread = new Thread(new Runnable() {
-                public void run() {
-                    process();
-                }
-            });
-            thread.start();
-        }
-    };
-
-    public synchronized void pushRequest(ExportRequest parameter) {
-        if (mResolver == null) {
-            // Service object may not ready at the construction time
-            // (e.g. ContentResolver may be null).
-            mResolver = mContext.getContentResolver();
-            mNotificationManager =
-                    (NotificationManager)mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-        }
-
-        final boolean needThreadStart;
-        if (!mReadyForRequest) {
-            needThreadStart = true;
-        } else {
-            needThreadStart = false;
-        }
-        mPendingRequests.add(parameter);
-        if (needThreadStart) {
-            mThreadStarter.start();
-        }
-
-        mReadyForRequest = true;
-    }
-
-    /* package */ void process() {
-        if (!mReadyForRequest) {
-            throw new RuntimeException(
-                    "process() is called before request being pushed "
-                    + "or after this object's finishing its processing.");
-        }
-
+    @Override
+    public void run() {
+        // ExecutorService ignores RuntimeException, so we need to show it here.
         try {
-            while (!mCanceled) {
-                final ExportRequest parameter;
-                synchronized (this) {
-                    if (mPendingRequests.size() == 0) {
-                        mReadyForRequest = false;
-                        break;
-                    } else {
-                        parameter = mPendingRequests.poll();
-                    }
-                }  // synchronized (this)
-                handleOneRequest(parameter);
-            }
-
-            doFinishNotification(mContext.getString(R.string.exporting_vcard_finished_title),
-                    "");
-        } finally {
-            // Not thread safe. Just in case.
-            // TODO: verify this works fine.
-            mReadyForRequest = false;
+            runInternal();
+        } catch (RuntimeException e) {
+            Log.e(LOG_TAG, "RuntimeException thrown during export", e);
+            throw e;
         }
     }
 
-    /* package */ void handleOneRequest(ExportRequest request) {
-        boolean shouldCallFinish = true;
+    private void runInternal() {
+        Log.i(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
+        final ExportRequest request = mExportRequest;
         VCardComposer composer = null;
+        boolean successful = false;
         try {
             final Uri uri = request.destUri;
             final OutputStream outputStream;
             try {
                 outputStream = mResolver.openOutputStream(uri);
             } catch (FileNotFoundException e) {
+                Log.w(LOG_TAG, "FileNotFoundException thrown", e);
                 // Need concise title.
 
                 final String errorReason =
-                    mContext.getString(R.string.fail_reason_could_not_open_file,
+                    mService.getString(R.string.fail_reason_could_not_open_file,
                             uri, e.getMessage());
-                shouldCallFinish = false;
+                Log.i(LOG_TAG, "failed to export (could not open output stream)");
                 doFinishNotification(errorReason, "");
                 return;
             }
+
             final String exportType = request.exportType;
             final int vcardType;
             if (TextUtils.isEmpty(exportType)) {
                 vcardType = VCardConfig.getVCardTypeFromString(
-                        mContext.getString(R.string.config_export_vcard_type));
+                        mService.getString(R.string.config_export_vcard_type));
             } else {
                 vcardType = VCardConfig.getVCardTypeFromString(exportType);
             }
 
-            composer = new VCardComposer(mContext, vcardType, true);
+            composer = new VCardComposer(mService, vcardType, true);
 
             // for test
             // int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
@@ -155,12 +112,13 @@
             composer.addHandler(composer.new HandlerForOutputStream(outputStream));
 
             if (!composer.init()) {
+                Log.w(LOG_TAG, "vCard composer init failed");
                 final String errorReason = composer.getErrorReason();
                 Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
                 final String translatedErrorReason =
                         translateComposerError(errorReason);
                 final String title =
-                        mContext.getString(R.string.fail_reason_could_not_initialize_exporter,
+                        mService.getString(R.string.fail_reason_could_not_initialize_exporter,
                                 translatedErrorReason);
                 doFinishNotification(title, "");
                 return;
@@ -169,7 +127,7 @@
             final int total = composer.getCount();
             if (total == 0) {
                 final String title =
-                        mContext.getString(R.string.fail_reason_no_exportable_contact);
+                        mService.getString(R.string.fail_reason_no_exportable_contact);
                 doFinishNotification(title, "");
                 return;
             }
@@ -185,7 +143,7 @@
                     final String translatedErrorReason =
                             translateComposerError(errorReason);
                     final String title =
-                            mContext.getString(R.string.fail_reason_error_occurred_during_export,
+                            mService.getString(R.string.fail_reason_error_occurred_during_export,
                                     translatedErrorReason);
                     doFinishNotification(title, "");
                     return;
@@ -193,15 +151,21 @@
                 doProgressNotification(uri, total, current);
                 current++;
             }
+            Log.i(LOG_TAG, "Successfully finished exporting vCard " + request.destUri);
+
+            successful = true;
+            // TODO: Show "successful"
         } finally {
             if (composer != null) {
                 composer.terminate();
             }
+
+            mService.handleFinishExportNotification(mJobId, successful);
         }
     }
 
     private String translateComposerError(String errorMessage) {
-        final Resources resources = mContext.getResources();
+        final Resources resources = mService.getResources();
         if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
             return resources.getString(R.string.composer_failed_to_get_database_infomation);
         } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
@@ -214,11 +178,11 @@
     }
 
     private void doProgressNotification(Uri uri, int total, int current) {
-        final String title = mContext.getString(R.string.exporting_contact_list_title);
+        final String title = mService.getString(R.string.exporting_contact_list_title);
         final String description =
-                mContext.getString(R.string.exporting_contact_list_message, uri);
+                mService.getString(R.string.exporting_contact_list_message, uri);
 
-        /* TODO: fix this
+        /* TODO: we should show more informative Notification to users.
         final RemoteViews remoteViews = new RemoteViews(mService.getPackageName(),
                 R.layout.status_bar_ongoing_event_progress_bar);
         remoteViews.setTextViewText(R.id.status_description, message);
@@ -244,7 +208,7 @@
                 description,
                 when);
 
-        final Context context = mContext.getApplicationContext();
+        final Context context = mService.getApplicationContext();
         final PendingIntent pendingIntent =
                 PendingIntent.getActivity(context, 0,
                         new Intent(context, ContactBrowserActivity.class),
@@ -258,10 +222,15 @@
         final Notification notification = new Notification();
         notification.icon = android.R.drawable.stat_sys_upload_done;
         notification.flags |= Notification.FLAG_AUTO_CANCEL;
-        notification.setLatestEventInfo(mContext, title, message, null);
-        final Intent intent = new Intent(mContext, ContactBrowserActivity.class);
+        notification.setLatestEventInfo(mService, title, message, null);
+        final Intent intent = new Intent(mService, ContactBrowserActivity.class);
         notification.contentIntent =
-                PendingIntent.getActivity(mContext, 0, intent, 0);
+                PendingIntent.getActivity(mService, 0, intent, 0);
         mNotificationManager.notify(VCardService.EXPORT_NOTIFICATION_ID, notification);
     }
+
+    public void cancel() {
+        Log.i(LOG_TAG, "received cancel request");
+        mCanceled = true;
+    }
 }
diff --git a/src/com/android/contacts/vcard/ExportVCardActivity.java b/src/com/android/contacts/vcard/ExportVCardActivity.java
index fbe6d48..d53d5b2 100644
--- a/src/com/android/contacts/vcard/ExportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ExportVCardActivity.java
@@ -51,7 +51,7 @@
  * dialogs stuffs (like how onCreateDialog() is used).
  */
 public class ExportVCardActivity extends Activity {
-    private static final String LOG_TAG = "ExportVCardActivity";
+    private static final String LOG_TAG = "VCardExport";
 
     // If true, VCardExporter is able to emits files longer than 8.3 format.
     private static final boolean ALLOW_LONG_FILE_NAME = false;
@@ -75,8 +75,7 @@
         private Queue<ExportRequest> mPendingRequests = new LinkedList<ExportRequest>();
 
         public void doBindService() {
-            bindService(new Intent(ExportVCardActivity.this,
-                    VCardService.class), this, Context.BIND_AUTO_CREATE);
+
         }
 
         public synchronized void requestSend(final ExportRequest parameter) {
@@ -174,7 +173,10 @@
 
         public void onClick(DialogInterface dialog, int which) {
             if (which == DialogInterface.BUTTON_POSITIVE) {
-                mConnection.doBindService();
+                // We don't want the service finishes itself just after this connection.
+                startService(new Intent(ExportVCardActivity.this, VCardService.class));
+                bindService(new Intent(ExportVCardActivity.this, VCardService.class),
+                        mConnection, Context.BIND_AUTO_CREATE);
 
                 final ExportRequest request = new ExportRequest(mDestUri);
 
diff --git a/src/com/android/contacts/vcard/ImportProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java
index 0e6acee..ca7223d 100644
--- a/src/com/android/contacts/vcard/ImportProcessor.java
+++ b/src/com/android/contacts/vcard/ImportProcessor.java
@@ -15,25 +15,6 @@
  */
 package com.android.contacts.vcard;
 
-import android.accounts.Account;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.net.Uri;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
-import android.provider.ContactsContract.RawContacts;
-import android.util.Log;
-
 import com.android.contacts.R;
 import com.android.vcard.VCardEntryCommitter;
 import com.android.vcard.VCardEntryConstructor;
@@ -46,210 +27,73 @@
 import com.android.vcard.exception.VCardNotSupportedException;
 import com.android.vcard.exception.VCardVersionException;
 
+import android.accounts.Account;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+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.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Queue;
 
 /**
- * Class for processing incoming import request from {@link ImportVCardActivity}.
- *
- * This class is designed so that a user ({@link Service}) does not need to (and should not)
- * recreate multiple instances, as this holds total count of vCard entries to be imported.
+ * 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 {
-    private static final String LOG_TAG = "VCardImporter";
+public class ImportProcessor implements Runnable {
+    private static final String LOG_TAG = "VCardImport";
 
-    private class ImportProcessorConnection implements ServiceConnection {
-        private Messenger mMessenger;
-        private boolean mBound;
-
-        public synchronized void tryBind() {
-            mContext.startService(new Intent(mContext, VCardService.class));
-            if (!mContext.bindService(new Intent(mContext, VCardService.class),
-                    this, Context.BIND_AUTO_CREATE)) {
-                throw new RuntimeException("Failed to bind to VCardService.");
-            }
-            mBound = true;
-        }
-
-        public synchronized void tryUnbind() {
-            if (mBound) {
-                mContext.unbindService(this);
-                // TODO: This is not appropriate. It would be better to send some "stop" request
-                // to service and let the service stop itself.
-                mContext.stopService(new Intent(mContext, VCardService.class));
-            } else {
-                // TODO: Not graceful.
-                Log.w(LOG_TAG, "unbind() is tried while ServiceConnection is not bound yet");
-            }
-            mBound = false;
-        }
-
-        public void sendFinishNotification() {
-            try {
-                mMessenger.send(Message.obtain(null,
-                        VCardService.MSG_NOTIFY_IMPORT_FINISHED,
-                        null));
-            } catch (RemoteException e) {
-                Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
-            }
-        }
-
-        @Override
-        public void onServiceConnected(ComponentName name, IBinder service) {
-            mMessenger = new Messenger(service);
-        }
-        @Override
-        public void onServiceDisconnected(ComponentName name) {
-            mMessenger = null;
-        }
-    }
-
-    private final Context mContext;
-    private ContentResolver mResolver;
-    private NotificationManager mNotificationManager;
+    private final VCardService mService;
+    private final ContentResolver mResolver;
+    private final NotificationManager mNotificationManager;
 
     private final List<Uri> mFailedUris = new ArrayList<Uri>();
-    private final List<Uri> mCreatedUris = new ArrayList<Uri>();
     private final ImportProgressNotifier mNotifier = new ImportProgressNotifier();
 
     private VCardParser mVCardParser;
 
-    /**
-     * Meaning a controller of this object requests the operation should be canceled
-     * or not, which implies {@link #mReadyForRequest} should be set to false soon, but
-     * it does not meaning cancel request is able to immediately stop this object,
-     * so we have two variables.
-     */
+    private ImportRequest mImportRequest;
+    private int mJobId;
+
     private boolean mCanceled;
 
-    /**
-     * Meaning that this object is able to accept import requests.
-     */
-    private boolean mReadyForRequest;
-    private final Queue<ImportRequest> mPendingRequests =
-            new LinkedList<ImportRequest>();
-
-    // For testability.
-    /* package */ ThreadStarter mThreadStarter = new ThreadStarter() {
-        public void start() {
-            final Thread thread = new Thread(new Runnable() {
-                public void run() {
-                    process();
-                }
-            });
-            thread.start();
-        }
-    };
-    /* package */ interface CommitterGenerator {
-        public VCardEntryCommitter generate(ContentResolver resolver);
-    }
-    /* package */ class DefaultCommitterGenerator implements CommitterGenerator {
-        public VCardEntryCommitter generate(ContentResolver resolver) {
-            return new VCardEntryCommitter(mResolver);
-        }
+    public ImportProcessor(final VCardService service, final ImportRequest request,
+            int jobId) {
+        mService = service;
+        mResolver = mService.getContentResolver();
+        mNotificationManager = (NotificationManager)
+                mService.getSystemService(Context.NOTIFICATION_SERVICE);
+        mNotifier.init(mService, mNotificationManager);
+        mImportRequest = request;
+        mJobId = jobId;
     }
 
-    private CommitterGenerator mCommitterGenerator =
-        new DefaultCommitterGenerator();
 
-    public ImportProcessor(final Context context) {
-        mContext = context;
-    }
-
-    /**
-     * Checks this object and initialize it if not.
-     *
-     * This method is needed since {@link VCardService} is not ready when this object is
-     * created and we need to delay this initialization, while we want to initialize
-     * this object soon in tests.
-     */
-    /* package */ void ensureInit() {
-        if (mResolver == null) {
-            // Service object may not ready at the construction time
-            // (e.g. ContentResolver may be null).
-            mResolver = mContext.getContentResolver();
-            mNotificationManager =
-                    (NotificationManager)mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-        }
-    }
-
-    public synchronized void pushRequest(final ImportRequest request) {
-        ensureInit();
-
-        final boolean needThreadStart;
-        if (!mReadyForRequest) {
-            mFailedUris.clear();
-            mCreatedUris.clear();
-
-            mNotifier.init(mContext, mNotificationManager);
-            needThreadStart = true;
-        } else {
-            needThreadStart = false;
-        }
-        final int count = request.entryCount;
-        if (count > 0) {
-            mNotifier.addTotalCount(count);
-        }
-        mPendingRequests.add(request);
-        if (needThreadStart) {
-            mThreadStarter.start();
-        }
-
-        mReadyForRequest = true;
-    }
-
-    /**
-     * Starts processing import requests. Never stops until all given requests are
-     * processed or some error happens, assuming this method is called from a
-     * {@link Thread} object.
-     */
-    private void process() {
-        if (!mReadyForRequest) {
-            throw new RuntimeException(
-                    "process() is called before request being pushed "
-                    + "or after this object's finishing its processing.");
-        }
-
-        final ImportProcessorConnection connection = new ImportProcessorConnection();
-        connection.tryBind();
+    @Override
+    public void run() {
+        // ExecutorService ignores RuntimeException, so we need to show it here.
         try {
-            while (!mCanceled) {
-                final ImportRequest parameter;
-                synchronized (this) {
-                    if (mPendingRequests.size() == 0) {
-                        mReadyForRequest = false;
-                        break;
-                    } else {
-                        parameter = mPendingRequests.poll();
-                    }
-                }  // synchronized (this)
-                handleOneRequest(parameter);
-            }
-
-            // Currenty we don't have an appropriate way to let users see all URIs imported.
-            // Instead, we show one only when there's just one created uri.
-            doFinishNotification(mCreatedUris.size() > 0 ? mCreatedUris.get(0) : null);
-            connection.sendFinishNotification();
-            Log.i(LOG_TAG, "Finished successfully importing all vCard");
-        } finally {
-            mReadyForRequest = false;  // Just in case.
-            mNotifier.resetTotalCount();
-            connection.tryUnbind();
+            runInternal();
+        } catch (RuntimeException e) {
+            Log.e(LOG_TAG, "RuntimeException thrown during import", e);
+            throw e;
         }
     }
 
-    /**
-     * Would be run inside synchronized block.
-     */
-    /* package */ boolean handleOneRequest(final ImportRequest request) {
+    private void runInternal() {
+        Log.i(LOG_TAG, String.format("vCard import (id: %d) has started.", mJobId));
+        final ImportRequest request = mImportRequest;
         if (mCanceled) {
-            Log.i(LOG_TAG, "Canceled before actually handling parameter ("
-                    + request.uri + ")");
-            return false;
+            Log.i(LOG_TAG, "Canceled before actually handling parameter (" + request.uri + ")");
+            return;
         }
         final int[] possibleVCardVersions;
         if (request.vcardVersion == ImportVCardActivity.VCARD_VERSION_AUTO_DETECT) {
@@ -274,29 +118,32 @@
 
         final VCardEntryConstructor constructor =
                 new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
-        final VCardEntryCommitter committer = mCommitterGenerator.generate(mResolver);
+        final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
         constructor.addEntryHandler(committer);
         constructor.addEntryHandler(mNotifier);
 
         final boolean successful =
             readOneVCard(uri, estimatedVCardType, estimatedCharset,
                     constructor, possibleVCardVersions);
+
+        mService.handleFinishImportNotification(mJobId, successful);
+
         if (successful) {
-            Log.i(LOG_TAG, "Successfully finished reading one vCard file finished: " + uri);
+            Log.i(LOG_TAG, "Successfully finished importing one vCard file: " + uri);
             List<Uri> uris = committer.getCreatedUris();
-            if (uris != null) {
-                mCreatedUris.addAll(uris);
+            if (uris != null && uris.size() > 0) {
+                doFinishNotification(uris.get(0));
             } else {
                 // Not critical, but suspicious.
                 Log.w(LOG_TAG,
-                        "Created Uris is null while the creation itself is successful.");
+                        "Created Uris is null or 0 length " +
+                        "though the creation itself is successful.");
+                doFinishNotification(null);
             }
         } else {
             Log.w(LOG_TAG, "Failed to read one vCard file: " + uri);
             mFailedUris.add(uri);
         }
-
-        return successful;
     }
 
     /*
@@ -319,10 +166,10 @@
 
         if (isCanceled()) {
             notification.icon = android.R.drawable.stat_notify_error;
-            title = mContext.getString(R.string.importing_vcard_canceled_title);
+            title = mService.getString(R.string.importing_vcard_canceled_title);
         } else {
             notification.icon = android.R.drawable.stat_sys_download_done;
-            title = mContext.getString(R.string.importing_vcard_finished_title);
+            title = mService.getString(R.string.importing_vcard_finished_title);
         }
 
         final Intent intent;
@@ -336,8 +183,8 @@
             intent = null;
         }
 
-        notification.setLatestEventInfo(mContext, title, "",
-                PendingIntent.getActivity(mContext, 0, intent, 0));
+        notification.setLatestEventInfo(mService, title, "",
+                PendingIntent.getActivity(mService, 0, intent, 0));
         mNotificationManager.notify(VCardService.IMPORT_NOTIFICATION_ID, notification);
     }
 
@@ -411,10 +258,6 @@
         return successful;
     }
 
-    public synchronized boolean isReadyForRequest() {
-        return mReadyForRequest;
-    }
-
     public boolean isCanceled() {
         return mCanceled;
     }
@@ -428,21 +271,4 @@
             }
         }
     }
-
-    public List<Uri> getCreatedUrisForTest() {
-        return mCreatedUris;
-    }
-
-    public List<Uri> getFailedUrisForTest() {
-        return mFailedUris;
-    }
-
-    public void injectCommitterGeneratorForTest(
-            final CommitterGenerator generator) {
-        mCommitterGenerator = generator;
-    }
-
-    public void initNotifierForTest() {
-        mNotifier.init(mContext, mNotificationManager);
-    }
 }
diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java
index 24df063..7a72ca2 100644
--- a/src/com/android/contacts/vcard/ImportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ImportVCardActivity.java
@@ -54,7 +54,6 @@
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.TextUtils;
-import android.text.format.DateUtils;
 import android.text.style.RelativeSizeSpan;
 import android.util.Log;
 
@@ -71,9 +70,7 @@
 import java.util.Arrays;
 import java.util.Date;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Queue;
 import java.util.Set;
 import java.util.Vector;
 
@@ -87,7 +84,7 @@
  * dialogs stuffs (like how onCreateDialog() is used).
  */
 public class ImportVCardActivity extends Activity {
-    private static final String LOG_TAG = "VCardImporter";
+    private static final String LOG_TAG = "VCardImport";
 
     private static final int SELECT_ACCOUNT = 0;
 
@@ -113,116 +110,16 @@
     private Uri mUri;
 
     private ProgressDialog mProgressDialogForScanVCard;
-    private ProgressDialog mProgressDialogForCacheVCard;
+    private ProgressDialog mProgressDialogForCachingVCard;
 
     private List<VCardFile> mAllVCardFileList;
     private VCardScanThread mVCardScanThread;
 
     private VCardCacheThread mVCardCacheThread;
+    private ImportRequestConnection mConnection;
 
     private String mErrorMessage;
 
-    private class CustomConnection implements ServiceConnection {
-        private Messenger mMessenger;
-        /**
-         * Stores {@link ImportRequest} objects until actual connection is established.
-         */
-        private Queue<ImportRequest> mPendingRequests =
-                new LinkedList<ImportRequest>();
-
-        private boolean mConnected = false;
-        private boolean mNeedToCallFinish = false;
-        private boolean mDisconnectAndFinishDone = false;
-
-        /**
-         * Tries to unbind this connection and call {@link ImportVCardActivity#finish()}.
-         * When timing is not appropriate, this object remembers this call and
-         * call {@link ImportVCardActivity#unbindService(ServiceConnection)} and
-         * {@link ImportVCardActivity#finish()} afterwards.
-         */
-        public void tryDisconnectAndFinish() {
-            synchronized (this) {
-                if (!mDisconnectAndFinishDone) {
-                    mNeedToCallFinish = true;
-                    if (mConnected) {
-                        // onServiceConnected() is already finished and we need to
-                        // "manually" call unbindService() here.
-                        unbindService(this);
-                        mConnected = false;
-                        mDisconnectAndFinishDone = true;
-                        finish();
-                    } else {
-                        // If not connected, finish() must be called when connected, as
-                        // We cannot call finish() now.
-                    }
-                }
-            }
-        }
-
-        public void requestSend(final ImportRequest parameter) {
-            synchronized (mPendingRequests) {
-                if (mMessenger != null) {
-                    sendMessage(parameter);
-                } else {
-                    mPendingRequests.add(parameter);
-                }
-            }
-        }
-
-        private void sendMessage(final ImportRequest request) {
-            try {
-                mMessenger.send(Message.obtain(null,
-                        VCardService.MSG_IMPORT_REQUEST,
-                        request));
-            } catch (RemoteException e) {
-                Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
-                runOnUiThread(new DialogDisplayer(
-                        getString(R.string.fail_reason_unknown)));
-            }
-        }
-
-        @Override
-        public void onServiceConnected(ComponentName name, IBinder service) {
-            synchronized (mPendingRequests) {
-                mMessenger = new Messenger(service);
-
-                // Send pending requests thrown from this Activity before an actual connection
-                // is established.
-                while (!mPendingRequests.isEmpty()) {
-                    final ImportRequest parameter = mPendingRequests.poll();
-                    if (parameter == null) {
-                        throw new NullPointerException();
-                    }
-                    sendMessage(parameter);
-                }
-            }
-
-            synchronized (this) {
-                if (!mDisconnectAndFinishDone) {
-                    mConnected = true;
-                    if (mNeedToCallFinish) {
-                        mDisconnectAndFinishDone = true;
-                        finish();
-                    }
-                }
-            }
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName name) {
-            synchronized (mPendingRequests) {
-                if (!mPendingRequests.isEmpty()) {
-                    Log.w(LOG_TAG, "Some request(s) are dropped.");
-                }
-            }
-
-            // Set to null so that we can detect inappropriate re-connection toward
-            // the Service via NullPointerException;
-            mPendingRequests = null;
-            mMessenger = null;
-        }
-    }
-
     private static class VCardFile {
         private final String mName;
         private final String mCanonicalPath;
@@ -275,25 +172,53 @@
 
     private CancelListener mCancelListener = new CancelListener();
 
+    private class ImportRequestConnection implements ServiceConnection {
+        private Messenger mMessenger;
+
+        public void sendImportRequest(final ImportRequest request) {
+            Log.i(LOG_TAG, String.format("Send an import request (Uri: %s)", request.uri));
+            try {
+                mMessenger.send(Message.obtain(null,
+                        VCardService.MSG_IMPORT_REQUEST,
+                        request));
+            } catch (RemoteException e) {
+                Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
+                runOnUiThread(new DialogDisplayer(getString(R.string.fail_reason_unknown)));
+            }
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mMessenger = new Messenger(service);
+            Log.i(LOG_TAG,
+                    String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
+                            Arrays.toString(mVCardCacheThread.getSourceUris())));
+            mVCardCacheThread.start();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.i(LOG_TAG, "Disconnected from VCardService");
+        }
+    }
+
     /**
-     * Caches all vCard data into local data directory so that we allow
-     * {@link VCardService} to access all the contents in given Uris, some of
-     * which may not be accessible from other components due to permission problem.
-     * (Activity which gives the Uri may allow only this Activity to access that content,
-     * not the other components like {@link VCardService}.
+     * Caches given vCard files into a local directory, and sends actual import request to
+     * {@link VCardService}.
      *
-     * We also allow the Service to happen to exit during the vCard import procedure.
+     * We need to cache given files into local storage. One of reasons is that some data (as Uri)
+     * may have special permissions. Callers may allow only this Activity to access that content,
+     * not what this Activity launched (like {@link VCardService}).
      */
     private class VCardCacheThread extends Thread
             implements DialogInterface.OnCancelListener {
         private boolean mCanceled;
         private PowerManager.WakeLock mWakeLock;
         private VCardParser mVCardParser;
-        private final Uri[] mSourceUris;
+        private final Uri[] mSourceUris;  // Given from a caller.
 
         public VCardCacheThread(final Uri[] sourceUris) {
             mSourceUris = sourceUris;
-            final int length = sourceUris.length;
             final Context context = ImportVCardActivity.this;
             final PowerManager powerManager =
                     (PowerManager)context.getSystemService(Context.POWER_SERVICE);
@@ -305,21 +230,27 @@
         @Override
         public void finalize() {
             if (mWakeLock != null && mWakeLock.isHeld()) {
+                Log.w(LOG_TAG, "WakeLock is being held.");
                 mWakeLock.release();
             }
         }
 
         @Override
         public void run() {
-            final Context context = ImportVCardActivity.this;
-            final ContentResolver resolver = context.getContentResolver();
-            String errorMessage = null;
+            Log.i(LOG_TAG, "vCard cache thread starts running.");
+            if (mConnection == null) {
+                throw new NullPointerException("vCard cache thread must be launched "
+                        + "after a service connection is established");
+            }
+
             mWakeLock.acquire();
-            final CustomConnection connection = new CustomConnection();
-            startService(new Intent(ImportVCardActivity.this, VCardService.class));
-            bindService(new Intent(ImportVCardActivity.this,
-                    VCardService.class), connection, Context.BIND_AUTO_CREATE);
             try {
+                if (mCanceled == true) {
+                    Log.i(LOG_TAG, "vCard cache operation is canceled.");
+                    return;
+                }
+
+                final Context context = ImportVCardActivity.this;
                 // Uris given from caller applications may not be opened twice: consider when
                 // it is not from local storage (e.g. "file:///...") but from some special
                 // provider (e.g. "content://...").
@@ -346,31 +277,36 @@
                     }
                     final Uri localDataUri = copyTo(sourceUri, filename);
                     if (mCanceled) {
+                        Log.i(LOG_TAG, "vCard cache operation is canceled.");
                         break;
                     }
                     if (localDataUri == null) {
                         Log.w(LOG_TAG, "destUri is null");
                         break;
                     }
-                    final ImportRequest parameter = constructRequestParameter(localDataUri);
+                    final ImportRequest parameter = constructImportRequest(localDataUri);
                     if (mCanceled) {
+                        Log.i(LOG_TAG, "vCard cache operation is canceled.");
                         return;
                     }
-                    connection.requestSend(parameter);
+                    mConnection.sendImportRequest(parameter);
                 }
             } catch (OutOfMemoryError e) {
-                Log.e(LOG_TAG, "OutOfMemoryError");
+                Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
                 System.gc();
                 runOnUiThread(new DialogDisplayer(
                         getString(R.string.fail_reason_low_memory_during_import)));
             } catch (IOException e) {
-                Log.e(LOG_TAG, e.getMessage());
+                Log.e(LOG_TAG, "IOException during caching vCard", e);
                 runOnUiThread(new DialogDisplayer(
                         getString(R.string.fail_reason_io_error)));
             } finally {
+                Log.i(LOG_TAG, "Finished caching vCard.");
                 mWakeLock.release();
-                mProgressDialogForCacheVCard.dismiss();
-                connection.tryDisconnectAndFinish();
+                unbindService(mConnection);
+                mProgressDialogForCachingVCard.dismiss();
+                mProgressDialogForCachingVCard = null;
+                finish();
             }
         }
 
@@ -423,28 +359,27 @@
         }
 
         /**
-         * Reads the Uri once (or twice) and constructs {@link ImportRequest} from
+         * Reads the Uri (possibly multiple times) and constructs {@link ImportRequest} from
          * its content.
+         *
+         * Uri should be local one, as we cannot guarantee other types of Uris can be read
+         * multiple times.
          */
-        private ImportRequest constructRequestParameter(final Uri uri) {
-            final ContentResolver resolver =
-                    ImportVCardActivity.this.getContentResolver();
+        private ImportRequest constructImportRequest(final Uri localDataUri) {
+            final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
             VCardEntryCounter counter = null;
             VCardSourceDetector detector = null;
             VCardInterpreterCollection interpreter = null;
             int vcardVersion = VCARD_VERSION_V21;
             try {
                 boolean shouldUseV30 = false;
-                InputStream is;
-
-                is = resolver.openInputStream(uri);
+                InputStream is = resolver.openInputStream(localDataUri);
                 mVCardParser = new VCardParser_V21();
                 try {
                     counter = new VCardEntryCounter();
                     detector = new VCardSourceDetector();
-                    interpreter =
-                            new VCardInterpreterCollection(
-                                    Arrays.asList(counter, detector));
+                    interpreter = new VCardInterpreterCollection(
+                            Arrays.asList(counter, detector));
                     mVCardParser.parse(is, interpreter);
                 } catch (VCardVersionException e1) {
                     try {
@@ -453,14 +388,13 @@
                     }
 
                     shouldUseV30 = true;
-                    is = resolver.openInputStream(uri);
+                    is = resolver.openInputStream(localDataUri);
                     mVCardParser = new VCardParser_V30();
                     try {
                         counter = new VCardEntryCounter();
                         detector = new VCardSourceDetector();
-                        interpreter =
-                                new VCardInterpreterCollection(
-                                        Arrays.asList(counter, detector));
+                        interpreter = new VCardInterpreterCollection(
+                                Arrays.asList(counter, detector));
                         mVCardParser.parse(is, interpreter);
                     } catch (VCardVersionException e2) {
                         throw new VCardException("vCard with unspported version.");
@@ -479,13 +413,13 @@
                 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
                 // Go through without returning null.
             } catch (VCardException e) {
-                Log.e(LOG_TAG, e.getMessage());
+                Log.e(LOG_TAG, "VCardException during constructing ImportRequest", e);
                 return null;
             } catch (IOException e) {
-                Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+                Log.e(LOG_TAG, "IOException during constructing ImportRequest", e);
                 return null;
             }
-            return new ImportRequest(mAccount, uri,
+            return new ImportRequest(mAccount, localDataUri,
                     detector.getEstimatedType(),
                     detector.getEstimatedCharset(),
                     vcardVersion, counter.getCount());
@@ -502,7 +436,9 @@
             }
         }
 
+        @Override
         public void onCancel(DialogInterface dialog) {
+            Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
             cancel();
         }
     }
@@ -921,17 +857,23 @@
                 return getVCardFileSelectDialog(false);
             }
             case R.id.dialog_cache_vcard: {
-                if (mProgressDialogForCacheVCard == null) {
+                if (mProgressDialogForCachingVCard == null) {
                     final String title = getString(R.string.caching_vcard_title);
                     final String message = getString(R.string.caching_vcard_message);
-                    mProgressDialogForCacheVCard = new ProgressDialog(this);
-                    mProgressDialogForCacheVCard.setTitle(title);
-                    mProgressDialogForCacheVCard.setMessage(message);
-                    mProgressDialogForCacheVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
-                    mProgressDialogForCacheVCard.setOnCancelListener(mVCardCacheThread);
-                    mVCardCacheThread.start();
+                    mProgressDialogForCachingVCard = new ProgressDialog(this);
+                    mProgressDialogForCachingVCard.setTitle(title);
+                    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);
                 }
-                return mProgressDialogForCacheVCard;
+                return mProgressDialogForCachingVCard;
             }
             case R.id.dialog_io_exception: {
                 String message = (getString(R.string.scanning_sdcard_failed_message,
@@ -964,31 +906,11 @@
     }
 
     @Override
-    protected void onSaveInstanceState(Bundle outState) {
-        if (mVCardCacheThread != null) {
-            final Uri[] uris = mVCardCacheThread.getSourceUris();
-            final int length = uris.length;
-            final String[] uriStrings = new String[length];
-            for (int i = 0; i < length; i++) {
-                    uriStrings[i] = uris[i].toString();
-            }
-            outState.putStringArray(CACHED_URIS, uriStrings);
-
-            mVCardCacheThread.cancel();
-        }
-    }
-
-    @Override
-    protected void onRestoreInstanceState(Bundle inState) {
-        final String[] uriStrings = inState.getStringArray(CACHED_URIS);
-        if (uriStrings != null && uriStrings.length > 0) {
-            final int length = uriStrings.length;
-            final Uri[] uris = new Uri[length];
-            for (int i = 0; i < length; i++) {
-                uris[i] = Uri.parse(uriStrings[i]);
-            }
-
-            mVCardCacheThread = new VCardCacheThread(uris);
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        super.onRestoreInstanceState(savedInstanceState);
+        if (mProgressDialogForCachingVCard != null) {
+            Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again.");
+            showDialog(R.id.dialog_cache_vcard);
         }
     }
 
diff --git a/src/com/android/contacts/vcard/VCardService.java b/src/com/android/contacts/vcard/VCardService.java
index 475d465..77dc8bd 100644
--- a/src/com/android/contacts/vcard/VCardService.java
+++ b/src/com/android/contacts/vcard/VCardService.java
@@ -15,98 +15,63 @@
  */
 package com.android.contacts.vcard;
 
+import com.android.contacts.R;
+
 import android.app.Service;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
-import android.text.format.DateUtils;
 import android.util.Log;
 import android.widget.Toast;
 
 import java.io.File;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.util.Date;
-
-import com.android.contacts.R;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
 
 /**
- * The class responsible for importing vCard from one ore multiple Uris.
+ * The class responsible for handling vCard import/export requests.
+ *
+ * This Service creates one ImportRequest/ExportRequest object (as Runnable) per request and push
+ * it to {@link ExecutorService} with single thread executor. The executor handles each request
+ * one by one, and notifies users when needed.
  */
+// TODO: Using IntentService looks simpler than using Service + ServiceConnection though this
+// works fine enough. Investigate the feasibility.
 public class VCardService extends Service {
-    private final static String LOG_TAG = VCardService.class.getSimpleName();
+    private final static String LOG_TAG = "VCardService";
 
     /* package */ static final int MSG_IMPORT_REQUEST = 1;
     /* package */ static final int MSG_EXPORT_REQUEST = 2;
     /* package */ static final int MSG_CANCEL_IMPORT_REQUEST = 3;
-    /* package */ static final int MSG_NOTIFY_IMPORT_FINISHED = 5;
 
     /* package */ static final int IMPORT_NOTIFICATION_ID = 1000;
     /* package */ static final int EXPORT_NOTIFICATION_ID = 1001;
 
     /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_";
 
-    public class ImportRequestHandler extends Handler {
-        private ImportProcessor mImportProcessor;
-        private ExportProcessor mExportProcessor = new ExportProcessor(VCardService.this);
-        private boolean mDoDelayedCancel = false;
-
-        public ImportRequestHandler() {
-            super();
-        }
-
+    public class RequestHandler extends Handler {
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MSG_IMPORT_REQUEST: {
-                    Log.i(LOG_TAG, "Received vCard import request.");
-                    if (mDoDelayedCancel) {
-                        Log.i(LOG_TAG, "A cancel request came before import request. " +
-                                "Refrain current import once.");
-                        mDoDelayedCancel = false;
-                    } else {
-                        final ImportRequest parameter = (ImportRequest)msg.obj;
-
-                        if (mImportProcessor == null || !mImportProcessor.isReadyForRequest()) {
-                            mImportProcessor = new ImportProcessor(VCardService.this);
-                        } else if (mImportProcessor.isCanceled()) {
-                            Log.i(LOG_TAG,
-                                    "Existing ImporterProcessor is canceled. create another.");
-                            mImportProcessor = new ImportProcessor(VCardService.this);
-                        }
-
-                        mImportProcessor.pushRequest(parameter);
-                        Toast.makeText(VCardService.this,
-                                getString(R.string.vcard_importer_start_message),
-                                Toast.LENGTH_LONG).show();
-                    }
+                    handleImportRequest((ImportRequest)msg.obj);
                     break;
                 }
                 case MSG_EXPORT_REQUEST: {
-                    Log.i(LOG_TAG, "Received vCard export request.");
-                    final ExportRequest parameter = (ExportRequest)msg.obj;
-                    mExportProcessor.pushRequest(parameter);
-                    Toast.makeText(VCardService.this,
-                            getString(R.string.vcard_exporter_start_message),
-                            Toast.LENGTH_LONG).show();
+                    handleExportRequest((ExportRequest)msg.obj);
                     break;
                 }
                 case MSG_CANCEL_IMPORT_REQUEST: {
-                    Log.i(LOG_TAG, "Received cancel import request.");
-                    if (mImportProcessor != null) {
-                        mImportProcessor.cancel();
-                    } else {
-                        Log.w(LOG_TAG, "ImportProcessor isn't ready. Delay the cancel request.");
-                        mDoDelayedCancel = true;
-                    }
+                    handleCancelAllImportRequest();
                     break;
                 }
-                case MSG_NOTIFY_IMPORT_FINISHED: {
-                    Log.i(LOG_TAG, "Received vCard import finish notification.");
-                    break;
-                }
+                // TODO: add cancel capability for export..
                 default: {
                     Log.w(LOG_TAG, "Received unknown request, ignoring it.");
                     super.hasMessages(msg.what);
@@ -115,8 +80,17 @@
         }
     }
 
-    private ImportRequestHandler mHandler = new ImportRequestHandler();
-    private Messenger mMessenger = new Messenger(mHandler);
+    private final Handler mHandler = new RequestHandler();
+    private final Messenger mMessenger = new Messenger(mHandler);
+    // Should be single thread, as we don't want to simultaneously handle import and export
+    // requests.
+    private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
+
+    private int mCurrentJobId;
+    private final Map<Integer, ImportProcessor> mRunningJobMapForImport =
+            new HashMap<Integer, ImportProcessor>();
+    private final Map<Integer, ExportProcessor> mRunningJobMapForExport =
+            new HashMap<Integer, ExportProcessor>();
 
     @Override
     public int onStartCommand(Intent intent, int flags, int id) {
@@ -130,10 +104,123 @@
 
     @Override
     public void onDestroy() {
+        Log.i(LOG_TAG, "VCardService is finishing()");
+        cancelRequestsAndshutdown();
         clearCache();
         super.onDestroy();
     }
 
+    private synchronized void handleImportRequest(ImportRequest request) {
+        Log.i(LOG_TAG, String.format("Received vCard import request. id: %d", mCurrentJobId));
+        final ImportProcessor importProcessor =
+                new ImportProcessor(this, request, mCurrentJobId);
+        try {
+            mExecutorService.submit(importProcessor);
+        } catch (RejectedExecutionException e) {
+            Log.w(LOG_TAG, "vCard import request is rejected.", e);
+            // 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();
+            return;
+        }
+        mRunningJobMapForImport.put(mCurrentJobId, importProcessor);
+        mCurrentJobId++;
+        // 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, getString(R.string.vcard_import_will_start_message),
+                Toast.LENGTH_LONG).show();
+    }
+
+    private synchronized void handleExportRequest(ExportRequest request) {
+        Log.i(LOG_TAG, String.format("Received vCard export request. id: %d", mCurrentJobId));
+        final ExportProcessor exportProcessor =
+                new ExportProcessor(this, request, mCurrentJobId);
+        try {
+            mExecutorService.submit(exportProcessor);
+        } catch (RejectedExecutionException e) {
+            Log.w(LOG_TAG, "vCard export request is rejected.", e);
+            Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
+                    Toast.LENGTH_LONG).show();
+            return;
+        }
+        mRunningJobMapForExport.put(mCurrentJobId, exportProcessor);
+        mCurrentJobId++;
+        // See the comment in handleImportRequest()
+        Toast.makeText(this, getString(R.string.vcard_export_will_start_message),
+                Toast.LENGTH_LONG).show();
+    }
+
+    private synchronized void handleCancelAllImportRequest() {
+        Log.i(LOG_TAG, "Received cancel import request.");
+        cancelAllImportRequest();
+        mRunningJobMapForImport.clear();
+    }
+
+    private void cancelAllImportRequest() {
+        for (final Map.Entry<Integer, ImportProcessor> entry :
+                mRunningJobMapForImport.entrySet()) {
+            final int jobId = entry.getKey();
+            final ImportProcessor importProcessor = entry.getValue();
+            importProcessor.cancel();
+            Log.i(LOG_TAG, String.format("Canceling job %d", jobId));
+        }
+    }
+
+    private void cancelAllExportRequest() {
+        for (final Map.Entry<Integer, ExportProcessor> entry :
+                mRunningJobMapForExport.entrySet()) {
+            final int jobId = entry.getKey();
+            final ExportProcessor exportProcessor = entry.getValue();
+            exportProcessor.cancel();
+            Log.i(LOG_TAG, String.format("Canceling job %d", jobId));
+        }
+    }
+
+    /* package */ synchronized void handleFinishImportNotification(
+            int jobId, boolean successful) {
+        Log.i(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
+                + "Result: %b", jobId, (successful ? "success" : "failure")));
+        if (mRunningJobMapForImport.remove(jobId) == null) {
+            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+        }
+    }
+
+    /* package */ synchronized void handleFinishExportNotification(
+            int jobId, boolean successful) {
+        Log.i(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
+                + "Result: %b", jobId, (successful ? "success" : "failure")));
+        if (mRunningJobMapForExport.remove(jobId) == null) {
+            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+        }
+    }
+
+    /**
+     * Cancels all the import/export requests and call {@link ExecutorService#shutdown()}, which
+     * means this Service becomes no longer ready for import/export requests. Mainly used in
+     * onDestroy().
+     */
+    private synchronized void cancelRequestsAndshutdown() {
+        synchronized (this) {
+            if (mRunningJobMapForImport.size() > 0) {
+                Log.i(LOG_TAG,
+                        String.format("Cancel existing all import requests (remains: ",
+                                mRunningJobMapForImport.size()));
+                cancelAllImportRequest();
+            }
+            if (mRunningJobMapForExport.size() > 0) {
+                Log.i(LOG_TAG,
+                        String.format("Cancel existing all import requests (remains: ",
+                                mRunningJobMapForExport.size()));
+                cancelAllExportRequest();
+            }
+            mExecutorService.shutdown();
+        }
+    }
+
+    /**
+     * Removes import caches stored locally.
+     */
     private void clearCache() {
         Log.i(LOG_TAG, "start removing cache files if exist.");
         final String[] fileLists = fileList();
diff --git a/tests/src/com/android/contacts/vcard/ImportProcessorTest.java b/tests/src/com/android/contacts/vcard/ImportProcessorTest.java
deleted file mode 100644
index 72245ba..0000000
--- a/tests/src/com/android/contacts/vcard/ImportProcessorTest.java
+++ /dev/null
@@ -1,180 +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 android.content.ContentResolver;
-import android.content.Context;
-import android.net.Uri;
-import android.test.AndroidTestCase;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.contacts.vcard.ImportProcessor.CommitterGenerator;
-import com.android.vcard.VCardEntryCommitter;
-import com.android.vcard.VCardInterpreter;
-import com.android.vcard.VCardSourceDetector;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.Channels;
-import java.nio.channels.ReadableByteChannel;
-import java.nio.channels.WritableByteChannel;
-import java.util.List;
-
-public class ImportProcessorTest extends AndroidTestCase {
-    private static final String LOG_TAG = "ImportProcessorTest";
-    private ImportProcessor mImportProcessor;
-
-    private String mCopiedFileName;
-
-    // XXX: better way to copy stream?
-    private Uri copyToLocal(final String fileName) throws IOException {
-        final Context context = getContext();
-        // We need to use Context of this unit test runner (not of test to be tested),
-        // as only the former knows assets to be copied.
-        final Context testContext = getTestContext();
-        final ContentResolver resolver = testContext.getContentResolver();
-        mCopiedFileName = fileName;
-        ReadableByteChannel inputChannel = null;
-        WritableByteChannel outputChannel = null;
-        Uri destUri;
-        try {
-            inputChannel = Channels.newChannel(testContext.getAssets().open(fileName));
-            destUri = Uri.parse(context.getFileStreamPath(fileName).toURI().toString());
-            outputChannel =
-                    getContext().openFileOutput(fileName,
-                            Context.MODE_WORLD_WRITEABLE).getChannel();
-            final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
-            while (inputChannel.read(buffer) != -1) {
-                buffer.flip();
-                outputChannel.write(buffer);
-                buffer.compact();
-            }
-            buffer.flip();
-            while (buffer.hasRemaining()) {
-                outputChannel.write(buffer);
-            }
-        } finally {
-            if (inputChannel != null) {
-                try {
-                    inputChannel.close();
-                } catch (IOException e) {
-                    Log.w(LOG_TAG, "Failed to close inputChannel.");
-                }
-            }
-            if (outputChannel != null) {
-                try {
-                    outputChannel.close();
-                } catch(IOException e) {
-                    Log.w(LOG_TAG, "Failed to close outputChannel");
-                }
-            }
-        }
-        return destUri;
-    }
-
-    @Override
-    public void setUp() {
-        mImportProcessor = new ImportProcessor(getContext());
-        mImportProcessor.ensureInit();
-        mCopiedFileName = null;
-    }
-
-    @Override
-    public void tearDown() {
-        if (!TextUtils.isEmpty(mCopiedFileName)) {
-            getContext().deleteFile(mCopiedFileName);
-            mCopiedFileName = null;
-        }
-    }
-
-    /**
-     * Confirms {@link ImportProcessor#readOneVCard(android.net.Uri, int, String,
-     * com.android.vcard.VCardInterpreter, int[])} successfully handles correct input.
-     */
-    public void testProcessSimple() throws IOException {
-        final Uri uri = copyToLocal("v21_simple.vcf");
-        final int vcardType = VCardSourceDetector.PARSE_TYPE_UNKNOWN;
-        final String charset = null;
-        final VCardInterpreter interpreter = new EmptyVCardInterpreter();
-        final int[] versions = new int[] {
-                ImportVCardActivity.VCARD_VERSION_V21
-        };
-
-        assertTrue(mImportProcessor.readOneVCard(
-                uri, vcardType, charset, interpreter, versions));
-    }
-
-    /**
-     * Confirms {@link ImportProcessor#handleOneRequest(ImportRequest)} accepts
-     * one request and import it.
-     */
-    public void testHandleOneRequestSimple() throws IOException {
-        CommitterGenerator generator = new CommitterGenerator() {
-            public VCardEntryCommitter generate(ContentResolver resolver) {
-                return new MockVCardEntryCommitter();
-            }
-        };
-        mImportProcessor.injectCommitterGeneratorForTest(generator);
-        mImportProcessor.initNotifierForTest();
-
-        final ImportRequest request = new ImportRequest(
-                null,  // account
-                copyToLocal("v30_simple.vcf"),
-                VCardSourceDetector.PARSE_TYPE_UNKNOWN,
-                null,  // estimatedCharset
-                ImportVCardActivity.VCARD_VERSION_AUTO_DETECT,
-                1);
-        assertTrue(mImportProcessor.handleOneRequest(request));
-        assertEquals(1, mImportProcessor.getCreatedUrisForTest().size());
-    }
-}
-
-/* package */ class EmptyVCardInterpreter implements VCardInterpreter {
-    @Override
-    public void end() {
-    }
-    @Override
-    public void endEntry() {
-    }
-    @Override
-    public void endProperty() {
-    }
-    @Override
-    public void propertyGroup(String group) {
-    }
-    @Override
-    public void propertyName(String name) {
-    }
-    @Override
-    public void propertyParamType(String type) {
-    }
-    @Override
-    public void propertyParamValue(String value) {
-    }
-    @Override
-    public void propertyValues(List<String> values) {
-    }
-    @Override
-    public void start() {
-    }
-    @Override
-    public void startEntry() {
-    }
-    @Override
-    public void startProperty() {
-    }
-}
diff --git a/tests/src/com/android/contacts/vcard/MockVCardEntryCommitter.java b/tests/src/com/android/contacts/vcard/MockVCardEntryCommitter.java
deleted file mode 100644
index 4765b38..0000000
--- a/tests/src/com/android/contacts/vcard/MockVCardEntryCommitter.java
+++ /dev/null
@@ -1,56 +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 android.net.Uri;
-
-import com.android.vcard.VCardEntry;
-import com.android.vcard.VCardEntryCommitter;
-
-import java.util.ArrayList;
-
-public class MockVCardEntryCommitter extends VCardEntryCommitter {
-
-    private final ArrayList<Uri> mUris = new ArrayList<Uri>(); 
-
-    public MockVCardEntryCommitter() {
-        super(null);
-    }
-
-    /**
-     * Exists for forcing super class to do nothing.
-     */
-    @Override
-    public void onStart() {
-    }
-
-    /**
-     * Exists for forcing super class to do nothing.
-     */
-    @Override
-    public void onEnd() {
-    }
-
-    @Override
-    public void onEntryCreated(final VCardEntry vcardEntry) {
-        mUris.add(null);
-    }
-
-    @Override
-    public ArrayList<Uri> getCreatedUris() {
-        return mUris;
-    }
-}
\ No newline at end of file