Fix bugs around vCard export and use new APIs

1) let vCard exporter select correct destination.
Multiple vCard exports don't select different destination:
if users request "export", "import" and "export", they will have
just one exported vCard with imported data, not two (one without
import result and another with the result). This is because
each vCard exporter independently refers to existing files in USB
storage.

This changes make vCard service check available files and remember
file names already reserved.

2) use new Notification.Builder API
3) show user-friendly message when there's no vCard on USB storage

Bug: 3219880
Bug: 3219906
Change-Id: I159d25439023eb10934729a00f4da6d157e44b09
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ddc1537..fdcc6a7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -758,12 +758,14 @@
          is rarely needed in the actual vCard...). -->
     <string name="fail_reason_vcard_not_supported_error">Failed to parse vCard though it seems in valid format, since the current implementation does not support it</string>
 
-    <!-- The failed reason shown when the system could not find any vCard file
-         (with extension ".vcf" in SDCard.) [CHAR LIMIT=NONE] -->
-    <string name="fail_reason_no_vcard_file" product="nosdcard">No vCard file found in the USB storage</string>
-    <!-- The failed reason shown when the system could not find any vCard file
-         (with extension ".vcf" in SDCard.) -->
-    <string name="fail_reason_no_vcard_file" product="default">No vCard file found on the SD card</string>
+    <!-- The failure message shown when the system could not find any vCard file.
+         (with extension ".vcf" in USB storage.)
+         [CHAR LIMIT=128] -->
+    <string name="import_failure_no_vcard_file" product="nosdcard">No vCard file found in the USB storage</string>
+    <!-- The failure message shown when the system could not find any vCard file.
+         (with extension ".vcf" in SDCard.)
+         [CHAR LIMIT=128] -->
+    <string name="import_failure_no_vcard_file" product="default">No vCard file found on the SD card</string>
 
     <!-- Fail reason shown when vCard importer failed to look over meta information stored in vCard file(s). -->
     <string name="fail_reason_failed_to_collect_vcard_meta_info">Failed to collect meta information of given vCard file(s).</string>
