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