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