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