• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.contacts;
18 
19 import com.android.contacts.model.Sources;
20 import com.android.contacts.util.AccountSelectionUtil;
21 
22 import android.accounts.Account;
23 import android.app.Activity;
24 import android.app.AlertDialog;
25 import android.app.Dialog;
26 import android.app.ProgressDialog;
27 import android.content.ContentResolver;
28 import android.content.ContentUris;
29 import android.content.Context;
30 import android.content.DialogInterface;
31 import android.content.DialogInterface.OnCancelListener;
32 import android.content.DialogInterface.OnClickListener;
33 import android.content.Intent;
34 import android.net.Uri;
35 import android.os.Bundle;
36 import android.os.Environment;
37 import android.os.Handler;
38 import android.os.PowerManager;
39 import android.pim.vcard.VCardConfig;
40 import android.pim.vcard.VCardEntryCommitter;
41 import android.pim.vcard.VCardEntryConstructor;
42 import android.pim.vcard.VCardEntryCounter;
43 import android.pim.vcard.VCardInterpreter;
44 import android.pim.vcard.VCardInterpreterCollection;
45 import android.pim.vcard.VCardParser;
46 import android.pim.vcard.VCardParser_V21;
47 import android.pim.vcard.VCardParser_V30;
48 import android.pim.vcard.VCardSourceDetector;
49 import android.pim.vcard.exception.VCardException;
50 import android.pim.vcard.exception.VCardNestedException;
51 import android.pim.vcard.exception.VCardNotSupportedException;
52 import android.pim.vcard.exception.VCardVersionException;
53 import android.provider.ContactsContract.RawContacts;
54 import android.text.SpannableStringBuilder;
55 import android.text.Spanned;
56 import android.text.TextUtils;
57 import android.text.style.RelativeSizeSpan;
58 import android.util.Log;
59 
60 import java.io.File;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.text.DateFormat;
64 import java.text.SimpleDateFormat;
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import java.util.Date;
68 import java.util.HashSet;
69 import java.util.List;
70 import java.util.Locale;
71 import java.util.Set;
72 import java.util.Vector;
73 
74 class VCardFile {
75     private String mName;
76     private String mCanonicalPath;
77     private long mLastModified;
78 
VCardFile(String name, String canonicalPath, long lastModified)79     public VCardFile(String name, String canonicalPath, long lastModified) {
80         mName = name;
81         mCanonicalPath = canonicalPath;
82         mLastModified = lastModified;
83     }
84 
getName()85     public String getName() {
86         return mName;
87     }
88 
getCanonicalPath()89     public String getCanonicalPath() {
90         return mCanonicalPath;
91     }
92 
getLastModified()93     public long getLastModified() {
94         return mLastModified;
95     }
96 }
97 
98 /**
99  * Class for importing vCard. Several user interaction will be required while reading
100  * (selecting a file, waiting a moment, etc.)
101  *
102  * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
103  * finished (with the method {@link Activity#finish()}) after the import and never reuse
104  * any Dialog in the instance. So this code is careless about the management around managed
105  * dialogs stuffs (like how onCreateDialog() is used).
106  */
107 public class ImportVCardActivity extends Activity {
108     private static final String LOG_TAG = "ImportVCardActivity";
109     private static final boolean DO_PERFORMANCE_PROFILE = false;
110 
111     private final static int VCARD_VERSION_V21 = 1;
112     private final static int VCARD_VERSION_V30 = 2;
113     private final static int VCARD_VERSION_V40 = 3;
114 
115     // Run on the UI thread. Must not be null except after onDestroy().
116     private Handler mHandler = new Handler();
117 
118     private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
119     private Account mAccount;
120 
121     private ProgressDialog mProgressDialogForScanVCard;
122 
123     private List<VCardFile> mAllVCardFileList;
124     private VCardScanThread mVCardScanThread;
125     private VCardReadThread mVCardReadThread;
126     private ProgressDialog mProgressDialogForReadVCard;
127 
128     private String mErrorMessage;
129 
130     private boolean mNeedReview = false;
131 
132     // Runs on the UI thread.
133     private class DialogDisplayer implements Runnable {
134         private final int mResId;
DialogDisplayer(int resId)135         public DialogDisplayer(int resId) {
136             mResId = resId;
137         }
DialogDisplayer(String errorMessage)138         public DialogDisplayer(String errorMessage) {
139             mResId = R.id.dialog_error_with_message;
140             mErrorMessage = errorMessage;
141         }
run()142         public void run() {
143             showDialog(mResId);
144         }
145     }
146 
147     private class CancelListener
148         implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
onClick(DialogInterface dialog, int which)149         public void onClick(DialogInterface dialog, int which) {
150             finish();
151         }
152 
onCancel(DialogInterface dialog)153         public void onCancel(DialogInterface dialog) {
154             finish();
155         }
156     }
157 
158     private CancelListener mCancelListener = new CancelListener();
159 
160     private class VCardReadThread extends Thread
161             implements DialogInterface.OnCancelListener {
162         private ContentResolver mResolver;
163         private VCardParser mVCardParser;
164         private boolean mCanceled;
165         private PowerManager.WakeLock mWakeLock;
166         private Uri mUri;
167         private File mTempFile;
168 
169         private List<VCardFile> mSelectedVCardFileList;
170         private List<String> mErrorFileNameList;
171 
VCardReadThread(Uri uri)172         public VCardReadThread(Uri uri) {
173             mUri = uri;
174             init();
175         }
176 
VCardReadThread(final List<VCardFile> selectedVCardFileList)177         public VCardReadThread(final List<VCardFile> selectedVCardFileList) {
178             mSelectedVCardFileList = selectedVCardFileList;
179             mErrorFileNameList = new ArrayList<String>();
180             init();
181         }
182 
init()183         private void init() {
184             Context context = ImportVCardActivity.this;
185             mResolver = context.getContentResolver();
186             PowerManager powerManager = (PowerManager)context.getSystemService(
187                     Context.POWER_SERVICE);
188             mWakeLock = powerManager.newWakeLock(
189                     PowerManager.SCREEN_DIM_WAKE_LOCK |
190                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
191         }
192 
193         @Override
finalize()194         public void finalize() {
195             if (mWakeLock != null && mWakeLock.isHeld()) {
196                 mWakeLock.release();
197             }
198         }
199 
200         @Override
run()201         public void run() {
202             boolean shouldCallFinish = true;
203             mWakeLock.acquire();
204             Uri createdUri = null;
205             mTempFile = null;
206             // Some malicious vCard data may make this thread broken
207             // (e.g. OutOfMemoryError).
208             // Even in such cases, some should be done.
209             try {
210                 if (mUri != null) {  // Read one vCard expressed by mUri
211                     final Uri targetUri = mUri;
212                     mProgressDialogForReadVCard.setProgressNumberFormat("");
213                     mProgressDialogForReadVCard.setProgress(0);
214 
215                     // Count the number of VCard entries
216                     mProgressDialogForReadVCard.setIndeterminate(true);
217                     long start;
218                     if (DO_PERFORMANCE_PROFILE) {
219                         start = System.currentTimeMillis();
220                     }
221                     final VCardEntryCounter counter = new VCardEntryCounter();
222                     final VCardSourceDetector detector = new VCardSourceDetector();
223                     final VCardInterpreterCollection builderCollection =
224                             new VCardInterpreterCollection(Arrays.asList(counter, detector));
225                     boolean result;
226                     try {
227                         // We don't know which type should be useld to parse the Uri.
228                         // It is possble to misinterpret the vCard, but we expect the parser
229                         // lets VCardSourceDetector detect the type before the misinterpretation.
230                         result = readOneVCardFile(targetUri, VCardConfig.VCARD_TYPE_UNKNOWN,
231                                 builderCollection, true, null);
232                     } catch (VCardNestedException e) {
233                         try {
234                             final int estimatedVCardType = detector.getEstimatedType();
235                             final String estimatedCharset = detector.getEstimatedCharset();
236                             // Assume that VCardSourceDetector was able to detect the source.
237                             // Try again with the detector.
238                             result = readOneVCardFile(targetUri, estimatedVCardType,
239                                     counter, false, null);
240                         } catch (VCardNestedException e2) {
241                             result = false;
242                             Log.e(LOG_TAG, "Must not reach here. " + e2);
243                         }
244                     }
245                     if (DO_PERFORMANCE_PROFILE) {
246                         long time = System.currentTimeMillis() - start;
247                         Log.d(LOG_TAG, "time for counting the number of vCard entries: " +
248                                 time + " ms");
249                     }
250                     if (!result) {
251                         shouldCallFinish = false;
252                         return;
253                     }
254 
255                     mProgressDialogForReadVCard.setProgressNumberFormat(
256                             getString(R.string.reading_vcard_contacts));
257                     mProgressDialogForReadVCard.setIndeterminate(false);
258                     mProgressDialogForReadVCard.setMax(counter.getCount());
259                     String charset = detector.getEstimatedCharset();
260                     createdUri = doActuallyReadOneVCard(targetUri, mAccount, true, detector,
261                             mErrorFileNameList);
262                 } else {  // Read multiple files.
263                     mProgressDialogForReadVCard.setProgressNumberFormat(
264                             getString(R.string.reading_vcard_files));
265                     mProgressDialogForReadVCard.setMax(mSelectedVCardFileList.size());
266                     mProgressDialogForReadVCard.setProgress(0);
267 
268                     for (VCardFile vcardFile : mSelectedVCardFileList) {
269                         if (mCanceled) {
270                             return;
271                         }
272                         // TODO: detect scheme!
273                         final Uri targetUri = Uri.parse("file://" + vcardFile.getCanonicalPath());
274                         VCardSourceDetector detector = new VCardSourceDetector();
275                         try {
276                             if (!readOneVCardFile(targetUri, VCardConfig.VCARD_TYPE_UNKNOWN,
277                                     detector, true, mErrorFileNameList)) {
278                                 continue;
279                             }
280                         } catch (VCardNestedException e) {
281                             // Assume that VCardSourceDetector was able to detect the source.
282                         }
283                         String charset = detector.getEstimatedCharset();
284                         doActuallyReadOneVCard(targetUri, mAccount,
285                                 false, detector, mErrorFileNameList);
286                         mProgressDialogForReadVCard.incrementProgressBy(1);
287                     }
288                 }
289             } finally {
290                 mWakeLock.release();
291                 mProgressDialogForReadVCard.dismiss();
292                 if (mTempFile != null) {
293                     if (!mTempFile.delete()) {
294                         Log.w(LOG_TAG, "Failed to delete a cache file.");
295                     }
296                     mTempFile = null;
297                 }
298                 // finish() is called via mCancelListener, which is used in DialogDisplayer.
299                 if (shouldCallFinish && !isFinishing()) {
300                     if (mErrorFileNameList == null || mErrorFileNameList.isEmpty()) {
301                         finish();
302                         if (mNeedReview) {
303                             mNeedReview = false;
304                             Log.v("importVCardActivity", "Prepare to review the imported contact");
305 
306                             if (createdUri != null) {
307                                 // get contact_id of this raw_contact
308                                 final long rawContactId = ContentUris.parseId(createdUri);
309                                 Uri contactUri = RawContacts.getContactLookupUri(
310                                         getContentResolver(), ContentUris.withAppendedId(
311                                                 RawContacts.CONTENT_URI, rawContactId));
312 
313                                 Intent viewIntent = new Intent(Intent.ACTION_VIEW, contactUri);
314                                 startActivity(viewIntent);
315                             }
316                         }
317                     } else {
318                         StringBuilder builder = new StringBuilder();
319                         boolean first = true;
320                         for (String fileName : mErrorFileNameList) {
321                             if (first) {
322                                 first = false;
323                             } else {
324                                 builder.append(", ");
325                             }
326                             builder.append(fileName);
327                         }
328 
329                         runOnUIThread(new DialogDisplayer(
330                                 getString(R.string.fail_reason_failed_to_read_files,
331                                         builder.toString())));
332                     }
333                 }
334             }
335         }
336 
doActuallyReadOneVCard(Uri uri, Account account, boolean showEntryParseProgress, VCardSourceDetector detector, List<String> errorFileNameList)337         private Uri doActuallyReadOneVCard(Uri uri, Account account,
338                 boolean showEntryParseProgress,
339                 VCardSourceDetector detector, List<String> errorFileNameList) {
340             final Context context = ImportVCardActivity.this;
341             int vcardType = detector.getEstimatedType();
342             if (vcardType == VCardConfig.VCARD_TYPE_UNKNOWN) {
343                 vcardType = VCardConfig.getVCardTypeFromString(
344                         context.getString(R.string.config_import_vcard_type));
345             }
346             final String estimatedCharset = detector.getEstimatedCharset();
347             final String currentLanguage = Locale.getDefault().getLanguage();
348             VCardEntryConstructor builder;
349             builder = new VCardEntryConstructor(vcardType, mAccount, estimatedCharset);
350             final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
351             builder.addEntryHandler(committer);
352             if (showEntryParseProgress) {
353                 builder.addEntryHandler(new ProgressShower(mProgressDialogForReadVCard,
354                         context.getString(R.string.reading_vcard_message),
355                         ImportVCardActivity.this,
356                         mHandler));
357             }
358 
359             try {
360                 if (!readOneVCardFile(uri, vcardType, builder, false, null)) {
361                     return null;
362                 }
363             } catch (VCardNestedException e) {
364                 Log.e(LOG_TAG, "Never reach here.");
365             }
366             final ArrayList<Uri> createdUris = committer.getCreatedUris();
367             return (createdUris == null || createdUris.size() != 1) ? null : createdUris.get(0);
368         }
369 
370         /**
371          * Charset should be handled by {@link VCardEntryConstructor}.
372          */
readOneVCardFile(Uri uri, int vcardType, VCardInterpreter interpreter, boolean throwNestedException, List<String> errorFileNameList)373         private boolean readOneVCardFile(Uri uri, int vcardType,
374                 VCardInterpreter interpreter,
375                 boolean throwNestedException, List<String> errorFileNameList)
376                 throws VCardNestedException {
377             InputStream is;
378             try {
379                 is = mResolver.openInputStream(uri);
380                 mVCardParser = new VCardParser_V21(vcardType);
381 
382                 try {
383                     mVCardParser.parse(is, interpreter);
384                 } catch (VCardVersionException e1) {
385                     try {
386                         is.close();
387                     } catch (IOException e) {
388                     }
389                     if (interpreter instanceof VCardEntryConstructor) {
390                         // Let the object clean up internal temporal objects,
391                         ((VCardEntryConstructor)interpreter).clear();
392                     } else if (interpreter instanceof VCardInterpreterCollection) {
393                         for (VCardInterpreter elem :
394                             ((VCardInterpreterCollection) interpreter).getCollection()) {
395                             if (elem instanceof VCardEntryConstructor) {
396                                 ((VCardEntryConstructor)elem).clear();
397                             }
398                         }
399                     }
400 
401                     is = mResolver.openInputStream(uri);
402 
403                     try {
404                         mVCardParser = new VCardParser_V30(vcardType);
405                         mVCardParser.parse(is, interpreter);
406                     } catch (VCardVersionException e2) {
407                         throw new VCardException("vCard with unspported version.");
408                     }
409                 } finally {
410                     if (is != null) {
411                         try {
412                             is.close();
413                         } catch (IOException e) {
414                         }
415                     }
416                 }
417             } catch (IOException e) {
418                 Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
419 
420                 mProgressDialogForReadVCard.dismiss();
421 
422                 if (errorFileNameList != null) {
423                     errorFileNameList.add(uri.toString());
424                 } else {
425                     runOnUIThread(new DialogDisplayer(
426                             getString(R.string.fail_reason_io_error) +
427                                     ": " + e.getLocalizedMessage()));
428                 }
429                 return false;
430             } catch (VCardNotSupportedException e) {
431                 if ((e instanceof VCardNestedException) && throwNestedException) {
432                     throw (VCardNestedException)e;
433                 }
434                 if (errorFileNameList != null) {
435                     errorFileNameList.add(uri.toString());
436                 } else {
437                     runOnUIThread(new DialogDisplayer(
438                             getString(R.string.fail_reason_vcard_not_supported_error) +
439                             " (" + e.getMessage() + ")"));
440                 }
441                 return false;
442             } catch (VCardException e) {
443                 if (errorFileNameList != null) {
444                     errorFileNameList.add(uri.toString());
445                 } else {
446                     runOnUIThread(new DialogDisplayer(
447                             getString(R.string.fail_reason_vcard_parse_error) +
448                             " (" + e.getMessage() + ")"));
449                 }
450                 return false;
451             }
452             return true;
453         }
454 
cancel()455         public void cancel() {
456             mCanceled = true;
457             if (mVCardParser != null) {
458                 mVCardParser.cancel();
459             }
460         }
461 
onCancel(DialogInterface dialog)462         public void onCancel(DialogInterface dialog) {
463             cancel();
464         }
465     }
466 
467     private class ImportTypeSelectedListener implements
468             DialogInterface.OnClickListener {
469         public static final int IMPORT_ONE = 0;
470         public static final int IMPORT_MULTIPLE = 1;
471         public static final int IMPORT_ALL = 2;
472         public static final int IMPORT_TYPE_SIZE = 3;
473 
474         private int mCurrentIndex;
475 
onClick(DialogInterface dialog, int which)476         public void onClick(DialogInterface dialog, int which) {
477             if (which == DialogInterface.BUTTON_POSITIVE) {
478                 switch (mCurrentIndex) {
479                 case IMPORT_ALL:
480                     importMultipleVCardFromSDCard(mAllVCardFileList);
481                     break;
482                 case IMPORT_MULTIPLE:
483                     showDialog(R.id.dialog_select_multiple_vcard);
484                     break;
485                 default:
486                     showDialog(R.id.dialog_select_one_vcard);
487                     break;
488                 }
489             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
490                 finish();
491             } else {
492                 mCurrentIndex = which;
493             }
494         }
495     }
496 
497     private class VCardSelectedListener implements
498             DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
499         private int mCurrentIndex;
500         private Set<Integer> mSelectedIndexSet;
501 
VCardSelectedListener(boolean multipleSelect)502         public VCardSelectedListener(boolean multipleSelect) {
503             mCurrentIndex = 0;
504             if (multipleSelect) {
505                 mSelectedIndexSet = new HashSet<Integer>();
506             }
507         }
508 
onClick(DialogInterface dialog, int which)509         public void onClick(DialogInterface dialog, int which) {
510             if (which == DialogInterface.BUTTON_POSITIVE) {
511                 if (mSelectedIndexSet != null) {
512                     List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
513                     int size = mAllVCardFileList.size();
514                     // We'd like to sort the files by its index, so we do not use Set iterator.
515                     for (int i = 0; i < size; i++) {
516                         if (mSelectedIndexSet.contains(i)) {
517                             selectedVCardFileList.add(mAllVCardFileList.get(i));
518                         }
519                     }
520                     importMultipleVCardFromSDCard(selectedVCardFileList);
521                 } else {
522                     String canonicalPath = mAllVCardFileList.get(mCurrentIndex).getCanonicalPath();
523                     final Uri uri = Uri.parse("file://" + canonicalPath);
524                     importOneVCardFromSDCard(uri);
525                 }
526             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
527                 finish();
528             } else {
529                 // Some file is selected.
530                 mCurrentIndex = which;
531                 if (mSelectedIndexSet != null) {
532                     if (mSelectedIndexSet.contains(which)) {
533                         mSelectedIndexSet.remove(which);
534                     } else {
535                         mSelectedIndexSet.add(which);
536                     }
537                 }
538             }
539         }
540 
onClick(DialogInterface dialog, int which, boolean isChecked)541         public void onClick(DialogInterface dialog, int which, boolean isChecked) {
542             if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
543                 Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
544                         mAllVCardFileList.get(which).getCanonicalPath()));
545             } else {
546                 onClick(dialog, which);
547             }
548         }
549     }
550 
551     /**
552      * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
553      * a vCard file is shown. After the choice, VCardReadThread starts running.
554      */
555     private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
556         private boolean mCanceled;
557         private boolean mGotIOException;
558         private File mRootDirectory;
559 
560         // To avoid recursive link.
561         private Set<String> mCheckedPaths;
562         private PowerManager.WakeLock mWakeLock;
563 
564         private class CanceledException extends Exception {
565         }
566 
VCardScanThread(File sdcardDirectory)567         public VCardScanThread(File sdcardDirectory) {
568             mCanceled = false;
569             mGotIOException = false;
570             mRootDirectory = sdcardDirectory;
571             mCheckedPaths = new HashSet<String>();
572             PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
573                     Context.POWER_SERVICE);
574             mWakeLock = powerManager.newWakeLock(
575                     PowerManager.SCREEN_DIM_WAKE_LOCK |
576                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
577         }
578 
579         @Override
run()580         public void run() {
581             mAllVCardFileList = new Vector<VCardFile>();
582             try {
583                 mWakeLock.acquire();
584                 getVCardFileRecursively(mRootDirectory);
585             } catch (CanceledException e) {
586                 mCanceled = true;
587             } catch (IOException e) {
588                 mGotIOException = true;
589             } finally {
590                 mWakeLock.release();
591             }
592 
593             if (mCanceled) {
594                 mAllVCardFileList = null;
595             }
596 
597             mProgressDialogForScanVCard.dismiss();
598             mProgressDialogForScanVCard = null;
599 
600             if (mGotIOException) {
601                 runOnUIThread(new DialogDisplayer(R.id.dialog_io_exception));
602             } else if (mCanceled) {
603                 finish();
604             } else {
605                 int size = mAllVCardFileList.size();
606                 final Context context = ImportVCardActivity.this;
607                 if (size == 0) {
608                     runOnUIThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
609                 } else {
610                     startVCardSelectAndImport();
611                 }
612             }
613         }
614 
getVCardFileRecursively(File directory)615         private void getVCardFileRecursively(File directory)
616                 throws CanceledException, IOException {
617             if (mCanceled) {
618                 throw new CanceledException();
619             }
620 
621             // e.g. secured directory may return null toward listFiles().
622             final File[] files = directory.listFiles();
623             if (files == null) {
624                 Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
625                 return;
626             }
627             for (File file : directory.listFiles()) {
628                 if (mCanceled) {
629                     throw new CanceledException();
630                 }
631                 String canonicalPath = file.getCanonicalPath();
632                 if (mCheckedPaths.contains(canonicalPath)) {
633                     continue;
634                 }
635 
636                 mCheckedPaths.add(canonicalPath);
637 
638                 if (file.isDirectory()) {
639                     getVCardFileRecursively(file);
640                 } else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
641                         file.canRead()){
642                     String fileName = file.getName();
643                     VCardFile vcardFile = new VCardFile(
644                             fileName, canonicalPath, file.lastModified());
645                     mAllVCardFileList.add(vcardFile);
646                 }
647             }
648         }
649 
onCancel(DialogInterface dialog)650         public void onCancel(DialogInterface dialog) {
651             mCanceled = true;
652         }
653 
onClick(DialogInterface dialog, int which)654         public void onClick(DialogInterface dialog, int which) {
655             if (which == DialogInterface.BUTTON_NEGATIVE) {
656                 mCanceled = true;
657             }
658         }
659     }
660 
startVCardSelectAndImport()661     private void startVCardSelectAndImport() {
662         int size = mAllVCardFileList.size();
663         if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically)) {
664             importMultipleVCardFromSDCard(mAllVCardFileList);
665         } else if (size == 1) {
666             String canonicalPath = mAllVCardFileList.get(0).getCanonicalPath();
667             Uri uri = Uri.parse("file://" + canonicalPath);
668             importOneVCardFromSDCard(uri);
669         } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
670             runOnUIThread(new DialogDisplayer(R.id.dialog_select_import_type));
671         } else {
672             runOnUIThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
673         }
674     }
675 
importMultipleVCardFromSDCard(final List<VCardFile> selectedVCardFileList)676     private void importMultipleVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
677         runOnUIThread(new Runnable() {
678             public void run() {
679                 mVCardReadThread = new VCardReadThread(selectedVCardFileList);
680                 showDialog(R.id.dialog_reading_vcard);
681             }
682         });
683     }
684 
importOneVCardFromSDCard(final Uri uri)685     private void importOneVCardFromSDCard(final Uri uri) {
686         runOnUIThread(new Runnable() {
687             public void run() {
688                 mVCardReadThread = new VCardReadThread(uri);
689                 showDialog(R.id.dialog_reading_vcard);
690             }
691         });
692     }
693 
getSelectImportTypeDialog()694     private Dialog getSelectImportTypeDialog() {
695         DialogInterface.OnClickListener listener =
696             new ImportTypeSelectedListener();
697         AlertDialog.Builder builder = new AlertDialog.Builder(this)
698             .setTitle(R.string.select_vcard_title)
699             .setPositiveButton(android.R.string.ok, listener)
700             .setOnCancelListener(mCancelListener)
701             .setNegativeButton(android.R.string.cancel, mCancelListener);
702 
703         String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
704         items[ImportTypeSelectedListener.IMPORT_ONE] =
705             getString(R.string.import_one_vcard_string);
706         items[ImportTypeSelectedListener.IMPORT_MULTIPLE] =
707             getString(R.string.import_multiple_vcard_string);
708         items[ImportTypeSelectedListener.IMPORT_ALL] =
709             getString(R.string.import_all_vcard_string);
710         builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener);
711         return builder.create();
712     }
713 
getVCardFileSelectDialog(boolean multipleSelect)714     private Dialog getVCardFileSelectDialog(boolean multipleSelect) {
715         int size = mAllVCardFileList.size();
716         VCardSelectedListener listener = new VCardSelectedListener(multipleSelect);
717         AlertDialog.Builder builder =
718             new AlertDialog.Builder(this)
719                 .setTitle(R.string.select_vcard_title)
720                 .setPositiveButton(android.R.string.ok, listener)
721                 .setOnCancelListener(mCancelListener)
722                 .setNegativeButton(android.R.string.cancel, mCancelListener);
723 
724         CharSequence[] items = new CharSequence[size];
725         DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
726         for (int i = 0; i < size; i++) {
727             VCardFile vcardFile = mAllVCardFileList.get(i);
728             SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
729             stringBuilder.append(vcardFile.getName());
730             stringBuilder.append('\n');
731             int indexToBeSpanned = stringBuilder.length();
732             // Smaller date text looks better, since each file name becomes easier to read.
733             // The value set to RelativeSizeSpan is arbitrary. You can change it to any other
734             // value (but the value bigger than 1.0f would not make nice appearance :)
735             stringBuilder.append(
736                         "(" + dateFormat.format(new Date(vcardFile.getLastModified())) + ")");
737             stringBuilder.setSpan(
738                     new RelativeSizeSpan(0.7f), indexToBeSpanned, stringBuilder.length(),
739                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
740             items[i] = stringBuilder;
741         }
742         if (multipleSelect) {
743             builder.setMultiChoiceItems(items, (boolean[])null, listener);
744         } else {
745             builder.setSingleChoiceItems(items, 0, listener);
746         }
747         return builder.create();
748     }
749 
750     @Override
onCreate(Bundle bundle)751     protected void onCreate(Bundle bundle) {
752         super.onCreate(bundle);
753 
754         final Intent intent = getIntent();
755         if (intent != null) {
756             final String accountName = intent.getStringExtra("account_name");
757             final String accountType = intent.getStringExtra("account_type");
758             if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
759                 mAccount = new Account(accountName, accountType);
760             }
761         } else {
762             Log.e(LOG_TAG, "intent does not exist");
763         }
764 
765         // The caller often does not know account information at all, so we show the UI instead.
766         if (mAccount == null) {
767             // There's three possibilities:
768             // - more than one accounts -> ask the user
769             // - just one account -> use the account without asking the user
770             // - no account -> use phone-local storage without asking the user
771             final Sources sources = Sources.getInstance(this);
772             final List<Account> accountList = sources.getAccounts(true);
773             final int size = accountList.size();
774             if (size > 1) {
775                 final int resId = R.string.import_from_sdcard;
776                 mAccountSelectionListener =
777                     new AccountSelectionUtil.AccountSelectedListener(
778                             this, accountList, resId) {
779                     @Override
780                     public void onClick(DialogInterface dialog, int which) {
781                         dialog.dismiss();
782                         mAccount = mAccountList.get(which);
783                         // Instead of using Intent mechanism, call the relevant private method,
784                         // to avoid throwing an Intent to itself again.
785                         startImport();
786                     }
787                 };
788                 showDialog(resId);
789                 return;
790             } else {
791                 mAccount = size > 0 ? accountList.get(0) : null;
792             }
793         }
794 
795         startImport();
796     }
797 
startImport()798     private void startImport() {
799         Intent intent = getIntent();
800         final String action = intent.getAction();
801         final Uri uri = intent.getData();
802         Log.v(LOG_TAG, "action = " + action + " ; path = " + uri);
803         if (Intent.ACTION_VIEW.equals(action)) {
804             // Import the file directly and then go to EDIT screen
805             mNeedReview = true;
806         }
807 
808         if (uri != null) {
809             importOneVCardFromSDCard(uri);
810         } else {
811             doScanExternalStorageAndImportVCard();
812         }
813     }
814 
815     @Override
onCreateDialog(int resId)816     protected Dialog onCreateDialog(int resId) {
817         switch (resId) {
818             case R.string.import_from_sdcard: {
819                 if (mAccountSelectionListener == null) {
820                     throw new NullPointerException(
821                             "mAccountSelectionListener must not be null.");
822                 }
823                 return AccountSelectionUtil.getSelectAccountDialog(this, resId,
824                         mAccountSelectionListener,
825                         new CancelListener());
826             }
827             case R.id.dialog_searching_vcard: {
828                 if (mProgressDialogForScanVCard == null) {
829                     String title = getString(R.string.searching_vcard_title);
830                     String message = getString(R.string.searching_vcard_message);
831                     mProgressDialogForScanVCard =
832                         ProgressDialog.show(this, title, message, true, false);
833                     mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread);
834                     mVCardScanThread.start();
835                 }
836                 return mProgressDialogForScanVCard;
837             }
838             case R.id.dialog_sdcard_not_found: {
839                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
840                     .setTitle(R.string.no_sdcard_title)
841                     .setIcon(android.R.drawable.ic_dialog_alert)
842                     .setMessage(R.string.no_sdcard_message)
843                     .setOnCancelListener(mCancelListener)
844                     .setPositiveButton(android.R.string.ok, mCancelListener);
845                 return builder.create();
846             }
847             case R.id.dialog_vcard_not_found: {
848                 String message = (getString(R.string.scanning_sdcard_failed_message,
849                         getString(R.string.fail_reason_no_vcard_file)));
850                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
851                     .setTitle(R.string.scanning_sdcard_failed_title)
852                     .setMessage(message)
853                     .setOnCancelListener(mCancelListener)
854                     .setPositiveButton(android.R.string.ok, mCancelListener);
855                 return builder.create();
856             }
857             case R.id.dialog_select_import_type: {
858                 return getSelectImportTypeDialog();
859             }
860             case R.id.dialog_select_multiple_vcard: {
861                 return getVCardFileSelectDialog(true);
862             }
863             case R.id.dialog_select_one_vcard: {
864                 return getVCardFileSelectDialog(false);
865             }
866             case R.id.dialog_reading_vcard: {
867                 if (mProgressDialogForReadVCard == null) {
868                     String title = getString(R.string.reading_vcard_title);
869                     String message = getString(R.string.reading_vcard_message);
870                     mProgressDialogForReadVCard = new ProgressDialog(this);
871                     mProgressDialogForReadVCard.setTitle(title);
872                     mProgressDialogForReadVCard.setMessage(message);
873                     mProgressDialogForReadVCard.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
874                     mProgressDialogForReadVCard.setOnCancelListener(mVCardReadThread);
875                     mVCardReadThread.start();
876                 }
877                 return mProgressDialogForReadVCard;
878             }
879             case R.id.dialog_io_exception: {
880                 String message = (getString(R.string.scanning_sdcard_failed_message,
881                         getString(R.string.fail_reason_io_error)));
882                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
883                     .setTitle(R.string.scanning_sdcard_failed_title)
884                     .setIcon(android.R.drawable.ic_dialog_alert)
885                     .setMessage(message)
886                     .setOnCancelListener(mCancelListener)
887                     .setPositiveButton(android.R.string.ok, mCancelListener);
888                 return builder.create();
889             }
890             case R.id.dialog_error_with_message: {
891                 String message = mErrorMessage;
892                 if (TextUtils.isEmpty(message)) {
893                     Log.e(LOG_TAG, "Error message is null while it must not.");
894                     message = getString(R.string.fail_reason_unknown);
895                 }
896                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
897                     .setTitle(getString(R.string.reading_vcard_failed_title))
898                     .setIcon(android.R.drawable.ic_dialog_alert)
899                     .setMessage(message)
900                     .setOnCancelListener(mCancelListener)
901                     .setPositiveButton(android.R.string.ok, mCancelListener);
902                 return builder.create();
903             }
904         }
905 
906         return super.onCreateDialog(resId);
907     }
908 
909     @Override
onPause()910     protected void onPause() {
911         super.onPause();
912         if (mVCardReadThread != null) {
913             // The Activity is no longer visible. Stop the thread.
914             mVCardReadThread.cancel();
915             mVCardReadThread = null;
916         }
917 
918         // ImportVCardActivity should not be persistent. In other words, if there's some
919         // event calling onPause(), this Activity should finish its work and give the main
920         // screen back to the caller Activity.
921         if (!isFinishing()) {
922             finish();
923         }
924     }
925 
926     @Override
onDestroy()927     protected void onDestroy() {
928         // The code assumes the handler runs on the UI thread. If not,
929         // clearing the message queue is not enough, one would have to
930         // make sure that the handler does not run any callback when
931         // this activity isFinishing().
932 
933         // Need to make sure any worker thread is done before we flush and
934         // nullify the message handler.
935         if (mVCardReadThread != null) {
936             Log.w(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!");
937             mVCardReadThread.cancel();
938             int attempts = 0;
939             while (mVCardReadThread.isAlive() && attempts < 10) {
940                 try {
941                     Thread.currentThread().sleep(20);
942                 } catch (InterruptedException ie) {
943                     // Keep on going until max attempts is reached.
944                 }
945                 attempts++;
946             }
947             if (mVCardReadThread.isAlive()) {
948                 // Find out why the thread did not exit in a timely
949                 // fashion. Last resort: increase the sleep duration
950                 // and/or the number of attempts.
951                 Log.e(LOG_TAG, "VCardReadThread is still alive after max attempts.");
952             }
953             mVCardReadThread = null;
954         }
955 
956         // Callbacks messages have what == 0.
957         if (mHandler.hasMessages(0)) {
958             mHandler.removeMessages(0);
959         }
960 
961         mHandler = null;  // Prevents memory leaks by breaking any circular dependency.
962         super.onDestroy();
963     }
964 
965     /**
966      * Tries to run a given Runnable object when the UI thread can. Ignore it otherwise
967      */
runOnUIThread(Runnable runnable)968     private void runOnUIThread(Runnable runnable) {
969         if (mHandler == null) {
970             Log.w(LOG_TAG, "Handler object is null. No dialog is shown.");
971         } else {
972             mHandler.post(runnable);
973         }
974     }
975 
976     @Override
finalize()977     public void finalize() {
978         // TODO: This should not be needed. Throw exception instead.
979         if (mVCardReadThread != null) {
980             // Not sure this procedure is really needed, but just in case...
981             Log.e(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!");
982             mVCardReadThread.cancel();
983             mVCardReadThread = null;
984         }
985     }
986 
987     /**
988      * Scans vCard in external storage (typically SDCard) and tries to import it.
989      * - When there's no SDCard available, an error dialog is shown.
990      * - When multiple vCard files are available, asks a user to select one.
991      */
doScanExternalStorageAndImportVCard()992     private void doScanExternalStorageAndImportVCard() {
993         // TODO: should use getExternalStorageState().
994         final File file = Environment.getExternalStorageDirectory();
995         if (!file.exists() || !file.isDirectory() || !file.canRead()) {
996             showDialog(R.id.dialog_sdcard_not_found);
997         } else {
998             mVCardScanThread = new VCardScanThread(file);
999             showDialog(R.id.dialog_searching_vcard);
1000         }
1001     }
1002 }
1003