diff --git a/src/com/android/contacts/vcard/ExportProcessor.java b/src/com/android/contacts/vcard/ExportProcessor.java
index 028bcc7..c5f293b 100644
--- a/src/com/android/contacts/vcard/ExportProcessor.java
+++ b/src/com/android/contacts/vcard/ExportProcessor.java
@@ -41,6 +41,7 @@
  */
 public class ExportProcessor extends ProcessorBase {
     private static final String LOG_TAG = "VCardExport";
+    private static final boolean DEBUG = VCardService.DEBUG;
 
     private final VCardService mService;
     private final ContentResolver mResolver;
@@ -88,7 +89,7 @@
     }
 
     private void runInternal() {
-        Log.i(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
+        if (DEBUG) Log.d(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
         final ExportRequest request = mExportRequest;
         VCardComposer composer = null;
         boolean successful = false;
@@ -108,7 +109,6 @@
                 final String errorReason =
                     mService.getString(R.string.fail_reason_could_not_open_file,
                             uri, e.getMessage());
-                Log.i(LOG_TAG, "failed to export (could not open output stream)");
                 doFinishNotification(errorReason, "");
                 return;
             }
@@ -132,7 +132,6 @@
             composer.addHandler(composer.new HandlerForOutputStream(outputStream));
 
             if (!composer.init()) {
-                Log.w(LOG_TAG, "vCard composer init failed");
                 final String errorReason = composer.getErrorReason();
                 Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
                 final String translatedErrorReason =
@@ -219,6 +218,7 @@
     }
 
     private void doCancelNotification() {
+        if (DEBUG) Log.d(LOG_TAG, "send cancel notification");
         final String description = mService.getString(R.string.exporting_vcard_canceled_title,
                 mExportRequest.destUri.getLastPathSegment());
         final Notification notification =
@@ -227,15 +227,16 @@
     }
 
     private void doFinishNotification(final String title, final String description) {
+        if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
         final Intent intent = new Intent(mService, ContactBrowserActivity.class);
         final Notification notification =
-                VCardService.constructFinishNotification(mService, description, intent);
+                VCardService.constructFinishNotification(mService, title, description, intent);
         mNotificationManager.notify(mJobId, notification);
     }
 
     @Override
     public synchronized boolean cancel(boolean mayInterruptIfRunning) {
-        Log.i(LOG_TAG, "received cancel request");
+        if (DEBUG) Log.d(LOG_TAG, "received cancel request");
         if (mDone || mCanceled) {
             return false;
         }
@@ -252,4 +253,8 @@
     public synchronized boolean isDone() {
         return mDone;
     }
+
+    public ExportRequest getRequest() {
+        return mExportRequest;
+    }
 }
diff --git a/src/com/android/contacts/vcard/ExportVCardActivity.java b/src/com/android/contacts/vcard/ExportVCardActivity.java
index d53d5b2..9211aec 100644
--- a/src/com/android/contacts/vcard/ExportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ExportVCardActivity.java
@@ -15,6 +15,9 @@
  */
 package com.android.contacts.vcard;
 
+import com.android.contacts.R;
+import com.android.vcard.VCardComposer;
+
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -23,9 +26,9 @@
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.ServiceConnection;
-import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
@@ -33,35 +36,82 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.contacts.R;
-import com.android.vcard.VCardComposer;
-
 import java.io.File;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.Queue;
-import java.util.Set;
 
 /**
- * Class for exporting vCard.
+ * Shows a dialog confirming the export and asks actual vCard export to {@link VCardService}
  *
- * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
- * finished (with the method {@link Activity#finish()}) after the export and never reuse
- * any Dialog in the instance. So this code is careless about the management around managed
- * dialogs stuffs (like how onCreateDialog() is used).
+ * This Activity first connects to VCardService and ask an available file name and shows it to
+ * a user. After the user's confirmation, it send export request with the file name, assuming the
+ * file name is not reserved yet.
  */
-public class ExportVCardActivity extends Activity {
+public class ExportVCardActivity extends Activity implements ServiceConnection,
+        DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
     private static final String LOG_TAG = "VCardExport";
+    private static final boolean DEBUG = VCardService.DEBUG;
 
-    // If true, VCardExporter is able to emits files longer than 8.3 format.
-    private static final boolean ALLOW_LONG_FILE_NAME = false;
-    private String mTargetDirectory;
-    private String mFileNamePrefix;
-    private String mFileNameSuffix;
-    private int mFileIndexMinimum;
-    private int mFileIndexMaximum;
-    private String mFileNameExtension;
-    private Set<String> mExtensionsToConsider;
+    /**
+     * Handler used when some Message has come from {@link VCardService}.
+     */
+    private class IncomingHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            if (DEBUG) Log.d(LOG_TAG, "IncomingHandler received message.");
+
+            if (msg.arg1 != 0) {
+                Log.i(LOG_TAG, "Message returned from vCard server contains error code.");
+                if (msg.obj != null) {
+                    mErrorReason = (String)msg.obj;
+                }
+                showDialog(msg.arg1);
+                return;
+            }
+
+            switch (msg.what) {
+            case VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION:
+                if (msg.obj == null) {
+                    Log.w(LOG_TAG, "Message returned from vCard server doesn't contain valid path");
+                    mErrorReason = getString(R.string.fail_reason_unknown);
+                    showDialog(R.id.dialog_fail_to_export_with_reason);
+                } else {
+                    mTargetFileName = (String)msg.obj;
+                    if (TextUtils.isEmpty(mTargetFileName)) {
+                        Log.w(LOG_TAG, "Destination file name coming from vCard service is empty.");
+                        mErrorReason = getString(R.string.fail_reason_unknown);
+                        showDialog(R.id.dialog_fail_to_export_with_reason);
+                    } else {
+                        if (DEBUG) {
+                            Log.d(LOG_TAG,
+                                    String.format("Target file name is set (%s). " +
+                                            "Show confirmation dialog", mTargetFileName));
+                        }
+                        showDialog(R.id.dialog_export_confirmation);
+                    }
+                }
+                break;
+            default:
+                Log.w(LOG_TAG, "Unknown message type: " + msg.what);
+                super.handleMessage(msg);
+            }
+        }
+    }
+
+    /**
+     * True when this Activity is connected to {@link VCardService}.
+     *
+     * Should be touched inside synchronized block.
+     */
+    private boolean mConnected;
+
+    /**
+     * True when users need to do something and this Activity should not disconnect from
+     * VCardService. False when all necessary procedures are done (including sending export request)
+     * or there's some error occured.
+     */
+    private volatile boolean mProcessOngoing = true;
+
+    private Messenger mOutgoingMessenger;
+    private final Messenger mIncomingMessenger = new Messenger(new IncomingHandler());
 
     // Used temporarily when asking users to confirm the file name
     private String mTargetFileName;
@@ -69,190 +119,128 @@
     // String for storing error reason temporarily.
     private String mErrorReason;
 
-
-    private class CustomConnection implements ServiceConnection {
-        private Messenger mMessenger;
-        private Queue<ExportRequest> mPendingRequests = new LinkedList<ExportRequest>();
-
-        public void doBindService() {
-
-        }
-
-        public synchronized void requestSend(final ExportRequest parameter) {
-            if (mMessenger != null) {
-                sendMessage(parameter);
-            } else {
-                mPendingRequests.add(parameter);
-            }
-        }
-
-        private void sendMessage(final ExportRequest request) {
-            try {
-                mMessenger.send(Message.obtain(null,
-                        VCardService.MSG_EXPORT_REQUEST,
-                        request));
-            } catch (RemoteException e) {
-                Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
-                runOnUiThread(new ErrorReasonDisplayer(
-                        getString(R.string.fail_reason_unknown)));
-            }
-        }
-
-        public void onServiceConnected(ComponentName name, IBinder service) {
-            synchronized (this) {
-                mMessenger = new Messenger(service);
-                // Send pending requests thrown from this Activity before an actual connection
-                // is established.
-                while (!mPendingRequests.isEmpty()) {
-                    final ExportRequest parameter = mPendingRequests.poll();
-                    if (parameter == null) {
-                        throw new NullPointerException();
-                    }
-                    sendMessage(parameter);
-                }
-
-                unbindService(this);
-                finish();
-            }
-        }
-
-        public void onServiceDisconnected(ComponentName name) {
-            synchronized (this) {
-                if (!mPendingRequests.isEmpty()) {
-                    Log.w(LOG_TAG, "Some request(s) are dropped.");
-                }
-                // Set to null so that we can detect inappropriate re-connection toward
-                // the Service via NullPointerException;
-                mPendingRequests = null;
-                mMessenger = null;
-            }
-        }
-    }
-
-    private final CustomConnection mConnection = new CustomConnection();
-
-    private class CancelListener
-            implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
-        public void onClick(DialogInterface dialog, int which) {
-            finish();
-        }
-        public void onCancel(DialogInterface dialog) {
-            finish();
-        }
-    }
-
-    private CancelListener mCancelListener = new CancelListener();
-
-    private class ErrorReasonDisplayer implements Runnable {
-        private final int mResId;
-        public ErrorReasonDisplayer(int resId) {
-            mResId = resId;
-        }
-        public ErrorReasonDisplayer(String errorReason) {
-            mResId = R.id.dialog_fail_to_export_with_reason;
-            mErrorReason = errorReason;
-        }
-        public void run() {
-            // Show the Dialog only when the parent Activity is still alive.
-            if (!ExportVCardActivity.this.isFinishing()) {
-                showDialog(mResId);
-            }
-        }
-    }
-
     private class ExportConfirmationListener implements DialogInterface.OnClickListener {
-        private final Uri mDestUri;
+        private final Uri mDestinationUri;
 
-        public ExportConfirmationListener(String fileName) {
-            this(Uri.parse("file://" + fileName));
+        public ExportConfirmationListener(String path) {
+            this(Uri.parse("file://" + path));
         }
 
         public ExportConfirmationListener(Uri uri) {
-            mDestUri = uri;
+            mDestinationUri = uri;
         }
 
         public void onClick(DialogInterface dialog, int which) {
             if (which == DialogInterface.BUTTON_POSITIVE) {
-                // We don't want the service finishes itself just after this connection.
-                startService(new Intent(ExportVCardActivity.this, VCardService.class));
-                bindService(new Intent(ExportVCardActivity.this, VCardService.class),
-                        mConnection, Context.BIND_AUTO_CREATE);
-
-                final ExportRequest request = new ExportRequest(mDestUri);
-
+                if (DEBUG) {
+                    Log.d(LOG_TAG,
+                            String.format("Try sending export request (uri: %s)", mDestinationUri));
+                }
+                final ExportRequest request = new ExportRequest(mDestinationUri);
                 // The connection object will call finish().
-                mConnection.requestSend(request);
+                if (trySend(Message.obtain(null, VCardService.MSG_EXPORT_REQUEST, request))) {
+                    Log.i(LOG_TAG, "Successfully sent export request. Finish itself");
+                    unbindAndFinish();
+                }
             }
         }
     }
 
-    private String translateComposerError(String errorMessage) {
-        Resources resources = getResources();
-        if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
-            return resources.getString(R.string.composer_failed_to_get_database_infomation);
-        } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
-            return resources.getString(R.string.composer_has_no_exportable_contact);
-        } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
-            return resources.getString(R.string.composer_not_initialized);
-        } else {
-            return errorMessage;
-        }
-    }
-
     @Override
     protected void onCreate(Bundle bundle) {
         super.onCreate(bundle);
 
-        mTargetDirectory = getString(R.string.config_export_dir);
-        mFileNamePrefix = getString(R.string.config_export_file_prefix);
-        mFileNameSuffix = getString(R.string.config_export_file_suffix);
-        mFileNameExtension = getString(R.string.config_export_file_extension);
-
-        mExtensionsToConsider = new HashSet<String>();
-        mExtensionsToConsider.add(mFileNameExtension);
-
-        final String additionalExtensions =
-            getString(R.string.config_export_extensions_to_consider);
-        if (!TextUtils.isEmpty(additionalExtensions)) {
-            for (String extension : additionalExtensions.split(",")) {
-                String trimed = extension.trim();
-                if (trimed.length() > 0) {
-                    mExtensionsToConsider.add(trimed);
-                }
-            }
+        // Check directory is available.
+        final File targetDirectory = new File(getString(R.string.config_export_dir));
+        if (!(targetDirectory.exists() &&
+                targetDirectory.isDirectory() &&
+                targetDirectory.canRead()) &&
+                !targetDirectory.mkdirs()) {
+            showDialog(R.id.dialog_sdcard_not_found);
+            return;
         }
 
-        final Resources resources = getResources();
-        mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
-        mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
+        if (startService(new Intent(this, VCardService.class)) == null) {
+            Log.e(LOG_TAG, "Failed to start vCard service");
+            mErrorReason = getString(R.string.fail_reason_unknown);
+            showDialog(R.id.dialog_fail_to_export_with_reason);
+            return;
+        }
 
-        startExportVCardToSdCard();
+        if (!bindService(new Intent(this, VCardService.class), this, Context.BIND_AUTO_CREATE)) {
+            Log.e(LOG_TAG, "Failed to connect to vCard service.");
+            mErrorReason = getString(R.string.fail_reason_unknown);
+            showDialog(R.id.dialog_fail_to_export_with_reason);
+        }
+        // Continued to onServiceConnected()
+    }
+
+    @Override
+    public synchronized void onServiceConnected(ComponentName name, IBinder service) {
+        if (DEBUG) Log.d(LOG_TAG, "connected to service, requesting a destination file name");
+        mConnected = true;
+        mOutgoingMessenger = new Messenger(service);
+        final Message message =
+                Message.obtain(null, VCardService.MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION);
+        message.replyTo = mIncomingMessenger;
+        trySend(message);
+        // Wait until MSG_SET_AVAILABLE_EXPORT_DESTINATION message is available.
+    }
+
+    // Use synchronized since we don't want to call unbindAndFinish() just after this call.
+    @Override
+    public synchronized void onServiceDisconnected(ComponentName name) {
+        if (DEBUG) Log.d(LOG_TAG, "onServiceDisconnected()");
+        mOutgoingMessenger = null;
+        mConnected = false;
+        if (mProcessOngoing) {
+            // Unexpected disconnect event.
+            Log.w(LOG_TAG, "Disconnected from service during the process ongoing.");
+            mErrorReason = getString(R.string.fail_reason_unknown);
+            showDialog(R.id.dialog_fail_to_export_with_reason);
+        }
     }
 
     @Override
     protected Dialog onCreateDialog(int id, Bundle bundle) {
         switch (id) {
             case R.id.dialog_export_confirmation: {
-                return getExportConfirmationDialog();
+                return new AlertDialog.Builder(this)
+                        .setTitle(R.string.confirm_export_title)
+                        .setMessage(getString(R.string.confirm_export_message, mTargetFileName))
+                        .setPositiveButton(android.R.string.ok,
+                                new ExportConfirmationListener(mTargetFileName))
+                        .setNegativeButton(android.R.string.cancel, this)
+                        .setOnCancelListener(this)
+                        .create();
             }
             case R.string.fail_reason_too_many_vcard: {
+                mProcessOngoing = false;
                 return new AlertDialog.Builder(this)
-                    .setTitle(R.string.exporting_contact_failed_title)
-                    .setMessage(getString(R.string.exporting_contact_failed_message,
+                        .setTitle(R.string.exporting_contact_failed_title)
+                        .setMessage(getString(R.string.exporting_contact_failed_message,
                                 getString(R.string.fail_reason_too_many_vcard)))
-                                .setPositiveButton(android.R.string.ok, mCancelListener)
-                                .create();
+                        .setPositiveButton(android.R.string.ok, this)
+                        .create();
             }
             case R.id.dialog_fail_to_export_with_reason: {
-                return getErrorDialogWithReason();
+                mProcessOngoing = false;
+                return new AlertDialog.Builder(this)
+                        .setTitle(R.string.exporting_contact_failed_title)
+                        .setMessage(getString(R.string.exporting_contact_failed_message,
+                                mErrorReason != null ? mErrorReason :
+                                        getString(R.string.fail_reason_unknown)))
+                        .setPositiveButton(android.R.string.ok, this)
+                        .setOnCancelListener(this)
+                        .create();
             }
             case R.id.dialog_sdcard_not_found: {
-                AlertDialog.Builder builder = new AlertDialog.Builder(this)
-                .setTitle(R.string.no_sdcard_title)
-                .setIcon(android.R.drawable.ic_dialog_alert)
-                .setMessage(R.string.no_sdcard_message)
-                .setPositiveButton(android.R.string.ok, mCancelListener);
-                return builder.create();
+                mProcessOngoing = false;
+                return new AlertDialog.Builder(this)
+                        .setTitle(R.string.no_sdcard_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.no_sdcard_message)
+                        .setPositiveButton(android.R.string.ok, this).create();
             }
         }
         return super.onCreateDialog(id, bundle);
@@ -261,7 +249,7 @@
     @Override
     protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
         if (id == R.id.dialog_fail_to_export_with_reason) {
-            ((AlertDialog)dialog).setMessage(getErrorReason());
+            ((AlertDialog)dialog).setMessage(mErrorReason);
         } else if (id == R.id.dialog_export_confirmation) {
             ((AlertDialog)dialog).setMessage(
                     getString(R.string.confirm_export_message, mTargetFileName));
@@ -275,119 +263,47 @@
         super.onStop();
 
         if (!isFinishing()) {
-            finish();
+            unbindAndFinish();
         }
     }
 
-    /**
-     * Tries to start exporting VCard. If there's no SDCard available,
-     * an error dialog is shown.
-     */
-    public void startExportVCardToSdCard() {
-        File targetDirectory = new File(mTargetDirectory);
-
-        if (!(targetDirectory.exists() &&
-                targetDirectory.isDirectory() &&
-                targetDirectory.canRead()) &&
-                !targetDirectory.mkdirs()) {
-            showDialog(R.id.dialog_sdcard_not_found);
-        } else {
-            mTargetFileName = getAppropriateFileName(mTargetDirectory);
-            if (TextUtils.isEmpty(mTargetFileName)) {
-                mTargetFileName = null;
-                // finish() is called via the error dialog. Do not call the method here.
-                return;
-            }
-
-            showDialog(R.id.dialog_export_confirmation);
-        }
-    }
-
-    /**
-     * Tries to get an appropriate filename. Returns null if it fails.
-     */
-    private String getAppropriateFileName(final String destDirectory) {
-        int fileNumberStringLength = 0;
-        {
-            // Calling Math.Log10() is costly.
-            int tmp;
-            for (fileNumberStringLength = 0, tmp = mFileIndexMaximum; tmp > 0;
-                fileNumberStringLength++, tmp /= 10) {
-            }
-        }
-        String bodyFormat = "%s%0" + fileNumberStringLength + "d%s";
-
-        if (!ALLOW_LONG_FILE_NAME) {
-            String possibleBody = String.format(bodyFormat,mFileNamePrefix, 1, mFileNameSuffix);
-            if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
-                Log.e(LOG_TAG, "This code does not allow any long file name.");
-                mErrorReason = getString(R.string.fail_reason_too_long_filename,
-                        String.format("%s.%s", possibleBody, mFileNameExtension));
-                showDialog(R.id.dialog_fail_to_export_with_reason);
-                // finish() is called via the error dialog. Do not call the method here.
-                return null;
-            }
-        }
-
-        // Note that this logic assumes that the target directory is case insensitive.
-        // As of 2009-07-16, it is true since the external storage is only sdcard, and
-        // it is formated as FAT/VFAT.
-        // TODO: fix this.
-        for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
-            boolean numberIsAvailable = true;
-            // SD Association's specification seems to require this feature, though we cannot
-            // have the specification since it is proprietary...
-            String body = null;
-            for (String possibleExtension : mExtensionsToConsider) {
-                body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
-                File file = new File(String.format("%s/%s.%s",
-                        destDirectory, body, possibleExtension));
-                if (file.exists()) {
-                    numberIsAvailable = false;
-                    break;
-                }
-            }
-            if (numberIsAvailable) {
-                return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension);
-            }
-        }
-        showDialog(R.string.fail_reason_too_many_vcard);
-        return null;
-    }
-
-    public Dialog getExportConfirmationDialog() {
-        if (TextUtils.isEmpty(mTargetFileName)) {
-            Log.e(LOG_TAG, "Target file name is empty, which must not be!");
-            // This situation is not acceptable (probably a bug!), but we don't have no reason to
-            // show...
-            mErrorReason = null;
-            return getErrorDialogWithReason();
-        }
-
-        return new AlertDialog.Builder(this)
-            .setTitle(R.string.confirm_export_title)
-            .setMessage(getString(R.string.confirm_export_message, mTargetFileName))
-            .setPositiveButton(android.R.string.ok,
-                    new ExportConfirmationListener(mTargetFileName))
-            .setNegativeButton(android.R.string.cancel, mCancelListener)
-            .setOnCancelListener(mCancelListener)
-            .create();
-    }
-
-    public Dialog getErrorDialogWithReason() {
-        if (mErrorReason == null) {
-            Log.e(LOG_TAG, "Error reason must have been set.");
+    private boolean trySend(Message message) {
+        try {
+            mOutgoingMessenger.send(message);
+            return true;
+        } catch (RemoteException e) {
+            Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
+            unbindService(this);
             mErrorReason = getString(R.string.fail_reason_unknown);
+            showDialog(R.id.dialog_fail_to_export_with_reason);
+            return false;
         }
-        return new AlertDialog.Builder(this)
-            .setTitle(R.string.exporting_contact_failed_title)
-                .setMessage(getString(R.string.exporting_contact_failed_message, mErrorReason))
-            .setPositiveButton(android.R.string.ok, mCancelListener)
-            .setOnCancelListener(mCancelListener)
-            .create();
     }
 
-    public String getErrorReason() {
-        return mErrorReason;
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onClick() is called");
+        unbindAndFinish();
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onCancel() is called");
+        mProcessOngoing = false;
+        unbindAndFinish();
+    }
+
+    @Override
+    public void unbindService(ServiceConnection conn) {
+        mProcessOngoing = false;
+        super.unbindService(conn);
+    }
+
+    private synchronized void unbindAndFinish() {
+        if (mConnected) {
+            unbindService(this);
+            mConnected = false;
+        }
+        finish();
     }
 }
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ImportProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java
index 5b5ae79..1b70025 100644
--- a/src/com/android/contacts/vcard/ImportProcessor.java
+++ b/src/com/android/contacts/vcard/ImportProcessor.java
@@ -30,7 +30,6 @@
 import android.accounts.Account;
 import android.app.Notification;
 import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
@@ -38,7 +37,6 @@
 import android.net.Uri;
 import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
-import android.widget.RemoteViews;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -51,6 +49,7 @@
  */
 public class ImportProcessor extends ProcessorBase {
     private static final String LOG_TAG = "VCardImport";
+    private static final boolean DEBUG = VCardService.DEBUG;
 
     private final VCardService mService;
     private final ContentResolver mResolver;
@@ -160,6 +159,7 @@
                 Log.i(LOG_TAG, "Successfully finished importing one vCard file: " + uri);
                 List<Uri> uris = committer.getCreatedUris();
                 if (uris != null && uris.size() > 0) {
+                    // TODO: construct intent showing a list of imported contact list.
                     doFinishNotification(uris.get(0));
                 } else {
                     // Not critical, but suspicious.
@@ -197,7 +197,8 @@
             intent = null;
         }
         final Notification notification =
-                   VCardService.constructFinishNotification(mService, description, intent);
+                   VCardService.constructFinishNotification(mService,
+                           description, description, intent);
         mNotificationManager.notify(mJobId, notification);
     }
 
@@ -272,7 +273,7 @@
 
     @Override
     public synchronized boolean cancel(boolean mayInterruptIfRunning) {
-        Log.i(LOG_TAG, "ImportProcessor received cancel request");
+        if (DEBUG) Log.d(LOG_TAG, "ImportProcessor received cancel request");
         if (mDone || mCanceled) {
             return false;
         }
diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java
index 948b465..b375804 100644
--- a/src/com/android/contacts/vcard/ImportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ImportVCardActivity.java
@@ -844,13 +844,12 @@
                 return builder.create();
             }
             case R.id.dialog_vcard_not_found: {
-                String message = (getString(R.string.scanning_sdcard_failed_message,
-                        getString(R.string.fail_reason_no_vcard_file)));
+                final String message = getString(R.string.import_failure_no_vcard_file);
                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
-                    .setTitle(R.string.scanning_sdcard_failed_title)
-                    .setMessage(message)
-                    .setOnCancelListener(mCancelListener)
-                    .setPositiveButton(android.R.string.ok, mCancelListener);
+                        .setTitle(R.string.scanning_sdcard_failed_title)
+                        .setMessage(message)
+                        .setOnCancelListener(mCancelListener)
+                        .setPositiveButton(android.R.string.ok, mCancelListener);
                 return builder.create();
             }
             case R.id.dialog_select_import_type: {
diff --git a/src/com/android/contacts/vcard/VCardService.java b/src/com/android/contacts/vcard/VCardService.java
index cad9fc6..7639c18 100644
--- a/src/com/android/contacts/vcard/VCardService.java
+++ b/src/com/android/contacts/vcard/VCardService.java
@@ -23,17 +23,23 @@
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
+import android.os.RemoteException;
+import android.text.TextUtils;
 import android.util.Log;
 import android.widget.RemoteViews;
 import android.widget.Toast;
 
+import java.io.File;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.RejectedExecutionException;
@@ -49,10 +55,13 @@
 // works fine enough. Investigate the feasibility.
 public class VCardService extends Service {
     private final static String LOG_TAG = "VCardService";
+    /* package */ final static boolean DEBUG = true;
 
     /* package */ static final int MSG_IMPORT_REQUEST = 1;
     /* package */ static final int MSG_EXPORT_REQUEST = 2;
     /* package */ static final int MSG_CANCEL_REQUEST = 3;
+    /* package */ static final int MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION = 4;
+    /* package */ static final int MSG_SET_AVAILABLE_EXPORT_DESTINATION = 5;
 
     /**
      * Specifies the type of operation. Used when constructing a {@link Notification}, canceling
@@ -79,6 +88,10 @@
                     handleCancelRequest((CancelRequest)msg.obj);
                     break;
                 }
+                case MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION: {
+                    handleRequestAvailableExportDestination(msg);
+                    break;
+                }
                 // TODO: add cancel capability for export..
                 default: {
                     Log.w(LOG_TAG, "Received unknown request, ignoring it.");
@@ -101,10 +114,53 @@
     private final Map<Integer, ProcessorBase> mRunningJobMap =
             new HashMap<Integer, ProcessorBase>();
 
+    /* ** vCard exporter params ** */
+    // If true, VCardExporter is able to emits files longer than 8.3 format.
+    private static final boolean ALLOW_LONG_FILE_NAME = false;
+    private String mTargetDirectory;
+    private String mFileNamePrefix;
+    private String mFileNameSuffix;
+    private int mFileIndexMinimum;
+    private int mFileIndexMaximum;
+    private String mFileNameExtension;
+    private Set<String> mExtensionsToConsider;
+    private String mErrorReason;
+
+    // File names currently reserved by some export job.
+    private final Set<String> mReservedDestination = new HashSet<String>();
+    /* ** end of vCard exporter params ** */
+
     @Override
     public void onCreate() {
         super.onCreate();
+        if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
         mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
+        initExporterParams();
+    }
+
+    private void initExporterParams() {
+        mTargetDirectory = getString(R.string.config_export_dir);
+        mFileNamePrefix = getString(R.string.config_export_file_prefix);
+        mFileNameSuffix = getString(R.string.config_export_file_suffix);
+        mFileNameExtension = getString(R.string.config_export_file_extension);
+
+        mExtensionsToConsider = new HashSet<String>();
+        mExtensionsToConsider.add(mFileNameExtension);
+
+        final String additionalExtensions =
+            getString(R.string.config_export_extensions_to_consider);
+        if (!TextUtils.isEmpty(additionalExtensions)) {
+            for (String extension : additionalExtensions.split(",")) {
+                String trimed = extension.trim();
+                if (trimed.length() > 0) {
+                    mExtensionsToConsider.add(trimed);
+                }
+            }
+        }
+
+        final Resources resources = getResources();
+        mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
+        mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
     }
 
     @Override
@@ -119,13 +175,18 @@
 
     @Override
     public void onDestroy() {
-        Log.i(LOG_TAG, "VCardService is being destroyed.");
+        if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed.");
         cancelAllRequestsAndShutdown();
         clearCache();
         super.onDestroy();
     }
 
     private synchronized void handleImportRequest(ImportRequest request) {
+        if (DEBUG) {
+            Log.d(LOG_TAG,
+                    String.format("received import request (uri: %s, originalUri: %s)",
+                            request.uri, request.originalUri));
+        }
         if (tryExecute(new ImportProcessor(this, request, mCurrentJobId))) {
             final String displayName = request.originalUri.getLastPathSegment(); 
             final String message = getString(R.string.vcard_import_will_start_message,
@@ -153,6 +214,18 @@
             final String displayName = request.destUri.getLastPathSegment();
             final String message = getString(R.string.vcard_export_will_start_message,
                     displayName);
+
+            final String path = request.destUri.getEncodedPath();
+            if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path);
+            if (!mReservedDestination.add(path)) {
+                Log.w(LOG_TAG,
+                        String.format("The path %s is already reserved. Reject export request",
+                                path));
+                Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
+                        Toast.LENGTH_LONG).show();
+                return;
+            }
+
             Toast.makeText(this, message, Toast.LENGTH_LONG).show();
             final Notification notification =
                     constructProgressNotification(this, TYPE_EXPORT, message, message,
@@ -180,9 +253,9 @@
         }
     }
 
-    private void handleCancelRequest(CancelRequest request) {
+    private synchronized void handleCancelRequest(CancelRequest request) {
         final int jobId = request.jobId;
-        Log.i(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
+        if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
         final ProcessorBase processor = mRunningJobMap.remove(jobId);
 
         if (processor != null) {
@@ -192,12 +265,41 @@
                             getString(R.string.exporting_vcard_canceled_title, request.displayName);
             final Notification notification = constructCancelNotification(this, description);
             mNotificationManager.notify(jobId, notification);
+            if (processor.getType() == TYPE_EXPORT) {
+                final String path =
+                        ((ExportProcessor)processor).getRequest().destUri.getEncodedPath();
+                Log.i(LOG_TAG,
+                        String.format("Cancel reservation for the path %s if appropriate", path));
+                if (!mReservedDestination.remove(path)) {
+                    Log.w(LOG_TAG, "Not reserved.");
+                }
+            }
         } else {
             Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
         }
         stopServiceWhenNoJob();
     }
 
+    private synchronized void handleRequestAvailableExportDestination(Message msg) {
+        if (DEBUG) Log.d(LOG_TAG, "Received available export destination request.");
+        final Messenger messenger = msg.replyTo;
+        final String path = getAppropriateDestination(mTargetDirectory);
+        final Message message;
+        if (path != null) {
+            message = Message.obtain(null,
+                    VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION, 0, 0, path);
+        } else {
+            message = Message.obtain(null,
+                    VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION,
+                    R.id.dialog_fail_to_export_with_reason, 0, mErrorReason);
+        }
+        try {
+            messenger.send(message);
+        } catch (RemoteException e) {
+            Log.w(LOG_TAG, "Failed to send reply for available export destination request.", e);
+        }
+    }
+
     /**
      * Checks job list and call {@link #stopSelf()} when there's no job now.
      * A new job cannot be submitted any more after this call.
@@ -223,8 +325,10 @@
 
     /* package */ synchronized void handleFinishImportNotification(
             int jobId, boolean successful) {
-        Log.v(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
-                + "Result: %b", jobId, (successful ? "success" : "failure")));
+        if (DEBUG) {
+            Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
+                    + "Result: %b", jobId, (successful ? "success" : "failure")));
+        }
         if (mRunningJobMap.remove(jobId) == null) {
             Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
         }
@@ -233,11 +337,22 @@
 
     /* package */ synchronized void handleFinishExportNotification(
             int jobId, boolean successful) {
-        Log.v(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
-                + "Result: %b", jobId, (successful ? "success" : "failure")));
-        if (mRunningJobMap.remove(jobId) == null) {
-            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+        if (DEBUG) {
+            Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
+                    + "Result: %b", jobId, (successful ? "success" : "failure")));
         }
+        final ProcessorBase job = mRunningJobMap.remove(jobId);
+        if (job == null) {
+            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+        } else if (!(job instanceof ExportProcessor)) {
+            Log.w(LOG_TAG,
+                    String.format("Removed job (id: %s) isn't ExportProcessor", jobId));
+        } else {
+            final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath();
+            if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path);
+            mReservedDestination.remove(path);
+        }
+
         stopServiceWhenNoJob();
     }
 
@@ -339,12 +454,13 @@
      */
     /* package */ static Notification constructCancelNotification(
             Context context, String description) {
-        final Notification notification = new Notification();
-        notification.flags |= Notification.FLAG_AUTO_CANCEL;
-        notification.icon = android.R.drawable.stat_notify_error;
-        notification.setLatestEventInfo(context, description, description,
-                PendingIntent.getActivity(context, 0, null, 0));
-        return notification;
+        return new Notification.Builder(context)
+                .setAutoCancel(true)
+                .setSmallIcon(android.R.drawable.stat_notify_error)
+                .setContentTitle(description)
+                .setContentText(description)
+                .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0))
+                .getNotification();
     }
 
     /**
@@ -355,12 +471,94 @@
      * @param intent Intent to be launched when the Notification is clicked. Can be null.
      */
     /* package */ static Notification constructFinishNotification(
-            Context context, String description, Intent intent) {
-        final Notification notification = new Notification();
-        notification.flags |= Notification.FLAG_AUTO_CANCEL;
-        notification.icon = android.R.drawable.stat_sys_download_done;
-        notification.setLatestEventInfo(context, description, description,
-                PendingIntent.getActivity(context, 0, intent, 0));
-        return notification;
+            Context context, String title, String description, Intent intent) {
+        return new Notification.Builder(context)
+                .setAutoCancel(true)
+                .setSmallIcon(android.R.drawable.stat_sys_download_done)
+                .setContentTitle(title)
+                .setContentText(description)
+                .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0))
+                .getNotification();
+    }
+
+    /**
+     * Returns an appropriate file name for vCard export. Returns null when impossible.
+     *
+     * @return destination path for a vCard file to be exported. null on error and mErrorReason
+     * is correctly set.
+     */
+    private String getAppropriateDestination(final String destDirectory) {
+        /*
+         * Here, file names have 5 parts: directory, prefix, index, suffix, and extension.
+         * e.g. "/mnt/sdcard/prfx00001sfx.vcf" -> "/mnt/sdcard", "prfx", "00001", "sfx", and ".vcf"
+         *      (In default, prefix and suffix is empty, so usually the destination would be
+         *       /mnt/sdcard/00001.vcf.)
+         *
+         * This method increments "index" part from 1 to maximum, and checks whether any file name
+         * following naming rule is available. If there's no file named /mnt/sdcard/00001.vcf, the
+         * name will be returned to a caller. If there are 00001.vcf 00002.vcf, 00003.vcf is
+         * returned.
+         *
+         * There may not be any appropriate file name. If there are 99999 vCard files in the
+         * storage, for example, there's no appropriate name, so this method returns
+         * null.
+         */
+
+        // Count the number of digits of mFileIndexMaximum
+        // e.g. When mFileIndexMaximum is 99999, fileIndexDigit becomes 5, as we will count the
+        int fileIndexDigit = 0;
+        {
+            // Calling Math.Log10() is costly.
+            int tmp;
+            for (fileIndexDigit = 0, tmp = mFileIndexMaximum; tmp > 0;
+                fileIndexDigit++, tmp /= 10) {
+            }
+        }
+
+        // %s05d%s (e.g. "p00001s")
+        final String bodyFormat = "%s%0" + fileIndexDigit + "d%s";
+
+        if (!ALLOW_LONG_FILE_NAME) {
+            final String possibleBody =
+                    String.format(bodyFormat, mFileNamePrefix, 1, mFileNameSuffix);
+            if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
+                Log.e(LOG_TAG, "This code does not allow any long file name.");
+                mErrorReason = getString(R.string.fail_reason_too_long_filename,
+                        String.format("%s.%s", possibleBody, mFileNameExtension));
+                Log.w(LOG_TAG, "File name becomes too long.");
+                return null;
+            }
+        }
+
+        for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
+            boolean numberIsAvailable = true;
+            String body = null;
+            for (String possibleExtension : mExtensionsToConsider) {
+                body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
+                final String path =
+                        String.format("%s/%s.%s", destDirectory, body, possibleExtension);
+                synchronized (this) {
+                    if (mReservedDestination.contains(path)) {
+                        if (DEBUG) {
+                            Log.d(LOG_TAG, String.format("The path %s is reserved.", path));
+                        }
+                        numberIsAvailable = false;
+                        break;
+                    }
+                }
+                final File file = new File(path);
+                if (file.exists()) {
+                    numberIsAvailable = false;
+                    break;
+                }
+            }
+            if (numberIsAvailable) {
+                return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension);
+            }
+        }
+
+        Log.w(LOG_TAG, "Reached vCard number limit. Maybe there are too many vCard in the storage");
+        mErrorReason = getString(R.string.fail_reason_too_many_vcard);
+        return null;
     }
 }