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