/* * 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 static final int SHOW_TOAST_MESSAGE = 0x12; 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); mActualExportThread.start(); } } } 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; int successCount = 0; int size = 0; 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; } 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; } successCount++; mProgressDialog.incrementProgressBy(1); } } finally { if (composer != null) { composer.terminate(); } showResultMessage(size, successCount); 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; } private void showResultMessage(int size, int successCount) { Context context = ExportVCardActivity.this; String message = ""; message += successCount + " " + context.getString(R.string.label_success) + ", " + (size - successCount) + " " + context.getString(R.string.label_fail); sendToastMessage(message); } private void sendToastMessage(String message) { mHandler.obtainMessage(SHOW_TOAST_MESSAGE, message).sendToTarget(); } }