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 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.ClipData; 26 import android.content.ComponentName; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.content.DialogInterface; 30 import android.content.Intent; 31 import android.content.ServiceConnection; 32 import android.database.Cursor; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.os.IBinder; 37 import android.os.PowerManager; 38 import android.provider.OpenableColumns; 39 import android.text.TextUtils; 40 import android.util.Log; 41 import android.widget.Toast; 42 43 import com.android.contacts.R; 44 import com.android.contacts.activities.RequestImportVCardPermissionsActivity; 45 import com.android.contacts.model.AccountTypeManager; 46 import com.android.contacts.model.account.AccountWithDataSet; 47 import com.android.contactsbind.FeedbackHelper; 48 import com.android.vcard.VCardEntryCounter; 49 import com.android.vcard.VCardParser; 50 import com.android.vcard.VCardParser_V21; 51 import com.android.vcard.VCardParser_V30; 52 import com.android.vcard.VCardSourceDetector; 53 import com.android.vcard.exception.VCardException; 54 import com.android.vcard.exception.VCardNestedException; 55 import com.android.vcard.exception.VCardVersionException; 56 57 import java.io.ByteArrayInputStream; 58 import java.io.File; 59 import java.io.IOException; 60 import java.io.InputStream; 61 import java.nio.ByteBuffer; 62 import java.nio.channels.Channels; 63 import java.nio.channels.ReadableByteChannel; 64 import java.nio.channels.WritableByteChannel; 65 import java.util.ArrayList; 66 import java.util.Arrays; 67 import java.util.List; 68 69 /** 70 * The class letting users to import vCard. This includes the UI part for letting them select 71 * an Account and posssibly a file if there's no Uri is given from its caller Activity. 72 * 73 * Note that this Activity assumes that the instance is a "one-shot Activity", which will be 74 * finished (with the method {@link Activity#finish()}) after the import and never reuse 75 * any Dialog in the instance. So this code is careless about the management around managed 76 * dialogs stuffs (like how onCreateDialog() is used). 77 */ 78 public class ImportVCardActivity extends Activity implements ImportVCardDialogFragment.Listener { 79 private static final String LOG_TAG = "VCardImport"; 80 81 private static final int SELECT_ACCOUNT = 0; 82 83 /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0; 84 /* package */ final static int VCARD_VERSION_V21 = 1; 85 /* package */ final static int VCARD_VERSION_V30 = 2; 86 87 private static final int REQUEST_OPEN_DOCUMENT = 100; 88 89 /** 90 * Notification id used when error happened before sending an import request to VCardServer. 91 */ 92 private static final int FAILURE_NOTIFICATION_ID = 1; 93 94 private static final String LOCAL_TMP_FILE_NAME_EXTRA = 95 "com.android.contacts.vcard.LOCAL_TMP_FILE_NAME"; 96 97 private static final String SOURCE_URI_DISPLAY_NAME = 98 "com.android.contacts.vcard.SOURCE_URI_DISPLAY_NAME"; 99 100 private static final String STORAGE_VCARD_URI_PREFIX = "file:///storage"; 101 102 private AccountWithDataSet mAccount; 103 104 private ProgressDialog mProgressDialogForCachingVCard; 105 106 private VCardCacheThread mVCardCacheThread; 107 private ImportRequestConnection mConnection; 108 /* package */ VCardImportExportListener mListener; 109 110 private String mErrorMessage; 111 112 private Handler mHandler = new Handler(); 113 114 // Runs on the UI thread. 115 private class DialogDisplayer implements Runnable { 116 private final int mResId; DialogDisplayer(int resId)117 public DialogDisplayer(int resId) { 118 mResId = resId; 119 } DialogDisplayer(String errorMessage)120 public DialogDisplayer(String errorMessage) { 121 mResId = R.id.dialog_error_with_message; 122 mErrorMessage = errorMessage; 123 } 124 @Override run()125 public void run() { 126 if (!isFinishing()) { 127 showDialog(mResId); 128 } 129 } 130 } 131 132 private class CancelListener 133 implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { 134 @Override onClick(DialogInterface dialog, int which)135 public void onClick(DialogInterface dialog, int which) { 136 finish(); 137 } 138 @Override onCancel(DialogInterface dialog)139 public void onCancel(DialogInterface dialog) { 140 finish(); 141 } 142 } 143 144 private CancelListener mCancelListener = new CancelListener(); 145 146 private class ImportRequestConnection implements ServiceConnection { 147 private VCardService mService; 148 sendImportRequest(final List<ImportRequest> requests)149 public void sendImportRequest(final List<ImportRequest> requests) { 150 Log.i(LOG_TAG, "Send an import request"); 151 mService.handleImportRequest(requests, mListener); 152 } 153 154 @Override onServiceConnected(ComponentName name, IBinder binder)155 public void onServiceConnected(ComponentName name, IBinder binder) { 156 mService = ((VCardService.MyBinder) binder).getService(); 157 Log.i(LOG_TAG, 158 String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)", 159 Arrays.toString(mVCardCacheThread.getSourceUris()))); 160 mVCardCacheThread.start(); 161 } 162 163 @Override onServiceDisconnected(ComponentName name)164 public void onServiceDisconnected(ComponentName name) { 165 Log.i(LOG_TAG, "Disconnected from VCardService"); 166 } 167 } 168 169 /** 170 * Caches given vCard files into a local directory, and sends actual import request to 171 * {@link VCardService}. 172 * 173 * We need to cache given files into local storage. One of reasons is that some data (as Uri) 174 * may have special permissions. Callers may allow only this Activity to access that content, 175 * not what this Activity launched (like {@link VCardService}). 176 */ 177 private class VCardCacheThread extends Thread 178 implements DialogInterface.OnCancelListener { 179 private boolean mCanceled; 180 private PowerManager.WakeLock mWakeLock; 181 private VCardParser mVCardParser; 182 private final Uri[] mSourceUris; // Given from a caller. 183 private final String[] mSourceDisplayNames; // Display names for each Uri in mSourceUris. 184 private final byte[] mSource; 185 private final String mDisplayName; 186 VCardCacheThread(final Uri[] sourceUris, String[] sourceDisplayNames)187 public VCardCacheThread(final Uri[] sourceUris, String[] sourceDisplayNames) { 188 mSourceUris = sourceUris; 189 mSourceDisplayNames = sourceDisplayNames; 190 mSource = null; 191 final Context context = ImportVCardActivity.this; 192 final PowerManager powerManager = 193 (PowerManager)context.getSystemService(Context.POWER_SERVICE); 194 mWakeLock = powerManager.newWakeLock( 195 PowerManager.SCREEN_DIM_WAKE_LOCK | 196 PowerManager.ON_AFTER_RELEASE, LOG_TAG); 197 mDisplayName = null; 198 } 199 200 @Override finalize()201 public void finalize() { 202 if (mWakeLock != null && mWakeLock.isHeld()) { 203 Log.w(LOG_TAG, "WakeLock is being held."); 204 mWakeLock.release(); 205 } 206 } 207 208 @Override run()209 public void run() { 210 Log.i(LOG_TAG, "vCard cache thread starts running."); 211 if (mConnection == null) { 212 throw new NullPointerException("vCard cache thread must be launched " 213 + "after a service connection is established"); 214 } 215 216 mWakeLock.acquire(); 217 try { 218 if (mCanceled == true) { 219 Log.i(LOG_TAG, "vCard cache operation is canceled."); 220 return; 221 } 222 223 final Context context = ImportVCardActivity.this; 224 // Uris given from caller applications may not be opened twice: consider when 225 // it is not from local storage (e.g. "file:///...") but from some special 226 // provider (e.g. "content://..."). 227 // Thus we have to once copy the content of Uri into local storage, and read 228 // it after it. 229 // 230 // We may be able to read content of each vCard file during copying them 231 // to local storage, but currently vCard code does not allow us to do so. 232 int cache_index = 0; 233 ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>(); 234 if (mSource != null) { 235 try { 236 requests.add(constructImportRequest(mSource, null, mDisplayName)); 237 } catch (VCardException e) { 238 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG, 239 "Failed to cache vcard", e); 240 showFailureNotification(R.string.fail_reason_not_supported); 241 return; 242 } 243 } else { 244 int i = 0; 245 for (Uri sourceUri : mSourceUris) { 246 if (mCanceled) { 247 Log.i(LOG_TAG, "vCard cache operation is canceled."); 248 break; 249 } 250 251 String sourceDisplayName = mSourceDisplayNames[i++]; 252 253 final ImportRequest request; 254 try { 255 request = constructImportRequest(null, sourceUri, sourceDisplayName); 256 } catch (VCardException e) { 257 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG, 258 "Failed to cache vcard", e); 259 showFailureNotification(R.string.fail_reason_not_supported); 260 return; 261 } catch (IOException e) { 262 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG, 263 "Failed to cache vcard", e); 264 showFailureNotification(R.string.fail_reason_io_error); 265 return; 266 } 267 if (mCanceled) { 268 Log.i(LOG_TAG, "vCard cache operation is canceled."); 269 return; 270 } 271 requests.add(request); 272 } 273 } 274 if (!requests.isEmpty()) { 275 mConnection.sendImportRequest(requests); 276 } else { 277 Log.w(LOG_TAG, "Empty import requests. Ignore it."); 278 } 279 } catch (OutOfMemoryError e) { 280 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG, 281 "OutOfMemoryError occured during caching vCard", e); 282 System.gc(); 283 runOnUiThread(new DialogDisplayer( 284 getString(R.string.fail_reason_low_memory_during_import))); 285 } catch (IOException e) { 286 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG, 287 "IOException during caching vCard", e); 288 runOnUiThread(new DialogDisplayer( 289 getString(R.string.fail_reason_io_error))); 290 } finally { 291 Log.i(LOG_TAG, "Finished caching vCard."); 292 mWakeLock.release(); 293 try { 294 unbindService(mConnection); 295 } catch (IllegalArgumentException e) { 296 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG, 297 "Cannot unbind service connection", e); 298 } 299 mProgressDialogForCachingVCard.dismiss(); 300 mProgressDialogForCachingVCard = null; 301 finish(); 302 } 303 } 304 305 /** 306 * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from 307 * its content. 308 * 309 * @arg localDataUri Uri actually used for the import. Should be stored in 310 * app local storage, as we cannot guarantee other types of Uris can be read 311 * multiple times. This variable populates {@link ImportRequest#uri}. 312 * @arg displayName Used for displaying information to the user. This variable populates 313 * {@link ImportRequest#displayName}. 314 */ constructImportRequest(final byte[] data, final Uri localDataUri, final String displayName)315 private ImportRequest constructImportRequest(final byte[] data, 316 final Uri localDataUri, final String displayName) 317 throws IOException, VCardException { 318 final ContentResolver resolver = ImportVCardActivity.this.getContentResolver(); 319 VCardEntryCounter counter = null; 320 VCardSourceDetector detector = null; 321 int vcardVersion = VCARD_VERSION_V21; 322 try { 323 boolean shouldUseV30 = false; 324 InputStream is; 325 if (data != null) { 326 is = new ByteArrayInputStream(data); 327 } else { 328 is = resolver.openInputStream(localDataUri); 329 } 330 mVCardParser = new VCardParser_V21(); 331 try { 332 counter = new VCardEntryCounter(); 333 detector = new VCardSourceDetector(); 334 mVCardParser.addInterpreter(counter); 335 mVCardParser.addInterpreter(detector); 336 mVCardParser.parse(is); 337 } catch (VCardVersionException e1) { 338 try { 339 is.close(); 340 } catch (IOException e) { 341 } 342 343 shouldUseV30 = true; 344 if (data != null) { 345 is = new ByteArrayInputStream(data); 346 } else { 347 is = resolver.openInputStream(localDataUri); 348 } 349 mVCardParser = new VCardParser_V30(); 350 try { 351 counter = new VCardEntryCounter(); 352 detector = new VCardSourceDetector(); 353 mVCardParser.addInterpreter(counter); 354 mVCardParser.addInterpreter(detector); 355 mVCardParser.parse(is); 356 } catch (VCardVersionException e2) { 357 throw new VCardException("vCard with unspported version."); 358 } 359 } finally { 360 if (is != null) { 361 try { 362 is.close(); 363 } catch (IOException e) { 364 } 365 } 366 } 367 368 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21; 369 } catch (VCardNestedException e) { 370 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive)."); 371 // Go through without throwing the Exception, as we may be able to detect the 372 // version before it 373 } 374 return new ImportRequest(mAccount, 375 data, localDataUri, displayName, 376 detector.getEstimatedType(), 377 detector.getEstimatedCharset(), 378 vcardVersion, counter.getCount()); 379 } 380 getSourceUris()381 public Uri[] getSourceUris() { 382 return mSourceUris; 383 } 384 cancel()385 public void cancel() { 386 mCanceled = true; 387 if (mVCardParser != null) { 388 mVCardParser.cancel(); 389 } 390 } 391 392 @Override onCancel(DialogInterface dialog)393 public void onCancel(DialogInterface dialog) { 394 Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard."); 395 cancel(); 396 } 397 } 398 importVCard(final Uri uri, final String sourceDisplayName)399 private void importVCard(final Uri uri, final String sourceDisplayName) { 400 importVCard(new Uri[] {uri}, new String[] {sourceDisplayName}); 401 } 402 importVCard(final Uri[] uris, final String[] sourceDisplayNames)403 private void importVCard(final Uri[] uris, final String[] sourceDisplayNames) { 404 runOnUiThread(new Runnable() { 405 @Override 406 public void run() { 407 if (!isFinishing()) { 408 mVCardCacheThread = new VCardCacheThread(uris, sourceDisplayNames); 409 mListener = new NotificationImportExportListener(ImportVCardActivity.this); 410 showDialog(R.id.dialog_cache_vcard); 411 } 412 } 413 }); 414 } 415 getDisplayName(Uri sourceUri)416 private String getDisplayName(Uri sourceUri) { 417 if (sourceUri == null) { 418 return null; 419 } 420 final ContentResolver resolver = ImportVCardActivity.this.getContentResolver(); 421 String displayName = null; 422 Cursor cursor = null; 423 // Try to get a display name from the given Uri. If it fails, we just 424 // pick up the last part of the Uri. 425 try { 426 cursor = resolver.query(sourceUri, 427 new String[] { OpenableColumns.DISPLAY_NAME }, 428 null, null, null); 429 if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) { 430 if (cursor.getCount() > 1) { 431 Log.w(LOG_TAG, "Unexpected multiple rows: " 432 + cursor.getCount()); 433 } 434 int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); 435 if (index >= 0) { 436 displayName = cursor.getString(index); 437 } 438 } 439 } finally { 440 if (cursor != null) { 441 cursor.close(); 442 } 443 } 444 if (TextUtils.isEmpty(displayName)){ 445 displayName = sourceUri.getLastPathSegment(); 446 } 447 return displayName; 448 } 449 450 /** 451 * Copy the content of sourceUri to the destination. 452 */ copyTo(final Uri sourceUri, String filename)453 private Uri copyTo(final Uri sourceUri, String filename) throws IOException { 454 Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)", 455 sourceUri, filename)); 456 final Context context = ImportVCardActivity.this; 457 final ContentResolver resolver = context.getContentResolver(); 458 ReadableByteChannel inputChannel = null; 459 WritableByteChannel outputChannel = null; 460 Uri destUri = null; 461 try { 462 inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri)); 463 destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString()); 464 outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel(); 465 final ByteBuffer buffer = ByteBuffer.allocateDirect(8192); 466 while (inputChannel.read(buffer) != -1) { 467 buffer.flip(); 468 outputChannel.write(buffer); 469 buffer.compact(); 470 } 471 buffer.flip(); 472 while (buffer.hasRemaining()) { 473 outputChannel.write(buffer); 474 } 475 } finally { 476 if (inputChannel != null) { 477 try { 478 inputChannel.close(); 479 } catch (IOException e) { 480 Log.w(LOG_TAG, "Failed to close inputChannel."); 481 } 482 } 483 if (outputChannel != null) { 484 try { 485 outputChannel.close(); 486 } catch(IOException e) { 487 Log.w(LOG_TAG, "Failed to close outputChannel"); 488 } 489 } 490 } 491 return destUri; 492 } 493 494 /** 495 * Reads the file from {@param sourceUri} and copies it to local cache file. 496 * Returns the local file name which stores the file from sourceUri. 497 */ readUriToLocalFile(Uri sourceUri)498 private String readUriToLocalFile(Uri sourceUri) { 499 // Read the uri to local first. 500 int cache_index = 0; 501 String localFilename = null; 502 // Note: caches are removed by VCardService. 503 while (true) { 504 localFilename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf"; 505 final File file = getFileStreamPath(localFilename); 506 if (!file.exists()) { 507 break; 508 } else { 509 if (cache_index == Integer.MAX_VALUE) { 510 throw new RuntimeException("Exceeded cache limit"); 511 } 512 cache_index++; 513 } 514 } 515 try { 516 copyTo(sourceUri, localFilename); 517 } catch (IOException|SecurityException e) { 518 FeedbackHelper.sendFeedback(this, LOG_TAG, "Failed to copy vcard to local file", e); 519 showFailureNotification(R.string.fail_reason_io_error); 520 return null; 521 } 522 523 if (localFilename == null) { 524 Log.e(LOG_TAG, "Cannot load uri to local storage."); 525 showFailureNotification(R.string.fail_reason_io_error); 526 return null; 527 } 528 529 return localFilename; 530 } 531 readUriToLocalUri(Uri sourceUri)532 private Uri readUriToLocalUri(Uri sourceUri) { 533 final String fileName = readUriToLocalFile(sourceUri); 534 if (fileName == null) { 535 return null; 536 } 537 return Uri.parse(getFileStreamPath(fileName).toURI().toString()); 538 } 539 540 // Returns true if uri is from Storage. isStorageUri(Uri uri)541 private boolean isStorageUri(Uri uri) { 542 return uri != null && uri.toString().startsWith(STORAGE_VCARD_URI_PREFIX); 543 } 544 545 @Override onCreate(Bundle bundle)546 protected void onCreate(Bundle bundle) { 547 super.onCreate(bundle); 548 549 getWindow().addSystemFlags(android.view.WindowManager.LayoutParams 550 .SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 551 552 Uri sourceUri = getIntent().getData(); 553 554 // Reading uris from non-storage needs the permission granted from the source intent, 555 // instead of permissions from RequestImportVCardPermissionActivity. So skipping requesting 556 // permissions from RequestImportVCardPermissionActivity for uris from non-storage source. 557 if (isStorageUri(sourceUri) && RequestImportVCardPermissionsActivity 558 .startPermissionActivity(this, isCallerSelf(this))) { 559 return; 560 } 561 562 String sourceDisplayName = null; 563 if (sourceUri != null) { 564 // Read the uri to local first. 565 String localTmpFileName = getIntent().getStringExtra(LOCAL_TMP_FILE_NAME_EXTRA); 566 sourceDisplayName = getIntent().getStringExtra(SOURCE_URI_DISPLAY_NAME); 567 if (TextUtils.isEmpty(localTmpFileName)) { 568 localTmpFileName = readUriToLocalFile(sourceUri); 569 sourceDisplayName = getDisplayName(sourceUri); 570 if (localTmpFileName == null) { 571 Log.e(LOG_TAG, "Cannot load uri to local storage."); 572 showFailureNotification(R.string.fail_reason_io_error); 573 return; 574 } 575 getIntent().putExtra(LOCAL_TMP_FILE_NAME_EXTRA, localTmpFileName); 576 getIntent().putExtra(SOURCE_URI_DISPLAY_NAME, sourceDisplayName); 577 } 578 sourceUri = Uri.parse(getFileStreamPath(localTmpFileName).toURI().toString()); 579 } 580 581 // Always request required permission for contacts before importing the vcard. 582 if (RequestImportVCardPermissionsActivity.startPermissionActivity(this, 583 isCallerSelf(this))) { 584 return; 585 } 586 587 String accountName = null; 588 String accountType = null; 589 String dataSet = null; 590 final Intent intent = getIntent(); 591 if (intent != null) { 592 accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME); 593 accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE); 594 dataSet = intent.getStringExtra(SelectAccountActivity.DATA_SET); 595 } else { 596 Log.e(LOG_TAG, "intent does not exist"); 597 } 598 599 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 600 mAccount = new AccountWithDataSet(accountName, accountType, dataSet); 601 } else { 602 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this); 603 final List<AccountWithDataSet> accountList = accountTypes.blockForWritableAccounts(); 604 if (accountList.size() == 0) { 605 mAccount = null; 606 } else if (accountList.size() == 1) { 607 mAccount = accountList.get(0); 608 } else { 609 startActivityForResult(new Intent(this, SelectAccountActivity.class), 610 SELECT_ACCOUNT); 611 return; 612 } 613 } 614 615 if (isCallerSelf(this)) { 616 startImport(sourceUri, sourceDisplayName); 617 } else { 618 ImportVCardDialogFragment.show(this, sourceUri, sourceDisplayName); 619 } 620 } 621 isCallerSelf(Activity activity)622 private static boolean isCallerSelf(Activity activity) { 623 // {@link Activity#getCallingActivity()} is a safer alternative to 624 // {@link Activity#getCallingPackage()} that works around a 625 // framework bug where getCallingPackage() can sometimes return null even when the 626 // current activity *was* in fact launched via a startActivityForResult() call. 627 // 628 // (The bug happens if the task stack needs to be re-created by the framework after 629 // having been killed due to memory pressure or by the "Don't keep activities" 630 // developer option; see bug 7494866 for the full details.) 631 // 632 // Turns out that {@link Activity#getCallingActivity()} *does* return correct info 633 // even in the case where getCallingPackage() is broken, so the workaround is simply 634 // to get the package name from getCallingActivity().getPackageName() instead. 635 final ComponentName callingActivity = activity.getCallingActivity(); 636 if (callingActivity == null) return false; 637 final String packageName = callingActivity.getPackageName(); 638 if (packageName == null) return false; 639 return packageName.equals(activity.getApplicationContext().getPackageName()); 640 } 641 642 @Override onImportVCardConfirmed(Uri sourceUri, String sourceDisplayName)643 public void onImportVCardConfirmed(Uri sourceUri, String sourceDisplayName) { 644 startImport(sourceUri, sourceDisplayName); 645 } 646 647 @Override onImportVCardDenied()648 public void onImportVCardDenied() { 649 finish(); 650 } 651 652 @Override onActivityResult(int requestCode, int resultCode, Intent intent)653 public void onActivityResult(int requestCode, int resultCode, Intent intent) { 654 if (requestCode == SELECT_ACCOUNT) { 655 if (resultCode == Activity.RESULT_OK) { 656 mAccount = new AccountWithDataSet( 657 intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME), 658 intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE), 659 intent.getStringExtra(SelectAccountActivity.DATA_SET)); 660 final Uri sourceUri = getIntent().getData(); 661 if (sourceUri == null) { 662 startImport(sourceUri, /* sourceDisplayName =*/ null); 663 } else { 664 final String sourceDisplayName = getIntent().getStringExtra( 665 SOURCE_URI_DISPLAY_NAME); 666 final String localFileName = getIntent().getStringExtra( 667 LOCAL_TMP_FILE_NAME_EXTRA); 668 final Uri localUri = Uri.parse( 669 getFileStreamPath(localFileName).toURI().toString()); 670 startImport(localUri, sourceDisplayName); 671 } 672 } else { 673 if (resultCode != Activity.RESULT_CANCELED) { 674 Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode); 675 } 676 finish(); 677 } 678 } else if (requestCode == REQUEST_OPEN_DOCUMENT) { 679 if (resultCode == Activity.RESULT_OK) { 680 final ClipData clipData = intent.getClipData(); 681 if (clipData != null) { 682 final ArrayList<Uri> uris = new ArrayList<>(); 683 final ArrayList<String> sourceDisplayNames = new ArrayList<>(); 684 for (int i = 0; i < clipData.getItemCount(); i++) { 685 ClipData.Item item = clipData.getItemAt(i); 686 final Uri uri = item.getUri(); 687 if (uri != null) { 688 final Uri localUri = readUriToLocalUri(uri); 689 if (localUri != null) { 690 final String sourceDisplayName = getDisplayName(uri); 691 uris.add(localUri); 692 sourceDisplayNames.add(sourceDisplayName); 693 } 694 } 695 } 696 if (uris.isEmpty()) { 697 Log.w(LOG_TAG, "No vCard was selected for import"); 698 finish(); 699 } else { 700 Log.i(LOG_TAG, "Multiple vCards selected for import: " + uris); 701 importVCard(uris.toArray(new Uri[0]), 702 sourceDisplayNames.toArray(new String[0])); 703 } 704 } else { 705 final Uri uri = intent.getData(); 706 if (uri != null) { 707 Log.i(LOG_TAG, "vCard selected for import: " + uri); 708 final Uri localUri = readUriToLocalUri(uri); 709 if (localUri != null) { 710 final String sourceDisplayName = getDisplayName(uri); 711 importVCard(localUri, sourceDisplayName); 712 } else { 713 Log.w(LOG_TAG, "No local URI for vCard import"); 714 finish(); 715 } 716 } else { 717 Log.w(LOG_TAG, "No vCard was selected for import"); 718 finish(); 719 } 720 } 721 } else { 722 if (resultCode != Activity.RESULT_CANCELED) { 723 Log.w(LOG_TAG, "Result code was not OK nor CANCELED" + resultCode); 724 } 725 finish(); 726 } 727 } 728 } 729 startImport(Uri uri, String sourceDisplayName)730 private void startImport(Uri uri, String sourceDisplayName) { 731 // Handle inbound files 732 if (uri != null) { 733 Log.i(LOG_TAG, "Starting vCard import using Uri " + uri); 734 importVCard(uri, sourceDisplayName); 735 } else { 736 Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually."); 737 final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); 738 intent.addCategory(Intent.CATEGORY_OPENABLE); 739 intent.setType(VCardService.X_VCARD_MIME_TYPE); 740 intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); 741 startActivityForResult(intent, REQUEST_OPEN_DOCUMENT); 742 } 743 } 744 745 @Override onCreateDialog(int resId, Bundle bundle)746 protected Dialog onCreateDialog(int resId, Bundle bundle) { 747 if (resId == R.id.dialog_cache_vcard) { 748 if (mProgressDialogForCachingVCard == null) { 749 final String title = getString(R.string.caching_vcard_title); 750 final String message = getString(R.string.caching_vcard_message); 751 mProgressDialogForCachingVCard = new ProgressDialog(this); 752 mProgressDialogForCachingVCard.setTitle(title); 753 mProgressDialogForCachingVCard.setMessage(message); 754 mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER); 755 mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread); 756 startVCardService(); 757 } 758 return mProgressDialogForCachingVCard; 759 } else if (resId == R.id.dialog_error_with_message) { 760 String message = mErrorMessage; 761 if (TextUtils.isEmpty(message)) { 762 Log.e(LOG_TAG, "Error message is null while it must not."); 763 message = getString(R.string.fail_reason_unknown); 764 } 765 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 766 .setTitle(getString(R.string.reading_vcard_failed_title)) 767 .setIconAttribute(android.R.attr.alertDialogIcon) 768 .setMessage(message) 769 .setOnCancelListener(mCancelListener) 770 .setPositiveButton(android.R.string.ok, mCancelListener); 771 return builder.create(); 772 } 773 774 return super.onCreateDialog(resId, bundle); 775 } 776 startVCardService()777 /* package */ void startVCardService() { 778 mConnection = new ImportRequestConnection(); 779 780 Log.i(LOG_TAG, "Bind to VCardService."); 781 // We don't want the service finishes itself just after this connection. 782 Intent intent = new Intent(this, VCardService.class); 783 startService(intent); 784 bindService(new Intent(this, VCardService.class), 785 mConnection, Context.BIND_AUTO_CREATE); 786 } 787 788 @Override onRestoreInstanceState(Bundle savedInstanceState)789 protected void onRestoreInstanceState(Bundle savedInstanceState) { 790 super.onRestoreInstanceState(savedInstanceState); 791 if (mProgressDialogForCachingVCard != null) { 792 Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again."); 793 showDialog(R.id.dialog_cache_vcard); 794 } 795 } 796 showFailureNotification(int reasonId)797 /* package */ void showFailureNotification(int reasonId) { 798 final NotificationManager notificationManager = 799 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); 800 final Notification notification = 801 NotificationImportExportListener.constructImportFailureNotification( 802 ImportVCardActivity.this, 803 getString(reasonId)); 804 notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG, 805 FAILURE_NOTIFICATION_ID, notification); 806 mHandler.post(new Runnable() { 807 @Override 808 public void run() { 809 Toast.makeText(ImportVCardActivity.this, 810 getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show(); 811 } 812 }); 813 } 814 } 815