Merge "Recycling bitmaps in TransitionAnimationView."
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;
     }
 }