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