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