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.common.vcard;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.Notification;
23import android.app.NotificationManager;
24import android.app.ProgressDialog;
25import android.content.ComponentName;
26import android.content.ContentResolver;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.DialogInterface.OnCancelListener;
30import android.content.DialogInterface.OnClickListener;
31import android.content.Intent;
32import android.content.ServiceConnection;
33import android.database.Cursor;
34import android.net.Uri;
35import android.os.Bundle;
36import android.os.Environment;
37import android.os.Handler;
38import android.os.IBinder;
39import android.os.PowerManager;
40import android.provider.OpenableColumns;
41import android.text.SpannableStringBuilder;
42import android.text.Spanned;
43import android.text.TextUtils;
44import android.text.style.RelativeSizeSpan;
45import android.util.Log;
46import android.widget.Toast;
47
48import com.android.contacts.common.R;
49import com.android.contacts.common.model.AccountTypeManager;
50import com.android.contacts.common.model.account.AccountWithDataSet;
51import com.android.contacts.common.util.AccountSelectionUtil;
52import com.android.vcard.VCardEntryCounter;
53import com.android.vcard.VCardParser;
54import com.android.vcard.VCardParser_V21;
55import com.android.vcard.VCardParser_V30;
56import com.android.vcard.VCardSourceDetector;
57import com.android.vcard.exception.VCardException;
58import com.android.vcard.exception.VCardNestedException;
59import com.android.vcard.exception.VCardVersionException;
60
61import java.io.ByteArrayInputStream;
62import java.io.File;
63import java.io.IOException;
64import java.io.InputStream;
65import java.nio.ByteBuffer;
66import java.nio.channels.Channels;
67import java.nio.channels.ReadableByteChannel;
68import java.nio.channels.WritableByteChannel;
69import java.text.DateFormat;
70import java.text.SimpleDateFormat;
71import java.util.ArrayList;
72import java.util.Arrays;
73import java.util.Date;
74import java.util.HashSet;
75import java.util.List;
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 = "VCardImport";
90
91    private static final int SELECT_ACCOUNT = 0;
92
93    /* package */ static final String VCARD_URI_ARRAY = "vcard_uri";
94    /* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type";
95    /* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset";
96    /* package */ static final String VCARD_VERSION_ARRAY = "vcard_version";
97    /* package */ static final String ENTRY_COUNT_ARRAY = "entry_count";
98
99    /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
100    /* package */ final static int VCARD_VERSION_V21 = 1;
101    /* package */ final static int VCARD_VERSION_V30 = 2;
102
103    private static final String SECURE_DIRECTORY_NAME = ".android_secure";
104
105    /**
106     * Notification id used when error happened before sending an import request to VCardServer.
107     */
108    private static final int FAILURE_NOTIFICATION_ID = 1;
109
110    final static String CACHED_URIS = "cached_uris";
111
112    private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
113
114    private AccountWithDataSet mAccount;
115
116    private ProgressDialog mProgressDialogForScanVCard;
117    private ProgressDialog mProgressDialogForCachingVCard;
118
119    private List<VCardFile> mAllVCardFileList;
120    private VCardScanThread mVCardScanThread;
121
122    private VCardCacheThread mVCardCacheThread;
123    private ImportRequestConnection mConnection;
124    /* package */ VCardImportExportListener mListener;
125
126    private String mErrorMessage;
127
128    private Handler mHandler = new Handler();
129
130    private static class VCardFile {
131        private final String mName;
132        private final String mCanonicalPath;
133        private final long mLastModified;
134
135        public VCardFile(String name, String canonicalPath, long lastModified) {
136            mName = name;
137            mCanonicalPath = canonicalPath;
138            mLastModified = lastModified;
139        }
140
141        public String getName() {
142            return mName;
143        }
144
145        public String getCanonicalPath() {
146            return mCanonicalPath;
147        }
148
149        public long getLastModified() {
150            return mLastModified;
151        }
152    }
153
154    // Runs on the UI thread.
155    private class DialogDisplayer implements Runnable {
156        private final int mResId;
157        public DialogDisplayer(int resId) {
158            mResId = resId;
159        }
160        public DialogDisplayer(String errorMessage) {
161            mResId = R.id.dialog_error_with_message;
162            mErrorMessage = errorMessage;
163        }
164        @Override
165        public void run() {
166            if (!isFinishing()) {
167                showDialog(mResId);
168            }
169        }
170    }
171
172    private class CancelListener
173        implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
174        @Override
175        public void onClick(DialogInterface dialog, int which) {
176            finish();
177        }
178        @Override
179        public void onCancel(DialogInterface dialog) {
180            finish();
181        }
182    }
183
184    private CancelListener mCancelListener = new CancelListener();
185
186    private class ImportRequestConnection implements ServiceConnection {
187        private VCardService mService;
188
189        public void sendImportRequest(final List<ImportRequest> requests) {
190            Log.i(LOG_TAG, "Send an import request");
191            mService.handleImportRequest(requests, mListener);
192        }
193
194        @Override
195        public void onServiceConnected(ComponentName name, IBinder binder) {
196            mService = ((VCardService.MyBinder) binder).getService();
197            Log.i(LOG_TAG,
198                    String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
199                            Arrays.toString(mVCardCacheThread.getSourceUris())));
200            mVCardCacheThread.start();
201        }
202
203        @Override
204        public void onServiceDisconnected(ComponentName name) {
205            Log.i(LOG_TAG, "Disconnected from VCardService");
206        }
207    }
208
209    /**
210     * Caches given vCard files into a local directory, and sends actual import request to
211     * {@link VCardService}.
212     *
213     * We need to cache given files into local storage. One of reasons is that some data (as Uri)
214     * may have special permissions. Callers may allow only this Activity to access that content,
215     * not what this Activity launched (like {@link VCardService}).
216     */
217    private class VCardCacheThread extends Thread
218            implements DialogInterface.OnCancelListener {
219        private boolean mCanceled;
220        private PowerManager.WakeLock mWakeLock;
221        private VCardParser mVCardParser;
222        private final Uri[] mSourceUris;  // Given from a caller.
223        private final byte[] mSource;
224        private final String mDisplayName;
225
226        public VCardCacheThread(final Uri[] sourceUris) {
227            mSourceUris = sourceUris;
228            mSource = null;
229            final Context context = ImportVCardActivity.this;
230            final PowerManager powerManager =
231                    (PowerManager)context.getSystemService(Context.POWER_SERVICE);
232            mWakeLock = powerManager.newWakeLock(
233                    PowerManager.SCREEN_DIM_WAKE_LOCK |
234                    PowerManager.ON_AFTER_RELEASE, LOG_TAG);
235            mDisplayName = null;
236        }
237
238        @Override
239        public void finalize() {
240            if (mWakeLock != null && mWakeLock.isHeld()) {
241                Log.w(LOG_TAG, "WakeLock is being held.");
242                mWakeLock.release();
243            }
244        }
245
246        @Override
247        public void run() {
248            Log.i(LOG_TAG, "vCard cache thread starts running.");
249            if (mConnection == null) {
250                throw new NullPointerException("vCard cache thread must be launched "
251                        + "after a service connection is established");
252            }
253
254            mWakeLock.acquire();
255            try {
256                if (mCanceled == true) {
257                    Log.i(LOG_TAG, "vCard cache operation is canceled.");
258                    return;
259                }
260
261                final Context context = ImportVCardActivity.this;
262                // Uris given from caller applications may not be opened twice: consider when
263                // it is not from local storage (e.g. "file:///...") but from some special
264                // provider (e.g. "content://...").
265                // Thus we have to once copy the content of Uri into local storage, and read
266                // it after it.
267                //
268                // We may be able to read content of each vCard file during copying them
269                // to local storage, but currently vCard code does not allow us to do so.
270                int cache_index = 0;
271                ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
272                if (mSource != null) {
273                    try {
274                        requests.add(constructImportRequest(mSource, null, mDisplayName));
275                    } catch (VCardException e) {
276                        Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
277                        showFailureNotification(R.string.fail_reason_not_supported);
278                        return;
279                    }
280                } else {
281                    final ContentResolver resolver =
282                            ImportVCardActivity.this.getContentResolver();
283                    for (Uri sourceUri : mSourceUris) {
284                        String filename = null;
285                        // Note: caches are removed by VCardService.
286                        while (true) {
287                            filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
288                            final File file = context.getFileStreamPath(filename);
289                            if (!file.exists()) {
290                                break;
291                            } else {
292                                if (cache_index == Integer.MAX_VALUE) {
293                                    throw new RuntimeException("Exceeded cache limit");
294                                }
295                                cache_index++;
296                            }
297                        }
298                        final Uri localDataUri = copyTo(sourceUri, filename);
299                        if (mCanceled) {
300                            Log.i(LOG_TAG, "vCard cache operation is canceled.");
301                            break;
302                        }
303                        if (localDataUri == null) {
304                            Log.w(LOG_TAG, "destUri is null");
305                            break;
306                        }
307
308                        String displayName = null;
309                        Cursor cursor = null;
310                        // Try to get a display name from the given Uri. If it fails, we just
311                        // pick up the last part of the Uri.
312                        try {
313                            cursor = resolver.query(sourceUri,
314                                    new String[] { OpenableColumns.DISPLAY_NAME },
315                                    null, null, null);
316                            if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
317                                if (cursor.getCount() > 1) {
318                                    Log.w(LOG_TAG, "Unexpected multiple rows: "
319                                            + cursor.getCount());
320                                }
321                                int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
322                                if (index >= 0) {
323                                    displayName = cursor.getString(index);
324                                }
325                            }
326                        } finally {
327                            if (cursor != null) {
328                                cursor.close();
329                            }
330                        }
331                        if (TextUtils.isEmpty(displayName)){
332                            displayName = sourceUri.getLastPathSegment();
333                        }
334
335                        final ImportRequest request;
336                        try {
337                            request = constructImportRequest(null, localDataUri, displayName);
338                        } catch (VCardException e) {
339                            Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
340                            showFailureNotification(R.string.fail_reason_not_supported);
341                            return;
342                        } catch (IOException e) {
343                            Log.e(LOG_TAG, "Unexpected IOException", e);
344                            showFailureNotification(R.string.fail_reason_io_error);
345                            return;
346                        }
347                        if (mCanceled) {
348                            Log.i(LOG_TAG, "vCard cache operation is canceled.");
349                            return;
350                        }
351                        requests.add(request);
352                    }
353                }
354                if (!requests.isEmpty()) {
355                    mConnection.sendImportRequest(requests);
356                } else {
357                    Log.w(LOG_TAG, "Empty import requests. Ignore it.");
358                }
359            } catch (OutOfMemoryError e) {
360                Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
361                System.gc();
362                runOnUiThread(new DialogDisplayer(
363                        getString(R.string.fail_reason_low_memory_during_import)));
364            } catch (IOException e) {
365                Log.e(LOG_TAG, "IOException during caching vCard", e);
366                runOnUiThread(new DialogDisplayer(
367                        getString(R.string.fail_reason_io_error)));
368            } finally {
369                Log.i(LOG_TAG, "Finished caching vCard.");
370                mWakeLock.release();
371                unbindService(mConnection);
372                mProgressDialogForCachingVCard.dismiss();
373                mProgressDialogForCachingVCard = null;
374                finish();
375            }
376        }
377
378        /**
379         * Copy the content of sourceUri to the destination.
380         */
381        private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
382            Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
383                    sourceUri, filename));
384            final Context context = ImportVCardActivity.this;
385            final ContentResolver resolver = context.getContentResolver();
386            ReadableByteChannel inputChannel = null;
387            WritableByteChannel outputChannel = null;
388            Uri destUri = null;
389            try {
390                inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
391                destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
392                outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
393                final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
394                while (inputChannel.read(buffer) != -1) {
395                    if (mCanceled) {
396                        Log.d(LOG_TAG, "Canceled during caching " + sourceUri);
397                        return null;
398                    }
399                    buffer.flip();
400                    outputChannel.write(buffer);
401                    buffer.compact();
402                }
403                buffer.flip();
404                while (buffer.hasRemaining()) {
405                    outputChannel.write(buffer);
406                }
407            } finally {
408                if (inputChannel != null) {
409                    try {
410                        inputChannel.close();
411                    } catch (IOException e) {
412                        Log.w(LOG_TAG, "Failed to close inputChannel.");
413                    }
414                }
415                if (outputChannel != null) {
416                    try {
417                        outputChannel.close();
418                    } catch(IOException e) {
419                        Log.w(LOG_TAG, "Failed to close outputChannel");
420                    }
421                }
422            }
423            return destUri;
424        }
425
426        /**
427         * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
428         * its content.
429         *
430         * @arg localDataUri Uri actually used for the import. Should be stored in
431         * app local storage, as we cannot guarantee other types of Uris can be read
432         * multiple times. This variable populates {@link ImportRequest#uri}.
433         * @arg displayName Used for displaying information to the user. This variable populates
434         * {@link ImportRequest#displayName}.
435         */
436        private ImportRequest constructImportRequest(final byte[] data,
437                final Uri localDataUri, final String displayName)
438                throws IOException, VCardException {
439            final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
440            VCardEntryCounter counter = null;
441            VCardSourceDetector detector = null;
442            int vcardVersion = VCARD_VERSION_V21;
443            try {
444                boolean shouldUseV30 = false;
445                InputStream is;
446                if (data != null) {
447                    is = new ByteArrayInputStream(data);
448                } else {
449                    is = resolver.openInputStream(localDataUri);
450                }
451                mVCardParser = new VCardParser_V21();
452                try {
453                    counter = new VCardEntryCounter();
454                    detector = new VCardSourceDetector();
455                    mVCardParser.addInterpreter(counter);
456                    mVCardParser.addInterpreter(detector);
457                    mVCardParser.parse(is);
458                } catch (VCardVersionException e1) {
459                    try {
460                        is.close();
461                    } catch (IOException e) {
462                    }
463
464                    shouldUseV30 = true;
465                    if (data != null) {
466                        is = new ByteArrayInputStream(data);
467                    } else {
468                        is = resolver.openInputStream(localDataUri);
469                    }
470                    mVCardParser = new VCardParser_V30();
471                    try {
472                        counter = new VCardEntryCounter();
473                        detector = new VCardSourceDetector();
474                        mVCardParser.addInterpreter(counter);
475                        mVCardParser.addInterpreter(detector);
476                        mVCardParser.parse(is);
477                    } catch (VCardVersionException e2) {
478                        throw new VCardException("vCard with unspported version.");
479                    }
480                } finally {
481                    if (is != null) {
482                        try {
483                            is.close();
484                        } catch (IOException e) {
485                        }
486                    }
487                }
488
489                vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
490            } catch (VCardNestedException e) {
491                Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
492                // Go through without throwing the Exception, as we may be able to detect the
493                // version before it
494            }
495            return new ImportRequest(mAccount,
496                    data, localDataUri, displayName,
497                    detector.getEstimatedType(),
498                    detector.getEstimatedCharset(),
499                    vcardVersion, counter.getCount());
500        }
501
502        public Uri[] getSourceUris() {
503            return mSourceUris;
504        }
505
506        public void cancel() {
507            mCanceled = true;
508            if (mVCardParser != null) {
509                mVCardParser.cancel();
510            }
511        }
512
513        @Override
514        public void onCancel(DialogInterface dialog) {
515            Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
516            cancel();
517        }
518    }
519
520    private class ImportTypeSelectedListener implements
521            DialogInterface.OnClickListener {
522        public static final int IMPORT_ONE = 0;
523        public static final int IMPORT_MULTIPLE = 1;
524        public static final int IMPORT_ALL = 2;
525        public static final int IMPORT_TYPE_SIZE = 3;
526
527        private int mCurrentIndex;
528
529        public void onClick(DialogInterface dialog, int which) {
530            if (which == DialogInterface.BUTTON_POSITIVE) {
531                switch (mCurrentIndex) {
532                case IMPORT_ALL:
533                    importVCardFromSDCard(mAllVCardFileList);
534                    break;
535                case IMPORT_MULTIPLE:
536                    showDialog(R.id.dialog_select_multiple_vcard);
537                    break;
538                default:
539                    showDialog(R.id.dialog_select_one_vcard);
540                    break;
541                }
542            } else if (which == DialogInterface.BUTTON_NEGATIVE) {
543                finish();
544            } else {
545                mCurrentIndex = which;
546            }
547        }
548    }
549
550    private class VCardSelectedListener implements
551            DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
552        private int mCurrentIndex;
553        private Set<Integer> mSelectedIndexSet;
554
555        public VCardSelectedListener(boolean multipleSelect) {
556            mCurrentIndex = 0;
557            if (multipleSelect) {
558                mSelectedIndexSet = new HashSet<Integer>();
559            }
560        }
561
562        public void onClick(DialogInterface dialog, int which) {
563            if (which == DialogInterface.BUTTON_POSITIVE) {
564                if (mSelectedIndexSet != null) {
565                    List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
566                    final int size = mAllVCardFileList.size();
567                    // We'd like to sort the files by its index, so we do not use Set iterator.
568                    for (int i = 0; i < size; i++) {
569                        if (mSelectedIndexSet.contains(i)) {
570                            selectedVCardFileList.add(mAllVCardFileList.get(i));
571                        }
572                    }
573                    importVCardFromSDCard(selectedVCardFileList);
574                } else {
575                    importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex));
576                }
577            } else if (which == DialogInterface.BUTTON_NEGATIVE) {
578                finish();
579            } else {
580                // Some file is selected.
581                mCurrentIndex = which;
582                if (mSelectedIndexSet != null) {
583                    if (mSelectedIndexSet.contains(which)) {
584                        mSelectedIndexSet.remove(which);
585                    } else {
586                        mSelectedIndexSet.add(which);
587                    }
588                }
589            }
590        }
591
592        public void onClick(DialogInterface dialog, int which, boolean isChecked) {
593            if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
594                Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
595                        mAllVCardFileList.get(which).getCanonicalPath()));
596            } else {
597                onClick(dialog, which);
598            }
599        }
600    }
601
602    /**
603     * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
604     * a vCard file is shown. After the choice, VCardReadThread starts running.
605     */
606    private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
607        private boolean mCanceled;
608        private boolean mGotIOException;
609        private File mRootDirectory;
610
611        // To avoid recursive link.
612        private Set<String> mCheckedPaths;
613        private PowerManager.WakeLock mWakeLock;
614
615        private class CanceledException extends Exception {
616        }
617
618        public VCardScanThread(File sdcardDirectory) {
619            mCanceled = false;
620            mGotIOException = false;
621            mRootDirectory = sdcardDirectory;
622            mCheckedPaths = new HashSet<String>();
623            PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
624                    Context.POWER_SERVICE);
625            mWakeLock = powerManager.newWakeLock(
626                    PowerManager.SCREEN_DIM_WAKE_LOCK |
627                    PowerManager.ON_AFTER_RELEASE, LOG_TAG);
628        }
629
630        @Override
631        public void run() {
632            mAllVCardFileList = new Vector<VCardFile>();
633            try {
634                mWakeLock.acquire();
635                getVCardFileRecursively(mRootDirectory);
636            } catch (CanceledException e) {
637                mCanceled = true;
638            } catch (IOException e) {
639                mGotIOException = true;
640            } finally {
641                mWakeLock.release();
642            }
643
644            if (mCanceled) {
645                mAllVCardFileList = null;
646            }
647
648            mProgressDialogForScanVCard.dismiss();
649            mProgressDialogForScanVCard = null;
650
651            if (mGotIOException) {
652                runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception));
653            } else if (mCanceled) {
654                finish();
655            } else {
656                int size = mAllVCardFileList.size();
657                final Context context = ImportVCardActivity.this;
658                if (size == 0) {
659                    runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
660                } else {
661                    startVCardSelectAndImport();
662                }
663            }
664        }
665
666        private void getVCardFileRecursively(File directory)
667                throws CanceledException, IOException {
668            if (mCanceled) {
669                throw new CanceledException();
670            }
671
672            // e.g. secured directory may return null toward listFiles().
673            final File[] files = directory.listFiles();
674            if (files == null) {
675                final String currentDirectoryPath = directory.getCanonicalPath();
676                final String secureDirectoryPath =
677                        mRootDirectory.getCanonicalPath().concat(SECURE_DIRECTORY_NAME);
678                if (!TextUtils.equals(currentDirectoryPath, secureDirectoryPath)) {
679                    Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
680                }
681                return;
682            }
683            for (File file : directory.listFiles()) {
684                if (mCanceled) {
685                    throw new CanceledException();
686                }
687                String canonicalPath = file.getCanonicalPath();
688                if (mCheckedPaths.contains(canonicalPath)) {
689                    continue;
690                }
691
692                mCheckedPaths.add(canonicalPath);
693
694                if (file.isDirectory()) {
695                    getVCardFileRecursively(file);
696                } else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
697                        file.canRead()){
698                    String fileName = file.getName();
699                    VCardFile vcardFile = new VCardFile(
700                            fileName, canonicalPath, file.lastModified());
701                    mAllVCardFileList.add(vcardFile);
702                }
703            }
704        }
705
706        public void onCancel(DialogInterface dialog) {
707            mCanceled = true;
708        }
709
710        public void onClick(DialogInterface dialog, int which) {
711            if (which == DialogInterface.BUTTON_NEGATIVE) {
712                mCanceled = true;
713            }
714        }
715    }
716
717    private void startVCardSelectAndImport() {
718        int size = mAllVCardFileList.size();
719        if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) ||
720                size == 1) {
721            importVCardFromSDCard(mAllVCardFileList);
722        } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
723            runOnUiThread(new DialogDisplayer(R.id.dialog_select_import_type));
724        } else {
725            runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
726        }
727    }
728
729    private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
730        final int size = selectedVCardFileList.size();
731        String[] uriStrings = new String[size];
732        int i = 0;
733        for (VCardFile vcardFile : selectedVCardFileList) {
734            uriStrings[i] = "file://" + vcardFile.getCanonicalPath();
735            i++;
736        }
737        importVCard(uriStrings);
738    }
739
740    private void importVCardFromSDCard(final VCardFile vcardFile) {
741        importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())});
742    }
743
744    private void importVCard(final Uri uri) {
745        importVCard(new Uri[] {uri});
746    }
747
748    private void importVCard(final String[] uriStrings) {
749        final int length = uriStrings.length;
750        final Uri[] uris = new Uri[length];
751        for (int i = 0; i < length; i++) {
752            uris[i] = Uri.parse(uriStrings[i]);
753        }
754        importVCard(uris);
755    }
756
757    private void importVCard(final Uri[] uris) {
758        runOnUiThread(new Runnable() {
759            @Override
760            public void run() {
761                if (!isFinishing()) {
762                    mVCardCacheThread = new VCardCacheThread(uris);
763                    mListener = new NotificationImportExportListener(ImportVCardActivity.this);
764                    showDialog(R.id.dialog_cache_vcard);
765                }
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 == Activity.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 != Activity.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    /**
1000     * Scans vCard in external storage (typically SDCard) and tries to import it.
1001     * - When there's no SDCard available, an error dialog is shown.
1002     * - When multiple vCard files are available, asks a user to select one.
1003     */
1004    private void doScanExternalStorageAndImportVCard() {
1005        // TODO: should use getExternalStorageState().
1006        final File file = Environment.getExternalStorageDirectory();
1007        if (!file.exists() || !file.isDirectory() || !file.canRead()) {
1008            showDialog(R.id.dialog_sdcard_not_found);
1009        } else {
1010            mVCardScanThread = new VCardScanThread(file);
1011            showDialog(R.id.dialog_searching_vcard);
1012        }
1013    }
1014
1015    /* package */ void showFailureNotification(int reasonId) {
1016        final NotificationManager notificationManager =
1017                (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
1018        final Notification notification =
1019                NotificationImportExportListener.constructImportFailureNotification(
1020                        ImportVCardActivity.this,
1021                        getString(reasonId));
1022        notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
1023                FAILURE_NOTIFICATION_ID, notification);
1024        mHandler.post(new Runnable() {
1025            @Override
1026            public void run() {
1027                Toast.makeText(ImportVCardActivity.this,
1028                        getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
1029            }
1030        });
1031    }
1032}
1033