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