diff options
author | Jeff Hamilton <jham@android.com> | 2011-06-30 12:01:23 -0500 |
---|---|---|
committer | Jeff Hamilton <jham@android.com> | 2011-07-20 17:00:04 -0500 |
commit | e967f7cb12e02f7c852670c315a284aed1310dc1 (patch) | |
tree | 8b853db874594fa5c926aade17b36f74f8be0853 /src/com/android/contacts/vcard | |
parent | 5cc94b4952167973e0467f4b3ff81e33fbf19108 (diff) | |
download | Contacts-e967f7cb12e02f7c852670c315a284aed1310dc1.tar.gz |
Allow importing vcards directly from NFC.
There is no longer a need for a content provider
to hold the vcard data, it is processed directly
from the inbound NFC intent.
Upon successful import the contact is opened.
Change-Id: Ib49305d36d2448097af60206eab49133ebca655f
Diffstat (limited to 'src/com/android/contacts/vcard')
-rw-r--r-- | src/com/android/contacts/vcard/ImportProcessor.java | 60 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/ImportRequest.java | 37 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/ImportVCardActivity.java | 190 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/VCardService.java | 56 |
4 files changed, 232 insertions, 111 deletions
diff --git a/src/com/android/contacts/vcard/ImportProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java index 2a5583db8..30920874f 100644 --- a/src/com/android/contacts/vcard/ImportProcessor.java +++ b/src/com/android/contacts/vcard/ImportProcessor.java @@ -38,6 +38,7 @@ import android.net.Uri; import android.provider.ContactsContract.RawContacts; import android.util.Log; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -77,7 +78,7 @@ public class ImportProcessor extends ProcessorBase { mImportRequest = request; mJobId = jobId; mNotifier = new ImportProgressNotifier(service, mNotificationManager, jobId, - request.originalUri.getLastPathSegment()); + request.displayName); } @Override @@ -141,11 +142,36 @@ public class ImportProcessor extends ProcessorBase { new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset); final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver); constructor.addEntryHandler(committer); - constructor.addEntryHandler(mNotifier); + if (!request.showImmediately) { + constructor.addEntryHandler(mNotifier); + } - final boolean successful = - readOneVCard(uri, estimatedVCardType, estimatedCharset, - constructor, possibleVCardVersions); + InputStream is = null; + boolean successful = false; + try { + if (uri != null) { + Log.i(LOG_TAG, "start importing one vCard (Uri: " + uri + ")"); + is = mResolver.openInputStream(uri); + } else if (request.data != null){ + Log.i(LOG_TAG, "start importing one vCard (byte[])"); + is = new ByteArrayInputStream(request.data); + } + + if (is != null) { + successful = readOneVCard(is, estimatedVCardType, estimatedCharset, constructor, + possibleVCardVersions); + } + } catch (IOException e) { + successful = false; + } finally { + if (is != null) { + try { + is.close(); + } catch (Exception e) { + // ignore + } + } + } mService.handleFinishImportNotification(mJobId, successful); @@ -177,7 +203,7 @@ public class ImportProcessor extends ProcessorBase { private void doCancelNotification() { final String description = mService.getString(R.string.importing_vcard_canceled_title, - mImportRequest.originalUri.getLastPathSegment()); + mImportRequest.displayName); final Notification notification = VCardService.constructCancelNotification(mService, description); mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, notification); @@ -185,7 +211,7 @@ public class ImportProcessor extends ProcessorBase { private void doFinishNotification(final Uri createdUri) { final String description = mService.getString(R.string.importing_vcard_finished_title, - mImportRequest.originalUri.getLastPathSegment()); + mImportRequest.displayName); final Intent intent; if (createdUri != null) { final long rawContactId = ContentUris.parseId(createdUri); @@ -196,20 +222,24 @@ public class ImportProcessor extends ProcessorBase { } else { intent = null; } - final Notification notification = - VCardService.constructFinishNotification(mService, - description, null, intent); - mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, notification); + if (mImportRequest.showImmediately && (intent != null)) { + mNotificationManager.cancel(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mService.startActivity(intent); + } else { + final Notification notification = VCardService.constructFinishNotification(mService, + description, null, intent); + mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mJobId, + notification); + } } - private boolean readOneVCard(Uri uri, int vcardType, String charset, + private boolean readOneVCard(InputStream is, int vcardType, String charset, final VCardInterpreter interpreter, final int[] possibleVCardVersions) { - Log.i(LOG_TAG, "start importing one vCard (Uri: " + uri + ")"); boolean successful = false; final int length = possibleVCardVersions.length; for (int i = 0; i < length; i++) { - InputStream is = null; final int vcardVersion = possibleVCardVersions[i]; try { if (i > 0 && (interpreter instanceof VCardEntryConstructor)) { @@ -217,8 +247,6 @@ public class ImportProcessor extends ProcessorBase { ((VCardEntryConstructor) interpreter).clear(); } - is = mResolver.openInputStream(uri); - // We need synchronized block here, // since we need to handle mCanceled and mVCardParser at once. // In the worst case, a user may call cancel() just before creating diff --git a/src/com/android/contacts/vcard/ImportRequest.java b/src/com/android/contacts/vcard/ImportRequest.java index e8b56068b..84fbb0e17 100644 --- a/src/com/android/contacts/vcard/ImportRequest.java +++ b/src/com/android/contacts/vcard/ImportRequest.java @@ -35,30 +35,42 @@ public class ImportRequest { * Can be null (typically when there's no Account available in the system). */ public final Account account; + /** * Uri to be imported. May have different content than originally given from users, so * when displaying user-friendly information (e.g. "importing xxx.vcf"), use - * {@link #originalUri} instead. + * {@link #displayName} instead. + * + * If this is null {@link #data} contains the byte stream of the vcard. */ public final Uri uri; /** - * Original uri given from users. - * Useful when showing user-friendly information ("importing xxx.vcf"), as - * {@link #uri} may have different name than the original (like "import_tmp_1.vcf"). - * - * This variable must not be used for doing actual processing like re-import, as the app - * may not have right permission to do so. + * Holds the byte stream of the vcard, if {@link #uri} is null. */ - public final Uri originalUri; + public final byte[] data; + + /** + * String to be displayed to the user to indicate the source of the VCARD. + */ + public final String displayName; + + /** + * Whether to show the imported vcard immediately after the import is done. + * If set to false, just a notification will be shown. + */ + public final boolean showImmediately; + /** * Can be {@link VCardSourceDetector#PARSE_TYPE_UNKNOWN}. */ public final int estimatedVCardType; + /** * Can be null, meaning no preferable charset is available. */ public final String estimatedCharset; + /** * Assumes that one Uri contains only one version, while there's a (tiny) possibility * we may have two types in one vCard. @@ -88,15 +100,18 @@ public class ImportRequest { * and may become invalid after its close() request). */ public final int entryCount; + public ImportRequest(Account account, - Uri uri, Uri originalUri, int estimatedType, String estimatedCharset, - int vcardVersion, int entryCount) { + byte[] data, Uri uri, String displayName, int estimatedType, String estimatedCharset, + int vcardVersion, int entryCount, boolean showImmediately) { this.account = account; + this.data = data; this.uri = uri; - this.originalUri = originalUri; + this.displayName = displayName; this.estimatedVCardType = estimatedType; this.estimatedCharset = estimatedCharset; this.vcardVersion = vcardVersion; this.entryCount = entryCount; + this.showImmediately = showImmediately; } } diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java index 2cfd71a05..0b844407c 100644 --- a/src/com/android/contacts/vcard/ImportVCardActivity.java +++ b/src/com/android/contacts/vcard/ImportVCardActivity.java @@ -46,6 +46,9 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.res.Configuration; import android.net.Uri; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; import android.os.Bundle; import android.os.Environment; import android.os.Handler; @@ -61,6 +64,7 @@ import android.text.style.RelativeSizeSpan; import android.util.Log; import android.widget.Toast; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -68,6 +72,7 @@ import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -115,9 +120,6 @@ public class ImportVCardActivity extends ContactsActivity { private Account mAccount; - private String mAction; - private Uri mUri; - private ProgressDialog mProgressDialogForScanVCard; private ProgressDialog mProgressDialogForCachingVCard; @@ -229,15 +231,38 @@ public class ImportVCardActivity extends ContactsActivity { private PowerManager.WakeLock mWakeLock; private VCardParser mVCardParser; private final Uri[] mSourceUris; // Given from a caller. + private final byte[] mSource; + private final String mDisplayName; + private final boolean mShowImmediately; public VCardCacheThread(final Uri[] sourceUris) { mSourceUris = sourceUris; + mSource = null; final Context context = ImportVCardActivity.this; final PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE); mWakeLock = powerManager.newWakeLock( PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, LOG_TAG); + mDisplayName = null; + // Showing immediately could make sense here if we restrict + // it to cases where we import a single vcard. For now disable + // this feature though. + mShowImmediately = false; + } + + public VCardCacheThread(final byte[] data, String displayName, + final boolean showImmediately) { + mSource = data; + mSourceUris = null; + final Context context = ImportVCardActivity.this; + final PowerManager powerManager = + (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mWakeLock = powerManager.newWakeLock( + PowerManager.SCREEN_DIM_WAKE_LOCK | + PowerManager.ON_AFTER_RELEASE, LOG_TAG); + mDisplayName = displayName; + mShowImmediately = showImmediately; } @Override @@ -274,47 +299,59 @@ public class ImportVCardActivity extends ContactsActivity { // to local storage, but currently vCard code does not allow us to do so. int cache_index = 0; ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>(); - for (Uri sourceUri : mSourceUris) { - String filename = null; - // Note: caches are removed by VCardService. - while (true) { - filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf"; - final File file = context.getFileStreamPath(filename); - if (!file.exists()) { - break; - } else { - if (cache_index == Integer.MAX_VALUE) { - throw new RuntimeException("Exceeded cache limit"); - } - cache_index++; - } - } - final Uri localDataUri = copyTo(sourceUri, filename); - if (mCanceled) { - Log.i(LOG_TAG, "vCard cache operation is canceled."); - break; - } - if (localDataUri == null) { - Log.w(LOG_TAG, "destUri is null"); - break; - } - final ImportRequest request; + if (mSource != null) { try { - request = constructImportRequest(localDataUri, sourceUri); + requests.add(constructImportRequest(mSource, null, mDisplayName, + mShowImmediately)); } catch (VCardException e) { Log.e(LOG_TAG, "Maybe the file is in wrong format", e); showFailureNotification(R.string.fail_reason_not_supported); return; - } catch (IOException e) { - Log.e(LOG_TAG, "Unexpected IOException", e); - showFailureNotification(R.string.fail_reason_io_error); - return; } - if (mCanceled) { - Log.i(LOG_TAG, "vCard cache operation is canceled."); - return; + } else { + for (Uri sourceUri : mSourceUris) { + String filename = null; + // Note: caches are removed by VCardService. + while (true) { + filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf"; + final File file = context.getFileStreamPath(filename); + if (!file.exists()) { + break; + } else { + if (cache_index == Integer.MAX_VALUE) { + throw new RuntimeException("Exceeded cache limit"); + } + cache_index++; + } + } + final Uri localDataUri = copyTo(sourceUri, filename); + if (mCanceled) { + Log.i(LOG_TAG, "vCard cache operation is canceled."); + break; + } + if (localDataUri == null) { + Log.w(LOG_TAG, "destUri is null"); + break; + } + final ImportRequest request; + try { + request = constructImportRequest(null, localDataUri, + sourceUri.getLastPathSegment(), mShowImmediately); + } catch (VCardException e) { + Log.e(LOG_TAG, "Maybe the file is in wrong format", e); + showFailureNotification(R.string.fail_reason_not_supported); + return; + } catch (IOException e) { + Log.e(LOG_TAG, "Unexpected IOException", e); + showFailureNotification(R.string.fail_reason_io_error); + return; + } + if (mCanceled) { + Log.i(LOG_TAG, "vCard cache operation is canceled."); + return; + } + requests.add(request); } - requests.add(request); } if (!requests.isEmpty()) { mConnection.sendImportRequest(requests); @@ -395,11 +432,12 @@ public class ImportVCardActivity extends ContactsActivity { * @arg localDataUri Uri actually used for the import. Should be stored in * app local storage, as we cannot guarantee other types of Uris can be read * multiple times. This variable populates {@link ImportRequest#uri}. - * @arg originalUri Uri requested to be imported. Used mainly for displaying - * information. This variable populates {@link ImportRequest#originalUri}. + * @arg displayName Used for displaying information to the user. This variable populates + * {@link ImportRequest#displayName}. */ - private ImportRequest constructImportRequest( - final Uri localDataUri, final Uri originalUri) + private ImportRequest constructImportRequest(final byte[] data, + final Uri localDataUri, final String displayName, + final boolean showImmediately) throws IOException, VCardException { final ContentResolver resolver = ImportVCardActivity.this.getContentResolver(); VCardEntryCounter counter = null; @@ -407,7 +445,12 @@ public class ImportVCardActivity extends ContactsActivity { int vcardVersion = VCARD_VERSION_V21; try { boolean shouldUseV30 = false; - InputStream is = resolver.openInputStream(localDataUri); + InputStream is; + if (data != null) { + is = new ByteArrayInputStream(data); + } else { + is = resolver.openInputStream(localDataUri); + } mVCardParser = new VCardParser_V21(); try { counter = new VCardEntryCounter(); @@ -422,7 +465,11 @@ public class ImportVCardActivity extends ContactsActivity { } shouldUseV30 = true; - is = resolver.openInputStream(localDataUri); + if (data != null) { + is = new ByteArrayInputStream(data); + } else { + is = resolver.openInputStream(localDataUri); + } mVCardParser = new VCardParser_V30(); try { counter = new VCardEntryCounter(); @@ -449,10 +496,11 @@ public class ImportVCardActivity extends ContactsActivity { // version before it } return new ImportRequest(mAccount, - localDataUri, originalUri, + data, localDataUri, displayName, detector.getEstimatedType(), detector.getEstimatedCharset(), - vcardVersion, counter.getCount()); + vcardVersion, counter.getCount(), + showImmediately); } public Uri[] getSourceUris() { @@ -719,6 +767,17 @@ public class ImportVCardActivity extends ContactsActivity { }); } + private void importVCard(final byte[] data, final String displayName, + final boolean showImmediately) { + runOnUiThread(new Runnable() { + public void run() { + mVCardCacheThread = new VCardCacheThread(data, displayName, + showImmediately); + showDialog(R.id.dialog_cache_vcard); + } + }); + } + private Dialog getSelectImportTypeDialog() { final DialogInterface.OnClickListener listener = new ImportTypeSelectedListener(); final AlertDialog.Builder builder = new AlertDialog.Builder(this) @@ -784,8 +843,6 @@ public class ImportVCardActivity extends ContactsActivity { if (intent != null) { accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME); accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE); - mAction = intent.getAction(); - mUri = intent.getData(); } else { Log.e(LOG_TAG, "intent does not exist"); } @@ -806,7 +863,7 @@ public class ImportVCardActivity extends ContactsActivity { } } - startImport(mAction, mUri); + startImport(); } @Override @@ -816,7 +873,7 @@ public class ImportVCardActivity extends ContactsActivity { mAccount = new Account( intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME), intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE)); - startImport(mAction, mUri); + startImport(); } else { if (resultCode != RESULT_CANCELED) { Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode); @@ -826,13 +883,34 @@ public class ImportVCardActivity extends ContactsActivity { } } - private void startImport(String action, Uri uri) { - if (uri != null) { - Log.i(LOG_TAG, "Starting vCard import using Uri " + uri); - importVCard(uri); + private void startImport() { + Intent intent = getIntent(); + if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) { + // Handle inbound NDEF + NdefMessage msg = (NdefMessage) intent.getParcelableArrayExtra( + NfcAdapter.EXTRA_NDEF_MESSAGES)[0]; + NdefRecord record = msg.getRecords()[0]; + String type = new String(record.getType(), Charset.forName("UTF8")); + if (record.getTnf() != NdefRecord.TNF_MIME_MEDIA || + (!"text/x-vcard".equalsIgnoreCase(type) && !"text/vcard".equals(type))) { + Log.d(LOG_TAG, "Not a vcard"); + showFailureNotification(R.string.fail_reason_not_supported); + finish(); + return; + } + // For NFC imports, we always show the contact once import is + // complete. + importVCard(record.getPayload(), getString(R.string.nfc_vcard_file_name), true); } else { - Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually."); - doScanExternalStorageAndImportVCard(); + // Handle inbound files + Uri uri = intent.getData(); + if (uri != null) { + Log.i(LOG_TAG, "Starting vCard import using Uri " + uri); + importVCard(uri); + } else { + Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually."); + doScanExternalStorageAndImportVCard(); + } } } @@ -967,7 +1045,7 @@ public class ImportVCardActivity extends ContactsActivity { } } - private void showFailureNotification(int reasonId) { + /* package */ void showFailureNotification(int reasonId) { final NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); final Notification notification = diff --git a/src/com/android/contacts/vcard/VCardService.java b/src/com/android/contacts/vcard/VCardService.java index 261c1c851..0358e229b 100644 --- a/src/com/android/contacts/vcard/VCardService.java +++ b/src/com/android/contacts/vcard/VCardService.java @@ -232,46 +232,46 @@ public class VCardService extends Service { private synchronized void handleImportRequest(List<ImportRequest> requests) { if (DEBUG) { final ArrayList<String> uris = new ArrayList<String>(); - final ArrayList<String> originalUris = new ArrayList<String>(); + final ArrayList<String> displayNames = new ArrayList<String>(); for (ImportRequest request : requests) { uris.add(request.uri.toString()); - originalUris.add(request.originalUri.toString()); + displayNames.add(request.displayName); } Log.d(LOG_TAG, - String.format("received multiple import request (uri: %s, originalUri: %s)", - uris.toString(), originalUris.toString())); + String.format("received multiple import request (uri: %s, displayName: %s)", + uris.toString(), displayNames.toString())); } final int size = requests.size(); for (int i = 0; i < size; i++) { ImportRequest request = requests.get(i); if (tryExecute(new ImportProcessor(this, request, mCurrentJobId))) { - final String displayName; - final String message; - final String lastPathSegment = request.originalUri.getLastPathSegment(); - if ("file".equals(request.originalUri.getScheme()) && - lastPathSegment != null) { - displayName = lastPathSegment; - message = getString(R.string.vcard_import_will_start_message, displayName); - } else { - displayName = getString(R.string.vcard_unknown_filename); - message = getString( - R.string.vcard_import_will_start_message_with_default_name); - } + if (!request.showImmediately) { + // Show a notification about the status + final String displayName; + final String message; + if (request.displayName != null) { + displayName = request.displayName; + message = getString(R.string.vcard_import_will_start_message, displayName); + } else { + displayName = getString(R.string.vcard_unknown_filename); + message = getString( + R.string.vcard_import_will_start_message_with_default_name); + } - // We just want to show notification for the first vCard. - if (i == 0) { - // TODO: Ideally we should detect the current status of import/export and show - // "started" when we can import right now and show "will start" when we cannot. - Toast.makeText(this, message, Toast.LENGTH_LONG).show(); - } + // We just want to show notification for the first vCard. + if (i == 0) { + // TODO: Ideally we should detect the current status of import/export and + // show "started" when we can import right now and show "will start" when + // we cannot. + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + } - final Notification notification = - constructProgressNotification( - this, TYPE_IMPORT, message, message, mCurrentJobId, - displayName, -1, 0); - mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, mCurrentJobId, - notification); + final Notification notification = constructProgressNotification(this, + TYPE_IMPORT, message, message, mCurrentJobId, displayName, -1, 0); + mNotificationManager.notify(VCardService.DEFAULT_NOTIFICATION_TAG, + mCurrentJobId, notification); + } mCurrentJobId++; } else { // TODO: a little unkind to show Toast in this case, which is shown just a moment. |