Make ImportVCardService aware of multiple requests from Activity and
unit test friendly.

This change works fine but is not sufficient enough. Need to be
refactored more.

Bug: 2733143
Change-Id: Icea90e68bb61591d67e03a6c22a5046c42e606b8
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index 09c4980..e021457 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -21,23 +21,30 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.ProgressDialog;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.DialogInterface.OnCancelListener;
 import android.content.DialogInterface.OnClickListener;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
 import android.os.PowerManager;
+import android.os.RemoteException;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.style.RelativeSizeSpan;
 import android.util.Log;
 
+import com.android.contacts.ImportVCardService.RequestParameter;
 import com.android.contacts.model.Sources;
 import com.android.contacts.util.AccountSelectionUtil;
 import com.android.vcard.VCardEntryCounter;
@@ -48,14 +55,12 @@
 import com.android.vcard.VCardSourceDetector;
 import com.android.vcard.exception.VCardException;
 import com.android.vcard.exception.VCardNestedException;
-import com.android.vcard.exception.VCardNotSupportedException;
 import com.android.vcard.exception.VCardVersionException;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
-import java.nio.channels.Channel;
 import java.nio.channels.Channels;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.WritableByteChannel;
@@ -88,15 +93,19 @@
     /* package */ static final String VCARD_URI_ARRAY = "vcard_uri";
     /* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type";
     /* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset";
-    /* package */ static final String USE_V30_ARRAY = "use_v30";
+    /* package */ static final String VCARD_VERSION_ARRAY = "vcard_version";
     /* package */ static final String ENTRY_COUNT_ARRAY = "entry_count";
 
+    /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
+    /* package */ final static int VCARD_VERSION_V21 = 1;
+    /* package */ final static int VCARD_VERSION_V30 = 2;
+
     // Run on the UI thread. Must not be null except after onDestroy().
     private Handler mHandler = new Handler();
 
     private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
-    private String mAccountName;
-    private String mAccountType;
+
+    private Account mAccount;
 
     private String mAction;
     private Uri mUri;
@@ -111,6 +120,19 @@
 
     private String mErrorMessage;
 
