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.vcard;
     18 
     19 import com.android.contacts.ContactsActivity;
     20 import com.android.contacts.R;
     21 import com.android.contacts.model.AccountTypeManager;
     22 import com.android.contacts.model.AccountWithDataSet;
     23 import com.android.contacts.util.AccountSelectionUtil;
     24 import com.android.vcard.VCardEntryCounter;
     25 import com.android.vcard.VCardParser;
     26 import com.android.vcard.VCardParser_V21;
     27 import com.android.vcard.VCardParser_V30;
     28 import com.android.vcard.VCardSourceDetector;
     29 import com.android.vcard.exception.VCardException;
     30 import com.android.vcard.exception.VCardNestedException;
     31 import com.android.vcard.exception.VCardVersionException;
     32 
     33 import android.app.Activity;
     34 import android.app.AlertDialog;
     35 import android.app.Dialog;
     36 import android.app.Notification;
     37 import android.app.NotificationManager;
     38 import android.app.ProgressDialog;
     39 import android.content.ComponentName;
     40 import android.content.ContentResolver;
     41 import android.content.Context;
     42 import android.content.DialogInterface;
     43 import android.content.DialogInterface.OnCancelListener;
     44 import android.content.DialogInterface.OnClickListener;
     45 import android.content.Intent;
     46 import android.content.ServiceConnection;
     47 import android.content.res.Configuration;
     48 import android.database.Cursor;
     49 import android.net.Uri;
     50 import android.os.Bundle;
     51 import android.os.Environment;
     52 import android.os.Handler;
     53 import android.os.IBinder;
     54 import android.os.PowerManager;
     55 import android.provider.OpenableColumns;
     56 import android.text.SpannableStringBuilder;
     57 import android.text.Spanned;
     58 import android.text.TextUtils;
     59 import android.text.style.RelativeSizeSpan;
     60 import android.util.Log;
     61 import android.widget.Toast;
     62 
     63 import java.io.ByteArrayInputStream;
     64 import java.io.File;
     65 import java.io.IOException;
     66 import java.io.InputStream;
     67 import java.nio.ByteBuffer;
     68 import java.nio.channels.Channels;
     69 import java.nio.channels.ReadableByteChannel;
     70 import java.nio.channels.WritableByteChannel;
     71 import java.text.DateFormat;
     72 import java.text.SimpleDateFormat;
     73 import java.util.ArrayList;
     74 import java.util.Arrays;
     75 import java.util.Date;
     76 import java.util.HashSet;
     77 import java.util.List;
     78 import java.util.Set;
     79 import java.util.Vector;
     80 
     81 /**
     82  * The class letting users to import vCard. This includes the UI part for letting them select
     83  * an Account and posssibly a file if there's no Uri is given from its caller Activity.
     84  *
     85  * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
     86  * finished (with the method {@link Activity#finish()}) after the import and never reuse
     87  * any Dialog in the instance. So this code is careless about the management around managed
     88  * dialogs stuffs (like how onCreateDialog() is used).
     89  */
     90 public class ImportVCardActivity extends ContactsActivity {
     91     private static final String LOG_TAG = "VCardImport";
     92 
     93     private static final int SELECT_ACCOUNT = 0;
     94 
     95     /* package */ static final String VCARD_URI_ARRAY = "vcard_uri";
     96     /* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type";
     97     /* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset";
     98     /* package */ static final String VCARD_VERSION_ARRAY = "vcard_version";
     99     /* package */ static final String ENTRY_COUNT_ARRAY = "entry_count";
    100 
    101     /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
    102     /* package */ final static int VCARD_VERSION_V21 = 1;
    103     /* package */ final static int VCARD_VERSION_V30 = 2;
    104 
    105     private static final String SECURE_DIRECTORY_NAME = ".android_secure";
    106 
    107     /**
    108      * Notification id used when error happened before sending an import request to VCardServer.
    109      */
    110     private static final int FAILURE_NOTIFICATION_ID = 1;
    111 
    112     final static String CACHED_URIS = "cached_uris";
    113 
    114     private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
    115 
    116     private AccountWithDataSet mAccount;
    117 
    118     private ProgressDialog mProgressDialogForScanVCard;
    119     private ProgressDialog mProgressDialogForCachingVCard;
    120 
    121     private List<VCardFile> mAllVCardFileList;
    122     private VCardScanThread mVCardScanThread;
    123 
    124     private VCardCacheThread mVCardCacheThread;
    125     private ImportRequestConnection mConnection;
    126     /* package */ VCardImportExportListener mListener;
    127 
    128     private String mErrorMessage;
    129 
    130     private Handler mHandler = new Handler();
    131 
    132     private static class VCardFile {
    133         private final String mName;
    134         private final String mCanonicalPath;
    135         private final long mLastModified;
    136 
    137         public VCardFile(String name, String canonicalPath, long lastModified) {
    138             mName = name;
    139             mCanonicalPath = canonicalPath;
    140             mLastModified = lastModified;
    141         }
    142 
    143         public String getName() {
    144             return mName;
    145         }
    146 
    147         public String getCanonicalPath() {
    148             return mCanonicalPath;
    149         }
    150 
    151         public long getLastModified() {
    152             return mLastModified;
    153         }
    154     }
    155 
    156     // Runs on the UI thread.
    157     private class DialogDisplayer implements Runnable {
    158         private final int mResId;
    159         public DialogDisplayer(int resId) {
    160             mResId = resId;
    161         }
    162         public DialogDisplayer(String errorMessage) {
    163             mResId = R.id.dialog_error_with_message;
    164             mErrorMessage = errorMessage;
    165         }
    166         @Override
    167         public void run() {
    168             if (!isFinishing()) {
    169                 showDialog(mResId);
    170             }
    171         }
    172     }
    173 
    174     private class CancelListener
    175         implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
    176         @Override
    177         public void onClick(DialogInterface dialog, int which) {
    178             finish();
    179         }
    180         @Override
    181         public void onCancel(DialogInterface dialog) {
    182             finish();
    183         }
    184     }
    185 
    186     private CancelListener mCancelListener = new CancelListener();
    187 
    188     private class ImportRequestConnection implements ServiceConnection {
    189         private VCardService mService;
    190 
    191         public void sendImportRequest(final List<ImportRequest> requests) {
    192             Log.i(LOG_TAG, "Send an import request");
    193             mService.handleImportRequest(requests, mListener);
    194         }
    195 
    196         @Override
    197         public void onServiceConnected(ComponentName name, IBinder binder) {
    198             mService = ((VCardService.MyBinder) binder).getService();
    199             Log.i(LOG_TAG,
    200                     String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
    201                             Arrays.toString(mVCardCacheThread.getSourceUris())));
    202             mVCardCacheThread.start();
    203         }
    204 
    205         @Override
    206         public void onServiceDisconnected(ComponentName name) {
    207             Log.i(LOG_TAG, "Disconnected from VCardService");
    208         }
    209     }
    210 
    211     /**
    212      * Caches given vCard files into a local directory, and sends actual import request to
    213      * {@link VCardService}.
    214      *
    215      * We need to cache given files into local storage. One of reasons is that some data (as Uri)
    216      * may have special permissions. Callers may allow only this Activity to access that content,
    217      * not what this Activity launched (like {@link VCardService}).
    218      */
    219     private class VCardCacheThread extends Thread
    220             implements DialogInterface.OnCancelListener {
    221         private boolean mCanceled;
    222         private PowerManager.WakeLock mWakeLock;
    223         private VCardParser mVCardParser;
    224         private final Uri[] mSourceUris;  // Given from a caller.
    225         private final byte[] mSource;
    226         private final String mDisplayName;
    227 
    228         public VCardCacheThread(final Uri[] sourceUris) {
    229             mSourceUris = sourceUris;
    230             mSource = null;
    231             final Context context = ImportVCardActivity.this;
    232             final PowerManager powerManager =
    233                     (PowerManager)context.getSystemService(Context.POWER_SERVICE);
    234             mWakeLock = powerManager.newWakeLock(
    235                     PowerManager.SCREEN_DIM_WAKE_LOCK |
    236                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
    237             mDisplayName = null;
    238         }
    239 
    240         @Override
    241         public void finalize() {
    242             if (mWakeLock != null && mWakeLock.isHeld()) {
    243                 Log.w(LOG_TAG, "WakeLock is being held.");
    244                 mWakeLock.release();
    245             }
    246         }
    247 
    248         @Override
    249         public void run() {
    250             Log.i(LOG_TAG, "vCard cache thread starts running.");
    251             if (mConnection == null) {
    252                 throw new NullPointerException("vCard cache thread must be launched "
    253                         + "after a service connection is established");
    254             }
    255 
    256             mWakeLock.acquire();
    257             try {
    258                 if (mCanceled == true) {
    259                     Log.i(LOG_TAG, "vCard cache operation is canceled.");
    260                     return;
    261                 }
    262 
    263                 final Context context = ImportVCardActivity.this;
    264                 // Uris given from caller applications may not be opened twice: consider when
    265                 // it is not from local storage (e.g. "file:///...") but from some special
    266                 // provider (e.g. "content://...").
    267                 // Thus we have to once copy the content of Uri into local storage, and read
    268                 // it after it.
    269                 //
    270                 // We may be able to read content of each vCard file during copying them
    271                 // to local storage, but currently vCard code does not allow us to do so.
    272                 int cache_index = 0;
    273                 ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
    274                 if (mSource != null) {
    275                     try {
    276                         requests.add(constructImportRequest(mSource, null, mDisplayName));
    277                     } catch (VCardException e) {
    278                         Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
    279                         showFailureNotification(R.string.fail_reason_not_supported);
    280                         return;
    281                     }
    282                 } else {
    283                     final ContentResolver resolver =
    284                             ImportVCardActivity.this.getContentResolver();
    285                     for (Uri sourceUri : mSourceUris) {
    286                         String filename = null;
    287                         // Note: caches are removed by VCardService.
    288                         while (true) {
    289                             filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
    290                             final File file = context.getFileStreamPath(filename);
    291                             if (!file.exists()) {
    292                                 break;
    293                             } else {
    294                                 if (cache_index == Integer.MAX_VALUE) {
    295                                     throw new RuntimeException("Exceeded cache limit");
    296                                 }
    297                                 cache_index++;
    298                             }
    299                         }
    300                         final Uri localDataUri = copyTo(sourceUri, filename);
    301                         if (mCanceled) {
    302                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
    303                             break;
    304                         }
    305                         if (localDataUri == null) {
    306                             Log.w(LOG_TAG, "destUri is null");
    307                             break;
    308                         }
    309 
    310                         String displayName = null;
    311                         Cursor cursor = null;
    312                         // Try to get a display name from the given Uri. If it fails, we just
    313                         // pick up the last part of the Uri.
    314                         try {
    315                             cursor = resolver.query(sourceUri,
    316                                     new String[] { OpenableColumns.DISPLAY_NAME },
    317                                     null, null, null);
    318                             if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
    319                                 if (cursor.getCount() > 1) {
    320                                     Log.w(LOG_TAG, "Unexpected multiple rows: "
    321                                             + cursor.getCount());
    322                                 }
    323                                 int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
    324                                 if (index >= 0) {
    325                                     displayName = cursor.getString(index);
    326                                 }
    327                             }
    328                         } finally {
    329                             if (cursor != null) {
    330                                 cursor.close();
    331                             }
    332                         }
    333                         if (TextUtils.isEmpty(displayName)){
    334                             displayName = sourceUri.getLastPathSegment();
    335                         }
    336 
    337                         final ImportRequest request;
    338                         try {
    339                             request = constructImportRequest(null, localDataUri, displayName);
    340                         } catch (VCardException e) {
    341                             Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
    342                             showFailureNotification(R.string.fail_reason_not_supported);
    343                             return;
    344                         } catch (IOException e) {
    345                             Log.e(LOG_TAG, "Unexpected IOException", e);
    346                             showFailureNotification(R.string.fail_reason_io_error);
    347                             return;
    348                         }
    349                         if (mCanceled) {
    350                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
    351                             return;
    352                         }
    353                         requests.add(request);
    354                     }
    355                 }
    356                 if (!requests.isEmpty()) {
    357                     mConnection.sendImportRequest(requests);
    358                 } else {
    359                     Log.w(LOG_TAG, "Empty import requests. Ignore it.");
    360                 }
    361             } catch (OutOfMemoryError e) {
    362                 Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
    363                 System.gc();
    364                 runOnUiThread(new DialogDisplayer(
    365                         getString(R.string.fail_reason_low_memory_during_import)));
    366             } catch (IOException e) {
    367                 Log.e(LOG_TAG, "IOException during caching vCard", e);
    368                 runOnUiThread(new DialogDisplayer(
    369                         getString(R.string.fail_reason_io_error)));
    370             } finally {
    371                 Log.i(LOG_TAG, "Finished caching vCard.");
    372                 mWakeLock.release();
    373                 unbindService(mConnection);
    374                 mProgressDialogForCachingVCard.dismiss();
    375                 mProgressDialogForCachingVCard = null;
    376                 finish();
    377             }
    378         }
    379 
    380         /**
    381          * Copy the content of sourceUri to the destination.
    382          */
    383         private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
    384             Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
    385                     sourceUri, filename));
    386             final Context context = ImportVCardActivity.this;
    387             final ContentResolver resolver = context.getContentResolver();
    388             ReadableByteChannel inputChannel = null;
    389             WritableByteChannel outputChannel = null;
    390             Uri destUri = null;
    391             try {
    392                 inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
    393                 destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
    394                 outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
    395                 final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
    396                 while (inputChannel.read(buffer) != -1) {
    397                     if (mCanceled) {
    398                         Log.d(LOG_TAG, "Canceled during caching " + sourceUri);
    399                         return null;
    400                     }
    401                     buffer.flip();
    402                     outputChannel.write(buffer);
    403                     buffer.compact();
    404                 }
    405                 buffer.flip();
    406                 while (buffer.hasRemaining()) {
    407                     outputChannel.write(buffer);
    408                 }
    409             } finally {
    410                 if (inputChannel != null) {
    411                     try {
    412                         inputChannel.close();
    413                     } catch (IOException e) {
    414                         Log.w(LOG_TAG, "Failed to close inputChannel.");
    415                     }
    416                 }
    417                 if (outputChannel != null) {
    418                     try {
    419                         outputChannel.close();
    420                     } catch(IOException e) {
    421                         Log.w(LOG_TAG, "Failed to close outputChannel");
    422                     }
    423                 }
    424             }
    425             return destUri;
    426         }
    427 
    428         /**
    429          * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
    430          * its content.
    431          *
    432          * @arg localDataUri Uri actually used for the import. Should be stored in
    433          * app local storage, as we cannot guarantee other types of Uris can be read
    434          * multiple times. This variable populates {@link ImportRequest#uri}.
    435          * @arg displayName Used for displaying information to the user. This variable populates
    436          * {@link ImportRequest#displayName}.
    437          */
    438         private ImportRequest constructImportRequest(final byte[] data,
    439                 final Uri localDataUri, final String displayName)
    440                 throws IOException, VCardException {
    441             final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
    442             VCardEntryCounter counter = null;
    443             VCardSourceDetector detector = null;
    444             int vcardVersion = VCARD_VERSION_V21;
    445             try {
    446                 boolean shouldUseV30 = false;
    447                 InputStream is;
    448                 if (data != null) {
    449                     is = new ByteArrayInputStream(data);
    450                 } else {
    451                     is = resolver.openInputStream(localDataUri);
    452                 }
    453                 mVCardParser = new VCardParser_V21();
    454                 try {
    455                     counter = new VCardEntryCounter();
    456                     detector = new VCardSourceDetector();
    457                     mVCardParser.addInterpreter(counter);
    458                     mVCardParser.addInterpreter(detector);
    459                     mVCardParser.parse(is);
    460                 } catch (VCardVersionException e1) {
    461                     try {
    462                         is.close();
    463                     } catch (IOException e) {
    464                     }
    465 
    466                     shouldUseV30 = true;
    467                     if (data != null) {
    468                         is = new ByteArrayInputStream(data);
    469                     } else {
    470                         is = resolver.openInputStream(localDataUri);
    471                     }
    472                     mVCardParser = new VCardParser_V30();
    473                     try {
    474                         counter = new VCardEntryCounter();
    475                         detector = new VCardSourceDetector();
    476                         mVCardParser.addInterpreter(counter);
    477                         mVCardParser.addInterpreter(detector);
    478                         mVCardParser.parse(is);
    479                     } catch (VCardVersionException e2) {
    480                         throw new VCardException("vCard with unspported version.");
    481                     }
    482                 } finally {
    483                     if (is != null) {
    484                         try {
    485                             is.close();
    486                         } catch (IOException e) {
    487                         }
    488                     }
    489                 }
    490 
    491                 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
    492             } catch (VCardNestedException e) {
    493                 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
    494                 // Go through without throwing the Exception, as we may be able to detect the
    495                 // version before it
    496             }
    497             return new ImportRequest(mAccount,
    498                     data, localDataUri, displayName,
    499                     detector.getEstimatedType(),
    500                     detector.getEstimatedCharset(),
    501                     vcardVersion, counter.getCount());
    502         }
    503 
    504         public Uri[] getSourceUris() {
    505             return mSourceUris;
    506         }
    507 
    508         public void cancel() {
    509             mCanceled = true;
    510             if (mVCardParser != null) {
    511                 mVCardParser.cancel();
    512             }
    513         }
    514 
    515         @Override
    516         public void onCancel(DialogInterface dialog) {
    517             Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
    518             cancel();
    519         }
    520     }
    521 
    522     private class ImportTypeSelectedListener implements
    523             DialogInterface.OnClickListener {
    524         public static final int IMPORT_ONE = 0;
    525         public static final int IMPORT_MULTIPLE = 1;
    526         public static final int IMPORT_ALL = 2;
    527         public static final int IMPORT_TYPE_SIZE = 3;
    528 
    529         private int mCurrentIndex;
    530 
    531         public void onClick(DialogInterface dialog, int which) {
    532             if (which == DialogInterface.BUTTON_POSITIVE) {
    533                 switch (mCurrentIndex) {
    534                 case IMPORT_ALL:
    535                     importVCardFromSDCard(mAllVCardFileList);
    536                     break;
    537                 case IMPORT_MULTIPLE:
    538                     showDialog(R.id.dialog_select_multiple_vcard);
    539                     break;
    540                 default:
    541                     showDialog(R.id.dialog_select_one_vcard);
    542                     break;
    543                 }
    544             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
    545                 finish();
    546             } else {
    547                 mCurrentIndex = which;
    548             }
    549         }
    550     }
    551 
    552     private class VCardSelectedListener implements
    553             DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
    554         private int mCurrentIndex;
    555         private Set<Integer> mSelectedIndexSet;
    556 
    557         public VCardSelectedListener(boolean multipleSelect) {
    558             mCurrentIndex = 0;
    559             if (multipleSelect) {
    560                 mSelectedIndexSet = new HashSet<Integer>();
    561             }
    562         }
    563 
    564         public void onClick(DialogInterface dialog, int which) {
    565             if (which == DialogInterface.BUTTON_POSITIVE) {
    566                 if (mSelectedIndexSet != null) {
    567                     List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
    568                     final int size = mAllVCardFileList.size();
    569                     // We'd like to sort the files by its index, so we do not use Set iterator.
    570                     for (int i = 0; i < size; i++) {
    571                         if (mSelectedIndexSet.contains(i)) {
    572                             selectedVCardFileList.add(mAllVCardFileList.get(i));
    573                         }
    574                     }
    575                     importVCardFromSDCard(selectedVCardFileList);
    576                 } else {
    577                     importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex));
    578                 }
    579             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
    580                 finish();
    581             } else {
    582                 // Some file is selected.
    583                 mCurrentIndex = which;
    584                 if (mSelectedIndexSet != null) {
    585                     if (mSelectedIndexSet.contains(which)) {
    586                         mSelectedIndexSet.remove(which);
    587                     } else {
    588                         mSelectedIndexSet.add(which);
    589                     }
    590                 }
    591             }
    592         }
    593 
    594         public void onClick(DialogInterface dialog, int which, boolean isChecked) {
    595             if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
    596                 Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
    597                         mAllVCardFileList.get(which).getCanonicalPath()));
    598             } else {
    599                 onClick(dialog, which);
    600             }
    601         }
    602     }
    603 
    604     /**
    605      * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
    606      * a vCard file is shown. After the choice, VCardReadThread starts running.
    607      */
    608     private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
    609         private boolean mCanceled;
    610         private boolean mGotIOException;
    611         private File mRootDirectory;
    612 
    613         // To avoid recursive link.
    614         private Set<String> mCheckedPaths;
    615         private PowerManager.WakeLock mWakeLock;
    616 
    617         private class CanceledException extends Exception {
    618         }
    619 
    620         public VCardScanThread(File sdcardDirectory) {
    621             mCanceled = false;
    622             mGotIOException = false;
    623             mRootDirectory = sdcardDirectory;
    624             mCheckedPaths = new HashSet<String>();
    625             PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
    626                     Context.POWER_SERVICE);
    627             mWakeLock = powerManager.newWakeLock(
    628                     PowerManager.SCREEN_DIM_WAKE_LOCK |
    629                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
    630         }
    631 
    632         @Override
    633         public void run() {
    634             mAllVCardFileList = new Vector<VCardFile>();
    635             try {
    636                 mWakeLock.acquire();
    637                 getVCardFileRecursively(mRootDirectory);
    638             } catch (CanceledException e) {
    639                 mCanceled = true;
    640             } catch (IOException e) {
    641                 mGotIOException = true;
    642             } finally {
    643                 mWakeLock.release();
    644             }
    645 
    646             if (mCanceled) {
    647                 mAllVCardFileList = null;
    648             }
    649 
    650             mProgressDialogForScanVCard.dismiss();
    651             mProgressDialogForScanVCard = null;
    652 
    653             if (mGotIOException) {
    654                 runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception));
    655             } else if (mCanceled) {
    656                 finish();
    657             } else {
    658                 int size = mAllVCardFileList.size();
    659                 final Context context = ImportVCardActivity.this;
    660                 if (size == 0) {
    661                     runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
    662                 } else {
    663                     startVCardSelectAndImport();
    664                 }
    665             }
    666         }
    667 
    668         private void getVCardFileRecursively(File directory)
    669                 throws CanceledException, IOException {
    670             if (mCanceled) {
    671                 throw new CanceledException();
    672             }
    673 
    674             // e.g. secured directory may return null toward listFiles().
    675             final File[] files = directory.listFiles();
    676             if (files == null) {
    677                 final String currentDirectoryPath = directory.getCanonicalPath();
    678                 final String secureDirectoryPath =
    679                         mRootDirectory.getCanonicalPath().concat(SECURE_DIRECTORY_NAME);
    680                 if (!TextUtils.equals(currentDirectoryPath, secureDirectoryPath)) {
    681                     Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
    682                 }
    683                 return;
    684             }
    685             for (File file : directory.listFiles()) {
    686                 if (mCanceled) {
    687                     throw new CanceledException();
    688                 }
    689                 String canonicalPath = file.getCanonicalPath();
    690                 if (mCheckedPaths.contains(canonicalPath)) {
    691                     continue;
    692                 }
    693 
    694                 mCheckedPaths.add(canonicalPath);
    695 
    696                 if (file.isDirectory()) {
    697                     getVCardFileRecursively(file);
    698                 } else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
    699                         file.canRead()){
    700                     String fileName = file.getName();
    701                     VCardFile vcardFile = new VCardFile(
    702                             fileName, canonicalPath, file.lastModified());
    703                     mAllVCardFileList.add(vcardFile);
    704                 }
    705             }
    706         }
    707 
    708         public void onCancel(DialogInterface dialog) {
    709             mCanceled = true;
    710         }
    711 
    712         public void onClick(DialogInterface dialog, int which) {
    713             if (which == DialogInterface.BUTTON_NEGATIVE) {
    714                 mCanceled = true;
    715             }
    716         }
    717     }
    718 
    719     private void startVCardSelectAndImport() {
    720         int size = mAllVCardFileList.size();
    721         if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) ||
    722                 size == 1) {
    723             importVCardFromSDCard(mAllVCardFileList);
    724         } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
    725             runOnUiThread(new DialogDisplayer(R.id.dialog_select_import_type));
    726         } else {
    727             runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
    728         }
    729     }
    730 
    731     private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
    732         final int size = selectedVCardFileList.size();
    733         String[] uriStrings = new String[size];
    734         int i = 0;
    735         for (VCardFile vcardFile : selectedVCardFileList) {
    736             uriStrings[i] = "file://" + vcardFile.getCanonicalPath();
    737             i++;
    738         }
    739         importVCard(uriStrings);
    740     }
    741 
    742     private void importVCardFromSDCard(final VCardFile vcardFile) {
    743         importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())});
    744     }
    745 
    746     private void importVCard(final Uri uri) {
    747         importVCard(new Uri[] {uri});
    748     }
    749 
    750     private void importVCard(final String[] uriStrings) {
    751         final int length = uriStrings.length;
    752         final Uri[] uris = new Uri[length];
    753         for (int i = 0; i < length; i++) {
    754             uris[i] = Uri.parse(uriStrings[i]);
    755         }
    756         importVCard(uris);
    757     }
    758 
    759     private void importVCard(final Uri[] uris) {
    760         runOnUiThread(new Runnable() {
    761             @Override
    762             public void run() {
    763                 mVCardCacheThread = new VCardCacheThread(uris);
    764                 mListener = new NotificationImportExportListener(ImportVCardActivity.this);
    765                 showDialog(R.id.dialog_cache_vcard);
    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 == 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 != 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     @Override
   1000     public void onConfigurationChanged(Configuration newConfig) {
   1001         super.onConfigurationChanged(newConfig);
   1002         // This Activity should finish itself on orientation change, and give the main screen back
   1003         // to the caller Activity.
   1004         finish();
   1005     }
   1006 
   1007     /**
   1008      * Scans vCard in external storage (typically SDCard) and tries to import it.
   1009      * - When there's no SDCard available, an error dialog is shown.
   1010      * - When multiple vCard files are available, asks a user to select one.
   1011      */
   1012     private void doScanExternalStorageAndImportVCard() {
   1013         // TODO: should use getExternalStorageState().
   1014         final File file = Environment.getExternalStorageDirectory();
   1015         if (!file.exists() || !file.isDirectory() || !file.canRead()) {
   1016             showDialog(R.id.dialog_sdcard_not_found);
   1017         } else {
   1018             mVCardScanThread = new VCardScanThread(file);
   1019             showDialog(R.id.dialog_searching_vcard);
   1020         }
   1021     }
   1022 
   1023     /* package */ void showFailureNotification(int reasonId) {
   1024         final NotificationManager notificationManager =
   1025                 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
   1026         final Notification notification =
   1027                 NotificationImportExportListener.constructImportFailureNotification(
   1028                         ImportVCardActivity.this,
   1029                         getString(reasonId));
   1030         notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
   1031                 FAILURE_NOTIFICATION_ID, notification);
   1032         mHandler.post(new Runnable() {
   1033             @Override
   1034             public void run() {
   1035                 Toast.makeText(ImportVCardActivity.this,
   1036                         getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
   1037             }
   1038         });
   1039     }
   1040 }
   1041