ImportVCardActivity.java revision 1b918e58f4a3ae8d32af83f6f69bbf2de57a94f9
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.contacts.vcard; 18 19import android.accounts.Account; 20import android.app.Activity; 21import android.app.AlertDialog; 22import android.app.Dialog; 23import android.app.ProgressDialog; 24import android.content.ComponentName; 25import android.content.ContentResolver; 26import android.content.Context; 27import android.content.DialogInterface; 28import android.content.Intent; 29import android.content.ServiceConnection; 30import android.content.DialogInterface.OnCancelListener; 31import android.content.DialogInterface.OnClickListener; 32import android.net.Uri; 33import android.os.Bundle; 34import android.os.Environment; 35import android.os.Handler; 36import android.os.IBinder; 37import android.os.Message; 38import android.os.Messenger; 39import android.os.PowerManager; 40import android.os.RemoteException; 41import android.text.SpannableStringBuilder; 42import android.text.Spanned; 43import android.text.TextUtils; 44import android.text.style.RelativeSizeSpan; 45import android.util.Log; 46 47import com.android.contacts.R; 48import com.android.contacts.model.Sources; 49import com.android.contacts.util.AccountSelectionUtil; 50import com.android.vcard.VCardEntryCounter; 51import com.android.vcard.VCardInterpreterCollection; 52import com.android.vcard.VCardParser; 53import com.android.vcard.VCardParser_V21; 54import com.android.vcard.VCardParser_V30; 55import com.android.vcard.VCardSourceDetector; 56import com.android.vcard.exception.VCardException; 57import com.android.vcard.exception.VCardNestedException; 58import com.android.vcard.exception.VCardVersionException; 59 60import java.io.File; 61import java.io.IOException; 62import java.io.InputStream; 63import java.nio.ByteBuffer; 64import java.nio.channels.Channels; 65import java.nio.channels.ReadableByteChannel; 66import java.nio.channels.WritableByteChannel; 67import java.text.DateFormat; 68import java.text.SimpleDateFormat; 69import java.util.ArrayList; 70import java.util.Arrays; 71import java.util.Date; 72import java.util.HashSet; 73import java.util.LinkedList; 74import java.util.List; 75import java.util.Queue; 76import java.util.Set; 77import java.util.Vector; 78 79/** 80 * The class letting users to import vCard. This includes the UI part for letting them select 81 * an Account and posssibly a file if there's no Uri is given from its caller Activity. 82 * 83 * Note that this Activity assumes that the instance is a "one-shot Activity", which will be 84 * finished (with the method {@link Activity#finish()}) after the import and never reuse 85 * any Dialog in the instance. So this code is careless about the management around managed 86 * dialogs stuffs (like how onCreateDialog() is used). 87 */ 88public class ImportVCardActivity extends Activity { 89 private static final String LOG_TAG = "ImportVCardActivity"; 90 91 private static final int SELECT_ACCOUNT = 0; 92 93 /* package */ static final String VCARD_URI_ARRAY = "vcard_uri"; 94 /* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type"; 95 /* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset"; 96 /* package */ static final String VCARD_VERSION_ARRAY = "vcard_version"; 97 /* package */ static final String ENTRY_COUNT_ARRAY = "entry_count"; 98 99 /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0; 100 /* package */ final static int VCARD_VERSION_V21 = 1; 101 /* package */ final static int VCARD_VERSION_V30 = 2; 102 103 // Run on the UI thread. Must not be null except after onDestroy(). 104 private Handler mHandler = new Handler(); 105 106 private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener; 107 108 private Account mAccount; 109 110 private String mAction; 111 private Uri mUri; 112 113 private ProgressDialog mProgressDialogForScanVCard; 114 private ProgressDialog mProgressDialogForCacheVCard; 115 116 private List<VCardFile> mAllVCardFileList; 117 private VCardScanThread mVCardScanThread; 118 119 private VCardCacheThread mVCardCacheThread; 120 121 private String mErrorMessage; 122 123 private class CustomConnection implements ServiceConnection { 124 private Messenger mMessenger; 125 /** 126 * Stores {@link ImportRequest} objects until actual connection is established. 127 */ 128 private Queue<ImportRequest> mPendingRequests = 129 new LinkedList<ImportRequest>(); 130 131 private boolean mConnected = false; 132 private boolean mNeedFinish = false; 133 134 public void doBindService() { 135 bindService(new Intent(ImportVCardActivity.this, 136 ImportVCardService.class), this, Context.BIND_AUTO_CREATE); 137 } 138 139 public void setNeedFinish() { 140 synchronized (this) { 141 mNeedFinish = true; 142 if (mConnected) { 143 unbindService(this); 144 finish(); 145 } 146 } 147 } 148 149 public synchronized void requestSend(final ImportRequest parameter) { 150 if (mMessenger != null) { 151 sendMessage(parameter); 152 } else { 153 mPendingRequests.add(parameter); 154 } 155 } 156 157 private void sendMessage(final ImportRequest parameter) { 158 try { 159 mMessenger.send(Message.obtain(null, 160 ImportVCardService.MSG_IMPORT_REQUEST, 161 parameter)); 162 } catch (RemoteException e) { 163 Log.e(LOG_TAG, "RemoteException is thrown when trying to import vCard"); 164 runOnUIThread(new DialogDisplayer( 165 getString(R.string.fail_reason_unknown))); 166 } 167 } 168 169 public void onServiceConnected(ComponentName name, IBinder service) { 170 synchronized (this) { 171 mMessenger = new Messenger(service); 172 // Send pending requests thrown from this Activity before an actual connection 173 // is established. 174 while (!mPendingRequests.isEmpty()) { 175 final ImportRequest parameter = mPendingRequests.poll(); 176 if (parameter == null) { 177 throw new NullPointerException(); 178 } 179 sendMessage(parameter); 180 } 181 mConnected = true; 182 if (mNeedFinish) { 183 unbindService(this); 184 finish(); 185 } 186 } 187 } 188 189 public void onServiceDisconnected(ComponentName name) { 190 synchronized (this) { 191 if (!mPendingRequests.isEmpty()) { 192 Log.w(LOG_TAG, "Some request(s) are dropped."); 193 } 194 // Set to null so that we can detect inappropriate re-connection toward 195 // the Service via NullPointerException; 196 mPendingRequests = null; 197 mMessenger = null; 198 } 199 } 200 } 201 202 private final CustomConnection mConnection = new CustomConnection(); 203 204 private static class VCardFile { 205 private final String mName; 206 private final String mCanonicalPath; 207 private final long mLastModified; 208 209 public VCardFile(String name, String canonicalPath, long lastModified) { 210 mName = name; 211 mCanonicalPath = canonicalPath; 212 mLastModified = lastModified; 213 } 214 215 public String getName() { 216 return mName; 217 } 218 219 public String getCanonicalPath() { 220 return mCanonicalPath; 221 } 222 223 public long getLastModified() { 224 return mLastModified; 225 } 226 } 227 228 // Runs on the UI thread. 229 private class DialogDisplayer implements Runnable { 230 private final int mResId; 231 public DialogDisplayer(int resId) { 232 mResId = resId; 233 } 234 public DialogDisplayer(String errorMessage) { 235 mResId = R.id.dialog_error_with_message; 236 mErrorMessage = errorMessage; 237 } 238 public void run() { 239 showDialog(mResId); 240 } 241 } 242 243 private class CancelListener 244 implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { 245 public void onClick(DialogInterface dialog, int which) { 246 finish(); 247 } 248 249 public void onCancel(DialogInterface dialog) { 250 finish(); 251 } 252 } 253 254 private CancelListener mCancelListener = new CancelListener(); 255 256 /** 257 * Caches all vCard data into local data directory so that we allow 258 * {@link ImportVCardService} to access all the contents in given Uris, some of 259 * which may not be accessible from other components due to permission problem. 260 * (Activity which gives the Uri may allow only this Activity to access that content, 261 * not the ohter components like {@link ImportVCardService}. 262 * 263 * We also allow the Service to happen to exit during the vCard import procedure. 264 */ 265 private class VCardCacheThread extends Thread 266 implements DialogInterface.OnCancelListener { 267 private static final String CACHE_FILE_PREFIX = "import_tmp_"; 268 private boolean mCanceled; 269 private PowerManager.WakeLock mWakeLock; 270 private VCardParser mVCardParser; 271 private final Uri[] mSourceUris; 272 273 public VCardCacheThread(final Uri[] sourceUris) { 274 mSourceUris = sourceUris; 275 final int length = sourceUris.length; 276 final Context context = ImportVCardActivity.this; 277 final PowerManager powerManager = 278 (PowerManager)context.getSystemService(Context.POWER_SERVICE); 279 mWakeLock = powerManager.newWakeLock( 280 PowerManager.SCREEN_DIM_WAKE_LOCK | 281 PowerManager.ON_AFTER_RELEASE, LOG_TAG); 282 } 283 284 @Override 285 public void finalize() { 286 if (mWakeLock != null && mWakeLock.isHeld()) { 287 mWakeLock.release(); 288 } 289 } 290 291 @Override 292 public void run() { 293 final Context context = ImportVCardActivity.this; 294 final ContentResolver resolver = context.getContentResolver(); 295 String errorMessage = null; 296 mWakeLock.acquire(); 297 boolean needFinish = true; 298 try { 299 clearOldCache(); 300 mConnection.doBindService(); 301 302 final int length = mSourceUris.length; 303 // Uris given from caller applications may not be opened twice: consider when 304 // it is not from local storage (e.g. "file:///...") but from some special 305 // provider (e.g. "content://..."). 306 // Thus we have to once copy the content of Uri into local storage, and read 307 // it after it. This copy is also useful fro the view of stability of the import, 308 // as we are able to restore the procedure even when it is aborted during it. 309 // Imagine the case the importer encountered memory-low situation when 310 // reading 10th entry of a vCard file. 311 // 312 // We may be able to read content of each vCard file during copying them 313 // to local storage, but currently vCard code does not allow us to do so. 314 for (int i = 0; i < length; i++) { 315 final Uri sourceUri = mSourceUris[i]; 316 final Uri localDataUri = copyToLocal(sourceUri, i); 317 if (mCanceled) { 318 break; 319 } 320 if (localDataUri == null) { 321 Log.w(LOG_TAG, "destUri is null"); 322 break; 323 } 324 final ImportRequest parameter = constructRequestParameter(localDataUri); 325 if (mCanceled) { 326 return; 327 } 328 mConnection.requestSend(parameter); 329 } 330 } catch (OutOfMemoryError e) { 331 Log.e(LOG_TAG, "OutOfMemoryError"); 332 // We should take care of this case since Android devices may have 333 // smaller memory than we usually expect. 334 System.gc(); 335 needFinish = false; 336 unbindService(mConnection); 337 runOnUIThread(new DialogDisplayer( 338 getString(R.string.fail_reason_low_memory_during_import))); 339 } catch (IOException e) { 340 Log.e(LOG_TAG, e.getMessage()); 341 needFinish = false; 342 unbindService(mConnection); 343 runOnUIThread(new DialogDisplayer( 344 getString(R.string.fail_reason_io_error))); 345 } finally { 346 mWakeLock.release(); 347 mProgressDialogForCacheVCard.dismiss(); 348 if (needFinish) { 349 mConnection.setNeedFinish(); 350 } 351 } 352 } 353 354 /** 355 * Copy the content of sourceUri to local storage. 356 */ 357 private Uri copyToLocal(final Uri sourceUri, int i) throws IOException { 358 final Context context = ImportVCardActivity.this; 359 final ContentResolver resolver = context.getContentResolver(); 360 ReadableByteChannel inputChannel = null; 361 WritableByteChannel outputChannel = null; 362 Uri destUri; 363 try { 364 // XXX: better way to copy stream? 365 { 366 inputChannel = Channels.newChannel(resolver.openInputStream(mSourceUris[i])); 367 final String filename = CACHE_FILE_PREFIX + i + ".vcf"; 368 destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString()); 369 outputChannel = 370 context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel(); 371 final ByteBuffer buffer = ByteBuffer.allocateDirect(8192); 372 while (inputChannel.read(buffer) != -1) { 373 if (mCanceled) { 374 Log.d(LOG_TAG, "Canceled during caching " + mSourceUris[i]); 375 return null; 376 } 377 buffer.flip(); 378 outputChannel.write(buffer); 379 buffer.compact(); 380 } 381 buffer.flip(); 382 while (buffer.hasRemaining()) { 383 outputChannel.write(buffer); 384 } 385 } 386 } finally { 387 if (inputChannel != null) { 388 try { 389 inputChannel.close(); 390 } catch (IOException e) { 391 Log.w(LOG_TAG, "Failed to close inputChannel."); 392 } 393 } 394 if (outputChannel != null) { 395 try { 396 outputChannel.close(); 397 } catch(IOException e) { 398 Log.w(LOG_TAG, "Failed to close outputChannel"); 399 } 400 } 401 } 402 return destUri; 403 } 404 405 /** 406 * Reads the Uri once (or twice) and constructs {@link ImportRequest} from 407 * its content. 408 */ 409 private ImportRequest constructRequestParameter(final Uri uri) { 410 final ContentResolver resolver = 411 ImportVCardActivity.this.getContentResolver(); 412 VCardEntryCounter counter = null; 413 VCardSourceDetector detector = null; 414 VCardInterpreterCollection interpreter = null; 415 int vcardVersion = VCARD_VERSION_V21; 416 try { 417 boolean shouldUseV30 = false; 418 InputStream is; 419 420 is = resolver.openInputStream(uri); 421 mVCardParser = new VCardParser_V21(); 422 try { 423 counter = new VCardEntryCounter(); 424 detector = new VCardSourceDetector(); 425 interpreter = 426 new VCardInterpreterCollection( 427 Arrays.asList(counter, detector)); 428 mVCardParser.parse(is, interpreter); 429 } catch (VCardVersionException e1) { 430 try { 431 is.close(); 432 } catch (IOException e) { 433 } 434 435 shouldUseV30 = true; 436 is = resolver.openInputStream(uri); 437 mVCardParser = new VCardParser_V30(); 438 try { 439 counter = new VCardEntryCounter(); 440 detector = new VCardSourceDetector(); 441 interpreter = 442 new VCardInterpreterCollection( 443 Arrays.asList(counter, detector)); 444 mVCardParser.parse(is, interpreter); 445 } catch (VCardVersionException e2) { 446 throw new VCardException("vCard with unspported version."); 447 } 448 } finally { 449 if (is != null) { 450 try { 451 is.close(); 452 } catch (IOException e) { 453 } 454 } 455 } 456 457 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21; 458 } catch (VCardNestedException e) { 459 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive)."); 460 // Go through without returning null. 461 } catch (VCardException e) { 462 Log.e(LOG_TAG, e.getMessage()); 463 return null; 464 } catch (IOException e) { 465 Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage()); 466 return null; 467 } 468 return new ImportRequest(mAccount, uri, 469 detector.getEstimatedType(), 470 detector.getEstimatedCharset(), 471 vcardVersion, counter.getCount()); 472 } 473 474 /** 475 * We (currently) don't have any way to clean up cache files used in the previous 476 * import process, 477 * TODO(dmiyakawa): Can we do it after Service being done? 478 */ 479 private void clearOldCache() { 480 final Context context = ImportVCardActivity.this; 481 final String[] fileLists = context.fileList(); 482 for (String fileName : fileLists) { 483 if (fileName.startsWith(CACHE_FILE_PREFIX)) { 484 Log.d(LOG_TAG, "Remove temporary file: " + fileName); 485 context.deleteFile(fileName); 486 } 487 } 488 } 489 490 public void cancel() { 491 mCanceled = true; 492 if (mVCardParser != null) { 493 mVCardParser.cancel(); 494 } 495 } 496 497 public void onCancel(DialogInterface dialog) { 498 cancel(); 499 } 500 } 501 502 private class ImportTypeSelectedListener implements 503 DialogInterface.OnClickListener { 504 public static final int IMPORT_ONE = 0; 505 public static final int IMPORT_MULTIPLE = 1; 506 public static final int IMPORT_ALL = 2; 507 public static final int IMPORT_TYPE_SIZE = 3; 508 509 private int mCurrentIndex; 510 511 public void onClick(DialogInterface dialog, int which) { 512 if (which == DialogInterface.BUTTON_POSITIVE) { 513 switch (mCurrentIndex) { 514 case IMPORT_ALL: 515 importVCardFromSDCard(mAllVCardFileList); 516 break; 517 case IMPORT_MULTIPLE: 518 showDialog(R.id.dialog_select_multiple_vcard); 519 break; 520 default: 521 showDialog(R.id.dialog_select_one_vcard); 522 break; 523 } 524 } else if (which == DialogInterface.BUTTON_NEGATIVE) { 525 finish(); 526 } else { 527 mCurrentIndex = which; 528 } 529 } 530 } 531 532 private class VCardSelectedListener implements 533 DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener { 534 private int mCurrentIndex; 535 private Set<Integer> mSelectedIndexSet; 536 537 public VCardSelectedListener(boolean multipleSelect) { 538 mCurrentIndex = 0; 539 if (multipleSelect) { 540 mSelectedIndexSet = new HashSet<Integer>(); 541 } 542 } 543 544 public void onClick(DialogInterface dialog, int which) { 545 if (which == DialogInterface.BUTTON_POSITIVE) { 546 if (mSelectedIndexSet != null) { 547 List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>(); 548 final int size = mAllVCardFileList.size(); 549 // We'd like to sort the files by its index, so we do not use Set iterator. 550 for (int i = 0; i < size; i++) { 551 if (mSelectedIndexSet.contains(i)) { 552 selectedVCardFileList.add(mAllVCardFileList.get(i)); 553 } 554 } 555 importVCardFromSDCard(selectedVCardFileList); 556 } else { 557 importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex)); 558 } 559 } else if (which == DialogInterface.BUTTON_NEGATIVE) { 560 finish(); 561 } else { 562 // Some file is selected. 563 mCurrentIndex = which; 564 if (mSelectedIndexSet != null) { 565 if (mSelectedIndexSet.contains(which)) { 566 mSelectedIndexSet.remove(which); 567 } else { 568 mSelectedIndexSet.add(which); 569 } 570 } 571 } 572 } 573 574 public void onClick(DialogInterface dialog, int which, boolean isChecked) { 575 if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) { 576 Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which, 577 mAllVCardFileList.get(which).getCanonicalPath())); 578 } else { 579 onClick(dialog, which); 580 } 581 } 582 } 583 584 /** 585 * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select 586 * a vCard file is shown. After the choice, VCardReadThread starts running. 587 */ 588 private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener { 589 private boolean mCanceled; 590 private boolean mGotIOException; 591 private File mRootDirectory; 592 593 // To avoid recursive link. 594 private Set<String> mCheckedPaths; 595 private PowerManager.WakeLock mWakeLock; 596 597 private class CanceledException extends Exception { 598 } 599 600 public VCardScanThread(File sdcardDirectory) { 601 mCanceled = false; 602 mGotIOException = false; 603 mRootDirectory = sdcardDirectory; 604 mCheckedPaths = new HashSet<String>(); 605 PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService( 606 Context.POWER_SERVICE); 607 mWakeLock = powerManager.newWakeLock( 608 PowerManager.SCREEN_DIM_WAKE_LOCK | 609 PowerManager.ON_AFTER_RELEASE, LOG_TAG); 610 } 611 612 @Override 613 public void run() { 614 mAllVCardFileList = new Vector<VCardFile>(); 615 try { 616 mWakeLock.acquire(); 617 getVCardFileRecursively(mRootDirectory); 618 } catch (CanceledException e) { 619 mCanceled = true; 620 } catch (IOException e) { 621 mGotIOException = true; 622 } finally { 623 mWakeLock.release(); 624 } 625 626 if (mCanceled) { 627 mAllVCardFileList = null; 628 } 629 630 mProgressDialogForScanVCard.dismiss(); 631 mProgressDialogForScanVCard = null; 632 633 if (mGotIOException) { 634 runOnUIThread(new DialogDisplayer(R.id.dialog_io_exception)); 635 } else if (mCanceled) { 636 finish(); 637 } else { 638 int size = mAllVCardFileList.size(); 639 final Context context = ImportVCardActivity.this; 640 if (size == 0) { 641 runOnUIThread(new DialogDisplayer(R.id.dialog_vcard_not_found)); 642 } else { 643 startVCardSelectAndImport(); 644 } 645 } 646 } 647 648 private void getVCardFileRecursively(File directory) 649 throws CanceledException, IOException { 650 if (mCanceled) { 651 throw new CanceledException(); 652 } 653 654 // e.g. secured directory may return null toward listFiles(). 655 final File[] files = directory.listFiles(); 656 if (files == null) { 657 Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")"); 658 return; 659 } 660 for (File file : directory.listFiles()) { 661 if (mCanceled) { 662 throw new CanceledException(); 663 } 664 String canonicalPath = file.getCanonicalPath(); 665 if (mCheckedPaths.contains(canonicalPath)) { 666 continue; 667 } 668 669 mCheckedPaths.add(canonicalPath); 670 671 if (file.isDirectory()) { 672 getVCardFileRecursively(file); 673 } else if (canonicalPath.toLowerCase().endsWith(".vcf") && 674 file.canRead()){ 675 String fileName = file.getName(); 676 VCardFile vcardFile = new VCardFile( 677 fileName, canonicalPath, file.lastModified()); 678 mAllVCardFileList.add(vcardFile); 679 } 680 } 681 } 682 683 public void onCancel(DialogInterface dialog) { 684 mCanceled = true; 685 } 686 687 public void onClick(DialogInterface dialog, int which) { 688 if (which == DialogInterface.BUTTON_NEGATIVE) { 689 mCanceled = true; 690 } 691 } 692 } 693 694 private void startVCardSelectAndImport() { 695 int size = mAllVCardFileList.size(); 696 if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) || 697 size == 1) { 698 importVCardFromSDCard(mAllVCardFileList); 699 } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) { 700 runOnUIThread(new DialogDisplayer(R.id.dialog_select_import_type)); 701 } else { 702 runOnUIThread(new DialogDisplayer(R.id.dialog_select_one_vcard)); 703 } 704 } 705 706 private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) { 707 final int size = selectedVCardFileList.size(); 708 String[] uriStrings = new String[size]; 709 int i = 0; 710 for (VCardFile vcardFile : selectedVCardFileList) { 711 uriStrings[i] = "file://" + vcardFile.getCanonicalPath(); 712 i++; 713 } 714 importVCard(uriStrings); 715 } 716 717 private void importVCardFromSDCard(final VCardFile vcardFile) { 718 importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())}); 719 } 720 721 private void importVCard(final Uri uri) { 722 importVCard(new Uri[] {uri}); 723 } 724 725 private void importVCard(final String[] uriStrings) { 726 final int length = uriStrings.length; 727 final Uri[] uris = new Uri[length]; 728 for (int i = 0; i < length; i++) { 729 uris[i] = Uri.parse(uriStrings[i]); 730 } 731 importVCard(uris); 732 } 733 734 private void importVCard(final Uri[] uris) { 735 runOnUIThread(new Runnable() { 736 public void run() { 737 mVCardCacheThread = new VCardCacheThread(uris); 738 showDialog(R.id.dialog_cache_vcard); 739 } 740 }); 741 } 742 743 private Dialog getSelectImportTypeDialog() { 744 final DialogInterface.OnClickListener listener = new ImportTypeSelectedListener(); 745 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 746 .setTitle(R.string.select_vcard_title) 747 .setPositiveButton(android.R.string.ok, listener) 748 .setOnCancelListener(mCancelListener) 749 .setNegativeButton(android.R.string.cancel, mCancelListener); 750 751 final String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE]; 752 items[ImportTypeSelectedListener.IMPORT_ONE] = 753 getString(R.string.import_one_vcard_string); 754 items[ImportTypeSelectedListener.IMPORT_MULTIPLE] = 755 getString(R.string.import_multiple_vcard_string); 756 items[ImportTypeSelectedListener.IMPORT_ALL] = 757 getString(R.string.import_all_vcard_string); 758 builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener); 759 return builder.create(); 760 } 761 762 private Dialog getVCardFileSelectDialog(boolean multipleSelect) { 763 final int size = mAllVCardFileList.size(); 764 final VCardSelectedListener listener = new VCardSelectedListener(multipleSelect); 765 final AlertDialog.Builder builder = 766 new AlertDialog.Builder(this) 767 .setTitle(R.string.select_vcard_title) 768 .setPositiveButton(android.R.string.ok, listener) 769 .setOnCancelListener(mCancelListener) 770 .setNegativeButton(android.R.string.cancel, mCancelListener); 771 772 CharSequence[] items = new CharSequence[size]; 773 DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 774 for (int i = 0; i < size; i++) { 775 VCardFile vcardFile = mAllVCardFileList.get(i); 776 SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); 777 stringBuilder.append(vcardFile.getName()); 778 stringBuilder.append('\n'); 779 int indexToBeSpanned = stringBuilder.length(); 780 // Smaller date text looks better, since each file name becomes easier to read. 781 // The value set to RelativeSizeSpan is arbitrary. You can change it to any other 782 // value (but the value bigger than 1.0f would not make nice appearance :) 783 stringBuilder.append( 784 "(" + dateFormat.format(new Date(vcardFile.getLastModified())) + ")"); 785 stringBuilder.setSpan( 786 new RelativeSizeSpan(0.7f), indexToBeSpanned, stringBuilder.length(), 787 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 788 items[i] = stringBuilder; 789 } 790 if (multipleSelect) { 791 builder.setMultiChoiceItems(items, (boolean[])null, listener); 792 } else { 793 builder.setSingleChoiceItems(items, 0, listener); 794 } 795 return builder.create(); 796 } 797 798 @Override 799 protected void onCreate(Bundle bundle) { 800 super.onCreate(bundle); 801 802 String accountName = null; 803 String accountType = null; 804 final Intent intent = getIntent(); 805 if (intent != null) { 806 accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME); 807 accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE); 808 mAction = intent.getAction(); 809 mUri = intent.getData(); 810 } else { 811 Log.e(LOG_TAG, "intent does not exist"); 812 } 813 814 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 815 mAccount = new Account(accountName, accountType); 816 } else { 817 final Sources sources = Sources.getInstance(this); 818 final List<Account> accountList = sources.getAccounts(true); 819 if (accountList.size() == 0) { 820 mAccount = null; 821 } else if (accountList.size() == 1) { 822 mAccount = accountList.get(0); 823 } else { 824 startActivityForResult(new Intent(this, SelectAccountActivity.class), 825 SELECT_ACCOUNT); 826 return; 827 } 828 } 829 830 startImport(mAction, mUri); 831 } 832 833 @Override 834 public void onActivityResult(int requestCode, int resultCode, Intent intent) { 835 if (requestCode == SELECT_ACCOUNT) { 836 if (resultCode == RESULT_OK) { 837 mAccount = new Account( 838 intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME), 839 intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE)); 840 startImport(mAction, mUri); 841 } else { 842 if (resultCode != RESULT_CANCELED) { 843 Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode); 844 } 845 finish(); 846 } 847 } 848 } 849 850 private void startImport(String action, Uri uri) { 851 Log.d(LOG_TAG, "action = " + action + " ; path = " + uri); 852 853 if (uri != null) { 854 importVCard(uri); 855 } else { 856 doScanExternalStorageAndImportVCard(); 857 } 858 } 859 860 @Override 861 protected Dialog onCreateDialog(int resId, Bundle bundle) { 862 switch (resId) { 863 case R.string.import_from_sdcard: { 864 if (mAccountSelectionListener == null) { 865 throw new NullPointerException( 866 "mAccountSelectionListener must not be null."); 867 } 868 return AccountSelectionUtil.getSelectAccountDialog(this, resId, 869 mAccountSelectionListener, mCancelListener); 870 } 871 case R.id.dialog_searching_vcard: { 872 if (mProgressDialogForScanVCard == null) { 873 String title = getString(R.string.searching_vcard_title); 874 String message = getString(R.string.searching_vcard_message); 875 mProgressDialogForScanVCard = 876 ProgressDialog.show(this, title, message, true, false); 877 mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread); 878 mVCardScanThread.start(); 879 } 880 return mProgressDialogForScanVCard; 881 } 882 case R.id.dialog_sdcard_not_found: { 883 AlertDialog.Builder builder = new AlertDialog.Builder(this) 884 .setTitle(R.string.no_sdcard_title) 885 .setIcon(android.R.drawable.ic_dialog_alert) 886 .setMessage(R.string.no_sdcard_message) 887 .setOnCancelListener(mCancelListener) 888 .setPositiveButton(android.R.string.ok, mCancelListener); 889 return builder.create(); 890 } 891 case R.id.dialog_vcard_not_found: { 892 String message = (getString(R.string.scanning_sdcard_failed_message, 893 getString(R.string.fail_reason_no_vcard_file))); 894 AlertDialog.Builder builder = new AlertDialog.Builder(this) 895 .setTitle(R.string.scanning_sdcard_failed_title) 896 .setMessage(message) 897 .setOnCancelListener(mCancelListener) 898 .setPositiveButton(android.R.string.ok, mCancelListener); 899 return builder.create(); 900 } 901 case R.id.dialog_select_import_type: { 902 return getSelectImportTypeDialog(); 903 } 904 case R.id.dialog_select_multiple_vcard: { 905 return getVCardFileSelectDialog(true); 906 } 907 case R.id.dialog_select_one_vcard: { 908 return getVCardFileSelectDialog(false); 909 } 910 case R.id.dialog_cache_vcard: { 911 if (mProgressDialogForCacheVCard == null) { 912 final String title = getString(R.string.caching_vcard_title); 913 final String message = getString(R.string.caching_vcard_message); 914 mProgressDialogForCacheVCard = new ProgressDialog(this); 915 mProgressDialogForCacheVCard.setTitle(title); 916 mProgressDialogForCacheVCard.setMessage(message); 917 mProgressDialogForCacheVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER); 918 mProgressDialogForCacheVCard.setOnCancelListener(mVCardCacheThread); 919 mVCardCacheThread.start(); 920 } 921 return mProgressDialogForCacheVCard; 922 } 923 case R.id.dialog_io_exception: { 924 String message = (getString(R.string.scanning_sdcard_failed_message, 925 getString(R.string.fail_reason_io_error))); 926 AlertDialog.Builder builder = new AlertDialog.Builder(this) 927 .setTitle(R.string.scanning_sdcard_failed_title) 928 .setIcon(android.R.drawable.ic_dialog_alert) 929 .setMessage(message) 930 .setOnCancelListener(mCancelListener) 931 .setPositiveButton(android.R.string.ok, mCancelListener); 932 return builder.create(); 933 } 934 case R.id.dialog_error_with_message: { 935 String message = mErrorMessage; 936 if (TextUtils.isEmpty(message)) { 937 Log.e(LOG_TAG, "Error message is null while it must not."); 938 message = getString(R.string.fail_reason_unknown); 939 } 940 AlertDialog.Builder builder = new AlertDialog.Builder(this) 941 .setTitle(getString(R.string.reading_vcard_failed_title)) 942 .setIcon(android.R.drawable.ic_dialog_alert) 943 .setMessage(message) 944 .setOnCancelListener(mCancelListener) 945 .setPositiveButton(android.R.string.ok, mCancelListener); 946 return builder.create(); 947 } 948 } 949 950 return super.onCreateDialog(resId, bundle); 951 } 952 953 @Override 954 protected void onPause() { 955 super.onPause(); 956 957 // ImportVCardActivity should not be persistent. In other words, if there's some 958 // event calling onPause(), this Activity should finish its work and give the main 959 // screen back to the caller Activity. 960 if (!isFinishing()) { 961 finish(); 962 } 963 } 964 965 @Override 966 protected void onDestroy() { 967 // The code assumes the handler runs on the UI thread. If not, 968 // clearing the message queue is not enough, one would have to 969 // make sure that the handler does not run any callback when 970 // this activity isFinishing(). 971 972 // Callbacks messages have what == 0. 973 if (mHandler.hasMessages(0)) { 974 mHandler.removeMessages(0); 975 } 976 977 mHandler = null; // Prevents memory leaks by breaking any circular dependency. 978 super.onDestroy(); 979 } 980 981 /** 982 * Tries to run a given Runnable object when the UI thread can. Ignore it otherwise 983 */ 984 private void runOnUIThread(Runnable runnable) { 985 if (mHandler == null) { 986 Log.w(LOG_TAG, "Handler object is null. No dialog is shown."); 987 } else { 988 mHandler.post(runnable); 989 } 990 } 991 992 /** 993 * Scans vCard in external storage (typically SDCard) and tries to import it. 994 * - When there's no SDCard available, an error dialog is shown. 995 * - When multiple vCard files are available, asks a user to select one. 996 */ 997 private void doScanExternalStorageAndImportVCard() { 998 // TODO: should use getExternalStorageState(). 999 final File file = Environment.getExternalStorageDirectory(); 1000 if (!file.exists() || !file.isDirectory() || !file.canRead()) { 1001 showDialog(R.id.dialog_sdcard_not_found); 1002 } else { 1003 mVCardScanThread = new VCardScanThread(file); 1004 showDialog(R.id.dialog_searching_vcard); 1005 } 1006 } 1007} 1008