+    private Messenger mMessenger;
+
+    private final ServiceConnection mConnection = new ServiceConnection() {
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mMessenger = new Messenger(service);
+        }
+
+        public void onServiceDisconnected(ComponentName name) {
+            mMessenger = null;
+            finish();
+        }
+    };
+
     private static class VCardFile {
         private final String mName;
         private final String mCanonicalPath;
@@ -180,24 +202,9 @@
         private VCardParser mVCardParser;
         private final Uri[] mSourceUris;
 
-        // Not using Uri[] since we throw this object to Service via Intent.
-        private final String[] mDestUriStrings;
-
-        private final int[] mEstimatedVCardTypes;
-        private final String[] mEstimatedCharsets;
-        private final boolean[] mShouldUseV30;
-        private final int[] mEntryCounts;
-
         public VCardCacheThread(final Uri[] sourceUris) {
             mSourceUris = sourceUris;
             final int length = sourceUris.length;
-
-            mDestUriStrings = new String[length];
-            mEstimatedVCardTypes = new int[length];
-            mEstimatedCharsets = new String[length];
-            mShouldUseV30 = new boolean[length];
-            mEntryCounts = new int[length];
-
             final Context context = ImportVCardActivity.this;
             PowerManager powerManager = (PowerManager)context.getSystemService(
                     Context.POWER_SERVICE);
@@ -219,34 +226,48 @@
             final ContentResolver resolver = context.getContentResolver();
             String errorMessage = null;
             mWakeLock.acquire();
-
+            boolean successful = false;
             try {
                 clearOldCache();
+                bindService(new Intent(ImportVCardActivity.this, 
+                        ImportVCardService.class), mConnection, Context.BIND_AUTO_CREATE);
 
+                final int length = mSourceUris.length;
+                // 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://...").
+                // Thus we have to once copy the content of Uri into local storage, and read
+                // it after it. This copy is also useful fro the view of stability of the import,
+                // as we are able to restore the procedure even when it is aborted during it.
+                // Imagine the case the importer encountered memory-low situation when
+                // reading 10th entry of a vCard file.
+                //
                 // We may be able to read content of each vCard file during copying them
                 // to local storage, but currently vCard code does not allow us to do so.
-                //
-                // Note that Uris given from caller applications may not be opened twice,
-                // so we have to use local data after this copy instead of relying on
-                // the original source Uris.
-                copyVCardToLocal();
-                if (mCanceled) {
-                    return;
+                for (int i = 0; i < length; i++) {
+                    final Uri sourceUri = mSourceUris[i];
+                    final Uri localDataUri = copyToLocal(sourceUri, i);
+                    if (mCanceled) {
+                        break;
+                    }
+                    if (localDataUri == null) {
+                        Log.w(LOG_TAG, "destUri is null");
+                        break;
+                    }
+                    final RequestParameter parameter = constructRequestParameter(localDataUri);
+                    if (mCanceled) {
+                        return;
+                    }
+                    mMessenger.send(Message.obtain(null,
+                            ImportVCardService.IMPORT_REQUEST,
+                            parameter));
+
                 }
-                Log.d("@@@", "Caching done. Count the number of vCard entries.");
-                if (!collectVCardMetaInfo()) {
-                    Log.e(LOG_TAG, "Failed to collect vCard meta information");
-                    runOnUIThread(new DialogDisplayer(
-                            getString(R.string.fail_reason_failed_to_collect_vcard_meta_info)));
-                    return;
-                }
-                if (mCanceled) {
-                    return;
-                }
-                for (int i = 0; i < mEntryCounts.length; i++) {
-                    Log.d("@@@", String.format("length for %s: %d",
-                            mDestUriStrings[i], mEntryCounts[i]));
-                }
+
+                successful = true;
+            } catch (RemoteException e) {
+                Log.e(LOG_TAG, "RemoteException is thrown when trying to import vCard");
+                runOnUIThread(new DialogDisplayer(getString(R.string.fail_reason_unknown)));
             } catch (OutOfMemoryError e) {
                 Log.e(LOG_TAG, "OutOfMemoryError");
                 // We should take care of this case since Android devices may have
@@ -255,149 +276,54 @@
                 runOnUIThread(new DialogDisplayer(
                         getString(R.string.fail_reason_io_error) +
                         ": " + e.getLocalizedMessage()));
-                return;
             } catch (IOException e) {
                 Log.e(LOG_TAG, e.getMessage());
                 runOnUIThread(new DialogDisplayer(
                         getString(R.string.fail_reason_io_error) +
                         ": " + e.getLocalizedMessage()));
-                return;
             } finally {
-
                 mWakeLock.release();
                 mProgressDialogForCacheVCard.dismiss();
             }
 
-            // TODO(dmiyakawa): do we need runOnUIThread?
-            final Intent intent = new Intent(
-                    ImportVCardActivity.this, ImportVCardService.class);
-            intent.putExtra(VCARD_URI_ARRAY, mDestUriStrings);
-            intent.putExtra(SelectAccountActivity.ACCOUNT_NAME, mAccountName);
-            intent.putExtra(SelectAccountActivity.ACCOUNT_TYPE, mAccountType);
-            intent.putExtra(ESTIMATED_VCARD_TYPE_ARRAY, mEstimatedVCardTypes);
-            intent.putExtra(ESTIMATED_CHARSET_ARRAY, mEstimatedCharsets);
-            intent.putExtra(USE_V30_ARRAY, mShouldUseV30);
-            intent.putExtra(ENTRY_COUNT_ARRAY, mEntryCounts);
-            startService(intent);
-            finish();
-        }
-
-
-        private boolean collectVCardMetaInfo() {
-            final ContentResolver resolver =
-                    ImportVCardActivity.this.getContentResolver();
-            final int length = mDestUriStrings.length;
-            try {
-                for (int i = 0; i < length; i++) {
-                    if (mCanceled) {
-                        return false;
-                    }
-                    final Uri uri = Uri.parse(mDestUriStrings[i]);
-                    boolean shouldUseV30 = false;
-                    InputStream is;
-                    VCardEntryCounter counter;
-                    VCardSourceDetector detector;
-                    VCardInterpreterCollection interpreter;
-
-                    is = resolver.openInputStream(uri);
-                    mVCardParser = new VCardParser_V21();
-                    try {
-                        counter = new VCardEntryCounter();
-                        detector = new VCardSourceDetector();
-                        interpreter =
-                                new VCardInterpreterCollection(
-                                        Arrays.asList(counter, detector));
-                        mVCardParser.parse(is, interpreter);
-                    } catch (VCardVersionException e1) {
-                        try {
-                            is.close();
-                        } catch (IOException e) {
-                        }
-
-                        shouldUseV30 = true;
-                        is = resolver.openInputStream(uri);
-                        mVCardParser = new VCardParser_V30();
-                        try {
-                            counter = new VCardEntryCounter();
-                            detector = new VCardSourceDetector();
-                            interpreter =
-                                    new VCardInterpreterCollection(
-                                            Arrays.asList(counter, detector));
-                            mVCardParser.parse(is, interpreter);
-                        } catch (VCardVersionException e2) {
-                            throw new VCardException("vCard with unspported version.");
-                        }
-                    } finally {
-                        if (is != null) {
-                            try {
-                                is.close();
-                            } catch (IOException e) {
-                            }
-                        }
-                    }
-
-                    mEstimatedVCardTypes[i] = detector.getEstimatedType();
-                    mEstimatedCharsets[i] = detector.getEstimatedCharset();
-                    mShouldUseV30[i] = shouldUseV30;
-                    mEntryCounts[i] = counter.getCount();
-                }
-            } catch (IOException e) {
-                Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
-                return false;
-            } catch (VCardNestedException e) {
-                Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
-            } catch (VCardNotSupportedException e) {
-                return false;
-            } catch (VCardException e) {
-                return false;
+            if (successful) {
+                finish();
+            } else {
+                // finish() should be called via DialogDisplayer().
             }
-            return true;
         }
 
-        private void copyVCardToLocal() throws IOException {
+        /**
+         * Copy the content of sourceUri to local storage. 
+         */
+        private Uri copyToLocal(final Uri sourceUri, int i) throws IOException {
             final Context context = ImportVCardActivity.this;
             final ContentResolver resolver = context.getContentResolver();
             ReadableByteChannel inputChannel = null;
             WritableByteChannel outputChannel = null;
+            Uri destUri;
             try {
-                int length = mSourceUris.length;
-                for (int i = 0; i < length; i++) {
-                    if (mCanceled) {
-                        Log.d(LOG_TAG, "Canceled during import");
-                        break;
-                    }
-                    // XXX: better way to copy stream?
+                // XXX: better way to copy stream?
+                {
                     inputChannel = Channels.newChannel(resolver.openInputStream(mSourceUris[i]));
                     final String filename = CACHE_FILE_PREFIX + i + ".vcf";
-                    mDestUriStrings[i] =
-                            context.getFileStreamPath(filename).toURI().toString();
-                    Log.d("@@@", "temporary file: " + filename + ", dest: " + mDestUriStrings[i]);
+                    destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
                     outputChannel =
                             context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
-                    {
-                        final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
-                        while (inputChannel.read(buffer) != -1) {
-                            if (mCanceled) {
-                                Log.d(LOG_TAG, "Canceled during caching " + mSourceUris[i]);
-                                break;
-                            }
-                            buffer.flip();
-                            outputChannel.write(buffer);
-                            buffer.compact();
+                    final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
+                    while (inputChannel.read(buffer) != -1) {
+                        if (mCanceled) {
+                            Log.d(LOG_TAG, "Canceled during caching " + mSourceUris[i]);
+                            return null;
                         }
                         buffer.flip();
-                        while (buffer.hasRemaining()) {
-                            outputChannel.write(buffer);
-                        }
+                        outputChannel.write(buffer);
+                        buffer.compact();
                     }
-
-                    // Avoid double close() in the "finally" block bellow.
-                    Channel tmp = inputChannel;
-                    inputChannel = null;
-                    tmp.close();
-                    tmp = outputChannel;
-                    outputChannel = null;
-                    tmp.close();
+                    buffer.flip();
+                    while (buffer.hasRemaining()) {
+                        outputChannel.write(buffer);
+                    }
                 }
             } finally {
                 if (inputChannel != null) {
@@ -415,6 +341,76 @@
                     }
                 }
             }
+            return destUri;
+        }
+
+        /**
+         * Reads the Uri once (or twice) and constructs {@link RequestParameter} from
+         * its content.
+         */
+        private RequestParameter constructRequestParameter(final Uri uri) {
+            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);
+                mVCardParser = new VCardParser_V21();
+                try {
+                    counter = new VCardEntryCounter();
+                    detector = new VCardSourceDetector();
+                    interpreter =
+                            new VCardInterpreterCollection(
+                                    Arrays.asList(counter, detector));
+                    mVCardParser.parse(is, interpreter);
+                } catch (VCardVersionException e1) {
+                    try {
+                        is.close();
+                    } catch (IOException e) {
+                    }
+
+                    shouldUseV30 = true;
+                    is = resolver.openInputStream(uri);
+                    mVCardParser = new VCardParser_V30();
+                    try {
+                        counter = new VCardEntryCounter();
+                        detector = new VCardSourceDetector();
+                        interpreter =
+                                new VCardInterpreterCollection(
+                                        Arrays.asList(counter, detector));
+                        mVCardParser.parse(is, interpreter);
+                    } catch (VCardVersionException e2) {
+                        throw new VCardException("vCard with unspported version.");
+                    }
+                } finally {
+                    if (is != null) {
+                        try {
+                            is.close();
+                        } catch (IOException e) {
+                        }
+                    }
+                }
+
+                vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
+            } catch (VCardNestedException e) {
+                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());
+                return null;
+            } catch (IOException e) {
+                Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+                return null;
+            }
+            return new RequestParameter(mAccount, uri,
+                    detector.getEstimatedType(),
+                    detector.getEstimatedCharset(),
+                    vcardVersion, counter.getCount());
         }
 
         /**
@@ -666,10 +662,8 @@
         importVCard(uriStrings);
     }
 
-    private void importVCard(final String uriString) {
-        String[] uriStrings = new String[1];
-        uriStrings[0] = uriString;
-        importVCard(uriStrings);
+    private void importVCard(final Uri uri) {
+        importVCard(new Uri[] {uri});
     }
 
     private void importVCard(final String[] uriStrings) {
@@ -678,6 +672,10 @@
         for (int i = 0; i < length; i++) {
             uris[i] = Uri.parse(uriStrings[i]);
         }
+        importVCard(uris);
+    }
+
+    private void importVCard(final Uri[] uris) {
         runOnUIThread(new Runnable() {
             public void run() {
                 mVCardCacheThread = new VCardCacheThread(uris);
@@ -745,33 +743,34 @@
     protected void onCreate(Bundle bundle) {
         super.onCreate(bundle);
 
+        String accountName = null;
+        String accountType = null;
         final Intent intent = getIntent();
         if (intent != null) {
-            mAccountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
-            mAccountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
+            accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
+            accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
             mAction = intent.getAction();
             mUri = intent.getData();
         } else {
             Log.e(LOG_TAG, "intent does not exist");
         }
 
-        // The caller may not know account information at all, so we show the UI instead.
-        if (TextUtils.isEmpty(mAccountName) || TextUtils.isEmpty(mAccountType)) {
+        if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+            mAccount = new Account(accountName, accountType);
+        } else {
             final Sources sources = Sources.getInstance(this);
             final List<Account> accountList = sources.getAccounts(true);
             if (accountList.size() == 0) {
-                mAccountName = null;
-                mAccountType = null;
+                mAccount = null;
             } else if (accountList.size() == 1) {
-                final Account account = accountList.get(0);
-                mAccountName = account.name;
-                mAccountType = account.type;
+                mAccount = accountList.get(0);
             } else {
                 startActivityForResult(new Intent(this, SelectAccountActivity.class),
                         SELECT_ACCOUNT);
                 return;
             }
         }
+
         startImport(mAction, mUri);
     }
 
@@ -779,8 +778,9 @@
     public void onActivityResult(int requestCode, int resultCode, Intent intent) {
         if (requestCode == SELECT_ACCOUNT) {
             if (resultCode == RESULT_OK) {
-                mAccountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
-                mAccountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
+                mAccount = new Account(
+                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
+                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE));
                 startImport(mAction, mUri);
             } else {
                 if (resultCode != RESULT_CANCELED) {
@@ -795,7 +795,7 @@
         Log.d(LOG_TAG, "action = " + action + " ; path = " + uri);
 
         if (uri != null) {
-            importVCard(uri.toString());
+            importVCard(uri);
         } else {
             doScanExternalStorageAndImportVCard();
         }
diff --git a/src/com/android/contacts/ImportVCardService.java b/src/com/android/contacts/ImportVCardService.java
index 293255d..f940761 100644
--- a/src/com/android/contacts/ImportVCardService.java
+++ b/src/com/android/contacts/ImportVCardService.java
@@ -23,24 +23,22 @@
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
 import android.provider.ContactsContract.RawContacts;
-import android.text.TextUtils;
 import android.util.Log;
 import android.widget.RemoteViews;
 import android.widget.Toast;
 
-import com.android.vcard.VCardConfig;
 import com.android.vcard.VCardEntry;
 import com.android.vcard.VCardEntryCommitter;
 import com.android.vcard.VCardEntryConstructor;
-import com.android.vcard.VCardEntryCounter;
 import com.android.vcard.VCardEntryHandler;
 import com.android.vcard.VCardInterpreter;
-import com.android.vcard.VCardInterpreterCollection;
 import com.android.vcard.VCardParser;
 import com.android.vcard.VCardParser_V21;
 import com.android.vcard.VCardParser_V30;
@@ -53,7 +51,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Queue;
@@ -64,11 +61,379 @@
 public class ImportVCardService extends Service {
     private final static String LOG_TAG = "ImportVCardService";
 
-    private class ProgressNotifier implements VCardEntryHandler {
-        private final int mId;
+    /* package */ static final int IMPORT_REQUEST = 1;
 
-        public ProgressNotifier(int id) {
-            mId = id;
+    private static final int MESSAGE_ID = 1000;
+
+    // TODO: Too many static classes. Create separate files for them.
+
+    /**
+     * Class representing one request for reading vCard (as a Uri representation).
+     */
+    /* package */ static class RequestParameter {
+        public final Account account;
+        /**
+         * Note: One Uri does not mean there's only one vCard entry since one Uri
+         * often has multiple vCard entries. 
+         */
+        public final Uri uri;
+        /**
+         * Can be {@link VCardSourceDetector#PARSE_TYPE_UNKNOWN}.
+         */
+        public final int estimatedVCardType;
+        /**
+         * Can be null, meaning no preferable charset is available.
+         */
+        public final String estimatedCharset;
+        /**
+         * Assumes that one Uri contains only one version, while there's a (tiny) possibility
+         * we may have two types in one vCard.
+         * 
+         * e.g.
+         * BEGIN:VCARD
+         * VERSION:2.1
+         * ...
+         * END:VCARD
+         * BEGIN:VCARD
+         * VERSION:3.0
+         * ...
+         * END:VCARD
+         *
+         * We've never seen this kind of a file, but we may have to cope with it in the future.
+         */
+        public final int vcardVersion;
+        public final int entryCount;
+
+        public RequestParameter(Account account,
+                Uri uri, int estimatedType, String estimatedCharset,
+                int vcardVersion, int entryCount) {
+            this.account = account;
+            this.uri = uri;
+            this.estimatedVCardType = estimatedType;
+            this.estimatedCharset = estimatedCharset;
+            this.vcardVersion = vcardVersion;
+            this.entryCount = entryCount;
+        }
+    }
+
+    public class ImportRequestHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case IMPORT_REQUEST:
+                    RequestParameter parameter = (RequestParameter)msg.obj;
+                    Toast.makeText(ImportVCardService.this,
+                            getString(R.string.vcard_importer_start_message),
+                            Toast.LENGTH_LONG).show();
+                    final boolean needThreadStart;
+                    if (mRequestHandler == null || !mRequestHandler.isRunning()) {
+                        mRequestHandler = new RequestHandler();
+                        mRequestHandler.init(ImportVCardService.this);
+                        needThreadStart = true;
+                    } else {
+                        needThreadStart = false;
+                    }
+                    mRequestHandler.addRequest(parameter);
+                    if (needThreadStart) {
+                        mThread = new Thread(new Runnable() {
+                            public void run() {
+                                mRequestHandler.handleRequests();
+                            }
+                        });
+                        mThread.start();
+                    }
+                    break;
+                default:
+                    Log.e(LOG_TAG, "Unknown request type: " + msg.what);
+                    super.hasMessages(msg.what);
+            }
+        }
+    }
+
+    // TODO(dmiyakawa): better design for testing?
+    /* package */ interface CommitterGenerator {
+        public VCardEntryCommitter generate(ContentResolver resolver);
+    }
+    /* package */ static class DefaultCommitterGenerator implements CommitterGenerator {
+        public VCardEntryCommitter generate(ContentResolver resolver) {
+            return new VCardEntryCommitter(resolver);
+        }
+    }
+
+    /**
+     * For testability, we don't inherit Thread here.
+     */
+    private static class RequestHandler {
+        private ImportVCardService mService;
+        private ContentResolver mResolver;
+        private NotificationManager mNotificationManager;
+        private ProgressNotifier mProgressNotifier;
+
+        private final List<Uri> mFailedUris;
+        private final List<Uri> mCreatedUris;
+
+        private VCardParser mVCardParser;
+
+        /* package */ CommitterGenerator mCommitterGenerator = new DefaultCommitterGenerator();
+
+        /**
+         * Meaning a controller of this object requests the operation should be canceled
+         * or not, which implies {@link #mRunning} 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 boolean mCanceled;
+        /**
+         * Meaning this object is actually running.
+         */
+        private boolean mRunning = true;
+        private final Queue<RequestParameter> mPendingRequests =
+                new LinkedList<RequestParameter>();
+
+        public RequestHandler() {
+            // We cannot set Service here since Service is not fully ready at this point.
+            // TODO: refactor this class.
+
+            mFailedUris = new ArrayList<Uri>();
+            mCreatedUris = new ArrayList<Uri>();
+            mProgressNotifier = new ProgressNotifier();            
+        }
+
+        public void init(ImportVCardService service) {
+            // TODO: Based on fragile fact. fix this.
+            mService = service;
+            mResolver = service.getContentResolver();
+            mNotificationManager =
+                (NotificationManager)service.getSystemService(Context.NOTIFICATION_SERVICE);
+            mProgressNotifier.init(mService, mNotificationManager);
+        }
+
+        public void handleRequests() {
+            try {
+                while (!mCanceled) {
+                    final RequestParameter parameter;
+                    synchronized (this) {
+                        if (mPendingRequests.size() == 0) {
+                            mRunning = false;
+                            break;
+                        } else {
+                            parameter = mPendingRequests.poll();
+                        }
+                    }  // synchronized (this)
+                    handleOneRequest(parameter);
+                }
+
+                // Currenty we don't have an appropriate way to let users see all entries
+                // imported in this procedure. Instead, we show them entries only when
+                // there's just one created uri.
+                doFinishNotification(mCreatedUris.size() == 1 ? mCreatedUris.get(0) : null);
+            } finally {
+                // TODO: verify this works fine.
+                mRunning = false;  // Just in case.
+                // mService.stopSelf();
+            }
+        }
+
+        /**
+         * Would be run inside syncronized block.
+         */
+        public boolean handleOneRequest(final RequestParameter parameter) {
+            if (mCanceled) {
+                Log.i(LOG_TAG, "Canceled before actually handling parameter ("
+                        + parameter.uri + ")");
+                return false;
+            }
+            final int[] possibleVCardVersions;
+            if (parameter.vcardVersion == ImportVCardActivity.VCARD_VERSION_AUTO_DETECT) {
+                /**
+                 * Note: this code assumes that a given Uri is able to be opened more than once,
+                 * which may not be true in certain conditions. 
+                 */
+                possibleVCardVersions = new int[] {
+                        ImportVCardActivity.VCARD_VERSION_V21,
+                        ImportVCardActivity.VCARD_VERSION_V30
+                };
+            } else {
+                possibleVCardVersions = new int[] {
+                        parameter.vcardVersion
+                };
+            }
+
+            final Uri uri = parameter.uri;
+            final Account account = parameter.account;
+            final int estimatedVCardType = parameter.estimatedVCardType;
+            final String estimatedCharset = parameter.estimatedCharset;
+
+            final VCardEntryConstructor constructor =
+                new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
+            final VCardEntryCommitter committer = mCommitterGenerator.generate(mResolver);
+            constructor.addEntryHandler(committer);
+            constructor.addEntryHandler(mProgressNotifier);
+
+            final boolean successful =
+                readOneVCard(uri, estimatedVCardType, estimatedCharset,
+                        constructor, possibleVCardVersions);
+            if (successful) {
+                List<Uri> uris = committer.getCreatedUris();
+                if (uris != null) {
+                    mCreatedUris.addAll(uris);
+                } else {
+                    // Not critical, but suspicious.
+                    Log.w(LOG_TAG,
+                            "Created Uris is null while the creation itself is successful.");
+                }
+            } else {
+                mFailedUris.add(uri);
+            }
+
+            return successful;
+        }
+
+        /*
+        private void doErrorNotification(int id) {
+            final Notification notification = new Notification();
+            notification.icon = android.R.drawable.stat_sys_download_done;
+            final String title = mService.getString(R.string.reading_vcard_failed_title);
+            final PendingIntent intent =
+                    PendingIntent.getActivity(mService, 0, new Intent(), 0);
+            notification.setLatestEventInfo(mService, title, "", intent);
+            mNotificationManager.notify(MESSAGE_ID, notification);
+        }
+        */
+
+        private void doFinishNotification(Uri createdUri) {
+            final Notification notification = new Notification();
+            notification.icon = android.R.drawable.stat_sys_download_done;
+            final String title = mService.getString(R.string.reading_vcard_finished_title);
+
+            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;
+            }
+
+            final PendingIntent pendingIntent =
+                    PendingIntent.getActivity(mService, 0, intent, 0);
+            notification.setLatestEventInfo(mService, title, "", pendingIntent);
+            mNotificationManager.notify(MESSAGE_ID, notification);
+        }
+        
+        private boolean readOneVCard(Uri uri, int vcardType, String charset,
+                VCardInterpreter interpreter,
+                final int[] possibleVCardVersions) {
+            boolean successful = false;
+            final int length = possibleVCardVersions.length;
+            for (int i = 0; i < length; i++) {
+                InputStream is = null;
+                final int vcardVersion = possibleVCardVersions[i]; 
+                try {
+                    if (i > 0 && (interpreter instanceof VCardEntryConstructor)) {
+                        // Let the object clean up internal temporary objects,
+                        ((VCardEntryConstructor) interpreter).clear();
+                    }
+
+                    is = mResolver.openInputStream(uri);
+
+                    // We need synchronized block here,
+                    // since we need to handle mCanceled and mVCardParser at once.
+                    // In the worst case, a user may call cancel() just before creating
+                    // mVCardParser.
+                    synchronized (this) {
+                        mVCardParser = (vcardVersion == ImportVCardActivity.VCARD_VERSION_V30 ?
+                                new VCardParser_V30(vcardType) :
+                                    new VCardParser_V21(vcardType));
+                        if (mCanceled) {
+                            mVCardParser.cancel();
+                        }
+                    }
+                    mVCardParser.parse(is, interpreter);
+
+                    successful = true;
+                    break;
+                } catch (IOException e) {
+                    Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+                } catch (VCardNestedException e) {
+                    // This exception should not be thrown here. We should intsead handle it
+                    // in the preprocessing session in ImportVCardActivity, as we don't try
+                    // to detect the type of given vCard here.
+                    //
+                    // TODO: Handle this case appropriately, which should mean we have to have
+                    // code trying to auto-detect the type of given vCard twice (both in
+                    // ImportVCardActivity and ImportVCardService).
+                    Log.e(LOG_TAG, "Nested Exception is found.");
+                } catch (VCardNotSupportedException e) {
+                    Log.e(LOG_TAG, e.getMessage());
+                } catch (VCardVersionException e) {
+                    if (i == length - 1) {
+                        Log.e(LOG_TAG, "Appropriate version for this vCard is not found.");
+                    } else {
+                        // We'll try the other (v30) version.
+                    }
+                } catch (VCardException e) {
+                    Log.e(LOG_TAG, e.getMessage());
+                } finally {
+                    if (is != null) {
+                        try {
+                            is.close();
+                        } catch (IOException e) {
+                        }
+                    }
+                }
+            }
+
+            return successful;
+        }
+
+        public boolean isRunning() {
+            return mRunning;
+        }
+
+        public boolean isCanceled() {
+            return mCanceled;
+        }
+
+        public void cancel() {
+            mCanceled = true;
+            synchronized (this) {
+                if (mVCardParser != null) {
+                    mVCardParser.cancel();
+                }
+            }
+        }
+
+        public synchronized void addRequest(RequestParameter parameter) {
+            if (mRunning) {
+                mProgressNotifier.addTotalCount(parameter.entryCount);
+                mPendingRequests.add(parameter);
+            } else {
+                Log.w(LOG_TAG, "Request came while the service is not running any more.");
+            }
+        }
+
+        public List<Uri> getCreatedUris() {
+            return mCreatedUris;
+        }
+
+        public List<Uri> getFailedUris() {
+            return mFailedUris;
+        }
+    }
+
+    private static class ProgressNotifier implements VCardEntryHandler {
+        private Context mContext;
+        private NotificationManager mNotificationManager;
+
+        private int mCurrentCount;
+        private int mTotalCount;
+
+        public void init(Context context, NotificationManager notificationManager) {
+            mContext = context;
+            mNotificationManager = notificationManager;
         }
 
         public void onStart() {
@@ -80,7 +445,6 @@
                 return;
             }
 
-            final Context context = ImportVCardService.this;
             // We don't use startEntry() since:
             // - We cannot know name there but here.
             // - There's high probability where name comes soon after the beginning of entry, so
@@ -88,10 +452,14 @@
             final String packageName = "com.android.contacts";
             final RemoteViews remoteViews = new RemoteViews(packageName,
                     R.layout.status_bar_ongoing_event_progress_bar);
-            final String title = getString(R.string.reading_vcard_title);
-            final String text = getString(R.string.progress_notifier_message,
+            final String title = mContext.getString(R.string.reading_vcard_title);
+            String totalCountString;
+            synchronized (this) {
+                totalCountString = String.valueOf(mTotalCount); 
+            }
+            final String text = mContext.getString(R.string.progress_notifier_message,
                     String.valueOf(mCurrentCount),
-                    String.valueOf(mTotalCount),
+                    totalCountString,
                     contactStruct.getDisplayName());
 
             // TODO: uploading image does not work correctly. (looks like a static image).
@@ -99,7 +467,7 @@
             remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
                     mTotalCount == -1);
             final String percentage =
-                    getString(R.string.percentage,
+                    mContext.getString(R.string.percentage,
                             String.valueOf(mCurrentCount * 100/mTotalCount));
             remoteViews.setTextViewText(R.id.progress_text, percentage);
             remoteViews.setImageViewResource(R.id.appIcon, android.R.drawable.stat_sys_download);
@@ -110,359 +478,30 @@
             notification.contentView = remoteViews;
 
             notification.contentIntent =
-                    PendingIntent.getActivity(context, 0,
-                            new Intent(context, ContactsListActivity.class), 0);
-            mNotificationManager.notify(mId, notification);
+                    PendingIntent.getActivity(mContext, 0,
+                            new Intent(mContext, ContactsListActivity.class), 0);
+            mNotificationManager.notify(MESSAGE_ID, notification);
+        }
+
+        public synchronized void addTotalCount(int additionalCount) {
+            mTotalCount += additionalCount;
         }
 
         public void onEnd() {
         }
     }
 
