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 
     17 package com.android.contacts;
     18 
     19 import com.android.contacts.model.Sources;
     20 import com.android.contacts.util.AccountSelectionUtil;
     21 
     22 import android.accounts.Account;
     23 import android.app.Activity;
     24 import android.app.AlertDialog;
     25 import android.app.Dialog;
     26 import android.app.ProgressDialog;
     27 import android.content.ContentResolver;
     28 import android.content.ContentUris;
     29 import android.content.Context;
     30 import android.content.DialogInterface;
     31 import android.content.DialogInterface.OnCancelListener;
     32 import android.content.DialogInterface.OnClickListener;
     33 import android.content.Intent;
     34 import android.net.Uri;
     35 import android.os.Bundle;
     36 import android.os.Environment;
     37 import android.os.Handler;
     38 import android.os.PowerManager;
     39 import android.pim.vcard.VCardConfig;
     40 import android.pim.vcard.VCardEntryCommitter;
     41 import android.pim.vcard.VCardEntryConstructor;
     42 import android.pim.vcard.VCardEntryCounter;
     43 import android.pim.vcard.VCardInterpreter;
     44 import android.pim.vcard.VCardInterpreterCollection;
     45 import android.pim.vcard.VCardParser;
     46 import android.pim.vcard.VCardParser_V21;
     47 import android.pim.vcard.VCardParser_V30;
     48 import android.pim.vcard.VCardSourceDetector;
     49 import android.pim.vcard.exception.VCardException;
     50 import android.pim.vcard.exception.VCardNestedException;
     51 import android.pim.vcard.exception.VCardNotSupportedException;
     52 import android.pim.vcard.exception.VCardVersionException;
     53 import android.provider.ContactsContract.RawContacts;
     54 import android.text.SpannableStringBuilder;
     55 import android.text.Spanned;
     56 import android.text.TextUtils;
     57 import android.text.style.RelativeSizeSpan;
     58 import android.util.Log;
     59 
     60 import java.io.File;
     61 import java.io.IOException;
     62 import java.io.InputStream;
     63 import java.text.DateFormat;
     64 import java.text.SimpleDateFormat;
     65 import java.util.ArrayList;
     66 import java.util.Arrays;
     67 import java.util.Date;
     68 import java.util.HashSet;
     69 import java.util.List;
     70 import java.util.Locale;
     71 import java.util.Set;
     72 import java.util.Vector;
     73 
     74 class VCardFile {
     75     private String mName;
     76     private String mCanonicalPath;
     77     private long mLastModified;
     78 
     79     public VCardFile(String name, String canonicalPath, long lastModified) {
     80         mName = name;
     81         mCanonicalPath = canonicalPath;
     82         mLastModified = lastModified;
     83     }
     84 
     85     public String getName() {
     86         return mName;
     87     }
     88 
     89     public String getCanonicalPath() {
     90         return mCanonicalPath;
     91     }
     92 
     93     public long getLastModified() {
     94         return mLastModified;
     95     }
     96 }
     97 
     98 /**
     99  * Class for importing vCard. Several user interaction will be required while reading
    100  * (selecting a file, waiting a moment, etc.)
    101  *
    102  * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
    103  * finished (with the method {@link Activity#finish()}) after the import and never reuse
    104  * any Dialog in the instance. So this code is careless about the management around managed
    105  * dialogs stuffs (like how onCreateDialog() is used).
    106  */
    107 public class ImportVCardActivity extends Activity {
    108     private static final String LOG_TAG = "ImportVCardActivity";
    109     private static final boolean DO_PERFORMANCE_PROFILE = false;
    110 
    111     private final static int VCARD_VERSION_V21 = 1;
    112     private final static int VCARD_VERSION_V30 = 2;
    113     private final static int VCARD_VERSION_V40 = 3;
    114 
    115     // Run on the UI thread. Must not be null except after onDestroy().
    116     private Handler mHandler = new Handler();
    117 
    118     private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
    119     private Account mAccount;
    120 
    121     private ProgressDialog mProgressDialogForScanVCard;
    122 
    123     private List<VCardFile> mAllVCardFileList;
    124     private VCardScanThread mVCardScanThread;
    125     private VCardReadThread mVCardReadThread;
    126     private ProgressDialog mProgressDialogForReadVCard;
    127 
    128     private String mErrorMessage;
    129 
    130     private boolean mNeedReview = false;
    131 
    132     // Runs on the UI thread.
    133     private class DialogDisplayer implements Runnable {
    134         private final int mResId;
    135         public DialogDisplayer(int resId) {
    136             mResId = resId;
    137         }
    138         public DialogDisplayer(String errorMessage) {
    139             mResId = R.id.dialog_error_with_message;
    140             mErrorMessage = errorMessage;
    141         }
    142         public void run() {
    143             showDialog(mResId);
    144         }
    145     }
    146 
    147     private class CancelListener
    148         implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
    149         public void onClick(DialogInterface dialog, int which) {
    150             finish();
    151         }
    152 
    153         public void onCancel(DialogInterface dialog) {
    154             finish();
    155         }
    156     }
    157 
    158     private CancelListener mCancelListener = new CancelListener();
    159 
    160     private class VCardReadThread extends Thread
    161             implements DialogInterface.OnCancelListener {
    162         private ContentResolver mResolver;
    163         private VCardParser mVCardParser;
    164         private boolean mCanceled;
    165         private PowerManager.WakeLock mWakeLock;
    166         private Uri mUri;
    167         private File mTempFile;
    168 
    169         private List<VCardFile> mSelectedVCardFileList;
    170         private List<String> mErrorFileNameList;
    171 
    172         public VCardReadThread(Uri uri) {
    173             mUri = uri;
    174             init();
    175         }
    176 
    177         public VCardReadThread(final List<VCardFile> selectedVCardFileList) {
    178             mSelectedVCardFileList = selectedVCardFileList;
    179             mErrorFileNameList = new ArrayList<String>();
    180             init();
    181         }
    182 
    183         private void init() {
    184             Context context = ImportVCardActivity.this;
    185             mResolver = context.getContentResolver();
    186             PowerManager powerManager = (PowerManager)context.getSystemService(
    187                     Context.POWER_SERVICE);
    188             mWakeLock = powerManager.newWakeLock(
    189                     PowerManager.SCREEN_DIM_WAKE_LOCK |
    190                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
    191         }
    192 
    193         @Override
    194         public void finalize() {
    195             if (mWakeLock != null && mWakeLock.isHeld()) {
    196                 mWakeLock.release();
    197             }
    198         }
    199 
    200         @Override
    201         public void run() {
    202             boolean shouldCallFinish = true;
    203             mWakeLock.acquire();
    204             Uri createdUri = null;
    205             mTempFile = null;
    206             // Some malicious vCard data may make this thread broken
    207             // (e.g. OutOfMemoryError).
    208             // Even in such cases, some should be done.
    209             try {
    210                 if (mUri != null) {  // Read one vCard expressed by mUri
    211                     final Uri targetUri = mUri;
    212                     mProgressDialogForReadVCard.setProgressNumberFormat("");
    213                     mProgressDialogForReadVCard.setProgress(0);
    214 
    215                     // Count the number of VCard entries
    216                     mProgressDialogForReadVCard.setIndeterminate(true);
    217                     long start;
    218                     if (DO_PERFORMANCE_PROFILE) {
    219                         start = System.currentTimeMillis();
    220                     }
    221                     final VCardEntryCounter counter = new VCardEntryCounter();
    222                     final VCardSourceDetector detector = new VCardSourceDetector();
    223                     final VCardInterpreterCollection builderCollection =
    224                             new VCardInterpreterCollection(Arrays.asList(counter, detector));
    225                     boolean result;
    226                     try {
    227                         // We don't know which type should be useld to parse the Uri.
    228                         // It is possble to misinterpret the vCard, but we expect the parser
    229                         // lets VCardSourceDetector detect the type before the misinterpretation.
    230                         result = readOneVCardFile(targetUri, VCardConfig.VCARD_TYPE_UNKNOWN,
    231                                 builderCollection, true, null);
    232                     } catch (VCardNestedException e) {
    233                         try {
    234                             final int estimatedVCardType = detector.getEstimatedType();
    235                             final String estimatedCharset = detector.getEstimatedCharset();
    236                             // Assume that VCardSourceDetector was able to detect the source.
    237                             // Try again with the detector.
    238                             result = readOneVCardFile(targetUri, estimatedVCardType,
    239                                     counter, false, null);
    240                         } catch (VCardNestedException e2) {
    241                             result = false;
    242                             Log.e(LOG_TAG, "Must not reach here. " + e2);
    243                         }
    244                     }
    245                     if (DO_PERFORMANCE_PROFILE) {
    246                         long time = System.currentTimeMillis() - start;
    247                         Log.d(LOG_TAG, "time for counting the number of vCard entries: " +
    248                                 time + " ms");
    249                     }
    250                     if (!result) {
    251                         shouldCallFinish = false;
    252                         return;
    253                     }
    254 
    255                     mProgressDialogForReadVCard.setProgressNumberFormat(
    256                             getString(R.string.reading_vcard_contacts));
    257                     mProgressDialogForReadVCard.setIndeterminate(false);
    258                     mProgressDialogForReadVCard.setMax(counter.getCount());
    259                     String charset = detector.getEstimatedCharset();
    260                     createdUri = doActuallyReadOneVCard(targetUri, mAccount, true, detector,
    261                             mErrorFileNameList);
    262                 } else {  // Read multiple files.
    263                     mProgressDialogForReadVCard.setProgressNumberFormat(
    264                             getString(R.string.reading_vcard_files));
    265                     mProgressDialogForReadVCard.setMax(mSelectedVCardFileList.size());
    266                     mProgressDialogForReadVCard.setProgress(0);
    267 
    268                     for (VCardFile vcardFile : mSelectedVCardFileList) {
    269                         if (mCanceled) {
    270                             return;
    271                         }
    272                         // TODO: detect scheme!
    273                         final Uri targetUri = Uri.parse("file://" + vcardFile.getCanonicalPath());
    274                         VCardSourceDetector detector = new VCardSourceDetector();
    275                         try {
    276                             if (!readOneVCardFile(targetUri, VCardConfig.VCARD_TYPE_UNKNOWN,
    277                                     detector, true, mErrorFileNameList)) {
    278                                 continue;
    279                             }
    280                         } catch (VCardNestedException e) {
    281                             // Assume that VCardSourceDetector was able to detect the source.
    282                         }
    283                         String charset = detector.getEstimatedCharset();
    284                         doActuallyReadOneVCard(targetUri, mAccount,
    285                                 false, detector, mErrorFileNameList);
    286                         mProgressDialogForReadVCard.incrementProgressBy(1);
    287                     }
    288                 }
    289             } finally {
    290                 mWakeLock.release();
    291                 mProgressDialogForReadVCard.dismiss();
    292                 if (mTempFile != null) {
    293                     if (!mTempFile.delete()) {
    294                         Log.w(LOG_TAG, "Failed to delete a cache file.");
    295                     }
    296                     mTempFile = null;
    297                 }
    298                 // finish() is called via mCancelListener, which is used in DialogDisplayer.
    299                 if (shouldCallFinish && !isFinishing()) {
    300                     if (mErrorFileNameList == null || mErrorFileNameList.isEmpty()) {
    301                         finish();
    302                         if (mNeedReview) {
    303                             mNeedReview = false;
    304                             Log.v("importVCardActivity", "Prepare to review the imported contact");
    305 
    306                             if (createdUri != null) {
    307                                 // get contact_id of this raw_contact
    308                                 final long rawContactId = ContentUris.parseId(createdUri);
    309                                 Uri contactUri = RawContacts.getContactLookupUri(
    310                                         getContentResolver(), ContentUris.withAppendedId(
    311                                                 RawContacts.CONTENT_URI, rawContactId));
    312 
    313                                 Intent viewIntent = new Intent(Intent.ACTION_VIEW, contactUri);
    314                                 startActivity(viewIntent);
    315                             }
    316                         }
    317                     } else {
    318                         StringBuilder builder = new StringBuilder();
    319                         boolean first = true;
    320                         for (String fileName : mErrorFileNameList) {
    321                             if (first) {
    322                                 first = false;
    323                             } else {
    324                                 builder.append(", ");
    325                             }
    326                             builder.append(fileName);
    327                         }
    328 
    329                         runOnUIThread(new DialogDisplayer(
    330                                 getString(R.string.fail_reason_failed_to_read_files,
    331                                         builder.toString())));
    332                     }
    333                 }
    334             }
    335         }
    336 
    337         private Uri doActuallyReadOneVCard(Uri uri, Account account,
    338                 boolean showEntryParseProgress,
    339                 VCardSourceDetector detector, List<String> errorFileNameList) {
    340             final Context context = ImportVCardActivity.this;
    341             int vcardType = detector.getEstimatedType();
    342             if (vcardType == VCardConfig.VCARD_TYPE_UNKNOWN) {
    343                 vcardType = VCardConfig.getVCardTypeFromString(
    344                         context.getString(R.string.config_import_vcard_type));
    345             }
    346             final String estimatedCharset = detector.getEstimatedCharset();
    347             final String currentLanguage = Locale.getDefault().getLanguage();
    348             VCardEntryConstructor builder;
    349             builder = new VCardEntryConstructor(vcardType, mAccount, estimatedCharset);
    350             final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
    351             builder.addEntryHandler(committer);
    352             if (showEntryParseProgress) {
    353                 builder.addEntryHandler(new ProgressShower(mProgressDialogForReadVCard,
    354                         context.getString(R.string.reading_vcard_message),
    355                         ImportVCardActivity.this,
    356                         mHandler));
    357             }
    358 
    359             try {
    360                 if (!readOneVCardFile(uri, vcardType, builder, false, null)) {
    361                     return null;
    362                 }
    363             } catch (VCardNestedException e) {
    364                 Log.e(LOG_TAG, "Never reach here.");
    365             }
    366             final ArrayList<Uri> createdUris = committer.getCreatedUris();
    367             return (createdUris == null || createdUris.size() != 1) ? null : createdUris.get(0);
    368         }
    369 
    370         /**
    371          * Charset should be handled by {@link VCardEntryConstructor}.
    372          */
    373         private boolean readOneVCardFile(Uri uri, int vcardType,
    374                 VCardInterpreter interpreter,
    375                 boolean throwNestedException, List<String> errorFileNameList)
    376                 throws VCardNestedException {
    377             InputStream is;
    378             try {
    379                 is = mResolver.openInputStream(uri);
    380                 mVCardParser = new VCardParser_V21(vcardType);
    381 
    382                 try {
    383                     mVCardParser.parse(is, interpreter);
    384                 } catch (VCardVersionException e1) {
    385                     try {
    386                         is.close();
    387                     } catch (IOException e) {
    388                     }
    389                     if (interpreter instanceof VCardEntryConstructor) {
    390                         // Let the object clean up internal temporal objects,
    391                         ((VCardEntryConstructor)interpreter).clear();
    392                     } else if (interpreter instanceof VCardInterpreterCollection) {
    393                         for (VCardInterpreter elem :
    394                             ((VCardInterpreterCollection) interpreter).getCollection()) {
    395                             if (elem instanceof VCardEntryConstructor) {
    396                                 ((VCardEntryConstructor)elem).clear();
    397                             }
    398                         }
    399                     }
    400 
    401                     is = mResolver.openInputStream(uri);
    402 
    403                     try {
    404                         mVCardParser = new VCardParser_V30(vcardType);
    405                         mVCardParser.parse(is, interpreter);
    406                     } catch (VCardVersionException e2) {
    407                         throw new VCardException("vCard with unspported version.");
    408                     }
    409                 } finally {
    410                     if (is != null) {
    411                         try {
    412                             is.close();
    413                         } catch (IOException e) {
    414                         }
    415                     }
    416                 }
    417             } catch (IOException e) {
    418                 Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
    419 
    420                 mProgressDialogForReadVCard.dismiss();
    421 
    422                 if (errorFileNameList != null) {
    423                     errorFileNameList.add(uri.toString());
    424                 } else {
    425                     runOnUIThread(new DialogDisplayer(
    426                             getString(R.string.fail_reason_io_error) +
    427                                     ": " + e.getLocalizedMessage()));
    428                 }
    429                 return false;
    430             } catch (VCardNotSupportedException e) {
    431                 if ((e instanceof VCardNestedException) && throwNestedException) {
    432                     throw (VCardNestedException)e;
    433                 }
    434                 if (errorFileNameList != null) {
    435                     errorFileNameList.add(uri.toString());
    436                 } else {
    437                     runOnUIThread(new DialogDisplayer(
    438                             getString(R.string.fail_reason_vcard_not_supported_error) +
    439                             " (" + e.getMessage() + ")"));
    440                 }
    441                 return false;
    442             } catch (VCardException e) {
    443                 if (errorFileNameList != null) {
    444                     errorFileNameList.add(uri.toString());
    445                 } else {
    446                     runOnUIThread(new DialogDisplayer(
    447                             getString(R.string.fail_reason_vcard_parse_error) +
    448                             " (" + e.getMessage() + ")"));
    449                 }
    450                 return false;
    451             }
    452             return true;
    453         }
    454 
    455         public void cancel() {
    456             mCanceled = true;
    457             if (mVCardParser != null) {
    458                 mVCardParser.cancel();
    459             }
    460         }
    461 
    462         public void onCancel(DialogInterface dialog) {
    463             cancel();
    464         }
    465     }
    466 
    467     private class ImportTypeSelectedListener implements
    468             DialogInterface.OnClickListener {
    469         public static final int IMPORT_ONE = 0;
    470         public static final int IMPORT_MULTIPLE = 1;
    471         public static final int IMPORT_ALL = 2;
    472         public static final int IMPORT_TYPE_SIZE = 3;
    473 
    474         private int mCurrentIndex;
    475 
    476         public void onClick(DialogInterface dialog, int which) {
    477             if (which == DialogInterface.BUTTON_POSITIVE) {
    478                 switch (mCurrentIndex) {
    479                 case IMPORT_ALL:
    480                     importMultipleVCardFromSDCard(mAllVCardFileList);
    481                     break;
    482                 case IMPORT_MULTIPLE:
    483                     showDialog(R.id.dialog_select_multiple_vcard);
    484                     break;
    485                 default:
    486                     showDialog(R.id.dialog_select_one_vcard);
    487                     break;
    488                 }
    489             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
    490                 finish();
    491             } else {
    492                 mCurrentIndex = which;
    493             }
    494         }
    495     }
    496 
    497     private class VCardSelectedListener implements
    498             DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
    499         private int mCurrentIndex;
    500         private Set<Integer> mSelectedIndexSet;
    501 
    502         public VCardSelectedListener(boolean multipleSelect) {
    503             mCurrentIndex = 0;
    504             if (multipleSelect) {
    505                 mSelectedIndexSet = new HashSet<Integer>();
    506             }
    507         }
    508 
    509         public void onClick(DialogInterface dialog, int which) {
    510             if (which == DialogInterface.BUTTON_POSITIVE) {
    511                 if (mSelectedIndexSet != null) {
    512                     List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
    513                     int size = mAllVCardFileList.size();
    514                     // We'd like to sort the files by its index, so we do not use Set iterator.
    515                     for (int i = 0; i < size; i++) {
    516                         if (mSelectedIndexSet.contains(i)) {
    517                             selectedVCardFileList.add(mAllVCardFileList.get(i));
    518                         }
    519                     }
    520                     importMultipleVCardFromSDCard(selectedVCardFileList);
    521                 } else {
    522                     String canonicalPath = mAllVCardFileList.get(mCurrentIndex).getCanonicalPath();
    523                     final Uri uri = Uri.parse("file://" + canonicalPath);
    524                     importOneVCardFromSDCard(uri);
    525                 }
    526             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
    527                 finish();
    528             } else {
    529                 // Some file is selected.
    530                 mCurrentIndex = which;
    531                 if (mSelectedIndexSet != null) {
    532                     if (mSelectedIndexSet.contains(which)) {
    533                         mSelectedIndexSet.remove(which);
    534                     } else {
    535                         mSelectedIndexSet.add(which);
    536                     }
    537                 }
    538             }
    539         }
    540 
    541         public void onClick(DialogInterface dialog, int which, boolean isChecked) {
    542             if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
    543                 Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
    544                         mAllVCardFileList.get(which).getCanonicalPath()));
    545             } else {
    546                 onClick(dialog, which);
    547             }
    548         }
    549     }
    550 
    551     /**
    552      * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
    553      * a vCard file is shown. After the choice, VCardReadThread starts running.
    554      */
    555     private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
    556         private boolean mCanceled;
    557         private boolean mGotIOException;
    558         private File mRootDirectory;
    559 
    560         // To avoid recursive link.
    561         private Set<String> mCheckedPaths;
    562         private PowerManager.WakeLock mWakeLock;
    563 
    564         private class CanceledException extends Exception {
    565         }
    566 
    567         public VCardScanThread(File sdcardDirectory) {
    568             mCanceled = false;
    569             mGotIOException = false;
    570             mRootDirectory = sdcardDirectory;
    571             mCheckedPaths = new HashSet<String>();
    572             PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
    573                     Context.POWER_SERVICE);
    574             mWakeLock = powerManager.newWakeLock(
    575                     PowerManager.SCREEN_DIM_WAKE_LOCK |
    576                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
    577         }
    578 
    579         @Override
    580         public void run() {
    581             mAllVCardFileList = new Vector<VCardFile>();
    582             try {
    583                 mWakeLock.acquire();
    584                 getVCardFileRecursively(mRootDirectory);
    585             } catch (CanceledException e) {
    586                 mCanceled = true;
    587             } catch (IOException e) {
    588                 mGotIOException = true;
    589             } finally {
    590                 mWakeLock.release();
    591             }
    592 
    593             if (mCanceled) {
    594                 mAllVCardFileList = null;
    595             }
    596 
    597             mProgressDialogForScanVCard.dismiss();
    598             mProgressDialogForScanVCard = null;
    599 
    600             if (mGotIOException) {
    601                 runOnUIThread(new DialogDisplayer(R.id.dialog_io_exception));
    602             } else if (mCanceled) {
    603                 finish();
    604             } else {
    605                 int size = mAllVCardFileList.size();
    606                 final Context context = ImportVCardActivity.this;
    607                 if (size == 0) {
    608                     runOnUIThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
    609                 } else {
    610                     startVCardSelectAndImport();
    611                 }
    612             }
    613         }
    614 
    615         private void getVCardFileRecursively(File directory)
    616                 throws CanceledException, IOException {
    617             if (mCanceled) {
    618                 throw new CanceledException();
    619             }
    620 
    621             // e.g. secured directory may return null toward listFiles().
    622             final File[] files = directory.listFiles();
    623             if (files == null) {
    624                 Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
    625                 return;
    626             }
    627             for (File file : directory.listFiles()) {
    628                 if (mCanceled) {
    629                     throw new CanceledException();
    630                 }
    631                 String canonicalPath = file.getCanonicalPath();
    632                 if (mCheckedPaths.contains(canonicalPath)) {
    633                     continue;
    634                 }
    635 
    636                 mCheckedPaths.add(canonicalPath);
    637 
    638                 if (file.isDirectory()) {
    639                     getVCardFileRecursively(file);
    640                 } else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
    641                         file.canRead()){
    642                     String fileName = file.getName();
    643                     VCardFile vcardFile = new VCardFile(
    644                             fileName, canonicalPath, file.lastModified());
    645                     mAllVCardFileList.add(vcardFile);
    646                 }
    647             }
    648         }
    649 
    650         public void onCancel(DialogInterface dialog) {
    651             mCanceled = true;
    652         }
    653 
    654         public void onClick(DialogInterface dialog, int which) {
    655             if (which == DialogInterface.BUTTON_NEGATIVE) {
    656                 mCanceled = true;
    657             }
    658         }
    659     }
    660 
    661     private void startVCardSelectAndImport() {
    662         int size = mAllVCardFileList.size();
    663         if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically)) {
    664             importMultipleVCardFromSDCard(mAllVCardFileList);
    665         } else if (size == 1) {
    666             String canonicalPath = mAllVCardFileList.get(0).getCanonicalPath();
    667             Uri uri = Uri.parse("file://" + canonicalPath);
    668             importOneVCardFromSDCard(uri);
    669         } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
    670             runOnUIThread(new DialogDisplayer(R.id.dialog_select_import_type));
    671         } else {
    672             runOnUIThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
    673         }
    674     }
    675 
    676     private void importMultipleVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
    677         runOnUIThread(new Runnable() {
    678             public void run() {
    679                 mVCardReadThread = new VCardReadThread(selectedVCardFileList);
    680                 showDialog(R.id.dialog_reading_vcard);
    681             }
    682         });
    683     }
    684 
    685     private void importOneVCardFromSDCard(final Uri uri) {
    686         runOnUIThread(new Runnable() {
    687             public void run() {
    688                 mVCardReadThread = new VCardReadThread(uri);
    689                 showDialog(R.id.dialog_reading_vcard);
    690             }
    691         });
    692     }
    693 
    694     private Dialog getSelectImportTypeDialog() {
    695         DialogInterface.OnClickListener listener =
    696             new ImportTypeSelectedListener();
    697         AlertDialog.Builder builder = new AlertDialog.Builder(this)
    698             .setTitle(R.string.select_vcard_title)
    699             .setPositiveButton(android.R.string.ok, listener)
    700             .setOnCancelListener(mCancelListener)
    701             .setNegativeButton(android.R.string.cancel, mCancelListener);
    702 
    703         String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
    704         items[ImportTypeSelectedListener.IMPORT_ONE] =
    705             getString(R.string.import_one_vcard_string);
    706         items[ImportTypeSelectedListener.IMPORT_MULTIPLE] =
    707             getString(R.string.import_multiple_vcard_string);
    708         items[ImportTypeSelectedListener.IMPORT_ALL] =
    709             getString(R.string.import_all_vcard_string);
    710         builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener);
    711         return builder.create();
    712     }
    713 
    714     private Dialog getVCardFileSelectDialog(boolean multipleSelect) {
    715         int size = mAllVCardFileList.size();
    716         VCardSelectedListener listener = new VCardSelectedListener(multipleSelect);
    717         AlertDialog.Builder builder =
    718             new AlertDialog.Builder(this)
    719                 .setTitle(R.string.select_vcard_title)
    720                 .setPositiveButton(android.R.string.ok, listener)
    721                 .setOnCancelListener(mCancelListener)
    722                 .setNegativeButton(android.R.string.cancel, mCancelListener);
    723 
    724         CharSequence[] items = new CharSequence[size];
    725         DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    726         for (int i = 0; i < size; i++) {
    727             VCardFile vcardFile = mAllVCardFileList.get(i);
    728             SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
    729             stringBuilder.append(vcardFile.getName());
    730             stringBuilder.append('\n');
    731             int indexToBeSpanned = stringBuilder.length();
    732             // Smaller date text looks better, since each file name becomes easier to read.
    733             // The value set to RelativeSizeSpan is arbitrary. You can change it to any other
    734             // value (but the value bigger than 1.0f would not make nice appearance :)
    735             stringBuilder.append(
    736                         "(" + dateFormat.format(new Date(vcardFile.getLastModified())) + ")");
    737             stringBuilder.setSpan(
    738                     new RelativeSizeSpan(0.7f), indexToBeSpanned, stringBuilder.length(),
    739                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    740             items[i] = stringBuilder;
    741         }
    742         if (multipleSelect) {
    743             builder.setMultiChoiceItems(items, (boolean[])null, listener);
    744         } else {
    745             builder.setSingleChoiceItems(items, 0, listener);
    746         }
    747         return builder.create();
    748     }
    749 
    750     @Override
    751     protected void onCreate(Bundle bundle) {
    752         super.onCreate(bundle);
    753 
    754         final Intent intent = getIntent();
    755         if (intent != null) {
    756             final String accountName = intent.getStringExtra("account_name");
    757             final String accountType = intent.getStringExtra("account_type");
    758             if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
    759                 mAccount = new Account(accountName, accountType);
    760             }
    761         } else {
    762             Log.e(LOG_TAG, "intent does not exist");
    763         }
    764 
    765         // The caller often does not know account information at all, so we show the UI instead.
    766         if (mAccount == null) {
    767             // There's three possibilities:
    768             // - more than one accounts -> ask the user
    769             // - just one account -> use the account without asking the user
    770             // - no account -> use phone-local storage without asking the user
    771             final Sources sources = Sources.getInstance(this);
    772             final List<Account> accountList = sources.getAccounts(true);
    773             final int size = accountList.size();
    774             if (size > 1) {
    775                 final int resId = R.string.import_from_sdcard;
    776                 mAccountSelectionListener =
    777                     new AccountSelectionUtil.AccountSelectedListener(
    778                             this, accountList, resId) {
    779                     @Override
    780                     public void onClick(DialogInterface dialog, int which) {
    781                         dialog.dismiss();
    782                         mAccount = mAccountList.get(which);
    783                         // Instead of using Intent mechanism, call the relevant private method,
    784                         // to avoid throwing an Intent to itself again.
    785                         startImport();
    786                     }
    787                 };
    788                 showDialog(resId);
    789                 return;
    790             } else {
    791                 mAccount = size > 0 ? accountList.get(0) : null;
    792             }
    793         }
    794 
    795         startImport();
    796     }
    797 
    798     private void startImport() {
    799         Intent intent = getIntent();
    800         final String action = intent.getAction();
    801         final Uri uri = intent.getData();
    802         Log.v(LOG_TAG, "action = " + action + " ; path = " + uri);
    803         if (Intent.ACTION_VIEW.equals(action)) {
    804             // Import the file directly and then go to EDIT screen
    805             mNeedReview = true;
    806         }
    807 
    808         if (uri != null) {
    809             importOneVCardFromSDCard(uri);
    810         } else {
    811             doScanExternalStorageAndImportVCard();
    812         }
    813     }
    814 
    815     @Override
    816     protected Dialog onCreateDialog(int resId) {
    817         switch (resId) {
    818             case R.string.import_from_sdcard: {
    819                 if (mAccountSelectionListener == null) {
    820                     throw new NullPointerException(
    821                             "mAccountSelectionListener must not be null.");
    822                 }
    823                 return AccountSelectionUtil.getSelectAccountDialog(this, resId,
    824                         mAccountSelectionListener,
    825                         new CancelListener());
    826             }
    827             case R.id.dialog_searching_vcard: {
    828                 if (mProgressDialogForScanVCard == null) {
    829                     String title = getString(R.string.searching_vcard_title);
    830                     String message = getString(R.string.searching_vcard_message);
    831                     mProgressDialogForScanVCard =
    832                         ProgressDialog.show(this, title, message, true, false);
    833                     mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread);
    834                     mVCardScanThread.start();
    835                 }
    836                 return mProgressDialogForScanVCard;
    837             }
    838             case R.id.dialog_sdcard_not_found: {
    839                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
    840                     .setTitle(R.string.no_sdcard_title)
    841                     .setIcon(android.R.drawable.ic_dialog_alert)
    842                     .setMessage(R.string.no_sdcard_message)
    843                     .setOnCancelListener(mCancelListener)
    844                     .setPositiveButton(android.R.string.ok, mCancelListener);
    845                 return builder.create();
    846             }
    847             case R.id.dialog_vcard_not_found: {
    848                 String message = (getString(R.string.scanning_sdcard_failed_message,
    849                         getString(R.string.fail_reason_no_vcard_file)));
    850                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
    851                     .setTitle(R.string.scanning_sdcard_failed_title)
    852                     .setMessage(message)
    853                     .setOnCancelListener(mCancelListener)
    854                     .setPositiveButton(android.R.string.ok, mCancelListener);
    855                 return builder.create();
    856             }
    857             case R.id.dialog_select_import_type: {
    858                 return getSelectImportTypeDialog();
    859             }
    860             case R.id.dialog_select_multiple_vcard: {
    861                 return getVCardFileSelectDialog(true);
    862             }
    863             case R.id.dialog_select_one_vcard: {
    864                 return getVCardFileSelectDialog(false);
    865             }
    866             case R.id.dialog_reading_vcard: {
    867                 if (mProgressDialogForReadVCard == null) {
    868                     String title = getString(R.string.reading_vcard_title);
    869                     String message = getString(R.string.reading_vcard_message);
    870                     mProgressDialogForReadVCard = new ProgressDialog(this);
    871                     mProgressDialogForReadVCard.setTitle(title);
    872                     mProgressDialogForReadVCard.setMessage(message);
    873                     mProgressDialogForReadVCard.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
    874                     mProgressDialogForReadVCard.setOnCancelListener(mVCardReadThread);
    875                     mVCardReadThread.start();
    876                 }
    877                 return mProgressDialogForReadVCard;
    878             }
    879             case R.id.dialog_io_exception: {
    880                 String message = (getString(R.string.scanning_sdcard_failed_message,
    881                         getString(R.string.fail_reason_io_error)));
    882                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
    883                     .setTitle(R.string.scanning_sdcard_failed_title)
    884                     .setIcon(android.R.drawable.ic_dialog_alert)
    885                     .setMessage(message)
    886                     .setOnCancelListener(mCancelListener)
    887                     .setPositiveButton(android.R.string.ok, mCancelListener);
    888                 return builder.create();
    889             }
    890             case R.id.dialog_error_with_message: {
    891                 String message = mErrorMessage;
    892                 if (TextUtils.isEmpty(message)) {
    893                     Log.e(LOG_TAG, "Error message is null while it must not.");
    894                     message = getString(R.string.fail_reason_unknown);
    895                 }
    896                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
    897                     .setTitle(getString(R.string.reading_vcard_failed_title))
    898                     .setIcon(android.R.drawable.ic_dialog_alert)
    899                     .setMessage(message)
    900                     .setOnCancelListener(mCancelListener)
    901                     .setPositiveButton(android.R.string.ok, mCancelListener);
    902                 return builder.create();
    903             }
    904         }
    905 
    906         return super.onCreateDialog(resId);
    907     }
    908 
    909     @Override
    910     protected void onPause() {
    911         super.onPause();
    912         if (mVCardReadThread != null) {
    913             // The Activity is no longer visible. Stop the thread.
    914             mVCardReadThread.cancel();
    915             mVCardReadThread = null;
    916         }
    917 
    918         // ImportVCardActivity should not be persistent. In other words, if there's some
    919         // event calling onPause(), this Activity should finish its work and give the main
    920         // screen back to the caller Activity.
    921         if (!isFinishing()) {
    922             finish();
    923         }
    924     }
    925 
    926     @Override
    927     protected void onDestroy() {
    928         // The code assumes the handler runs on the UI thread. If not,
    929         // clearing the message queue is not enough, one would have to
    930         // make sure that the handler does not run any callback when
    931         // this activity isFinishing().
    932 
    933         // Need to make sure any worker thread is done before we flush and
    934         // nullify the message handler.
    935         if (mVCardReadThread != null) {
    936             Log.w(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!");
    937             mVCardReadThread.cancel();
    938             int attempts = 0;
    939             while (mVCardReadThread.isAlive() && attempts < 10) {
    940                 try {
    941                     Thread.currentThread().sleep(20);
    942                 } catch (InterruptedException ie) {
    943                     // Keep on going until max attempts is reached.
    944                 }
    945                 attempts++;
    946             }
    947             if (mVCardReadThread.isAlive()) {
    948                 // Find out why the thread did not exit in a timely
    949                 // fashion. Last resort: increase the sleep duration
    950                 // and/or the number of attempts.
    951                 Log.e(LOG_TAG, "VCardReadThread is still alive after max attempts.");
    952             }
    953             mVCardReadThread = null;
    954         }
    955 
    956         // Callbacks messages have what == 0.
    957         if (mHandler.hasMessages(0)) {
    958             mHandler.removeMessages(0);
    959         }
    960 
    961         mHandler = null;  // Prevents memory leaks by breaking any circular dependency.
    962         super.onDestroy();
    963     }
    964 
    965     /**
    966      * Tries to run a given Runnable object when the UI thread can. Ignore it otherwise
    967      */
    968     private void runOnUIThread(Runnable runnable) {
    969         if (mHandler == null) {
    970             Log.w(LOG_TAG, "Handler object is null. No dialog is shown.");
    971         } else {
    972             mHandler.post(runnable);
    973         }
    974     }
    975 
    976     @Override
    977     public void finalize() {
    978         // TODO: This should not be needed. Throw exception instead.
    979         if (mVCardReadThread != null) {
    980             // Not sure this procedure is really needed, but just in case...
    981             Log.e(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!");
    982             mVCardReadThread.cancel();
    983             mVCardReadThread = null;
    984         }
    985     }
    986 
    987     /**
    988      * Scans vCard in external storage (typically SDCard) and tries to import it.
    989      * - When there's no SDCard available, an error dialog is shown.
    990      * - When multiple vCard files are available, asks a user to select one.
    991      */
    992     private void doScanExternalStorageAndImportVCard() {
    993         // TODO: should use getExternalStorageState().
    994         final File file = Environment.getExternalStorageDirectory();
    995         if (!file.exists() || !file.isDirectory() || !file.canRead()) {
    996             showDialog(R.id.dialog_sdcard_not_found);
    997         } else {
    998             mVCardScanThread = new VCardScanThread(file);
    999             showDialog(R.id.dialog_searching_vcard);
   1000         }
   1001     }
   1002 }
   1003