Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.contacts;
     17 
     18 import android.app.Activity;
     19 import android.app.AlertDialog;
     20 import android.app.Dialog;
     21 import android.app.ProgressDialog;
     22 import android.content.Context;
     23 import android.content.DialogInterface;
     24 import android.content.res.Resources;
     25 import android.os.Bundle;
     26 import android.os.Handler;
     27 import android.os.PowerManager;
     28 import android.pim.vcard.VCardComposer;
     29 import android.pim.vcard.VCardConfig;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 
     33 import java.io.File;
     34 import java.io.FileNotFoundException;
     35 import java.io.FileOutputStream;
     36 import java.io.OutputStream;
     37 import java.util.HashSet;
     38 import java.util.Set;
     39 
     40 /**
     41  * Class for exporting vCard.
     42  *
     43  * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
     44  * finished (with the method {@link Activity#finish()}) after the export and never reuse
     45  * any Dialog in the instance. So this code is careless about the management around managed
     46  * dialogs stuffs (like how onCreateDialog() is used).
     47  */
     48 public class ExportVCardActivity extends Activity {
     49     private static final String LOG_TAG = "ExportVCardActivity";
     50 
     51     // If true, VCardExporter is able to emits files longer than 8.3 format.
     52     private static final boolean ALLOW_LONG_FILE_NAME = false;
     53     private String mTargetDirectory;
     54     private String mFileNamePrefix;
     55     private String mFileNameSuffix;
     56     private int mFileIndexMinimum;
     57     private int mFileIndexMaximum;
     58     private String mFileNameExtension;
     59     private String mVCardTypeStr;
     60     private Set<String> mExtensionsToConsider;
     61 
     62     private ProgressDialog mProgressDialog;
     63     private String mExportingFileName;
     64 
     65     private Handler mHandler = new Handler();
     66 
     67     // Used temporaly when asking users to confirm the file name
     68     private String mTargetFileName;
     69 
     70     // String for storing error reason temporaly.
     71     private String mErrorReason;
     72 
     73     private ActualExportThread mActualExportThread;
     74 
     75     private class CancelListener
     76             implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
     77         public void onClick(DialogInterface dialog, int which) {
     78             finish();
     79         }
     80         public void onCancel(DialogInterface dialog) {
     81             finish();
     82         }
     83     }
     84 
     85     private CancelListener mCancelListener = new CancelListener();
     86 
     87     private class ErrorReasonDisplayer implements Runnable {
     88         private final int mResId;
     89         public ErrorReasonDisplayer(int resId) {
     90             mResId = resId;
     91         }
     92         public ErrorReasonDisplayer(String errorReason) {
     93             mResId = R.id.dialog_fail_to_export_with_reason;
     94             mErrorReason = errorReason;
     95         }
     96         public void run() {
     97             // Show the Dialog only when the parent Activity is still alive.
     98             if (!ExportVCardActivity.this.isFinishing()) {
     99                 showDialog(mResId);
    100             }
    101         }
    102     }
    103 
    104     private class ExportConfirmationListener implements DialogInterface.OnClickListener {
    105         private final String mFileName;
    106 
    107         public ExportConfirmationListener(String fileName) {
    108             mFileName = fileName;
    109         }
    110 
    111         public void onClick(DialogInterface dialog, int which) {
    112             if (which == DialogInterface.BUTTON_POSITIVE) {
    113                 mActualExportThread = new ActualExportThread(mFileName);
    114                 showDialog(R.id.dialog_exporting_vcard);
    115             }
    116         }
    117     }
    118 
    119     private class ActualExportThread extends Thread
    120             implements DialogInterface.OnCancelListener {
    121         private PowerManager.WakeLock mWakeLock;
    122         private boolean mCanceled = false;
    123 
    124         public ActualExportThread(String fileName) {
    125             mExportingFileName = fileName;
    126             PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
    127             mWakeLock = powerManager.newWakeLock(
    128                     PowerManager.SCREEN_DIM_WAKE_LOCK |
    129                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
    130         }
    131 
    132         @Override
    133         public void run() {
    134             boolean shouldCallFinish = true;
    135             mWakeLock.acquire();
    136             VCardComposer composer = null;
    137             try {
    138                 OutputStream outputStream = null;
    139                 try {
    140                     outputStream = new FileOutputStream(mExportingFileName);
    141                 } catch (FileNotFoundException e) {
    142                     final String errorReason =
    143                         getString(R.string.fail_reason_could_not_open_file,
    144                                 mExportingFileName, e.getMessage());
    145                     shouldCallFinish = false;
    146                     mHandler.post(new ErrorReasonDisplayer(errorReason));
    147                     return;
    148                 }
    149 
    150                 // composer = new VCardComposer(ExportVCardActivity.this, mVCardTypeStr, true);
    151                 int vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
    152                 composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
    153 
    154                 composer.addHandler(composer.new HandlerForOutputStream(outputStream));
    155 
    156                 if (!composer.init()) {
    157                     final String errorReason = composer.getErrorReason();
    158                     Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
    159                     final String translatedErrorReason =
    160                             translateComposerError(errorReason);
    161                     mHandler.post(new ErrorReasonDisplayer(
    162                             getString(R.string.fail_reason_could_not_initialize_exporter,
    163                                     translatedErrorReason)));
    164                     shouldCallFinish = false;
    165                     return;
    166                 }
    167 
    168                 int size = composer.getCount();
    169 
    170                 if (size == 0) {
    171                     mHandler.post(new ErrorReasonDisplayer(
    172                             getString(R.string.fail_reason_no_exportable_contact)));
    173                     shouldCallFinish = false;
    174                     return;
    175                 }
    176 
    177                 mProgressDialog.setProgressNumberFormat(
    178                         getString(R.string.exporting_contact_list_progress));
    179                 mProgressDialog.setMax(size);
    180                 mProgressDialog.setProgress(0);
    181 
    182                 while (!composer.isAfterLast()) {
    183                     if (mCanceled) {
    184                         return;
    185                     }
    186                     if (!composer.createOneEntry()) {
    187                         final String errorReason = composer.getErrorReason();
    188                         Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
    189                         final String translatedErrorReason =
    190                             translateComposerError(errorReason);
    191                         mHandler.post(new ErrorReasonDisplayer(
    192                                 getString(R.string.fail_reason_error_occurred_during_export,
    193                                         translatedErrorReason)));
    194                         shouldCallFinish = false;
    195                         return;
    196                     }
    197                     mProgressDialog.incrementProgressBy(1);
    198                 }
    199             } finally {
    200                 if (composer != null) {
    201                     composer.terminate();
    202                 }
    203                 mWakeLock.release();
    204                 mProgressDialog.dismiss();
    205                 if (shouldCallFinish && !isFinishing()) {
    206                     finish();
    207                 }
    208             }
    209         }
    210 
    211         @Override
    212         public void finalize() {
    213             if (mWakeLock != null && mWakeLock.isHeld()) {
    214                 mWakeLock.release();
    215             }
    216         }
    217 
    218         public void cancel() {
    219             mCanceled = true;
    220         }
    221 
    222         public void onCancel(DialogInterface dialog) {
    223             cancel();
    224         }
    225     }
    226 
    227     private String translateComposerError(String errorMessage) {
    228         Resources resources = getResources();
    229         if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
    230             return resources.getString(R.string.composer_failed_to_get_database_infomation);
    231         } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
    232             return resources.getString(R.string.composer_has_no_exportable_contact);
    233         } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
    234             return resources.getString(R.string.composer_not_initialized);
    235         } else {
    236             return errorMessage;
    237         }
    238     }
    239 
    240     @Override
    241     protected void onCreate(Bundle bundle) {
    242         super.onCreate(bundle);
    243 
    244         mTargetDirectory = getString(R.string.config_export_dir);
    245         mFileNamePrefix = getString(R.string.config_export_file_prefix);
    246         mFileNameSuffix = getString(R.string.config_export_file_suffix);
    247         mFileNameExtension = getString(R.string.config_export_file_extension);
    248         mVCardTypeStr = getString(R.string.config_export_vcard_type);
    249 
    250         mExtensionsToConsider = new HashSet<String>();
    251         mExtensionsToConsider.add(mFileNameExtension);
    252 
    253         final String additionalExtensions =
    254             getString(R.string.config_export_extensions_to_consider);
    255         if (!TextUtils.isEmpty(additionalExtensions)) {
    256             for (String extension : additionalExtensions.split(",")) {
    257                 String trimed = extension.trim();
    258                 if (trimed.length() > 0) {
    259                     mExtensionsToConsider.add(trimed);
    260                 }
    261             }
    262         }
    263 
    264         final Resources resources = getResources();
    265         mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
    266         mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
    267 
    268         startExportVCardToSdCard();
    269     }
    270 
    271     @Override
    272     protected Dialog onCreateDialog(int id) {
    273         switch (id) {
    274             case R.id.dialog_export_confirmation: {
    275                 return getExportConfirmationDialog();
    276             }
    277             case R.string.fail_reason_too_many_vcard: {
    278                 return new AlertDialog.Builder(this)
    279                     .setTitle(R.string.exporting_contact_failed_title)
    280                     .setMessage(getString(R.string.exporting_contact_failed_message,
    281                                 getString(R.string.fail_reason_too_many_vcard)))
    282                                 .setPositiveButton(android.R.string.ok, mCancelListener)
    283                                 .create();
    284             }
    285             case R.id.dialog_fail_to_export_with_reason: {
    286                 return getErrorDialogWithReason();
    287             }
    288             case R.id.dialog_sdcard_not_found: {
    289                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
    290                 .setTitle(R.string.no_sdcard_title)
    291                 .setIcon(android.R.drawable.ic_dialog_alert)
    292                 .setMessage(R.string.no_sdcard_message)
    293                 .setPositiveButton(android.R.string.ok, mCancelListener);
    294                 return builder.create();
    295             }
    296             case R.id.dialog_exporting_vcard: {
    297                 if (mProgressDialog == null) {
    298                     String title = getString(R.string.exporting_contact_list_title);
    299                     String message = getString(R.string.exporting_contact_list_message,
    300                             mExportingFileName);
    301                     mProgressDialog = new ProgressDialog(ExportVCardActivity.this);
    302                     mProgressDialog.setTitle(title);
    303                     mProgressDialog.setMessage(message);
    304                     mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
    305                     mProgressDialog.setOnCancelListener(mActualExportThread);
    306                     mActualExportThread.start();
    307                 }
    308                 return mProgressDialog;
    309             }
    310         }
    311         return super.onCreateDialog(id);
    312     }
    313 
    314     @Override
    315     protected void onPrepareDialog(int id, Dialog dialog) {
    316         if (id == R.id.dialog_fail_to_export_with_reason) {
    317             ((AlertDialog)dialog).setMessage(getErrorReason());
    318         } else if (id == R.id.dialog_export_confirmation) {
    319             ((AlertDialog)dialog).setMessage(
    320                     getString(R.string.confirm_export_message, mTargetFileName));
    321         } else {
    322             super.onPrepareDialog(id, dialog);
    323         }
    324     }
    325 
    326     @Override
    327     protected void onStop() {
    328         super.onStop();
    329         if (mActualExportThread != null) {
    330             // The Activity is no longer visible. Stop the thread.
    331             mActualExportThread.cancel();
    332             mActualExportThread = null;
    333         }
    334 
    335         if (!isFinishing()) {
    336             finish();
    337         }
    338     }
    339 
    340     /**
    341      * Tries to start exporting VCard. If there's no SDCard available,
    342      * an error dialog is shown.
    343      */
    344     public void startExportVCardToSdCard() {
    345         File targetDirectory = new File(mTargetDirectory);
    346 
    347         if (!(targetDirectory.exists() &&
    348                 targetDirectory.isDirectory() &&
    349                 targetDirectory.canRead()) &&
    350                 !targetDirectory.mkdirs()) {
    351             showDialog(R.id.dialog_sdcard_not_found);
    352         } else {
    353             mTargetFileName = getAppropriateFileName(mTargetDirectory);
    354             if (TextUtils.isEmpty(mTargetFileName)) {
    355                 mTargetFileName = null;
    356                 // finish() is called via the error dialog. Do not call the method here.
    357                 return;
    358             }
    359 
    360             showDialog(R.id.dialog_export_confirmation);
    361         }
    362     }
    363 
    364     /**
    365      * Tries to get an appropriate filename. Returns null if it fails.
    366      */
    367     private String getAppropriateFileName(final String destDirectory) {
    368         int fileNumberStringLength = 0;
    369         {
    370             // Calling Math.Log10() is costly.
    371             int tmp;
    372             for (fileNumberStringLength = 0, tmp = mFileIndexMaximum; tmp > 0;
    373                 fileNumberStringLength++, tmp /= 10) {
    374             }
    375         }
    376         String bodyFormat = "%s%0" + fileNumberStringLength + "d%s";
    377 
    378         if (!ALLOW_LONG_FILE_NAME) {
    379             String possibleBody = String.format(bodyFormat,mFileNamePrefix, 1, mFileNameSuffix);
    380             if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
    381                 Log.e(LOG_TAG, "This code does not allow any long file name.");
    382                 mErrorReason = getString(R.string.fail_reason_too_long_filename,
    383                         String.format("%s.%s", possibleBody, mFileNameExtension));
    384                 showDialog(R.id.dialog_fail_to_export_with_reason);
    385                 // finish() is called via the error dialog. Do not call the method here.
    386                 return null;
    387             }
    388         }
    389 
    390         // Note that this logic assumes that the target directory is case insensitive.
    391         // As of 2009-07-16, it is true since the external storage is only sdcard, and
    392         // it is formated as FAT/VFAT.
    393         // TODO: fix this.
    394         for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
    395             boolean numberIsAvailable = true;
    396             // SD Association's specification seems to require this feature, though we cannot
    397             // have the specification since it is proprietary...
    398             String body = null;
    399             for (String possibleExtension : mExtensionsToConsider) {
    400                 body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
    401                 File file = new File(String.format("%s/%s.%s",
    402                         destDirectory, body, possibleExtension));
    403                 if (file.exists()) {
    404                     numberIsAvailable = false;
    405                     break;
    406                 }
    407             }
    408             if (numberIsAvailable) {
    409                 return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension);
    410             }
    411         }
    412         showDialog(R.string.fail_reason_too_many_vcard);
    413         return null;
    414     }
    415 
    416     public Dialog getExportConfirmationDialog() {
    417         if (TextUtils.isEmpty(mTargetFileName)) {
    418             Log.e(LOG_TAG, "Target file name is empty, which must not be!");
    419             // This situation is not acceptable (probably a bug!), but we don't have no reason to
    420             // show...
    421             mErrorReason = null;
    422             return getErrorDialogWithReason();
    423         }
    424 
    425         return new AlertDialog.Builder(this)
    426             .setTitle(R.string.confirm_export_title)
    427             .setMessage(getString(R.string.confirm_export_message, mTargetFileName))
    428             .setPositiveButton(android.R.string.ok,
    429                     new ExportConfirmationListener(mTargetFileName))
    430             .setNegativeButton(android.R.string.cancel, mCancelListener)
    431             .setOnCancelListener(mCancelListener)
    432             .create();
    433     }
    434 
    435     public Dialog getErrorDialogWithReason() {
    436         if (mErrorReason == null) {
    437             Log.e(LOG_TAG, "Error reason must have been set.");
    438             mErrorReason = getString(R.string.fail_reason_unknown);
    439         }
    440         return new AlertDialog.Builder(this)
    441             .setTitle(R.string.exporting_contact_failed_title)
    442                 .setMessage(getString(R.string.exporting_contact_failed_message, mErrorReason))
    443             .setPositiveButton(android.R.string.ok, mCancelListener)
    444             .setOnCancelListener(mCancelListener)
    445             .create();
    446     }
    447 
    448     public void cancelExport() {
    449         if (mActualExportThread != null) {
    450             mActualExportThread.cancel();
    451             mActualExportThread = null;
    452         }
    453     }
    454 
    455     public String getErrorReason() {
    456         return mErrorReason;
    457     }
    458 }