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