Refactor ImportVCardService. Split the Service into several components.

Bug: 2733143
Change-Id: Iefcb0919d5c740f1ae8a63e5dc8095a39dbd0cd5
diff --git a/src/com/android/contacts/ImportProgressNotifier.java b/src/com/android/contacts/ImportProgressNotifier.java
new file mode 100644
index 0000000..e24b307
--- /dev/null
+++ b/src/com/android/contacts/ImportProgressNotifier.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.RemoteViews;
+
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryHandler;
+
+/**
+ * {@link VCardEntryHandler} implementation which lets the system update
+ * the current status of vCard import.
+ */
+public class ImportProgressNotifier 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() {
+    }
+
+    public void onEntryCreated(VCardEntry contactStruct) {
+        mCurrentCount++;  // 1 origin.
+        if (contactStruct.isIgnorable()) {
+            return;
+        }
+
+        // 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
+        //   we don't need to hurry to show something.
+        final String packageName = "com.android.contacts";
+        final RemoteViews remoteViews = new RemoteViews(packageName,
+                R.layout.status_bar_ongoing_event_progress_bar);
+        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),
+                totalCountString,
+                contactStruct.getDisplayName());
+
+        // TODO: uploading image does not work correctly. (looks like a static image).
+        remoteViews.setTextViewText(R.id.description, text);
+        remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
+                mTotalCount == -1);
+        final 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);
+
+        final Notification notification = new Notification();
+        notification.icon = android.R.drawable.stat_sys_download;
+        notification.flags |= Notification.FLAG_ONGOING_EVENT;
+        notification.contentView = remoteViews;
+
+        notification.contentIntent =
+                PendingIntent.getActivity(mContext, 0,
+                        new Intent(mContext, ContactsListActivity.class), 0);
+        mNotificationManager.notify(ImportVCardService.NOTIFICATION_ID, notification);
+    }
+
+    public synchronized void addTotalCount(int additionalCount) {
+        mTotalCount += additionalCount;
+    }
+
+    public synchronized void resetTotalCount() {
+        mTotalCount = 0;
+    }
+
+    public void onEnd() {
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ImportRequest.java b/src/com/android/contacts/ImportRequest.java
new file mode 100644
index 0000000..56a33b1
--- /dev/null
+++ b/src/com/android/contacts/ImportRequest.java
@@ -0,0 +1,86 @@
+/*
+ * 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;
+
+import android.accounts.Account;
+import android.net.Uri;
+
+import com.android.vcard.VCardSourceDetector;
+
+/**
+ * Class representing one request for reading vCard (as a Uri representation).
+ *
+ * Mainly used when {@link ImportVCardActivity} requests {@link ImportVCardService}
+ * to import some specific Uri.
+ *
+ * Note: This object's accepting only One Uri does NOT mean that
+ * there's only one vCard entry inside the instance, as one Uri often has multiple
+ * vCard entries inside it.
+ */
+public class ImportRequest {
+    /**
+     * Can be null (typically when there's no Account available in the system).
+     */
+    public final Account account;
+    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;
+
+    /**
+     * The count of vCard entries in {@link #uri}. A receiver of this object can use it
+     * when showing the progress of import. Thus a receiver must be able to torelate this
+     * variable being invalid because of vCard's limitation.
+     *
+     * vCard does not let us know this count without looking over a whole file content,
+     * which means we have to open and scan over {@link #uri} to know this value, while
+     * it may not be opened more than once (Uri does not require it to be opened multiple times
+     * and may become invalid after its close() request).
+     */
+    public final int entryCount;
+    public ImportRequest(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;
+    }
+}
diff --git a/src/com/android/contacts/ImportRequestProcessor.java b/src/com/android/contacts/ImportRequestProcessor.java
new file mode 100644
index 0000000..d19680b
--- /dev/null
+++ b/src/com/android/contacts/ImportRequestProcessor.java
@@ -0,0 +1,361 @@
+/*
+ * 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;
+
+import android.accounts.Account;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+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 com.android.vcard.VCardEntryCommitter;
+import com.android.vcard.VCardEntryConstructor;
+import com.android.vcard.VCardInterpreter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+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.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.
+ */
+public class ImportRequestProcessor {
+    private static final String LOG_TAG = "ImportRequestProcessor";
+
+    private final Service mService;
+
+    private ContentResolver mResolver;
+    private 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;
+
+    // TODO(dmiyakawa): better design for testing?
+    /* package */ interface CommitterGenerator {
+        public VCardEntryCommitter generate(ContentResolver resolver);
+    }
+
+    private static class DefaultCommitterGenerator implements CommitterGenerator {
+        public VCardEntryCommitter generate(ContentResolver resolver) {
+            return new VCardEntryCommitter(resolver);
+        }
+    }
+
+    /* package */ CommitterGenerator mCommitterGenerator = new DefaultCommitterGenerator();
+
+    /**
+     * 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 boolean mCanceled;
+
+    /**
+     * Meaning that this object is able to accept import requests.
+     */
+    private boolean mReadyForRequest;
+    private final Queue<ImportRequest> mPendingRequests =
+            new LinkedList<ImportRequest>();
+
+    /* package */ interface ThreadStarter {
+        public void start();
+    }
+    /* package */ ThreadStarter mThreadStarter = new ThreadStarter() {
+        public void start() {
+            final Thread thread = new Thread(new Runnable() {
+                public void run() {
+                    process();
+                }
+            });
+            thread.start();
+        }
+    };
+
+
+    public ImportRequestProcessor(final Service service) {
+        mService = service;
+    }
+
+    public synchronized void pushRequest(ImportRequest parameter) {
+        if (mResolver == null) {
+            // Service object may not ready at the construction time
+            // (e.g. ContentResolver may be null).
+            mResolver = mService.getContentResolver();
+            mNotificationManager =
+                    (NotificationManager)mService.getSystemService(Context.NOTIFICATION_SERVICE);
+        }
+
+        final boolean needThreadStart;
+        if (!mReadyForRequest) {
+            mFailedUris.clear();
+            mCreatedUris.clear();
+
+            mNotifier.init(mService, mNotificationManager);
+            needThreadStart = true;
+        } else {
+            needThreadStart = false;
+        }
+        final int count = parameter.entryCount;
+        if (count > 0) {
+            mNotifier.addTotalCount(count);
+        }
+        mPendingRequests.add(parameter);
+        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.
+     */
+    /* package */ void process() {
+        if (!mReadyForRequest) {
+            throw new RuntimeException(
+                    "process() is called after this object finishing its process.");
+        }
+        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 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.
+            mReadyForRequest = false;  // Just in case.
+            mNotifier.resetTotalCount();
+        }
+    }
+
+    /**
+     * Would be run inside syncronized block.
+     */
+    /* package */ boolean handleOneRequest(final ImportRequest 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(mNotifier);
+
+        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(ImportVCardService.NOTIFICATION_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 synchronized boolean isReadyForRequest() {
+        return mReadyForRequest;
+    }
+
+    public boolean isCanceled() {
+        return mCanceled;
+    }
+
+    public void cancel() {
+        mCanceled = true;
+        synchronized (this) {
+            if (mVCardParser != null) {
+                mVCardParser.cancel();
+            }
+        }
+    }
+
+    public List<Uri> getCreatedUris() {
+        return mCreatedUris;
+    }
+
+    public List<Uri> getFailedUris() {
+        return mFailedUris;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index f263f7b..ade0fdb 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -44,7 +44,6 @@
 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;
@@ -123,10 +122,10 @@
     private class CustomConnection implements ServiceConnection {
         private Messenger mMessenger;
         /**
-         * Stores {@link RequestParameter} objects until actual connection is established.
+         * Stores {@link ImportRequest} objects until actual connection is established.
          */
-        private Queue<RequestParameter> mPendingRequests =
-                new LinkedList<RequestParameter>();
+        private Queue<ImportRequest> mPendingRequests =
+                new LinkedList<ImportRequest>();
 
         private boolean mConnected = false;
         private boolean mNeedFinish = false;
@@ -147,7 +146,7 @@
             }
         }
 
-        public synchronized void requestSend(final RequestParameter parameter) {
+        public synchronized void requestSend(final ImportRequest parameter) {
             // Log.d("@@@", "requestSend(): " + (mMessenger != null) + ", "
             // + mPendingRequests.size());
             if (mMessenger != null) {
@@ -157,11 +156,11 @@
             }
         }
 
-        private void sendMessage(final RequestParameter parameter) {
+        private void sendMessage(final ImportRequest parameter) {
             // Log.d("@@@", "sendMessage()");
             try {
                 mMessenger.send(Message.obtain(null,
-                        ImportVCardService.IMPORT_REQUEST,
+                        ImportVCardService.MSG_IMPORT_REQUEST,
                         parameter));
             } catch (RemoteException e) {
                 Log.e(LOG_TAG, "RemoteException is thrown when trying to import vCard");
@@ -171,13 +170,12 @@
         }
 
         public void onServiceConnected(ComponentName name, IBinder service) {
-            // Log.d("@@@", "onServiceConnected()");
             synchronized (this) {
                 mMessenger = new Messenger(service);
                 // Send pending requests thrown from this Activity before an actual connection
                 // is established.
                 while (!mPendingRequests.isEmpty()) {
-                    final RequestParameter parameter = mPendingRequests.poll();
+                    final ImportRequest parameter = mPendingRequests.poll();
                     if (parameter == null) {
                         throw new NullPointerException();
                     }
@@ -328,7 +326,7 @@
                         Log.w(LOG_TAG, "destUri is null");
                         break;
                     }
-                    final RequestParameter parameter = constructRequestParameter(localDataUri);
+                    final ImportRequest parameter = constructRequestParameter(localDataUri);
                     if (mCanceled) {
                         return;
                     }
@@ -413,10 +411,10 @@
         }
 
         /**
-         * Reads the Uri once (or twice) and constructs {@link RequestParameter} from
+         * Reads the Uri once (or twice) and constructs {@link ImportRequest} from
          * its content.
          */
-        private RequestParameter constructRequestParameter(final Uri uri) {
+        private ImportRequest constructRequestParameter(final Uri uri) {
             final ContentResolver resolver =
                     ImportVCardActivity.this.getContentResolver();
             VCardEntryCounter counter = null;
@@ -475,7 +473,7 @@
                 Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
                 return null;
             }
-            return new RequestParameter(mAccount, uri,
+            return new ImportRequest(mAccount, uri,
                     detector.getEstimatedType(),
                     detector.getEstimatedCharset(),
                     vcardVersion, counter.getCount());
diff --git a/src/com/android/contacts/ImportVCardService.java b/src/com/android/contacts/ImportVCardService.java
index e79bf82..f576d4b 100644
--- a/src/com/android/contacts/ImportVCardService.java
+++ b/src/com/android/contacts/ImportVCardService.java
@@ -15,135 +15,45 @@
  */
 package com.android.contacts;
 
-import android.accounts.Account;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.app.Service;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
 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.util.Log;
-import android.widget.RemoteViews;
 import android.widget.Toast;
 
-import com.android.vcard.VCardEntry;
-import com.android.vcard.VCardEntryCommitter;
-import com.android.vcard.VCardEntryConstructor;
-import com.android.vcard.VCardEntryHandler;
-import com.android.vcard.VCardInterpreter;
-import com.android.vcard.VCardParser;
-import com.android.vcard.VCardParser_V21;
-import com.android.vcard.VCardParser_V30;
-import com.android.vcard.VCardSourceDetector;
-import com.android.vcard.exception.VCardException;
-import com.android.vcard.exception.VCardNestedException;
-import com.android.vcard.exception.VCardNotSupportedException;
-import com.android.vcard.exception.VCardVersionException;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-
 /**
  * The class responsible for importing vCard from one ore multiple Uris.
  */
 public class ImportVCardService extends Service {
     private final static String LOG_TAG = "ImportVCardService";
 
-    /* package */ static final int IMPORT_REQUEST = 1;
+    /* package */ static final int MSG_IMPORT_REQUEST = 1;
 
-    private static final int MESSAGE_ID = 1000;
-
-    // TODO: Too many static classes. Create separate files for them.
+    /* package */ static final int NOTIFICATION_ID = 1000;
 
     /**
-     * Class representing one request for reading vCard (as a Uri representation).
+     * Small vCard file is imported soon, so any meassage saying "vCard import started" is
+     * not needed. We show the message when the size of vCard is larger than this constant. 
      */
-    /* 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;
-        }
-    }
+    private static final int IMPORT_NOTIFICATION_THRESHOLD = 10; 
 
     public class ImportRequestHandler extends Handler {
+        private final ImportRequestProcessor mRequestProcessor =
+                new ImportRequestProcessor(ImportVCardService.this);
         @Override
         public void handleMessage(Message msg) {
-            Log.d("@@@", "handleMessange: " + msg.what);
             switch (msg.what) {
-                case IMPORT_REQUEST: {
-                    Log.d("@@@", "IMPORT_REQUEST");
-                    final 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;
+                case MSG_IMPORT_REQUEST: {
+                    final ImportRequest parameter = (ImportRequest)msg.obj;
+                    if (parameter.entryCount > IMPORT_NOTIFICATION_THRESHOLD) {
+                        Toast.makeText(ImportVCardService.this,
+                                getString(R.string.vcard_importer_start_message),
+                                Toast.LENGTH_LONG).show();
                     }
-                    mRequestHandler.addRequest(parameter);
-                    if (needThreadStart) {
-                        mThread = new Thread(new Runnable() {
-                            public void run() {
-                                mRequestHandler.handleRequests();
-                            }
-                        });
-                        mThread.start();
-                    }
+                    mRequestProcessor.pushRequest(parameter);
                     break;
                 }
                 default:
@@ -153,341 +63,7 @@
         }
     }
 
-    // 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 final List<Uri> mFailedUris = new ArrayList<Uri>();
-        private final List<Uri> mCreatedUris = new ArrayList<Uri>();
-        private ProgressNotifier mProgressNotifier = new ProgressNotifier();
-
-        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 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() {
-        }
-
-        public void onEntryCreated(VCardEntry contactStruct) {
-            mCurrentCount++;  // 1 origin.
-            if (contactStruct.isIgnorable()) {
-                return;
-            }
-
-            // 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
-            //   we don't need to hurry to show something.
-            final String packageName = "com.android.contacts";
-            final RemoteViews remoteViews = new RemoteViews(packageName,
-                    R.layout.status_bar_ongoing_event_progress_bar);
-            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),
-                    totalCountString,
-                    contactStruct.getDisplayName());
-
-            // TODO: uploading image does not work correctly. (looks like a static image).
-            remoteViews.setTextViewText(R.id.description, text);
-            remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
-                    mTotalCount == -1);
-            final 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);
-
-            final Notification notification = new Notification();
-            notification.icon = android.R.drawable.stat_sys_download;
-            notification.flags |= Notification.FLAG_ONGOING_EVENT;
-            notification.contentView = remoteViews;
-
-            notification.contentIntent =
-                    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 RequestHandler mRequestHandler;
     private Messenger mMessenger = new Messenger(new ImportRequestHandler());
-    private Thread mThread = null;
 
     @Override
     public int onStartCommand(Intent intent, int flags, int id) {
@@ -496,7 +72,6 @@
 
     @Override
     public IBinder onBind(Intent intent) {
-        Log.d("@@@", "onBind");
         return mMessenger.getBinder();
     }
 }