Let the user choose where to export vcards to 1/3

Instead of always using external storage.

Also we change the suggested export filename
from something like 00001.vcf to just contacts.vcf.
Since we don't know the destination directory
anymore untill after it is selected, we can't check
if there is already an exported vcf file there in.
The Activity handling ACTION_CREATE_DOCUMENT handles
file name collisions itself anyway.

Screenshots https://goto.google.com/b22208705

Bug 22208705

Change-Id: I17bac2f483e200c13adf1429c0eade17e4d2908a
diff --git a/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java b/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java
index 865f20b..831f6f3 100644
--- a/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java
+++ b/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java
@@ -26,7 +26,6 @@
 
     private static final String[] REQUIRED_PERMISSIONS = new String[] {
             permission.READ_CONTACTS,
-            permission.WRITE_EXTERNAL_STORAGE,
     };
 
     @Override
diff --git a/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java b/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java
index 791ad2d..acef822 100644
--- a/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java
+++ b/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java
@@ -136,10 +136,9 @@
                 }
             }
         }
-        if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) {
+        if (res.getBoolean(R.bool.config_allow_export)) {
             if (contactsAreAvailable) {
-                adapter.add(new AdapterEntry(getString(R.string.export_to_sdcard),
-                        R.string.export_to_sdcard));
+                adapter.add(new AdapterEntry(getString(R.string.export), R.string.export));
             }
         }
         if (res.getBoolean(R.bool.config_allow_share_visible_contacts)) {
@@ -162,7 +161,7 @@
                                 adapter.getItem(which).mSubscriptionId);
                         break;
                     }
-                    case R.string.export_to_sdcard: {
+                    case R.string.export: {
                         dismissDialog = true;
                         Intent exportIntent = new Intent(getActivity(), ExportVCardActivity.class);
                         exportIntent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY,
diff --git a/src/com/android/contacts/common/vcard/ExportVCardActivity.java b/src/com/android/contacts/common/vcard/ExportVCardActivity.java
index f956c7c..172f2f5 100644
--- a/src/com/android/contacts/common/vcard/ExportVCardActivity.java
+++ b/src/com/android/contacts/common/vcard/ExportVCardActivity.java
@@ -51,52 +51,8 @@
         DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
     private static final String LOG_TAG = "VCardExport";
     private static final boolean DEBUG = VCardService.DEBUG;
-
-    /**
-     * 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);
-            }
-        }
-    }
+    private static final int REQUEST_CREATE_DOCUMENT = 100;
+    private static final String VCARD_MIME_TYPE = "text/vcard";
 
     /**
      * True when this Activity is connected to {@link VCardService}.
@@ -113,41 +69,11 @@
     private volatile boolean mProcessOngoing = true;
 
     private VCardService mService;
-    private final Messenger mIncomingMessenger = new Messenger(new IncomingHandler());
     private static final BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
 
-    // Used temporarily when asking users to confirm the file name
-    private String mTargetFileName;
-
     // String for storing error reason temporarily.
     private String mErrorReason;
 
-    private class ExportConfirmationListener implements DialogInterface.OnClickListener {
-        private final Uri mDestinationUri;
-
-        public ExportConfirmationListener(String path) {
-            this(Uri.parse("file://" + path));
-        }
-
-        public ExportConfirmationListener(Uri uri) {
-            mDestinationUri = uri;
-        }
-
-        public void onClick(DialogInterface dialog, int which) {
-            if (which == DialogInterface.BUTTON_POSITIVE) {
-                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().
-                mService.handleExportRequest(request, new NotificationImportExportListener(
-                        ExportVCardActivity.this));
-            }
-            unbindAndFinish();
-        }
-    }
-
     @Override
     protected void onCreate(Bundle bundle) {
         super.onCreate(bundle);
@@ -156,23 +82,10 @@
             return;
         }
 
-        // Check directory is available.
-        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
-            Log.w(LOG_TAG, "External storage is in state " + Environment.getExternalStorageState() +
-                    ". Cancelling export");
-            showDialog(R.id.dialog_sdcard_not_found);
-            return;
-        }
+        connectVCardService();
+    }
 
-        final File targetDirectory = Environment.getExternalStorageDirectory();
-        if (!(targetDirectory.exists() &&
-                targetDirectory.isDirectory() &&
-                targetDirectory.canRead()) &&
-                !targetDirectory.mkdirs()) {
-            showDialog(R.id.dialog_sdcard_not_found);
-            return;
-        }
-
+    private void connectVCardService() {
         final String callingActivity = getIntent().getExtras()
                 .getString(VCardCommonArguments.ARG_CALLING_ACTIVITY);
         Intent intent = new Intent(this, VCardService.class);
@@ -194,12 +107,35 @@
     }
 
     @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CREATE_DOCUMENT) {
+            if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
+                final Uri mTargetFileName = data.getData();
+                if (DEBUG) Log.d(LOG_TAG, "exporting to " + mTargetFileName);
+                final ExportRequest request = new ExportRequest(mTargetFileName);
+                // The connection object will call finish().
+                mService.handleExportRequest(request, new NotificationImportExportListener(
+                        ExportVCardActivity.this));
+            } else if (DEBUG) {
+                Log.d(LOG_TAG, "create document cancelled or no data returned");
+            }
+            unbindAndFinish();
+        }
+    }
+
+    @Override
     public synchronized void onServiceConnected(ComponentName name, IBinder binder) {
         if (DEBUG) Log.d(LOG_TAG, "connected to service, requesting a destination file name");
         mConnected = true;
         mService = ((VCardService.MyBinder) binder).getService();
-        mService.handleRequestAvailableExportDestination(mIncomingMessenger);
-        // Wait until MSG_SET_AVAILABLE_EXPORT_DESTINATION message is available.
+
+        // Have the user choose where vcards will be exported to
+        final Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        intent.setType(VCARD_MIME_TYPE);
+        intent.putExtra(Intent.EXTRA_TITLE, mBidiFormatter.unicodeWrap(
+                getString(R.string.exporting_vcard_filename), TextDirectionHeuristics.LTR));
+        startActivityForResult(intent, REQUEST_CREATE_DOCUMENT);
     }
 
     // Use synchronized since we don't want to call unbindAndFinish() just after this call.
@@ -216,31 +152,9 @@
         }
     }
 
-    /**
-     * Returns the name of the target path with additional formatting characters to improve its
-     * appearance in bidirectional text.
-     */
-    private String getTargetFileForDisplay() {
-        if (mTargetFileName == null) {
-            return null;
-        }
-        return mBidiFormatter.unicodeWrap(mTargetFileName, TextDirectionHeuristics.LTR);
-    }
-
     @Override
     protected Dialog onCreateDialog(int id, Bundle bundle) {
         switch (id) {
-            case R.id.dialog_export_confirmation: {
-                return new AlertDialog.Builder(this)
-                        .setTitle(R.string.confirm_export_title)
-                        .setMessage(getString(R.string.confirm_export_message,
-                                getTargetFileForDisplay()))
-                        .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)
@@ -261,13 +175,6 @@
                         .setOnCancelListener(this)
                         .create();
             }
-            case R.id.dialog_sdcard_not_found: {
-                mProcessOngoing = false;
-                return new AlertDialog.Builder(this)
-                        .setIconAttribute(android.R.attr.alertDialogIcon)
-                        .setMessage(R.string.no_sdcard_message)
-                        .setPositiveButton(android.R.string.ok, this).create();
-            }
         }
         return super.onCreateDialog(id, bundle);
     }
@@ -276,24 +183,12 @@
     protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
         if (id == R.id.dialog_fail_to_export_with_reason) {
             ((AlertDialog)dialog).setMessage(mErrorReason);
-        } else if (id == R.id.dialog_export_confirmation) {
-            ((AlertDialog)dialog).setMessage(
-                    getString(R.string.confirm_export_message, getTargetFileForDisplay()));
         } else {
             super.onPrepareDialog(id, dialog, args);
         }
     }
 
     @Override
