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