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.vcard; 18 19 import com.android.contacts.ContactsActivity; 20 import com.android.contacts.R; 21 import com.android.contacts.model.AccountTypeManager; 22 import com.android.contacts.model.AccountWithDataSet; 23 import com.android.contacts.util.AccountSelectionUtil; 24 import com.android.vcard.VCardEntryCounter; 25 import com.android.vcard.VCardParser; 26 import com.android.vcard.VCardParser_V21; 27 import com.android.vcard.VCardParser_V30; 28 import com.android.vcard.VCardSourceDetector; 29 import com.android.vcard.exception.VCardException; 30 import com.android.vcard.exception.VCardNestedException; 31 import com.android.vcard.exception.VCardVersionException; 32 33 import android.app.Activity; 34 import android.app.AlertDialog; 35 import android.app.Dialog; 36 import android.app.Notification; 37 import android.app.NotificationManager; 38 import android.app.ProgressDialog; 39 import android.content.ComponentName; 40 import android.content.ContentResolver; 41 import android.content.Context; 42 import android.content.DialogInterface; 43 import android.content.DialogInterface.OnCancelListener; 44 import android.content.DialogInterface.OnClickListener; 45 import android.content.Intent; 46 import android.content.ServiceConnection; 47 import android.content.res.Configuration; 48 import android.database.Cursor; 49 import android.net.Uri; 50 import android.os.Bundle; 51 import android.os.Environment; 52 import android.os.Handler; 53 import android.os.IBinder; 54 import android.os.PowerManager; 55 import android.provider.OpenableColumns; 56 import android.text.SpannableStringBuilder; 57 import android.text.Spanned; 58 import android.text.TextUtils; 59 import android.text.style.RelativeSizeSpan; 60 import android.util.Log; 61 import android.widget.Toast; 62 63 import java.io.ByteArrayInputStream; 64 import java.io.File; 65 import java.io.IOException; 66 import java.io.InputStream; 67 import java.nio.ByteBuffer; 68 import java.nio.channels.Channels; 69 import java.nio.channels.ReadableByteChannel; 70 import java.nio.channels.WritableByteChannel; 71 import java.text.DateFormat; 72 import java.text.SimpleDateFormat; 73 import java.util.ArrayList; 74 import java.util.Arrays; 75 import java.util.Date; 76 import java.util.HashSet; 77 import java.util.List; 78 import java.util.Set; 79 import java.util.Vector; 80 81 /** 82 * The class letting users to import vCard. This includes the UI part for letting them select 83 * an Account and posssibly a file if there's no Uri is given from its caller Activity. 84 * 85 * Note that this Activity assumes that the instance is a "one-shot Activity", which will be 86 * finished (with the method {@link Activity#finish()}) after the import and never reuse 87 * any Dialog in the instance. So this code is careless about the management around managed 88 * dialogs stuffs (like how onCreateDialog() is used). 89 */ 90 public class ImportVCardActivity extends ContactsActivity { 91 private static final String LOG_TAG = "VCardImport"; 92 93 private static final int SELECT_ACCOUNT = 0; 94 95 /* package */ static final String VCARD_URI_ARRAY = "vcard_uri"; 96 /* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type"; 97 /* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset"; 98 /* package */ static final String VCARD_VERSION_ARRAY = "vcard_version"; 99 /* package */ static final String ENTRY_COUNT_ARRAY = "entry_count"; 100 101 /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0; 102 /* package */ final static int VCARD_VERSION_V21 = 1; 103 /* package */ final static int VCARD_VERSION_V30 = 2; 104 105 private static final String SECURE_DIRECTORY_NAME = ".android_secure"; 106 107 /** 108 * Notification id used when error happened before sending an import request to VCardServer. 109 */ 110 private static final int FAILURE_NOTIFICATION_ID = 1; 111 112 final static String CACHED_URIS = "cached_uris"; 113 114 private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener; 115 116 private AccountWithDataSet mAccount; 117 118 private ProgressDialog mProgressDialogForScanVCard; 119 private ProgressDialog mProgressDialogForCachingVCard; 120 121 private List<VCardFile> mAllVCardFileList; 122 private VCardScanThread mVCardScanThread; 123 124 private VCardCacheThread mVCardCacheThread; 125 private ImportRequestConnection mConnection; 126 /* package */ VCardImportExportListener mListener; 127 128 private String mErrorMessage; 129 130 private Handler mHandler = new Handler(); 131 132 private static class VCardFile { 133 private final String mName; 134 private final String mCanonicalPath; 135 private final long mLastModified; 136 137 public VCardFile(String name, String canonicalPath, long lastModified) { 138 mName = name; 139 mCanonicalPath = canonicalPath; 140 mLastModified = lastModified; 141 } 142 143 public String getName() { 144 return mName; 145 } 146 147 public String getCanonicalPath() { 148 return mCanonicalPath; 149 } 150 151 public long getLastModified() { 152 return mLastModified; 153 } 154 } 155 156 // Runs on the UI thread. 157 private class DialogDisplayer implements Runnable { 158 private final int mResId; 159 public DialogDisplayer(int resId) { 160 mResId = resId; 161 } 162 public DialogDisplayer(String errorMessage) { 163 mResId = R.id.dialog_error_with_message; 164 mErrorMessage = errorMessage; 165 } 166 @Override 167 public void run() { 168 if (!isFinishing()) { 169 showDialog(mResId); 170 } 171 } 172 } 173 174 private class CancelListener 175 implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { 176 @Override 177 public void onClick(DialogInterface dialog, int which) { 178 finish(); 179 } 180 @Override 181 public void onCancel(DialogInterface dialog) { 182 finish(); 183 } 184 } 185 186 private CancelListener mCancelListener = new CancelListener(); 187 188 private class ImportRequestConnection implements ServiceConnection { 189 private VCardService mService; 190 191 public void sendImportRequest(final List<ImportRequest> requests) { 192 Log.i(LOG_TAG, "Send an import request"); 193 mService.handleImportRequest(requests, mListener); 194 } 195 196 @Override 197 public void onServiceConnected(ComponentName name, IBinder binder) { 198 mService = ((VCardService.MyBinder) binder).getService(); 199 Log.i(LOG_TAG, 200 String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)", 201 Arrays.toString(mVCardCacheThread.getSourceUris()))); 202 mVCardCacheThread.start(); 203 } 204 205 @Override 206 public void onServiceDisconnected(ComponentName name) { 207 Log.i(LOG_TAG, "Disconnected from VCardService"); 208 } 209 } 210 211 /** 212 * Caches given vCard files into a local directory, and sends actual import request to 213 * {@link VCardService}. 214 * 215 * We need to cache given files into local storage. One of reasons is that some data (as Uri) 216 * may have special permissions. Callers may allow only this Activity to access that content, 217 * not what this Activity launched (like {@link VCardService}). 218 */ 219 private class VCardCacheThread extends Thread 220 implements DialogInterface.OnCancelListener { 221 private boolean mCanceled; 222 private PowerManager.WakeLock mWakeLock; 223 private VCardParser mVCardParser; 224 private final Uri[] mSourceUris; // Given from a caller. 225 private final byte[] mSource; 226 private final String mDisplayName; 227 228 public VCardCacheThread(final Uri[] sourceUris) { 229 mSourceUris = sourceUris; 230 mSource = null; 231 final Context context = ImportVCardActivity.this; 232 final PowerManager powerManager = 233 (PowerManager)context.getSystemService(Context.POWER_SERVICE); 234 mWakeLock = powerManager.newWakeLock( 235 PowerManager.SCREEN_DIM_WAKE_LOCK | 236 PowerManager.ON_AFTER_RELEASE, LOG_TAG); 237 mDisplayName = null; 238 } 239 240 @Override 241 public void finalize() { 242 if (mWakeLock != null && mWakeLock.isHeld()) { 243 Log.w(LOG_TAG, "WakeLock is being held."); 244 mWakeLock.release(); 245 } 246 } 247 248 @Override 249 public void run() { 250 Log.i(LOG_TAG, "vCard cache thread starts running."); 251 if (mConnection == null) { 252 throw new NullPointerException("vCard cache thread must be launched " 253 + "after a service connection is established"); 254 } 255 256 mWakeLock.acquire(); 257 try { 258 if (mCanceled == true) { 259 Log.i(LOG_TAG, "vCard cache operation is canceled."); 260 return; 261 } 262 263 final Context context = ImportVCardActivity.this; 264 // Uris given from caller applications may not be opened twice: consider when 265 // it is not from local storage (e.g. "file:///...") but from some special 266 // provider (e.g. "content://..."). 267 // Thus we have to once copy the content of Uri into local storage, and read 268 // it after it. 269 // 270 // We may be able to read content of each vCard file during copying them 271 // to local storage, but currently vCard code does not allow us to do so. 272 int cache_index = 0; 273 ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>(); 274 if (mSource != null) { 275 try { 276 requests.add(constructImportRequest(mSource, null, mDisplayName)); 277 } catch (VCardException e) { 278 Log.e(LOG_TAG, "Maybe the file is in wrong format", e); 279 showFailureNotification(R.string.fail_reason_not_supported); 280 return; 281 } 282 } else { 283 final ContentResolver resolver = 284 ImportVCardActivity.this.getContentResolver(); 285 for (Uri sourceUri : mSourceUris) { 286 String filename = null; 287 // Note: caches are removed by VCardService. 288 while (true) { 289 filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf"; 290 final File file = context.getFileStreamPath(filename); 291 if (!file.exists()) { 292 break; 293 } else { 294 if (cache_index == Integer.MAX_VALUE) { 295 throw new RuntimeException("Exceeded cache limit"); 296 } 297 cache_index++; 298 } 299 } 300 final Uri localDataUri = copyTo(sourceUri, filename); 301 if (mCanceled) { 302 Log.i(LOG_TAG, "vCard cache operation is canceled."); 303 break; 304 } 305 if (localDataUri == null) { 306 Log.w(LOG_TAG, "destUri is null"); 307 break; 308 } 309 310 String displayName = null; 311 Cursor cursor = null; 312 // Try to get a display name from the given Uri. If it fails, we just 313 // pick up the last part of the Uri. 314 try { 315 cursor = resolver.query(sourceUri, 316 new String[] { OpenableColumns.DISPLAY_NAME }, 317 null, null, null); 318 if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) { 319 if (cursor.getCount() > 1) { 320 Log.w(LOG_TAG, "Unexpected multiple rows: " 321 + cursor.getCount()); 322 } 323 int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); 324 if (index >= 0) { 325 displayName = cursor.getString(index); 326 } 327 } 328 } finally { 329 if (cursor != null) { 330 cursor.close(); 331 } 332 } 333 if (TextUtils.isEmpty(displayName)){ 334 displayName = sourceUri.getLastPathSegment(); 335 } 336 337 final ImportRequest request; 338 try { 339 request = constructImportRequest(null, localDataUri, displayName); 340 } catch (VCardException e) { 341 Log.e(LOG_TAG, "Maybe the file is in wrong format", e); 342 showFailureNotification(R.string.fail_reason_not_supported); 343 return; 344 } catch (IOException e) { 345 Log.e(LOG_TAG, "Unexpected IOException", e); 346 showFailureNotification(R.string.fail_reason_io_error); 347 return; 348 } 349 if (mCanceled) { 350 Log.i(LOG_TAG, "vCard cache operation is canceled."); 351 return; 352 } 353 requests.add(request); 354 } 355 } 356 if (!requests.isEmpty()) { 357 mConnection.sendImportRequest(requests); 358 } else { 359 Log.w(LOG_TAG, "Empty import requests. Ignore it."); 360 } 361 } catch (OutOfMemoryError e) { 362 Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard"); 363 System.gc(); 364 runOnUiThread(new DialogDisplayer( 365 getString(R.string.fail_reason_low_memory_during_import))); 366 } catch (IOException e) { 367 Log.e(LOG_TAG, "IOException during caching vCard", e); 368 runOnUiThread(new DialogDisplayer( 369 getString(R.string.fail_reason_io_error))); 370 } finally { 371 Log.i(LOG_TAG, "Finished caching vCard."); 372 mWakeLock.release(); 373 unbindService(mConnection); 374 mProgressDialogForCachingVCard.dismiss(); 375 mProgressDialogForCachingVCard = null; 376 finish(); 377 } 378 } 379 380 /** 381 * Copy the content of sourceUri to the destination. 382 */ 383 private Uri copyTo(final Uri sourceUri, String filename) throws IOException { 384 Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)", 385 sourceUri, filename)); 386 final Context context = ImportVCardActivity.this; 387 final ContentResolver resolver = context.getContentResolver(); 388 ReadableByteChannel inputChannel = null; 389 WritableByteChannel outputChannel = null; 390 Uri destUri = null; 391 try { 392 inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri)); 393 destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString()); 394 outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel(); 395 final ByteBuffer buffer = ByteBuffer.allocateDirect(8192); 396 while (inputChannel.read(buffer) != -1) { 397 if (mCanceled) { 398 Log.d(LOG_TAG, "Canceled during caching " + sourceUri); 399 return null; 400 } 401 buffer.flip(); 402 outputChannel.write(buffer); 403 buffer.compact(); 404 } 405 buffer.flip(); 406 while (buffer.hasRemaining()) { 407 outputChannel.write(buffer); 408 } 409 } finally { 410 if (inputChannel != null) { 411 try { 412 inputChannel.close(); 413 } catch (IOException e) { 414 Log.w(LOG_TAG, "Failed to close inputChannel."); 415 } 416 } 417 if (outputChannel != null) { 418 try { 419 outputChannel.close(); 420 } catch(IOException e) { 421 Log.w(LOG_TAG, "Failed to close outputChannel"); 422 } 423 } 424 } 425 return destUri; 426 } 427 428 /** 429 * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from 430 * its content. 431 * 432 * @arg localDataUri Uri actually used for the import. Should be stored in 433 * app local storage, as we cannot guarantee other types of Uris can be read 434 * multiple times. This variable populates {@link ImportRequest#uri}. 435 * @arg displayName Used for displaying information to the user. This variable populates 436 * {@link ImportRequest#displayName}. 437 */ 438 private ImportRequest constructImportRequest(final byte[] data, 439 final Uri localDataUri, final String displayName) 440 throws IOException, VCardException { 441 final ContentResolver resolver = ImportVCardActivity.this.getContentResolver(); 442 VCardEntryCounter counter = null; 443 VCardSourceDetector detector = null; 444 int vcardVersion = VCARD_VERSION_V21; 445 try { 446 boolean shouldUseV30 = false; 447 InputStream is; 448 if (data != null) { 449 is = new ByteArrayInputStream(data); 450 } else { 451 is = resolver.openInputStream(localDataUri); 452 } 453 mVCardParser = new VCardParser_V21(); 454 try { 455 counter = new VCardEntryCounter(); 456 detector = new VCardSourceDetector(); 457 mVCardParser.addInterpreter(counter); 458 mVCardParser.addInterpreter(detector); 459 mVCardParser.parse(is); 460 } catch (VCardVersionException e1) { 461 try { 462 is.close(); 463 } catch (IOException e) { 464 } 465 466 shouldUseV30 = true; 467 if (data != null) { 468 is = new ByteArrayInputStream(data); 469 } else { 470 is = resolver.openInputStream(localDataUri); 471 } 472 mVCardParser = new VCardParser_V30(); 473 try { 474 counter = new VCardEntryCounter(); 475 detector = new VCardSourceDetector(); 476 mVCardParser.addInterpreter(counter); 477 mVCardParser.addInterpreter(detector); 478 mVCardParser.parse(is); 479 } catch (VCardVersionException e2) { 480 throw new VCardException("vCard with unspported version."); 481 } 482 } finally { 483 if (is != null) { 484 try { 485 is.close(); 486 } catch (IOException e) { 487 } 488 } 489 } 490 491 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21; 492 } catch (VCardNestedException e) { 493 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive)."); 494 // Go through without throwing the Exception, as we may be able to detect the 495 // version before it 496 } 497 return new ImportRequest(mAccount, 498 data, localDataUri, displayName, 499 detector.getEstimatedType(), 500 detector.getEstimatedCharset(), 501 vcardVersion, counter.getCount()); 502 } 503 504 public Uri[] getSourceUris() { 505 return mSourceUris; 506 } 507 508 public void cancel() { 509 mCanceled = true; 510 if (mVCardParser != null) { 511 mVCardParser.cancel(); 512 } 513 } 514 515 @Override 516 public void onCancel(DialogInterface dialog) { 517 Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard."); 518 cancel(); 519 } 520 } 521 522 private class ImportTypeSelectedListener implements 523 DialogInterface.OnClickListener { 524 public static final int IMPORT_ONE = 0; 525 public static final int IMPORT_MULTIPLE = 1; 526 public static final int IMPORT_ALL = 2; 527 public static final int IMPORT_TYPE_SIZE = 3; 528 529 private int mCurrentIndex; 530 531 public void onClick(DialogInterface dialog, int which) { 532 if (which == DialogInterface.BUTTON_POSITIVE) { 533 switch (mCurrentIndex) { 534 case IMPORT_ALL: 535 importVCardFromSDCard(mAllVCardFileList); 536 break; 537 case IMPORT_MULTIPLE: 538 showDialog(R.id.dialog_select_multiple_vcard); 539 break; 540 default: 541 showDialog(R.id.dialog_select_one_vcard); 542 break; 543 } 544 } else if (which == DialogInterface.BUTTON_NEGATIVE) { 545 finish(); 546 } else { 547 mCurrentIndex = which; 548 } 549 } 550 } 551 552 private class VCardSelectedListener implements 553 DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener { 554 private int mCurrentIndex; 555 private Set<Integer> mSelectedIndexSet; 556 557 public VCardSelectedListener(boolean multipleSelect) { 558 mCurrentIndex = 0; 559 if (multipleSelect) { 560 mSelectedIndexSet = new HashSet<Integer>(); 561 } 562 } 563 564 public void onClick(DialogInterface dialog, int which) { 565 if (which == DialogInterface.BUTTON_POSITIVE) { 566 if (mSelectedIndexSet != null) { 567 List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>(); 568 final int size = mAllVCardFileList.size(); 569 // We'd like to sort the files by its index, so we do not use Set iterator. 570 for (int i = 0; i < size; i++) { 571 if (mSelectedIndexSet.contains(i)) { 572 selectedVCardFileList.add(mAllVCardFileList.get(i)); 573 } 574 } 575 importVCardFromSDCard(selectedVCardFileList); 576 } else { 577 importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex)); 578 } 579 } else if (which == DialogInterface.BUTTON_NEGATIVE) { 580 finish(); 581 } else { 582 // Some file is selected. 583 mCurrentIndex = which; 584 if (mSelectedIndexSet != null) { 585 if (mSelectedIndexSet.contains(which)) { 586 mSelectedIndexSet.remove(which); 587 } else { 588 mSelectedIndexSet.add(which); 589 } 590 } 591 } 592 } 593 594 public void onClick(DialogInterface dialog, int which, boolean isChecked) { 595 if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) { 596 Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which, 597 mAllVCardFileList.get(which).getCanonicalPath())); 598 } else { 599 onClick(dialog, which); 600 } 601 } 602 } 603 604 /** 605 * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select 606 * a vCard file is shown. After the choice, VCardReadThread starts running. 607 */ 608 private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener { 609 private boolean mCanceled; 610 private boolean mGotIOException; 611 private File mRootDirectory; 612 613 // To avoid recursive link. 614 private Set<String> mCheckedPaths; 615 private PowerManager.WakeLock mWakeLock; 616 617 private class CanceledException extends Exception { 618 } 619 620 public VCardScanThread(File sdcardDirectory) { 621 mCanceled = false; 622 mGotIOException = false; 623 mRootDirectory = sdcardDirectory; 624 mCheckedPaths = new HashSet<String>(); 625 PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService( 626 Context.POWER_SERVICE); 627 mWakeLock = powerManager.newWakeLock( 628 PowerManager.SCREEN_DIM_WAKE_LOCK | 629 PowerManager.ON_AFTER_RELEASE, LOG_TAG); 630 } 631 632 @Override 633 public void run() { 634 mAllVCardFileList = new Vector<VCardFile>(); 635 try { 636 mWakeLock.acquire(); 637 getVCardFileRecursively(mRootDirectory); 638 } catch (CanceledException e) { 639 mCanceled = true; 640 } catch (IOException e) { 641 mGotIOException = true; 642 } finally { 643 mWakeLock.release(); 644 } 645 646 if (mCanceled) { 647 mAllVCardFileList = null; 648 } 649 650 mProgressDialogForScanVCard.dismiss(); 651 mProgressDialogForScanVCard = null; 652 653 if (mGotIOException) { 654 runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception)); 655 } else if (mCanceled) { 656 finish(); 657 } else { 658 int size = mAllVCardFileList.size(); 659 final Context context = ImportVCardActivity.this; 660 if (size == 0) { 661 runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found)); 662 } else { 663 startVCardSelectAndImport(); 664 } 665 } 666 } 667 668 private void getVCardFileRecursively(File directory) 669 throws CanceledException, IOException { 670 if (mCanceled) { 671 throw new CanceledException(); 672 } 673 674 // e.g. secured directory may return null toward listFiles(). 675 final File[] files = directory.listFiles(); 676 if (files == null) { 677 final String currentDirectoryPath = directory.getCanonicalPath(); 678 final String secureDirectoryPath = 679 mRootDirectory.getCanonicalPath().concat(SECURE_DIRECTORY_NAME); 680 if (!TextUtils.equals(currentDirectoryPath, secureDirectoryPath)) { 681 Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")"); 682 } 683 return; 684 } 685 for (File file : directory.listFiles()) { 686 if (mCanceled) { 687 throw new CanceledException(); 688 } 689 String canonicalPath = file.getCanonicalPath(); 690 if (mCheckedPaths.contains(canonicalPath)) { 691 continue; 692 } 693 694 mCheckedPaths.add(canonicalPath); 695 696 if (file.isDirectory()) { 697 getVCardFileRecursively(file); 698 } else if (canonicalPath.toLowerCase().endsWith(".vcf") && 699 file.canRead()){ 700 String fileName = file.getName(); 701 VCardFile vcardFile = new VCardFile( 702 fileName, canonicalPath, file.lastModified()); 703 mAllVCardFileList.add(vcardFile); 704 } 705 } 706 } 707 708 public void onCancel(DialogInterface dialog) { 709 mCanceled = true; 710 } 711 712 public void onClick(DialogInterface dialog, int which) { 713 if (which == DialogInterface.BUTTON_NEGATIVE) { 714 mCanceled = true; 715 } 716 } 717 } 718 719 private void startVCardSelectAndImport() { 720 int size = mAllVCardFileList.size(); 721 if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) || 722 size == 1) { 723 importVCardFromSDCard(mAllVCardFileList); 724 } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) { 725 runOnUiThread(new DialogDisplayer(R.id.dialog_select_import_type)); 726 } else { 727 runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_vcard)); 728 } 729 } 730 731 private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) { 732 final int size = selectedVCardFileList.size(); 733 String[] uriStrings = new String[size]; 734 int i = 0; 735 for (VCardFile vcardFile : selectedVCardFileList) { 736 uriStrings[i] = "file://" + vcardFile.getCanonicalPath(); 737 i++; 738 } 739 importVCard(uriStrings); 740 } 741 742 private void importVCardFromSDCard(final VCardFile vcardFile) { 743 importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())}); 744 } 745 746 private void importVCard(final Uri uri) { 747 importVCard(new Uri[] {uri}); 748 } 749 750 private void importVCard(final String[] uriStrings) { 751 final int length = uriStrings.length; 752 final Uri[] uris = new Uri[length]; 753 for (int i = 0; i < length; i++) { 754 uris[i] = Uri.parse(uriStrings[i]); 755 } 756 importVCard(uris); 757 } 758 759 private void importVCard(final Uri[] uris) { 760 runOnUiThread(new Runnable() { 761 @Override 762 public void run() { 763 mVCardCacheThread = new VCardCacheThread(uris); 764 mListener = new NotificationImportExportListener(ImportVCardActivity.this); 765 showDialog(R.id.dialog_cache_vcard); 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 == 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 != 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 @Override 1000 public void onConfigurationChanged(Configuration newConfig) { 1001 super.onConfigurationChanged(newConfig); 1002 // This Activity should finish itself on orientation change, and give the main screen back 1003 // to the caller Activity. 1004 finish(); 1005 } 1006 1007 /** 1008 * Scans vCard in external storage (typically SDCard) and tries to import it. 1009 * - When there's no SDCard available, an error dialog is shown. 1010 * - When multiple vCard files are available, asks a user to select one. 1011 */ 1012 private void doScanExternalStorageAndImportVCard() { 1013 // TODO: should use getExternalStorageState(). 1014 final File file = Environment.getExternalStorageDirectory(); 1015 if (!file.exists() || !file.isDirectory() || !file.canRead()) { 1016 showDialog(R.id.dialog_sdcard_not_found); 1017 } else { 1018 mVCardScanThread = new VCardScanThread(file); 1019 showDialog(R.id.dialog_searching_vcard); 1020 } 1021 } 1022 1023 /* package */ void showFailureNotification(int reasonId) { 1024 final NotificationManager notificationManager = 1025 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); 1026 final Notification notification = 1027 NotificationImportExportListener.constructImportFailureNotification( 1028 ImportVCardActivity.this, 1029 getString(reasonId)); 1030 notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG, 1031 FAILURE_NOTIFICATION_ID, notification); 1032 mHandler.post(new Runnable() { 1033 @Override 1034 public void run() { 1035 Toast.makeText(ImportVCardActivity.this, 1036 getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show(); 1037 } 1038 }); 1039 } 1040 } 1041