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