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