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