diff options
author | Nick Pelly <npelly@google.com> | 2012-01-05 15:13:01 +1100 |
---|---|---|
committer | Nick Pelly <npelly@google.com> | 2012-01-25 13:17:19 -0800 |
commit | c97a552023c3c71079b39092e80c9b44f25a789b (patch) | |
tree | 639e700cdd538f2ebd080143182fa30bb32fdd2a /core/java | |
parent | dc828acd5fadb266b13cce459b1cacfad8ef7aef (diff) | |
download | frameworks_base-c97a552023c3c71079b39092e80c9b44f25a789b.zip frameworks_base-c97a552023c3c71079b39092e80c9b44f25a789b.tar.gz frameworks_base-c97a552023c3c71079b39092e80c9b44f25a789b.tar.bz2 |
Improve NDEF API's
o Add NdefRecord.toMimeType()
Maps the record to a MIME type
o Add NdefRecord.toUri()
Maps the record to a URI
o Add hidden NfcAdapter.dispatch()
Helps test the dispatch path.
o Modify createMime(), createUri() and createExternal():
Do not try and strictly follow RFC requirements for URI or MIME content
types. This just leads to heartbreak - the RFC requirements are too strict.
For example RFC1341 forbids the use of '.' in a MIME type, however this is in
common use in types such as "application/vnd.companyname". I think the best
approach is to only remove 'obvious' whitespace issues, and to convert
uppercase to lowercase as per Android guidelines.
Change-Id: Id686f5f3b05b2dceafad48e1cfcbdb2b3890b854
Diffstat (limited to 'core/java')
-rw-r--r-- | core/java/android/nfc/INfcAdapter.aidl | 3 | ||||
-rw-r--r-- | core/java/android/nfc/NdefMessage.java | 16 | ||||
-rw-r--r-- | core/java/android/nfc/NdefRecord.java | 307 | ||||
-rw-r--r-- | core/java/android/nfc/NfcAdapter.java | 36 |
4 files changed, 231 insertions, 131 deletions
diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 0b93ad0..d2afbb9 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -17,7 +17,6 @@ package android.nfc; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.IntentFilter; import android.nfc.NdefMessage; import android.nfc.Tag; @@ -44,4 +43,6 @@ interface INfcAdapter void setForegroundDispatch(in PendingIntent intent, in IntentFilter[] filters, in TechListParcel techLists); void setForegroundNdefPush(in NdefMessage msg, in INdefPushCallback callback); + + void dispatch(in Tag tag, in NdefMessage message); } diff --git a/core/java/android/nfc/NdefMessage.java b/core/java/android/nfc/NdefMessage.java index 38bc16d..c83144f 100644 --- a/core/java/android/nfc/NdefMessage.java +++ b/core/java/android/nfc/NdefMessage.java @@ -92,9 +92,7 @@ public final class NdefMessage implements Parcelable { * @throws FormatException if the data cannot be parsed */ public NdefMessage(byte[] data) throws FormatException { - if (data == null) { - throw new NullPointerException("null data"); - } + if (data == null) throw new NullPointerException("data is null"); ByteBuffer buffer = ByteBuffer.wrap(data); mRecords = NdefRecord.parse(buffer, false); @@ -112,9 +110,8 @@ public final class NdefMessage implements Parcelable { */ public NdefMessage(NdefRecord record, NdefRecord ... records) { // validate - if (record == null) { - throw new NullPointerException("record cannot be null"); - } + if (record == null) throw new NullPointerException("record cannot be null"); + for (NdefRecord r : records) { if (r == null) { throw new NullPointerException("record cannot be null"); @@ -147,7 +144,12 @@ public final class NdefMessage implements Parcelable { /** * Get the NDEF Records inside this NDEF Message.<p> - * An NDEF Message always has one or more NDEF Records. + * An {@link NdefMessage} always has one or more NDEF Records: so the + * following code to retrieve the first record is always safe + * (no need to check for null or array length >= 1): + * <pre> + * NdefRecord firstRecord = ndefMessage.getRecords()[0]; + * </pre> * * @return array of one or more NDEF records. */ diff --git a/core/java/android/nfc/NdefRecord.java b/core/java/android/nfc/NdefRecord.java index b4c488b..0e9e8f4 100644 --- a/core/java/android/nfc/NdefRecord.java +++ b/core/java/android/nfc/NdefRecord.java @@ -16,6 +16,7 @@ package android.nfc; +import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; @@ -25,6 +26,7 @@ import java.nio.charset.Charsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; /** * Represents an immutable NDEF Record. @@ -305,9 +307,9 @@ public final class NdefRecord implements Parcelable { * @return Android application NDEF record */ public static NdefRecord createApplicationRecord(String packageName) { - if (packageName.length() == 0) { - throw new IllegalArgumentException("empty package name"); - } + if (packageName == null) throw new NullPointerException("packageName is null"); + if (packageName.length() == 0) throw new IllegalArgumentException("packageName is empty"); + return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, null, packageName.getBytes(Charsets.UTF_8)); } @@ -318,32 +320,27 @@ public final class NdefRecord implements Parcelable { * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} * and {@link #RTD_URI}. This is the most efficient encoding * of a URI into NDEF.<p> + * The uri parameter will be normalized with + * {@link Uri#normalize} to set the scheme to lower case to + * follow Android best practices for intent filtering. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown if the uri + * parameter has serious problems, for example if it is empty, so always + * catch this exception if you are passing user-generated data into this + * method.<p> + * * Reference specification: NFCForum-TS-RTD_URI_1.0 * * @param uri URI to encode. * @return an NDEF Record containing the URI - * @throws IllegalArugmentException if a valid record cannot be created + * @throws IllegalArugmentException if the uri is empty or invalid */ public static NdefRecord createUri(Uri uri) { - return createUri(uri.toString()); - } + if (uri == null) throw new NullPointerException("uri is null"); - /** - * Create a new NDEF Record containing a URI.<p> - * Use this method to encode a URI (or URL) into an NDEF Record.<p> - * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} - * and {@link #RTD_URI}. This is the most efficient encoding - * of a URI into NDEF.<p> - * Reference specification: NFCForum-TS-RTD_URI_1.0 - * - * @param uriString string URI to encode. - * @return an NDEF Record containing the URI - * @throws IllegalArugmentException if a valid record cannot be created - */ - public static NdefRecord createUri(String uriString) { - if (uriString.length() == 0) { - throw new IllegalArgumentException("empty uriString"); - } + uri = uri.normalize(); + String uriString = uri.toString(); + if (uriString.length() == 0) throw new IllegalArgumentException("uri is empty"); byte prefix = 0; for (int i = 1; i < URI_PREFIX_MAP.length; i++) { @@ -361,28 +358,72 @@ public final class NdefRecord implements Parcelable { } /** + * Create a new NDEF Record containing a URI.<p> + * Use this method to encode a URI (or URL) into an NDEF Record.<p> + * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} + * and {@link #RTD_URI}. This is the most efficient encoding + * of a URI into NDEF.<p> + * The uriString parameter will be normalized with + * {@link Uri#normalize} to set the scheme to lower case to + * follow Android best practices for intent filtering. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown if the uriString + * parameter has serious problems, for example if it is empty, so always + * catch this exception if you are passing user-generated data into this + * method.<p> + * + * Reference specification: NFCForum-TS-RTD_URI_1.0 + * + * @param uriString string URI to encode. + * @return an NDEF Record containing the URI + * @throws IllegalArugmentException if the uriString is empty or invalid + */ + public static NdefRecord createUri(String uriString) { + return createUri(Uri.parse(uriString)); + } + + /** * Create a new NDEF Record containing MIME data.<p> * Use this method to encode MIME-typed data into an NDEF Record, * such as "text/plain", or "image/jpeg".<p> - * Expects US-ASCII characters in mimeType. The encoding of the - * mimeData depends on the mimeType.<p> + * The mimeType parameter will be normalized with + * {@link Intent#normalizeMimeType} to follow Android best + * practices for intent filtering, for example to force lower-case. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown + * if the mimeType parameter has serious problems, + * for example if it is empty, so always catch this + * exception if you are passing user-generated data into this method. + * <p> * For efficiency, This method might not make an internal copy of the * mimeData byte array, so take care not - * to re-use the mimeData byte array while still using the returned + * to modify the mimeData byte array while still using the returned * NdefRecord. * - * @param mimeType MIME type, expects US-ASCII characters only + * @param mimeType a valid MIME type * @param mimeData MIME data as bytes * @return an NDEF Record containing the MIME-typed data - * @throws IllegalArugmentException if a valid record cannot be created + * @throws IllegalArugmentException if the mimeType is empty or invalid + * */ public static NdefRecord createMime(String mimeType, byte[] mimeData) { - if (mimeType.length() == 0) { - throw new IllegalArgumentException("empty mimeType"); + if (mimeType == null) throw new NullPointerException("mimeType is null"); + + // We only do basic MIME type validation: trying to follow the + // RFCs strictly only ends in tears, since there are lots of MIME + // types in common use that are not strictly valid as per RFC rules + mimeType = Intent.normalizeMimeType(mimeType); + if (mimeType.length() == 0) throw new IllegalArgumentException("mimeType is empty"); + int slashIndex = mimeType.indexOf('/'); + if (slashIndex == 0) throw new IllegalArgumentException("mimeType must have major type"); + if (slashIndex == mimeType.length() - 1) { + throw new IllegalArgumentException("mimeType must have minor type"); } + // missing '/' is allowed - return new NdefRecord(TNF_MIME_MEDIA, mimeType.getBytes(Charsets.US_ASCII), null, - mimeData); + // MIME RFCs suggest ASCII encoding for content-type + byte[] typeBytes = mimeType.getBytes(Charsets.US_ASCII); + return new NdefRecord(TNF_MIME_MEDIA, typeBytes, null, mimeData); } /** @@ -391,32 +432,38 @@ public final class NdefRecord implements Parcelable { * The data is typed by a domain name (usually your Android package name) and * a domain-specific type. This data is packaged into a "NFC Forum External * Type" NDEF Record.<p> - * Both the domain and type used to construct an external record are case - * insensitive, and this implementation will encode all characters to lower - * case. Only a subset of ASCII characters are allowed for the domain - * and type. There are no restrictions on the payload data.<p> + * NFC Forum requires that the domain and type used in an external record + * are treated as case insensitive, however Android intent filtering is + * always case sensitive. So this method will force the domain and type to + * lower-case before creating the NDEF Record.<p> + * The unchecked exception {@link IllegalArgumentException} will be thrown + * if the domain and type have serious problems, for example if either field + * is empty, so always catch this + * exception if you are passing user-generated data into this method.<p> + * There are no such restrictions on the payload data.<p> * For efficiency, This method might not make an internal copy of the * data byte array, so take care not - * to re-use the data byte array while still using the returned + * to modify the data byte array while still using the returned * NdefRecord. * * Reference specification: NFCForum-TS-RTD_1.0 * @param domain domain-name of issuing organization * @param type domain-specific type of data * @param data payload as bytes - * @throws IllegalArugmentException if a valid record cannot be created + * @throws IllegalArugmentException if either domain or type are empty or invalid */ public static NdefRecord createExternal(String domain, String type, byte[] data) { - if (domain.length() == 0 || type.length() == 0) { - throw new IllegalArgumentException("empty domain or type"); - } - byte[] byteDomain = domain.getBytes(Charsets.US_ASCII); - ensureValidDomain(byteDomain); - toLowerCase(byteDomain); - byte[] byteType = type.getBytes(Charsets.US_ASCII); - ensureValidWkt(byteType); - toLowerCase(byteType); + if (domain == null) throw new NullPointerException("domain is null"); + if (type == null) throw new NullPointerException("type is null"); + + domain = domain.trim().toLowerCase(Locale.US); + type = type.trim().toLowerCase(Locale.US); + + if (domain.length() == 0) throw new IllegalArgumentException("domain is empty"); + if (type.length() == 0) throw new IllegalArgumentException("type is empty"); + byte[] byteDomain = domain.getBytes(Charsets.UTF_8); + byte[] byteType = type.getBytes(Charsets.UTF_8); byte[] b = new byte[byteDomain.length + 1 + byteType.length]; System.arraycopy(byteDomain, 0, b, 0, byteDomain.length); b[byteDomain.length] = ':'; @@ -574,51 +621,113 @@ public final class NdefRecord implements Parcelable { } /** - * Helper to return the NdefRecord as a URI. - * TODO: Consider making a member method instead of static - * TODO: Consider more validation that this is a URI record - * TODO: Make a public API - * @hide + * Map this record to a MIME type, or return null if it cannot be mapped.<p> + * Currently this method considers all {@link #TNF_MIME_MEDIA} records to + * be MIME records, as well as some {@link #TNF_WELL_KNOWN} records such as + * {@link #RTD_TEXT}. If this is a MIME record then the MIME type as string + * is returned, otherwise null is returned.<p> + * This method does not perform validation that the MIME type is + * actually valid. It always attempts to + * return a string containing the type if this is a MIME record.<p> + * The returned MIME type will by normalized to lower-case using + * {@link Intent#normalizeMimeType}.<p> + * The MIME payload can be obtained using {@link #getPayload}. + * + * @return MIME type as a string, or null if this is not a MIME record */ - public static Uri parseWellKnownUriRecord(NdefRecord record) throws FormatException { - byte[] payload = record.getPayload(); - if (payload.length < 2) { - throw new FormatException("Payload is not a valid URI (missing prefix)"); + public String toMimeType() { + switch (mTnf) { + case NdefRecord.TNF_WELL_KNOWN: + if (Arrays.equals(mType, NdefRecord.RTD_TEXT)) { + return "text/plain"; + } + break; + case NdefRecord.TNF_MIME_MEDIA: + String mimeType = new String(mType, Charsets.US_ASCII); + return Intent.normalizeMimeType(mimeType); } + return null; + } - /* - * payload[0] contains the URI Identifier Code, per the - * NFC Forum "URI Record Type Definition" section 3.2.2. - * - * payload[1]...payload[payload.length - 1] contains the rest of - * the URI. - */ - int prefixIndex = (payload[0] & 0xff); - if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) { - throw new FormatException("Payload is not a valid URI (invalid prefix)"); + /** + * Map this record to a URI, or return null if it cannot be mapped.<p> + * Currently this method considers the following to be URI records: + * <ul> + * <li>{@link #TNF_ABSOLUTE_URI} records.</li> + * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_URI}.</li> + * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_SMART_POSTER} + * and containing a URI record in the NDEF message nested in the payload. + * </li> + * <li>{@link #TNF_EXTERNAL_TYPE} records.</li> + * </ul> + * If this is not a URI record by the above rules, then null is returned.<p> + * This method does not perform validation that the URI is + * actually valid: it always attempts to create and return a URI if + * this record appears to be a URI record by the above rules.<p> + * The returned URI will be normalized to have a lower case scheme + * using {@link Uri#normalize}.<p> + * + * @return URI, or null if this is not a URI record + */ + public Uri toUri() { + return toUri(false); + } + + private Uri toUri(boolean inSmartPoster) { + switch (mTnf) { + case TNF_WELL_KNOWN: + if (Arrays.equals(mType, RTD_SMART_POSTER) && !inSmartPoster) { + try { + // check payload for a nested NDEF Message containing a URI + NdefMessage nestedMessage = new NdefMessage(mPayload); + for (NdefRecord nestedRecord : nestedMessage.getRecords()) { + Uri uri = nestedRecord.toUri(true); + if (uri != null) { + return uri; + } + } + } catch (FormatException e) { } + } else if (Arrays.equals(mType, RTD_URI)) { + return parseWktUri().normalize(); + } + break; + + case TNF_ABSOLUTE_URI: + Uri uri = Uri.parse(new String(mType, Charsets.UTF_8)); + return uri.normalize(); + + case TNF_EXTERNAL_TYPE: + if (inSmartPoster) { + break; + } + return Uri.parse("vnd.android.nfc://ext/" + new String(mType, Charsets.US_ASCII)); } - String prefix = URI_PREFIX_MAP[prefixIndex]; - byte[] fullUri = concat(prefix.getBytes(Charsets.UTF_8), - Arrays.copyOfRange(payload, 1, payload.length)); - return Uri.parse(new String(fullUri, Charsets.UTF_8)); + return null; } - private static byte[] concat(byte[]... arrays) { - int length = 0; - for (byte[] array : arrays) { - length += array.length; + /** + * Return complete URI of {@link #TNF_WELL_KNOWN}, {@link #RTD_URI} records. + * @return complete URI, or null if invalid + */ + private Uri parseWktUri() { + if (mPayload.length < 2) { + return null; } - byte[] result = new byte[length]; - int pos = 0; - for (byte[] array : arrays) { - System.arraycopy(array, 0, result, pos, array.length); - pos += array.length; + + // payload[0] contains the URI Identifier Code, as per + // NFC Forum "URI Record Type Definition" section 3.2.2. + int prefixIndex = (mPayload[0] & (byte)0xFF); + if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) { + return null; } - return result; + String prefix = URI_PREFIX_MAP[prefixIndex]; + String suffix = new String(Arrays.copyOfRange(mPayload, 1, mPayload.length), + Charsets.UTF_8); + return Uri.parse(prefix + suffix); } /** - * Main parsing method.<p> + * Main record parsing method.<p> * Expects NdefMessage to begin immediately, allows trailing data.<p> * Currently has strict validation of all fields as per NDEF 1.0 * specification section 2.5. We will attempt to keep this as strict as @@ -902,42 +1011,4 @@ public final class NdefRecord implements Parcelable { } return s; } - - /** Ensure valid 'DNS-char' as per RFC2234 */ - private static void ensureValidDomain(byte[] bs) { - for (int i = 0; i < bs.length; i++) { - byte b = bs[i]; - if ((b >= 'A' && b <= 'Z') || - (b >= 'a' && b <= 'z') || - (b >= '0' && b <= '9') || - b == '.' || b == '-') { - continue; - } - throw new IllegalArgumentException("invalid character in domain"); - } - } - - /** Ensure valid 'WKT-char' as per RFC2234 */ - private static void ensureValidWkt(byte[] bs) { - for (int i = 0; i < bs.length; i++) { - byte b = bs[i]; - if ((b >= 'A' && b <= 'Z') || - (b >= 'a' && b <= 'z') || - (b >= '0' && b <= '9') || - b == '(' || b == ')' || b == '+' || b == ',' || b == '-' || - b == ':' || b == '=' || b == '@' || b == ';' || b == '$' || - b == '_' || b == '!' || b == '*' || b == '\'' || b == '.') { - continue; - } - throw new IllegalArgumentException("invalid character in type"); - } - } - - private static void toLowerCase(byte[] b) { - for (int i = 0; i < b.length; i++) { - if (b[i] >= 'A' && b[i] <= 'Z') { - b[i] += 0x20; - } - } - } } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 53a0341..224a8bc 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -66,6 +66,9 @@ public final class NfcAdapter { * <p>If the tag has an NDEF payload this intent is started before * {@link #ACTION_TECH_DISCOVERED}. If any activities respond to this intent neither * {@link #ACTION_TECH_DISCOVERED} or {@link #ACTION_TAG_DISCOVERED} will be started. + * + * <p>The MIME type or data URI of this intent are normalized before dispatch - + * so that MIME, URI scheme and URI host are always lower-case. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED"; @@ -151,9 +154,13 @@ public final class NfcAdapter { public static final String EXTRA_TAG = "android.nfc.extra.TAG"; /** - * Optional extra containing an array of {@link NdefMessage} present on the discovered tag for - * the {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED}, and - * {@link #ACTION_TAG_DISCOVERED} intents. + * Extra containing an array of {@link NdefMessage} present on the discovered tag.<p> + * This extra is mandatory for {@link #ACTION_NDEF_DISCOVERED} intents, + * and optional for {@link #ACTION_TECH_DISCOVERED}, and + * {@link #ACTION_TAG_DISCOVERED} intents.<p> + * When this extra is present there will always be at least one + * {@link NdefMessage} element. Most NDEF tags have only one NDEF message, + * but we use an array for future compatibility. */ public static final String EXTRA_NDEF_MESSAGES = "android.nfc.extra.NDEF_MESSAGES"; @@ -386,10 +393,10 @@ public final class NfcAdapter { */ @Deprecated public static NfcAdapter getDefaultAdapter() { - // introduce in API version 9 (GB 2.3) + // introduced in API version 9 (GB 2.3) // deprecated in API version 10 (GB 2.3.3) // removed from public API in version 16 (ICS MR2) - // will need to maintain this as a hidden API for a while longer... + // should maintain as a hidden API for binary compatibility for a little longer Log.w(TAG, "WARNING: NfcAdapter.getDefaultAdapter() is deprecated, use " + "NfcAdapter.getDefaultAdapter(Context) instead", new Exception()); @@ -803,6 +810,7 @@ public final class NfcAdapter { * @throws IllegalStateException if the Activity has already been paused * @deprecated use {@link #setNdefPushMessage} instead */ + @Deprecated public void disableForegroundNdefPush(Activity activity) { if (activity == null) { throw new NullPointerException(); @@ -875,6 +883,24 @@ public final class NfcAdapter { } /** + * Inject a mock NFC tag.<p> + * Used for testing purposes. + * <p class="note">Requires the + * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} permission. + * @hide + */ + public void dispatch(Tag tag, NdefMessage message) { + if (tag == null) { + throw new NullPointerException("tag cannot be null"); + } + try { + sService.dispatch(tag, message); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + /** * @hide */ public INfcAdapterExtras getNfcAdapterExtrasInterface() { |