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