public class

ExportVCardActivity

extends Activity
/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.contacts;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.pim.vcard.VCardComposer;
import android.pim.vcard.VCardConfig;
import android.text.TextUtils;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;

/**
 * Class for exporting vCard.
 *
 * 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).
 */
public class ExportVCardActivity extends Activity {
    private static final String LOG_TAG = "ExportVCardActivity";

    // 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 String mVCardTypeStr;
    private Set<String> mExtensionsToConsider;

    private ProgressDialog mProgressDialog;
    private String mExportingFileName;

    private Handler mHandler = new Handler();

    // Used temporaly when asking users to confirm the file name
    private String mTargetFileName;

    // String for storing error reason temporaly.
    private String mErrorReason;

    private ActualExportThread mActualExportThread;

    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 String mFileName;

        public ExportConfirmationListener(String fileName) {
            mFileName = fileName;
        }

        public void onClick(DialogInterface dialog, int which) {
            if (which == DialogInterface.BUTTON_POSITIVE) {
                mActualExportThread = new ActualExportThread(mFileName);
                showDialog(R.id.dialog_exporting_vcard);
            }
        }
    }

    private class ActualExportThread extends Thread
            implements DialogInterface.OnCancelListener {
        private PowerManager.WakeLock mWakeLock;
        private boolean mCanceled = false;

        public ActualExportThread(String fileName) {
            mExportingFileName = fileName;
            PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
            mWakeLock = powerManager.newWakeLock(
                    PowerManager.SCREEN_DIM_WAKE_LOCK |
                    PowerManager.ON_AFTER_RELEASE, LOG_TAG);
        }

        @Override
        public void run() {
            boolean shouldCallFinish = true;
            mWakeLock.acquire();
            VCardComposer composer = null;
            try {
                OutputStream outputStream = null;
                try {
                    outputStream = new FileOutputStream(mExportingFileName);
                } catch (FileNotFoundException e) {
                    final String errorReason =
                        getString(R.string.fail_reason_could_not_open_file,
                                mExportingFileName, e.getMessage());
                    shouldCallFinish = false;
                    mHandler.post(new ErrorReasonDisplayer(errorReason));
                    return;
                }

                // composer = new VCardComposer(ExportVCardActivity.this, mVCardTypeStr, true);
                int vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
                composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);

                composer.addHandler(composer.new HandlerForOutputStream(outputStream));

                if (!composer.init()) {
                    final String errorReason = composer.getErrorReason();
                    Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
                    final String translatedErrorReason =
                            translateComposerError(errorReason);
                    mHandler.post(new ErrorReasonDisplayer(
                            getString(R.string.fail_reason_could_not_initialize_exporter,
                                    translatedErrorReason)));
                    shouldCallFinish = false;
                    return;
                }

                int size = composer.getCount();

                if (size == 0) {
                    mHandler.post(new ErrorReasonDisplayer(
                            getString(R.string.fail_reason_no_exportable_contact)));
                    shouldCallFinish = false;
                    return;
                }

                mProgressDialog.setProgressNumberFormat(
                        getString(R.string.exporting_contact_list_progress));
                mProgressDialog.setMax(size);
                mProgressDialog.setProgress(0);

                while (!composer.isAfterLast()) {
                    if (mCanceled) {
                        return;
                    }
                    if (!composer.createOneEntry()) {
                        final String errorReason = composer.getErrorReason();
                        Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
                        final String translatedErrorReason =
                            translateComposerError(errorReason);
                        mHandler.post(new ErrorReasonDisplayer(
                                getString(R.string.fail_reason_error_occurred_during_export,
                                        translatedErrorReason)));
                        shouldCallFinish = false;
                        return;
                    }
                    mProgressDialog.incrementProgressBy(1);
                }
            } finally {
                if (composer != null) {
                    composer.terminate();
                }
                mWakeLock.release();
                mProgressDialog.dismiss();
                if (shouldCallFinish && !isFinishing()) {
                    finish();
                }
            }
        }

        @Override
        public void finalize() {
            if (mWakeLock != null && mWakeLock.isHeld()) {
                mWakeLock.release();
            }
        }

        public void cancel() {
            mCanceled = true;
        }

        public void onCancel(DialogInterface dialog) {
            cancel();
        }
    }

    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);
        mVCardTypeStr = getString(R.string.config_export_vcard_type);

        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);

        startExportVCardToSdCard();
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        switch (id) {
            case R.id.dialog_export_confirmation: {
                return getExportConfirmationDialog();
            }
            case R.string.fail_reason_too_many_vcard: {
                return new AlertDialog.Builder(this)
                    .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();
            }
            case R.id.dialog_fail_to_export_with_reason: {
                return getErrorDialogWithReason();
            }
            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();
            }
            case R.id.dialog_exporting_vcard: {
                if (mProgressDialog == null) {
                    String title = getString(R.string.exporting_contact_list_title);
                    String message = getString(R.string.exporting_contact_list_message,
                            mExportingFileName);
                    mProgressDialog = new ProgressDialog(ExportVCardActivity.this);
                    mProgressDialog.setTitle(title);
                    mProgressDialog.setMessage(message);
                    mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
                    mProgressDialog.setOnCancelListener(mActualExportThread);
                    mActualExportThread.start();
                }
                return mProgressDialog;
            }
        }
        return super.onCreateDialog(id);
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        if (id == R.id.dialog_fail_to_export_with_reason) {
            ((AlertDialog)dialog).setMessage(getErrorReason());
        } else if (id == R.id.dialog_export_confirmation) {
            ((AlertDialog)dialog).setMessage(
                    getString(R.string.confirm_export_message, mTargetFileName));
        } else {
            super.onPrepareDialog(id, dialog);
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mActualExportThread != null) {
            // The Activity is no longer visible. Stop the thread.
            mActualExportThread.cancel();
            mActualExportThread = null;
        }

        if (!isFinishing()) {
            finish();
        }
    }

    /**
     * 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.");
            mErrorReason = getString(R.string.fail_reason_unknown);
        }
        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 void cancelExport() {
        if (mActualExportThread != null) {
            mActualExportThread.cancel();
            mActualExportThread = null;
        }
    }

    public String getErrorReason() {
        return mErrorReason;
    }
}