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