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