Home | History | Annotate | Download | only in vcard
      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.common.vcard;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.Dialog;
     22 import android.app.Notification;
     23 import android.app.NotificationManager;
     24 import android.app.ProgressDialog;
     25 import android.content.ComponentName;
     26 import android.content.ContentResolver;
     27 import android.content.Context;
     28 import android.content.DialogInterface;
     29 import android.content.DialogInterface.OnCancelListener;
     30 import android.content.DialogInterface.OnClickListener;
     31 import android.content.Intent;
     32 import android.content.ServiceConnection;
     33 import android.database.Cursor;
     34 import android.net.Uri;
     35 import android.os.Bundle;
     36 import android.os.Environment;
     37 import android.os.Handler;
     38 import android.os.IBinder;
     39 import android.os.PowerManager;
     40 import android.provider.OpenableColumns;
     41 import android.text.SpannableStringBuilder;
     42 import android.text.Spanned;
     43 import android.text.TextUtils;
     44 import android.text.style.RelativeSizeSpan;
     45 import android.util.Log;
     46 import android.widget.Toast;
     47 
     48 import com.android.contacts.common.R;
     49 import com.android.contacts.common.model.AccountTypeManager;
     50 import com.android.contacts.common.model.account.AccountWithDataSet;
     51 import com.android.contacts.common.util.AccountSelectionUtil;
     52 import com.android.vcard.VCardEntryCounter;
     53 import com.android.vcard.VCardParser;
     54 import com.android.vcard.VCardParser_V21;
     55 import com.android.vcard.VCardParser_V30;
     56 import com.android.vcard.VCardSourceDetector;
     57 import com.android.vcard.exception.VCardException;
     58 import com.android.vcard.exception.VCardNestedException;
     59 import com.android.vcard.exception.VCardVersionException;
     60 
     61 import java.io.ByteArrayInputStream;
     62 import java.io.File;
     63 import java.io.IOException;
     64 import java.io.InputStream;
     65 import java.nio.ByteBuffer;
     66 import java.nio.channels.Channels;
     67 import java.nio.channels.ReadableByteChannel;
     68 import java.nio.channels.WritableByteChannel;
     69 import java.text.DateFormat;
     70 import java.text.SimpleDateFormat;
     71 import java.util.ArrayList;
     72 import java.util.Arrays;
     73 import java.util.Date;
     74 import java.util.HashSet;
     75 import java.util.List;
     76 import java.util.Set;
     77 import java.util.Vector;
     78 
     79 /**
     80  * The class letting users to import vCard. This includes the UI part for letting them select
     81  * an Account and posssibly a file if there's no Uri is given from its caller Activity.
     82  *
     83  * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
     84  * finished (with the method {@link Activity#finish()}) after the import and never reuse
     85  * any Dialog in the instance. So this code is careless about the management around managed
     86  * dialogs stuffs (like how onCreateDialog() is used).
     87  */
     88 public class ImportVCardActivity extends Activity {
     89     private static final String LOG_TAG = "VCardImport";
     90 
     91     private static final int SELECT_ACCOUNT = 0;
     92 
     93     /* package */ static final String VCARD_URI_ARRAY = "vcard_uri";
     94     /* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type";
     95     /* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset";
     96     /* package */ static final String VCARD_VERSION_ARRAY = "vcard_version";
     97     /* package */ static final String ENTRY_COUNT_ARRAY = "entry_count";
     98 
     99     /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
    100     /* package */ final static int VCARD_VERSION_V21 = 1;
    101     /* package */ final static int VCARD_VERSION_V30 = 2;
    102 
    103     private static final String SECURE_DIRECTORY_NAME = ".android_secure";
    104 
    105     /**
    106      * Notification id used when error happened before sending an import request to VCardServer.
    107      */
    108     private static final int FAILURE_NOTIFICATION_ID = 1;
    109 
    110     final static String CACHED_URIS = "cached_uris";
    111 
    112     private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
    113 
    114     private AccountWithDataSet mAccount;
    115 
    116     private ProgressDialog mProgressDialogForScanVCard;
    117     private ProgressDialog mProgressDialogForCachingVCard;
    118 
    119     private List<VCardFile> mAllVCardFileList;
    120     private VCardScanThread mVCardScanThread;
    121 
    122     private VCardCacheThread mVCardCacheThread;
    123     private ImportRequestConnection mConnection;
    124     /* package */ VCardImportExportListener mListener;
    125 
    126     private String mErrorMessage;
    127 
    128     private Handler mHandler = new Handler();
    129 
    130     private static class VCardFile {
    131         private final String mName;
    132         private final String mCanonicalPath;
    133         private final long mLastModified;
    134 
    135         public VCardFile(String name, String canonicalPath, long lastModified) {
    136             mName = name;
    137             mCanonicalPath = canonicalPath;
    138             mLastModified = lastModified;
    139         }
    140 
    141         public String getName() {
    142             return mName;
    143         }
    144 
    145         public String getCanonicalPath() {
    146             return mCanonicalPath;
    147         }
    148 
    149         public long getLastModified() {
    150             return mLastModified;
    151         }
    152     }
    153 
    154     // Runs on the UI thread.
    155     private class DialogDisplayer implements Runnable {
    156         private final int mResId;
    157         public DialogDisplayer(int resId) {
    158             mResId = resId;
    159         }
    160         public DialogDisplayer(String errorMessage) {
    161             mResId = R.id.dialog_error_with_message;
    162             mErrorMessage = errorMessage;
    163         }
    164         @Override
    165         public void run() {
    166             if (!isFinishing()) {
    167                 showDialog(mResId);
    168             }
    169         }
    170     }
    171 
    172     private class CancelListener
    173         implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
    174         @Override
    175         public void onClick(DialogInterface dialog, int which) {
    176             finish();
    177         }
    178         @Override
    179         public void onCancel(DialogInterface dialog) {
    180             finish();
    181         }
    182     }
    183 
    184     private CancelListener mCancelListener = new CancelListener();
    185 
    186     private class ImportRequestConnection implements ServiceConnection {
    187         private VCardService mService;
    188 
    189         public void sendImportRequest(final List<ImportRequest> requests) {
    190             Log.i(LOG_TAG, "Send an import request");
    191             mService.handleImportRequest(requests, mListener);
    192         }
    193 
    194         @Override
    195         public void onServiceConnected(ComponentName name, IBinder binder) {
    196             mService = ((VCardService.MyBinder) binder).getService();
    197             Log.i(LOG_TAG,
    198                     String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
    199                             Arrays.toString(mVCardCacheThread.getSourceUris())));
    200             mVCardCacheThread.start();
    201         }
    202 
    203         @Override
    204         public void onServiceDisconnected(ComponentName name) {
    205             Log.i(LOG_TAG, "Disconnected from VCardService");
    206         }
    207     }
    208 
    209     /**
    210      * Caches given vCard files into a local directory, and sends actual import request to
    211      * {@link VCardService}.
    212      *
    213      * We need to cache given files into local storage. One of reasons is that some data (as Uri)
    214      * may have special permissions. Callers may allow only this Activity to access that content,
    215      * not what this Activity launched (like {@link VCardService}).
    216      */
    217     private class VCardCacheThread extends Thread
    218             implements DialogInterface.OnCancelListener {
    219         private boolean mCanceled;
    220         private PowerManager.WakeLock mWakeLock;
    221         private VCardParser mVCardParser;
    222         private final Uri[] mSourceUris;  // Given from a caller.
    223         private final byte[] mSource;
    224         private final String mDisplayName;
    225 
    226         public VCardCacheThread(final Uri[] sourceUris) {
    227             mSourceUris = sourceUris;
    228             mSource = null;
    229             final Context context = ImportVCardActivity.this;
    230             final PowerManager powerManager =
    231                     (PowerManager)context.getSystemService(Context.POWER_SERVICE);
    232             mWakeLock = powerManager.newWakeLock(
    233                     PowerManager.SCREEN_DIM_WAKE_LOCK |
    234                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
    235             mDisplayName = null;
    236         }
    237 
    238         @Override
    239         public void finalize() {
    240             if (mWakeLock != null && mWakeLock.isHeld()) {
    241                 Log.w(LOG_TAG, "WakeLock is being held.");
    242                 mWakeLock.release();
    243             }
    244         }
    245 
    246         @Override
    247         public void run() {
    248             Log.i(LOG_TAG, "vCard cache thread starts running.");
    249             if (mConnection == null) {
    250                 throw new NullPointerException("vCard cache thread must be launched "
    251                         + "after a service connection is established");
    252             }
    253 
    254             mWakeLock.acquire();
    255             try {
    256                 if (mCanceled == true) {
    257                     Log.i(LOG_TAG, "vCard cache operation is canceled.");
    258                     return;
    259                 }
    260 
    261                 final Context context = ImportVCardActivity.this;
    262                 // Uris given from caller applications may not be opened twice: consider when
    263                 // it is not from local storage (e.g. "file:///...") but from some special
    264                 // provider (e.g. "content://...").
    265                 // Thus we have to once copy the content of Uri into local storage, and read
    266                 // it after it.
    267                 //
    268                 // We may be able to read content of each vCard file during copying them
    269                 // to local storage, but currently vCard code does not allow us to do so.
    270                 int cache_index = 0;
    271                 ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
    272                 if (mSource != null) {
    273                     try {
    274                         requests.add(constructImportRequest(mSource, null, mDisplayName));
    275                     } catch (VCardException e) {
    276                         Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
    277                         showFailureNotification(R.string.fail_reason_not_supported);
    278                         return;
    279                     }
    280                 } else {
    281                     final ContentResolver resolver =
    282                             ImportVCardActivity.this.getContentResolver();
    283                     for (Uri sourceUri : mSourceUris) {
    284                         String filename = null;
    285                         // Note: caches are removed by VCardService.
    286                         while (true) {
    287                             filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
    288                             final File file = context.getFileStreamPath(filename);
    289                             if (!file.exists()) {
    290                                 break;
    291                             } else {
    292                                 if (cache_index == Integer.MAX_VALUE) {
    293                                     throw new RuntimeException("Exceeded cache limit");
    294                                 }
    295                                 cache_index++;
    296                             }
    297                         }
    298                         final Uri localDataUri = copyTo(sourceUri, filename);
    299                         if (mCanceled) {
    300                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
    301                             break;
    302                         }
    303                         if (localDataUri == null) {
    304                             Log.w(LOG_TAG, "destUri is null");
    305                             break;
    306                         }
    307 
    308                         String displayName = null;
    309                         Cursor cursor = null;
    310                         // Try to get a display name from the given Uri. If it fails, we just
    311                         // pick up the last part of the Uri.
    312                         try {
    313                             cursor = resolver.query(sourceUri,
    314                                     new String[] { OpenableColumns.DISPLAY_NAME },
    315                                     null, null, null);
    316                             if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
    317                                 if (cursor.getCount() > 1) {
    318                                     Log.w(LOG_TAG, "Unexpected multiple rows: "
    319                                             + cursor.getCount());
    320                                 }
    321                                 int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
    322                                 if (index >= 0) {
    323                                     displayName = cursor.getString(index);
    324                                 }
    325                             }
    326                         } finally {
    327                             if (cursor != null) {
    328                                 cursor.close();
    329                             }
    330                         }
    331                         if (TextUtils.isEmpty(displayName)){
    332                             displayName = sourceUri.getLastPathSegment();
    333                         }
    334 
    335                         final ImportRequest request;
    336                         try {
    337                             request = constructImportRequest(null, localDataUri, displayName);
    338                         } catch (VCardException e) {
    339                             Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
    340                             showFailureNotification(R.string.fail_reason_not_supported);
    341                             return;
    342                         } catch (IOException e) {
    343                             Log.e(LOG_TAG, "Unexpected IOException", e);
    344                             showFailureNotification(R.string.fail_reason_io_error);
    345                             return;
    346                         }
    347                         if (mCanceled) {
    348                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
    349                             return;
    350                         }
    351                         requests.add(request);
    352                     }
    353                 }
    354                 if (!requests.isEmpty()) {
    355                     mConnection.sendImportRequest(requests);
    356                 } else {
    357                     Log.w(LOG_TAG, "Empty import requests. Ignore it.");
    358                 }
    359             } catch (OutOfMemoryError e) {
    360                 Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
    361                 System.gc();
    362                 runOnUiThread(new DialogDisplayer(
    363                         getString(R.string.fail_reason_low_memory_during_import)));
    364             } catch (IOException e) {
    365                 Log.e(LOG_TAG, "IOException during caching vCard", e);
    366                 runOnUiThread(new DialogDisplayer(
    367                         getString(R.string.fail_reason_io_error)));
    368             } finally {
    369                 Log.i(LOG_TAG, "Finished caching vCard.");
    370                 mWakeLock.release();
    371                 unbindService(mConnection);
    372                 mProgressDialogForCachingVCard.dismiss();
    373                 mProgressDialogForCachingVCard = null;
    374                 finish();
    375             }
    376         }
    377 
    378         /**
    379          * Copy the content of sourceUri to the destination.
    380          */
    381         private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
    382             Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
    383                     sourceUri, filename));
    384             final Context context = ImportVCardActivity.this;
    385             final ContentResolver resolver = context.getContentResolver();
    386             ReadableByteChannel inputChannel = null;
    387             WritableByteChannel outputChannel = null;
    388             Uri destUri = null;
    389             try {
    390                 inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
    391                 destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
    392                 outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
    393                 final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
    394                 while (inputChannel.read(buffer) != -1) {
    395                     if (mCanceled) {
    396                         Log.d(LOG_TAG, "Canceled during caching " + sourceUri);
    397                         return null;
    398                     }
    399                     buffer.flip();
    400                     outputChannel.write(buffer);
    401                     buffer.compact();
    402                 }
    403                 buffer.flip();
    404                 while (buffer.hasRemaining()) {
    405                     outputChannel.write(buffer);
    406                 }
    407             } finally {
    408                 if (inputChannel != null) {
    409                     try {
    410                         inputChannel.close();
    411                     } catch (IOException e) {
    412                         Log.w(LOG_TAG, "Failed to close inputChannel.");
    413                     }
    414                 }
    415                 if (outputChannel != null) {
    416                     try {
    417                         outputChannel.close();
    418                     } catch(IOException e) {
    419                         Log.w(LOG_TAG, "Failed to close outputChannel");
    420                     }
    421                 }
    422             }
    423             return destUri;
    424         }
    425 
    426         /**
    427          * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
    428          * its content.
    429          *
    430          * @arg localDataUri Uri actually used for the import. Should be stored in
    431          * app local storage, as we cannot guarantee other types of Uris can be read
    432          * multiple times. This variable populates {@link ImportRequest#uri}.
    433          * @arg displayName Used for displaying information to the user. This variable populates
    434          * {@link ImportRequest#displayName}.
    435          */
    436         private ImportRequest constructImportRequest(final byte[] data,
    437                 final Uri localDataUri, final String displayName)
    438                 throws IOException, VCardException {
    439             final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
    440             VCardEntryCounter counter = null;
    441             VCardSourceDetector detector = null;
    442             int vcardVersion = VCARD_VERSION_V21;
    443             try {
    444                 boolean shouldUseV30 = false;
    445                 InputStream is;
    446                 if (data != null) {
    447                     is = new ByteArrayInputStream(data);
    448                 } else {
    449                     is = resolver.openInputStream(localDataUri);
    450                 }
    451                 mVCardParser = new VCardParser_V21();
    452                 try {
    453                     counter = new VCardEntryCounter();
    454                     detector = new VCardSourceDetector();
    455                     mVCardParser.addInterpreter(counter);
    456                     mVCardParser.addInterpreter(detector);
    457                     mVCardParser.parse(is);
    458                 } catch (VCardVersionException e1) {
    459                     try {
    460                         is.close();
    461                     } catch (IOException e) {
    462                     }
    463 
    464                     shouldUseV30 = true;
    465                     if (data != null) {
    466                         is = new ByteArrayInputStream(data);
    467                     } else {
    468                         is = resolver.openInputStream(localDataUri);
    469                     }
    470                     mVCardParser = new VCardParser_V30();
    471                     try {
    472                         counter = new VCardEntryCounter();
    473                         detector = new VCardSourceDetector();
    474                         mVCardParser.addInterpreter(counter);
    475                         mVCardParser.addInterpreter(detector);
    476                         mVCardParser.parse(is);
    477                     } catch (VCardVersionException e2) {
    478                         throw new VCardException("vCard with unspported version.");
    479                     }
    480                 } finally {
    481                     if (is != null) {
    482                         try {
    483                             is.close();
    484                         } catch (IOException e) {
    485                         }
    486                     }
    487                 }
    488 
    489                 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
    490             } catch (VCardNestedException e) {
    491                 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
    492                 // Go through without throwing the Exception, as we may be able to detect the
    493                 // version before it
    494             }
    495             return new ImportRequest(mAccount,
    496                     data, localDataUri, displayName,
    497                     detector.getEstimatedType(),
    498                     detector.getEstimatedCharset(),
    499                     vcardVersion, counter.getCount());
    500         }
    501 
    502         public Uri[] getSourceUris() {
    503             return mSourceUris;
    504         }
    505 
    506         public void cancel() {
    507             mCanceled = true;
    508             if (mVCardParser != null) {
    509                 mVCardParser.cancel();
    510             }
    511         }
    512 
    513         @Override
    514         public void onCancel(DialogInterface dialog) {
    515             Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
    516             cancel();
    517         }
    518     }
    519 
    520     private class ImportTypeSelectedListener implements
    521             DialogInterface.OnClickListener {
    522         public static final int IMPORT_ONE = 0;
    523         public static final int IMPORT_MULTIPLE = 1;
    524         public static final int IMPORT_ALL = 2;
    525         public static final int IMPORT_TYPE_SIZE = 3;
    526 
    527         private int mCurrentIndex;
    528 
    529         public void onClick(DialogInterface dialog, int which) {
    530             if (which == DialogInterface.BUTTON_POSITIVE) {
    531                 switch (mCurrentIndex) {
    532                 case IMPORT_ALL:
    533                     importVCardFromSDCard(mAllVCardFileList);
    534                     break;
    535                 case IMPORT_MULTIPLE:
    536                     showDialog(R.id.dialog_select_multiple_vcard);
    537                     break;
    538                 default:
    539                     showDialog(R.id.dialog_select_one_vcard);
    540                     break;
    541                 }
    542             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
    543                 finish();
    544             } else {
    545                 mCurrentIndex = which;
    546             }
    547         }
    548     }
    549 
    550     private class VCardSelectedListener implements
    551             DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
    552         private int mCurrentIndex;
    553         private Set<Integer> mSelectedIndexSet;
    554 
    555         public VCardSelectedListener(boolean multipleSelect) {
    556             mCurrentIndex = 0;
    557             if (multipleSelect) {
    558                 mSelectedIndexSet = new HashSet<Integer>();
    559             }
    560         }
    561 
    562         public void onClick(DialogInterface dialog, int which) {
    563             if (which == DialogInterface.BUTTON_POSITIVE) {
    564                 if (mSelectedIndexSet != null) {
    565                     List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
    566                     final int size = mAllVCardFileList.size();
    567                     // We'd like to sort the files by its index, so we do not use Set iterator.
    568                     for (int i = 0; i < size; i++) {
    569                         if (mSelectedIndexSet.contains(i)) {
    570                             selectedVCardFileList.add(mAllVCardFileList.get(i));
    571                         }
    572                     }
    573                     importVCardFromSDCard(selectedVCardFileList);
    574                 } else {
    575                     importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex));
    576                 }
    577             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
    578                 finish();
    579             } else {
    580                 // Some file is selected.
    581                 mCurrentIndex = which;
    582                 if (mSelectedIndexSet != null) {
    583                     if (mSelectedIndexSet.contains(which)) {
    584                         mSelectedIndexSet.remove(which);
    585                     } else {
    586                         mSelectedIndexSet.add(which);
    587                     }
    588                 }
    589             }
    590         }
    591 
    592         public void onClick(DialogInterface dialog, int which, boolean isChecked) {
    593             if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
    594                 Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
    595                         mAllVCardFileList.get(which).getCanonicalPath()));
    596             } else {
    597                 onClick(dialog, which);
    598             }
    599         }
    600     }
    601 
    602     /**
    603      * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
    604      * a vCard file is shown. After the choice, VCardReadThread starts running.
    605      */
    606     private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
    607         private boolean mCanceled;
    608         private boolean mGotIOException;
    609         private File mRootDirectory;
    610 
    611         // To avoid recursive link.
    612         private Set<String> mCheckedPaths;
    613         private PowerManager.WakeLock mWakeLock;
    614 
    615         private class CanceledException extends Exception {
    616         }
    617 
    618         public VCardScanThread(File sdcardDirectory) {
    619             mCanceled = false;
    620             mGotIOException = false;
    621             mRootDirectory = sdcardDirectory;
    622             mCheckedPaths = new HashSet<String>();
    623             PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
    624                     Context.POWER_SERVICE);
    625             mWakeLock = powerManager.newWakeLock(
    626                     PowerManager.SCREEN_DIM_WAKE_LOCK |
    627                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
    628         }
    629 
    630         @Override
    631         public void run() {
    632             mAllVCardFileList = new Vector<VCardFile>();
    633             try {
    634                 mWakeLock.acquire();
    635                 getVCardFileRecursively(mRootDirectory);
    636             } catch (CanceledException e) {
    637                 mCanceled = true;
    638             } catch (IOException e) {
    639                 mGotIOException = true;
    640             } finally {
    641                 mWakeLock.release();
    642             }
    643 
    644             if (mCanceled) {
    645                 mAllVCardFileList = null;
    646             }
    647 
    648             mProgressDialogForScanVCard.dismiss();
    649             mProgressDialogForScanVCard = null;
    650 
    651             if (mGotIOException) {
    652                 runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception));
    653             } else if (mCanceled) {
    654                 finish();
    655             } else {
    656                 int size = mAllVCardFileList.size();
    657                 final Context context = ImportVCardActivity.this;
    658                 if (size == 0) {
    659                     runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
    660                 } else {
    661                     startVCardSelectAndImport();
    662                 }
    663             }
    664         }
    665 
    666         private void getVCardFileRecursively(File directory)
    667                 throws CanceledException, IOException {
    668             if (mCanceled) {
    669                 throw new CanceledException();
    670             }
    671 
    672             // e.g. secured directory may return null toward listFiles().
    673             final File[] files = directory.listFiles();
    674             if (files == null) {
    675                 final String currentDirectoryPath = directory.getCanonicalPath();
    676                 final String secureDirectoryPath =
    677                         mRootDirectory.getCanonicalPath().concat(SECURE_DIRECTORY_NAME);
    678                 if (!TextUtils.equals(currentDirectoryPath, secureDirectoryPath)) {
    679                     Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
    680                 }
    681                 return;
    682             }
    683             for (File file : directory.listFiles()) {
    684                 if (mCanceled) {
    685                     throw new CanceledException();
    686                 }
    687                 String canonicalPath = file.getCanonicalPath();
    688                 if (mCheckedPaths.contains(canonicalPath)) {
    689                     continue;
    690                 }
    691 
    692                 mCheckedPaths.add(canonicalPath);
    693 
    694                 if (file.isDirectory()) {
    695                     getVCardFileRecursively(file);
    696                 } else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
    697                         file.canRead()){
    698                     String fileName = file.getName();
    699                     VCardFile vcardFile = new VCardFile(
    700                             fileName, canonicalPath, file.lastModified());
    701                     mAllVCardFileList.add(vcardFile);
    702                 }
    703             }
    704         }
    705 
    706         public void onCancel(DialogInterface dialog) {
    707             mCanceled = true;
    708         }
    709 
    710         public void onClick(DialogInterface dialog, int which) {
    711             if (which == DialogInterface.BUTTON_NEGATIVE) {
    712                 mCanceled = true;
    713             }
    714         }
    715     }
    716 
    717     private void startVCardSelectAndImport() {
    718         int size = mAllVCardFileList.size();
    719         if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) ||
    720                 size == 1) {
    721             importVCardFromSDCard(mAllVCardFileList);
    722         } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
    723             runOnUiThread(new DialogDisplayer(R.id.dialog_select_import_type));
    724         } else {
    725             runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
    726         }
    727     }
    728 
    729     private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
    730         final int size = selectedVCardFileList.size();
    731         String[] uriStrings = new String[size];
    732         int i = 0;
    733         for (VCardFile vcardFile : selectedVCardFileList) {
    734             uriStrings[i] = "file://" + vcardFile.getCanonicalPath();
    735             i++;
    736         }
    737         importVCard(uriStrings);
    738     }
    739 
    740     private void importVCardFromSDCard(final VCardFile vcardFile) {
    741         importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())});
    742     }
    743 
    744     private void importVCard(final Uri uri) {
    745         importVCard(new Uri[] {uri});
    746     }
    747 
    748     private void importVCard(final String[] uriStrings) {
    749         final int length = uriStrings.length;
    750         final Uri[] uris = new Uri[length];
    751         for (int i = 0; i < length; i++) {
    752             uris[i] = Uri.parse(uriStrings[i]);
    753         }
    754         importVCard(uris);
    755     }
    756 
    757     private void importVCard(final Uri[] uris) {
    758         runOnUiThread(new Runnable() {
    759             @Override
    760             public void run() {
    761                 if (!isFinishing()) {
    762                     mVCardCacheThread = new VCardCacheThread(uris);
    763                     mListener = new NotificationImportExportListener(ImportVCardActivity.this);
    764                     showDialog(R.id.dialog_cache_vcard);
    765                 }
    766             }
    767         });
    768     }
    769 
    770     private Dialog getSelectImportTypeDialog() {
    771         final DialogInterface.OnClickListener listener = new ImportTypeSelectedListener();
    772         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
    773                 .setTitle(R.string.select_vcard_title)
    774                 .setPositiveButton(android.R.string.ok, listener)
    775                 .setOnCancelListener(mCancelListener)
    776                 .setNegativeButton(android.R.string.cancel, mCancelListener);
    777 
    778         final String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
    779         items[ImportTypeSelectedListener.IMPORT_ONE] =
    780                 getString(R.string.import_one_vcard_string);
    781         items[ImportTypeSelectedListener.IMPORT_MULTIPLE] =
    782                 getString(R.string.import_multiple_vcard_string);
    783         items[ImportTypeSelectedListener.IMPORT_ALL] =
    784                 getString(R.string.import_all_vcard_string);
    785         builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener);
    786         return builder.create();
    787     }
    788 
    789     private Dialog getVCardFileSelectDialog(boolean multipleSelect) {
    790         final int size = mAllVCardFileList.size();
    791         final VCardSelectedListener listener = new VCardSelectedListener(multipleSelect);
    792         final AlertDialog.Builder builder =
    793                 new AlertDialog.Builder(this)
    794                         .setTitle(R.string.select_vcard_title)
    795                         .setPositiveButton(android.R.string.ok, listener)
    796                         .setOnCancelListener(mCancelListener)
    797                         .setNegativeButton(android.R.string.cancel, mCancelListener);
    798 
    799         CharSequence[] items = new CharSequence[size];
    800         DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    801         for (int i = 0; i < size; i++) {
    802             VCardFile vcardFile = mAllVCardFileList.get(i);
    803             SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
    804             stringBuilder.append(vcardFile.getName());
    805             stringBuilder.append('\n');
    806             int indexToBeSpanned = stringBuilder.length();
    807             // Smaller date text looks better, since each file name becomes easier to read.
    808             // The value set to RelativeSizeSpan is arbitrary. You can change it to any other
    809             // value (but the value bigger than 1.0f would not make nice appearance :)
    810             stringBuilder.append(
    811                         "(" + dateFormat.format(new Date(vcardFile.getLastModified())) + ")");
    812             stringBuilder.setSpan(
    813                     new RelativeSizeSpan(0.7f), indexToBeSpanned, stringBuilder.length(),
    814                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    815             items[i] = stringBuilder;
    816         }
    817         if (multipleSelect) {
    818             builder.setMultiChoiceItems(items, (boolean[])null, listener);
    819         } else {
    820             builder.setSingleChoiceItems(items, 0, listener);
    821         }
    822         return builder.create();
    823     }
    824 
    825     @Override
    826     protected void onCreate(Bundle bundle) {
    827         super.onCreate(bundle);
    828 
    829         String accountName = null;
    830         String accountType = null;
    831         String dataSet = null;
    832         final Intent intent = getIntent();
    833         if (intent != null) {
    834             accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
    835             accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
    836             dataSet = intent.getStringExtra(SelectAccountActivity.DATA_SET);
    837         } else {
    838             Log.e(LOG_TAG, "intent does not exist");
    839         }
    840 
    841         if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
    842             mAccount = new AccountWithDataSet(accountName, accountType, dataSet);
    843         } else {
    844             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
    845             final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
    846             if (accountList.size() == 0) {
    847                 mAccount = null;
    848             } else if (accountList.size() == 1) {
    849                 mAccount = accountList.get(0);
    850             } else {
    851                 startActivityForResult(new Intent(this, SelectAccountActivity.class),
    852                         SELECT_ACCOUNT);
    853                 return;
    854             }
    855         }
    856 
    857         startImport();
    858     }
    859 
    860     @Override
    861     public void onActivityResult(int requestCode, int resultCode, Intent intent) {
    862         if (requestCode == SELECT_ACCOUNT) {
    863             if (resultCode == Activity.RESULT_OK) {
    864                 mAccount = new AccountWithDataSet(
    865                         intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
    866                         intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
    867                         intent.getStringExtra(SelectAccountActivity.DATA_SET));
    868                 startImport();
    869             } else {
    870                 if (resultCode != Activity.RESULT_CANCELED) {
    871                     Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
    872                 }
    873                 finish();
    874             }
    875         }
    876     }
    877 
    878     private void startImport() {
    879         Intent intent = getIntent();
    880         // Handle inbound files
    881         Uri uri = intent.getData();
    882         if (uri != null) {
    883             Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
    884             importVCard(uri);
    885         } else {
    886             Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
    887             doScanExternalStorageAndImportVCard();
    888         }
    889     }
    890 
    891     @Override
    892     protected Dialog onCreateDialog(int resId, Bundle bundle) {
    893         switch (resId) {
    894             case R.string.import_from_sdcard: {
    895                 if (mAccountSelectionListener == null) {
    896                     throw new NullPointerException(
    897                             "mAccountSelectionListener must not be null.");
    898                 }
    899                 return AccountSelectionUtil.getSelectAccountDialog(this, resId,
    900                         mAccountSelectionListener, mCancelListener);
    901             }
    902             case R.id.dialog_searching_vcard: {
    903                 if (mProgressDialogForScanVCard == null) {
    904                     String message = getString(R.string.searching_vcard_message);
    905                     mProgressDialogForScanVCard =
    906                         ProgressDialog.show(this, "", message, true, false);
    907                     mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread);
    908                     mVCardScanThread.start();
    909                 }
    910                 return mProgressDialogForScanVCard;
    911             }
    912             case R.id.dialog_sdcard_not_found: {
    913                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
    914                     .setIconAttribute(android.R.attr.alertDialogIcon)
    915                     .setMessage(R.string.no_sdcard_message)
    916                     .setOnCancelListener(mCancelListener)
    917                     .setPositiveButton(android.R.string.ok, mCancelListener);
    918                 return builder.create();
    919             }
    920             case R.id.dialog_vcard_not_found: {
    921                 final String message = getString(R.string.import_failure_no_vcard_file);
    922                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
    923                         .setMessage(message)
    924                         .setOnCancelListener(mCancelListener)
    925                         .setPositiveButton(android.R.string.ok, mCancelListener);
    926                 return builder.create();
    927             }
    928             case R.id.dialog_select_import_type: {
    929                 return getSelectImportTypeDialog();
    930             }
    931             case R.id.dialog_select_multiple_vcard: {
    932                 return getVCardFileSelectDialog(true);
    933             }
    934             case R.id.dialog_select_one_vcard: {
    935                 return getVCardFileSelectDialog(false);
    936             }
    937             case R.id.dialog_cache_vcard: {
    938                 if (mProgressDialogForCachingVCard == null) {
    939                     final String title = getString(R.string.caching_vcard_title);
    940                     final String message = getString(R.string.caching_vcard_message);
    941                     mProgressDialogForCachingVCard = new ProgressDialog(this);
    942                     mProgressDialogForCachingVCard.setTitle(title);
    943                     mProgressDialogForCachingVCard.setMessage(message);
    944                     mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
    945                     mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread);
    946                     startVCardService();
    947                 }
    948                 return mProgressDialogForCachingVCard;
    949             }
    950             case R.id.dialog_io_exception: {
    951                 String message = (getString(R.string.scanning_sdcard_failed_message,
    952                         getString(R.string.fail_reason_io_error)));
    953                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
    954                     .setIconAttribute(android.R.attr.alertDialogIcon)
    955                     .setMessage(message)
    956                     .setOnCancelListener(mCancelListener)
    957                     .setPositiveButton(android.R.string.ok, mCancelListener);
    958                 return builder.create();
    959             }
    960             case R.id.dialog_error_with_message: {
    961                 String message = mErrorMessage;
    962                 if (TextUtils.isEmpty(message)) {
    963                     Log.e(LOG_TAG, "Error message is null while it must not.");
    964                     message = getString(R.string.fail_reason_unknown);
    965                 }
    966                 final AlertDialog.Builder builder = new AlertDialog.Builder(this)
    967                     .setTitle(getString(R.string.reading_vcard_failed_title))
    968                     .setIconAttribute(android.R.attr.alertDialogIcon)
    969                     .setMessage(message)
    970                     .setOnCancelListener(mCancelListener)
    971                     .setPositiveButton(android.R.string.ok, mCancelListener);
    972                 return builder.create();
    973             }
    974         }
    975 
    976         return super.onCreateDialog(resId, bundle);
    977     }
    978 
    979     /* package */ void startVCardService() {
    980         mConnection = new ImportRequestConnection();
    981 
    982         Log.i(LOG_TAG, "Bind to VCardService.");
    983         // We don't want the service finishes itself just after this connection.
    984         Intent intent = new Intent(this, VCardService.class);
    985         startService(intent);
    986         bindService(new Intent(this, VCardService.class),
    987                 mConnection, Context.BIND_AUTO_CREATE);
    988     }
    989 
    990     @Override
    991     protected void onRestoreInstanceState(Bundle savedInstanceState) {
    992         super.onRestoreInstanceState(savedInstanceState);
    993         if (mProgressDialogForCachingVCard != null) {
    994             Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again.");
    995             showDialog(R.id.dialog_cache_vcard);
    996         }
    997     }
    998 
    999     /**
   1000      * Scans vCard in external storage (typically SDCard) and tries to import it.
   1001      * - When there's no SDCard available, an error dialog is shown.
   1002      * - When multiple vCard files are available, asks a user to select one.
   1003      */
   1004     private void doScanExternalStorageAndImportVCard() {
   1005         // TODO: should use getExternalStorageState().
   1006         final File file = Environment.getExternalStorageDirectory();
   1007         if (!file.exists() || !file.isDirectory() || !file.canRead()) {
   1008             showDialog(R.id.dialog_sdcard_not_found);
   1009         } else {
   1010             mVCardScanThread = new VCardScanThread(file);
   1011             showDialog(R.id.dialog_searching_vcard);
   1012         }
   1013     }
   1014 
   1015     /* package */ void showFailureNotification(int reasonId) {
   1016         final NotificationManager notificationManager =
   1017                 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
   1018         final Notification notification =
   1019                 NotificationImportExportListener.constructImportFailureNotification(
   1020                         ImportVCardActivity.this,
   1021                         getString(reasonId));
   1022         notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
   1023                 FAILURE_NOTIFICATION_ID, notification);
   1024         mHandler.post(new Runnable() {
   1025             @Override
   1026             public void run() {
   1027                 Toast.makeText(ImportVCardActivity.this,
   1028                         getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
   1029             }
   1030         });
   1031     }
   1032 }
   1033