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