-    protected void onStop() {
-        super.onStop();
-
-        if (!isFinishing()) {
-            unbindAndFinish();
-        }
-    }
-
-    @Override
     public void onClick(DialogInterface dialog, int which) {
         if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onClick() is called");
         unbindAndFinish();
diff --git a/src/com/android/contacts/common/vcard/VCardService.java b/src/com/android/contacts/common/vcard/VCardService.java
index 0a50bec..b78c06f 100644
--- a/src/com/android/contacts/common/vcard/VCardService.java
+++ b/src/com/android/contacts/common/vcard/VCardService.java
@@ -57,12 +57,6 @@
 
     /* package */ final static boolean DEBUG = false;
 
-    /* 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 notification, canceling
      * some operation, etc.
@@ -115,18 +109,6 @@
     private final List<CustomMediaScannerConnectionClient> mRemainingScannerConnections =
             new ArrayList<CustomMediaScannerConnectionClient>();
 
-    /* ** 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 File mTargetDirectory;
-    private String mFileNamePrefix;
-    private String mFileNameSuffix;
-    private int mFileIndexMinimum;
-    private int mFileIndexMaximum;
-    private String mFileNameExtension;
-    private Set<String> mExtensionsToConsider;
-    private String mErrorReason;
     private MyBinder mBinder;
 
     private String mCallingActivity;
@@ -146,32 +128,6 @@
         super.onCreate();
         mBinder = new MyBinder();
         if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
-        initExporterParams();
-    }
-
-    private void initExporterParams() {
-        mTargetDirectory = Environment.getExternalStorageDirectory();
-        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
@@ -304,25 +260,6 @@
         stopServiceIfAppropriate();
     }
 
-    public synchronized void handleRequestAvailableExportDestination(final Messenger messenger) {
-        if (DEBUG) Log.d(LOG_TAG, "Received available export destination request.");
-        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 and no scanner connection
      * is remaining.
@@ -456,89 +393,4 @@
             }
         }
     }
-
-    /**
-     * 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 File 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. We format these numbers in the US locale to ensure we they appear as
-         * english numerals.
-         *
-         * 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(Locale.US, 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;
-            final String body
-                    = String.format(Locale.US, bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
-            // Make sure that none of the extensions of mExtensionsToConsider matches. If this
-            // number is free, we'll go ahead with mFileNameExtension (which is included in
-            // mExtensionsToConsider)
-            for (String possibleExtension : mExtensionsToConsider) {
-                final File file = new File(destDirectory, body + "." + possibleExtension);
-                final String path = file.getAbsolutePath();
-                synchronized (this) {
-                    // Is this being exported right now? Skip this number
-                    if (mReservedDestination.contains(path)) {
-                        if (DEBUG) {
-                            Log.d(LOG_TAG, String.format("%s is already being exported.", path));
-                        }
-                        numberIsAvailable = false;
-                        break;
-                    }
-                }
-                if (file.exists()) {
-                    numberIsAvailable = false;
-                    break;
-                }
-            }
-            if (numberIsAvailable) {
-                return new File(destDirectory, body + "." + mFileNameExtension).getAbsolutePath();
-            }
-        }
-
-        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;
-    }
 }