-    private class VCardReadThread extends Thread {
-        private final Context mContext;
-        private final ContentResolver mResolver;
-        private VCardParser mVCardParser;
-        private boolean mCanceled;
-        private final List<Uri> mErrorUris;
-        private final List<Uri> mCreatedUris;
-
-        public VCardReadThread() {
-            mContext = ImportVCardService.this;
-            mResolver = mContext.getContentResolver();
-            mErrorUris = new ArrayList<Uri>();
-            mCreatedUris = new ArrayList<Uri>();
-        }
-
-        @Override
-        public void run() {
-            while (!mCanceled) {
-                mErrorUris.clear();
-                mCreatedUris.clear();
-
-                final Account account;
-                final Uri uri;
-                final int estimatedType;
-                final String estimatedCharset;
-                final boolean useV30;
-                final int entryCount;
-                final int id;
-                final boolean needReview;
-                synchronized (mContext) {
-                    if (mPendingInputs.size() == 0) {
-                        mNowRunning = false;
-                        break;
-                    } else {
-                        final PendingInput pendingInput = mPendingInputs.poll();
-                        account = pendingInput.account;
-                        uri = pendingInput.uri;
-                        estimatedType = pendingInput.estimatedType;
-                        estimatedCharset = pendingInput.estimatedCharset;
-                        useV30 = pendingInput.useV30;
-                        entryCount = pendingInput.entryCount;
-                        id = pendingInput.id;
-                    }
-                }
-                runInternal(account, uri, estimatedType, estimatedCharset,
-                        useV30, entryCount, id);
-                doFinishNotification(id, uri);
-            }
-            Log.i(LOG_TAG, "Successfully imported. Total: " + mTotalCount);
-            stopSelf();
-        }
-
-        private void runInternal(Account account,
-                Uri uri, int estimatedType, String estimatedCharset,
-                boolean useV30, int entryCount,
-                int id) {
-            int totalCount = 0;
-            final ArrayList<VCardSourceDetector> detectorList =
-                new ArrayList<VCardSourceDetector>();
-            if (mCanceled) {
-                return;
-            }
-
-            // First scanning is over. Try to import each vCard, which causes side effects.
-            mTotalCount += entryCount;
-            mCurrentCount = 0;
-
-            if (mCanceled) {
-                Log.w(LOG_TAG, "Canceled during importing (with storing data in database)");
-                // TODO: implement cancel correctly.
-                return;
-            }
-
-            final VCardEntryConstructor constructor =
-                    new VCardEntryConstructor(estimatedType, account, estimatedCharset);
-            final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
-            constructor.addEntryHandler(committer);
-            constructor.addEntryHandler(new ProgressNotifier(id));
-
-            if (!readOneVCard(uri, estimatedType, estimatedCharset, constructor)) {
-                Log.e(LOG_TAG, "Failed to read \"" + uri.toString() + "\" " +
-                "while first scan was successful.");
-            }
-                final List<Uri> createdUris = committer.getCreatedUris();
-                if (createdUris != null && createdUris.size() > 0) {
-                    mCreatedUris.addAll(createdUris);
-                } else {
-                    Log.w(LOG_TAG, "Created Uris is null (src = " + uri.toString() + "\"");
-                }
-        }
-
-        private boolean readOneVCard(Uri uri, int vcardType, String charset,
-                VCardInterpreter interpreter) {
-            InputStream is;
-            try {
-                // TODO: use vcardType given from detector and stop trying to read the file twice.
-                is = mResolver.openInputStream(uri);
-
-                // We need synchronized since we need to handle mCanceled and mVCardParser
-                // at once. In the worst case, a user may call cancel() just before recreating
-                // mVCardParser.
-                synchronized (this) {
-                    // TODO: ensure this change works fine.
-                    // mVCardParser = new VCardParser_V21(vcardType, charset);
-                    mVCardParser = new VCardParser_V21(vcardType);
-                    if (mCanceled) {
-                        mVCardParser.cancel();
-                    }
-                }
-
-                try {
-                    mVCardParser.parse(is, interpreter);
-                } catch (VCardVersionException e1) {
-                    try {
-                        is.close();
-                    } catch (IOException e) {
-                    }
-                    if (interpreter instanceof VCardEntryConstructor) {
-                        // Let the object clean up internal temporal objects,
-                        ((VCardEntryConstructor) interpreter).clear();
-                    }
-                    is = mResolver.openInputStream(uri);
-
-                    synchronized (this) {
-                        // mVCardParser = new VCardParser_V30(vcardType, charset);
-                        mVCardParser = new VCardParser_V30(vcardType);
-                        if (mCanceled) {
-                            mVCardParser.cancel();
-                        }
-                    }
-
-                    try {
-                        mVCardParser.parse(is, interpreter);
-                    } catch (VCardVersionException e2) {
-                        throw new VCardException("vCard with unspported version.");
-                    }
-                } finally {
-                    if (is != null) {
-                        try {
-                            is.close();
-                        } catch (IOException e) {
-                        }
-                    }
-                }
-            } catch (IOException e) {
-                Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
-                return false;
-            } catch (VCardNestedException e) {
-                // In the first scan, we may (correctly) encounter this exception.
-                // We assume that we were able to detect the type of vCard before
-                // the exception being thrown.
-                //
-                // In the second scan, we may (inappropriately) encounter it.
-                // We silently ignore it, since
-                // - It is really unusual situation.
-                // - We cannot handle it by definition.
-                // - Users cannot either.
-                // - We should not accept unnecessarily complicated vCard, possibly by wrong manner.
-                Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
-            } catch (VCardNotSupportedException e) {
-                return false;
-            } catch (VCardException e) {
-                return false;
-            }
-            return true;
-        }
-
-        private void doErrorNotification(int id) {
-            final Notification notification = new Notification();
-            notification.icon = android.R.drawable.stat_sys_download_done;
-            final String title = mContext.getString(R.string.reading_vcard_failed_title);
-            final PendingIntent intent =
-                    PendingIntent.getActivity(mContext, 0, new Intent(), 0);
-            notification.setLatestEventInfo(mContext, title, "", intent);
-            mNotificationManager.notify(id, notification);
-        }
-
-        private void doFinishNotification(int id, Uri uri) {
-            final Notification notification = new Notification();
-            notification.icon = android.R.drawable.stat_sys_download_done;
-            final String title = mContext.getString(R.string.reading_vcard_finished_title);
-
-            final Intent intent;
-            final long rawContactId = ContentUris.parseId(mCreatedUris.get(0));
-            final Uri contactUri = RawContacts.getContactLookupUri(
-                    getContentResolver(), ContentUris.withAppendedId(
-                            RawContacts.CONTENT_URI, rawContactId));
-            intent = new Intent(Intent.ACTION_VIEW, contactUri);
-
-            final PendingIntent pendingIntent =
-                    PendingIntent.getActivity(mContext, 0, intent, 0);
-            notification.setLatestEventInfo(mContext, title, "", pendingIntent);
-            mNotificationManager.notify(id, notification);
-        }
-
-        // We need synchronized since we need to handle mCanceled and mVCardParser at once.
-        public synchronized void cancel() {
-            mCanceled = true;
-            if (mVCardParser != null) {
-                mVCardParser.cancel();
-            }
-        }
-
-        public void onCancel(DialogInterface dialog) {
-            cancel();
-        }
-    }
-
-    private static class PendingInput {
-        public final Account account;
-        public final Uri uri;
-        public final int estimatedType;
-        public final String estimatedCharset;
-        public final boolean useV30;
-        public final int entryCount;
-        public final int id;
-
-        public PendingInput(Account account,
-                Uri uri, int estimatedType, String estimatedCharset,
-                boolean useV30, int entryCount,
-                int id) {
-            this.account = account;
-            this.uri = uri;
-            this.estimatedType = estimatedType;
-            this.estimatedCharset = estimatedCharset;
-            this.useV30 = useV30;
-            this.entryCount = entryCount;
-            this.id = id;
-        }
-    }
-
-    // The two classes bellow must be called inside the synchronized block, using this
-    // Activity as a Context.
-    private boolean mNowRunning;
-    private final Queue<PendingInput> mPendingInputs = new LinkedList<PendingInput>();
-
-    private NotificationManager mNotificationManager;
-    private Thread mThread;
-    private int mTotalCount;
-    private int mCurrentCount;
-
-    private Uri[] tryGetUris(Intent intent) {
-        final String[] uriStrings =
-                intent.getStringArrayExtra(ImportVCardActivity.VCARD_URI_ARRAY);
-        if (uriStrings == null || uriStrings.length == 0) {
-            Log.e(LOG_TAG, "Given uri array is empty");
-            return null;
-        }
-
-        final int length = uriStrings.length;
-        final Uri[] uris = new Uri[length];
-        for (int i = 0; i < length; i++) {
-            uris[i] = Uri.parse(uriStrings[i]);
-        }
-
-        return uris;
-    }
-
-    private Account tryGetAccount(Intent intent) {
-        if (intent == null) {
-            Log.w(LOG_TAG, "Intent is null");
-            return null;
-        }
-
-        final String accountName = intent.getStringExtra("account_name");
-        final String accountType = intent.getStringExtra("account_type");
-        if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
-            return new Account(accountName, accountType);
-        } else {
-            Log.w(LOG_TAG, "Account is not set.");
-            return null;
-        }
-    }
+    private RequestHandler mRequestHandler;
+    private Messenger mMessenger = new Messenger(new ImportRequestHandler());
+    private Thread mThread = null;
 
     @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        if (mNotificationManager == null) {
-            mNotificationManager =
-                    (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
-        }
-
-        // TODO: use this.
-        final int[] estimatedTypeArray =
-            intent.getIntArrayExtra(ImportVCardActivity.ESTIMATED_VCARD_TYPE_ARRAY);
-        final String[] estimatedCharsetArray =
-            intent.getStringArrayExtra(ImportVCardActivity.ESTIMATED_CHARSET_ARRAY);
-        final boolean[] useV30Array =
-            intent.getBooleanArrayExtra(ImportVCardActivity.USE_V30_ARRAY);
-        final int[] entryCountArray =
-            intent.getIntArrayExtra(ImportVCardActivity.ENTRY_COUNT_ARRAY);
-
-        final Account account = tryGetAccount(intent);
-        final Uri[] uris = tryGetUris(intent);
-        if (uris == null) {
-            Log.e(LOG_TAG, "Uris are null.");
-            Toast.makeText(this, getString(R.string.reading_vcard_failed_title),
-                    Toast.LENGTH_LONG).show();
-            stopSelf();
-            return START_NOT_STICKY;
-        }
-
-        int length = uris.length;
-        if (estimatedTypeArray.length < length) {
-            Log.w(LOG_TAG, String.format("estimatedTypeArray.length < length (%d, %d)",
-                    estimatedTypeArray.length, length));
-            length = estimatedTypeArray.length;
-        }
-        if (useV30Array.length < length) {
-            Log.w(LOG_TAG, String.format("useV30Array.length < length (%d, %d)",
-                    useV30Array.length, length));
-            length = useV30Array.length;
-        }
-        if (entryCountArray.length < length) {
-            Log.w(LOG_TAG, String.format("entryCountArray.length < length (%d, %d)",
-                    entryCountArray.length, length));
-            length = entryCountArray.length;
-        }
-
-        synchronized (this) {
-            for (int i = 0; i < length; i++) {
-                mPendingInputs.add(new PendingInput(account,
-                        uris[i], estimatedTypeArray[i], estimatedCharsetArray[i],
-                        useV30Array[i], entryCountArray[i],
-                        startId));
-            }
-            if (!mNowRunning) {
-                Toast.makeText(this, getString(R.string.vcard_importer_start_message),
-                        Toast.LENGTH_LONG).show();
-                // Assume thread is alredy broken.
-                // Even when it still exists, it never scan the PendingInput newly added above.
-                mNowRunning = true;
-                mThread = new VCardReadThread();
-                mThread.start();
-            } else {
-                Toast.makeText(this, getString(R.string.vcard_importer_will_start_message),
-                        Toast.LENGTH_LONG).show();
-            }
-        }
-
-        return START_NOT_STICKY;
+    public int onStartCommand(Intent intent, int flags, int id) {
+        return START_STICKY;
     }
 
     @Override
     public IBinder onBind(Intent intent) {
-        return null;
+        return mMessenger.getBinder();
     }
 }