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