android.nfc.NdefRecord Maven / Gradle / Ivy
Show all versions of android-all Show documentation
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.nfc;
import android.content.Intent;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
/**
* Represents an immutable NDEF Record.
*
* NDEF (NFC Data Exchange Format) is a light-weight binary format,
* used to encapsulate typed data. It is specified by the NFC Forum,
* for transmission and storage with NFC, however it is transport agnostic.
*
* NDEF defines messages and records. An NDEF Record contains
* typed data, such as MIME-type media, a URI, or a custom
* application payload. An NDEF Message is a container for
* one or more NDEF Records.
*
* This class represents logical (complete) NDEF Records, and can not be
* used to represent chunked (partial) NDEF Records. However
* {@link NdefMessage#NdefMessage(byte[])} can be used to parse a message
* containing chunked records, and will return a message with unchunked
* (complete) records.
*
* A logical NDEF Record always contains a 3-bit TNF (Type Name Field)
* that provides high level typing for the rest of the record. The
* remaining fields are variable length and not always present:
*
* - type: detailed typing for the payload
* - id: identifier meta-data, not commonly used
* - payload: the actual payload
*
*
* Helpers such as {@link NdefRecord#createUri}, {@link NdefRecord#createMime}
* and {@link NdefRecord#createExternal} are included to create well-formatted
* NDEF Records with correctly set tnf, type, id and payload fields, please
* use these helpers whenever possible.
*
* Use the constructor {@link #NdefRecord(short, byte[], byte[], byte[])}
* if you know what you are doing and what to set the fields individually.
* Only basic validation is performed with this constructor, so it is possible
* to create records that do not confirm to the strict NFC Forum
* specifications.
*
* The binary representation of an NDEF Record includes additional flags to
* indicate location with an NDEF message, provide support for chunking of
* NDEF records, and to pack optional fields. This class does not expose
* those details. To write an NDEF Record as binary you must first put it
* into an {@link NdefMessage}, then call {@link NdefMessage#toByteArray()}.
*
* {@link NdefMessage} and {@link NdefRecord} implementations are
* always available, even on Android devices that do not have NFC hardware.
*
* {@link NdefRecord}s are intended to be immutable (and thread-safe),
* however they may contain mutable fields. So take care not to modify
* mutable fields passed into constructors, or modify mutable fields
* obtained by getter methods, unless such modification is explicitly
* marked as safe.
*
* @see NfcAdapter#ACTION_NDEF_DISCOVERED
* @see NdefMessage
*/
public final class NdefRecord implements Parcelable {
/**
* Indicates the record is empty.
* Type, id and payload fields are empty in a {@literal TNF_EMPTY} record.
*/
public static final short TNF_EMPTY = 0x00;
/**
* Indicates the type field contains a well-known RTD type name.
* Use this tnf with RTD types such as {@link #RTD_TEXT}, {@link #RTD_URI}.
*
* The RTD type name format is specified in NFCForum-TS-RTD_1.0.
*
* @see #RTD_URI
* @see #RTD_TEXT
* @see #RTD_SMART_POSTER
* @see #createUri
*/
public static final short TNF_WELL_KNOWN = 0x01;
/**
* Indicates the type field contains a media-type BNF
* construct, defined by RFC 2046.
* Use this with MIME type names such as {@literal "image/jpeg"}, or
* using the helper {@link #createMime}.
*
* @see #createMime
*/
public static final short TNF_MIME_MEDIA = 0x02;
/**
* Indicates the type field contains an absolute-URI
* BNF construct defined by RFC 3986.
* When creating new records prefer {@link #createUri},
* since it offers more compact URI encoding
* ({@literal #RTD_URI} allows compression of common URI prefixes).
*
* @see #createUri
*/
public static final short TNF_ABSOLUTE_URI = 0x03;
/**
* Indicates the type field contains an external type name.
* Used to encode custom payloads. When creating new records
* use the helper {@link #createExternal}.
* The external-type RTD format is specified in NFCForum-TS-RTD_1.0.
*
* Note this TNF should not be used with RTD_TEXT or RTD_URI constants.
* Those are well known RTD constants, not external RTD constants.
*
* @see #createExternal
*/
public static final short TNF_EXTERNAL_TYPE = 0x04;
/**
* Indicates the payload type is unknown.
* NFC Forum explains this should be treated similarly to the
* "application/octet-stream" MIME type. The payload
* type is not explicitly encoded within the record.
*
* The type field is empty in an {@literal TNF_UNKNOWN} record.
*/
public static final short TNF_UNKNOWN = 0x05;
/**
* Indicates the payload is an intermediate or final chunk of a chunked
* NDEF Record.
* {@literal TNF_UNCHANGED} can not be used with this class
* since all {@link NdefRecord}s are already unchunked, however they
* may appear in the binary format.
*/
public static final short TNF_UNCHANGED = 0x06;
/**
* Reserved TNF type.
*
* The NFC Forum NDEF Specification v1.0 suggests for NDEF parsers to treat this
* value like TNF_UNKNOWN.
* @hide
*/
public static final short TNF_RESERVED = 0x07;
/**
* RTD Text type. For use with {@literal TNF_WELL_KNOWN}.
* @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_TEXT = {0x54}; // "T"
/**
* RTD URI type. For use with {@literal TNF_WELL_KNOWN}.
* @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_URI = {0x55}; // "U"
/**
* RTD Smart Poster type. For use with {@literal TNF_WELL_KNOWN}.
* @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_SMART_POSTER = {0x53, 0x70}; // "Sp"
/**
* RTD Alternative Carrier type. For use with {@literal TNF_WELL_KNOWN}.
* @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_ALTERNATIVE_CARRIER = {0x61, 0x63}; // "ac"
/**
* RTD Handover Carrier type. For use with {@literal TNF_WELL_KNOWN}.
* @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_HANDOVER_CARRIER = {0x48, 0x63}; // "Hc"
/**
* RTD Handover Request type. For use with {@literal TNF_WELL_KNOWN}.
* @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_HANDOVER_REQUEST = {0x48, 0x72}; // "Hr"
/**
* RTD Handover Select type. For use with {@literal TNF_WELL_KNOWN}.
* @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_HANDOVER_SELECT = {0x48, 0x73}; // "Hs"
/**
* RTD Android app type. For use with {@literal TNF_EXTERNAL}.
*
* The payload of a record with type RTD_ANDROID_APP
* should be the package name identifying an application.
* Multiple RTD_ANDROID_APP records may be included
* in a single {@link NdefMessage}.
*
* Use {@link #createApplicationRecord(String)} to create
* RTD_ANDROID_APP records.
* @hide
*/
public static final byte[] RTD_ANDROID_APP = "android.com:pkg".getBytes();
private static final byte FLAG_MB = (byte) 0x80;
private static final byte FLAG_ME = (byte) 0x40;
private static final byte FLAG_CF = (byte) 0x20;
private static final byte FLAG_SR = (byte) 0x10;
private static final byte FLAG_IL = (byte) 0x08;
/**
* NFC Forum "URI Record Type Definition"
* This is a mapping of "URI Identifier Codes" to URI string prefixes,
* per section 3.2.2 of the NFC Forum URI Record Type Definition document.
*/
private static final String[] URI_PREFIX_MAP = new String[] {
"", // 0x00
"http://www.", // 0x01
"https://www.", // 0x02
"http://", // 0x03
"https://", // 0x04
"tel:", // 0x05
"mailto:", // 0x06
"ftp://anonymous:anonymous@", // 0x07
"ftp://ftp.", // 0x08
"ftps://", // 0x09
"sftp://", // 0x0A
"smb://", // 0x0B
"nfs://", // 0x0C
"ftp://", // 0x0D
"dav://", // 0x0E
"news:", // 0x0F
"telnet://", // 0x10
"imap:", // 0x11
"rtsp://", // 0x12
"urn:", // 0x13
"pop:", // 0x14
"sip:", // 0x15
"sips:", // 0x16
"tftp:", // 0x17
"btspp://", // 0x18
"btl2cap://", // 0x19
"btgoep://", // 0x1A
"tcpobex://", // 0x1B
"irdaobex://", // 0x1C
"file://", // 0x1D
"urn:epc:id:", // 0x1E
"urn:epc:tag:", // 0x1F
"urn:epc:pat:", // 0x20
"urn:epc:raw:", // 0x21
"urn:epc:", // 0x22
"urn:nfc:", // 0x23
};
private static final int MAX_PAYLOAD_SIZE = 10 * (1 << 20); // 10 MB payload limit
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private final short mTnf;
private final byte[] mType;
private final byte[] mId;
private final byte[] mPayload;
/**
* Create a new Android Application Record (AAR).
*
* This record indicates to other Android devices the package
* that should be used to handle the entire NDEF message.
* You can embed this record anywhere into your message
* to ensure that the intended package receives the message.
*
* When an Android device dispatches an {@link NdefMessage}
* containing one or more Android application records,
* the applications contained in those records will be the
* preferred target for the {@link NfcAdapter#ACTION_NDEF_DISCOVERED}
* intent, in the order in which they appear in the message.
* This dispatch behavior was first added to Android in
* Ice Cream Sandwich.
*
* If none of the applications have a are installed on the device,
* a Market link will be opened to the first application.
*
* Note that Android application records do not overrule
* applications that have called
* {@link NfcAdapter#enableForegroundDispatch}.
*
* @param packageName Android package name
* @return Android application NDEF record
*/
public static NdefRecord createApplicationRecord(String packageName) {
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(StandardCharsets.UTF_8));
}
/**
* Create a new NDEF Record containing a URI.
* Use this method to encode a URI (or URL) into an NDEF Record.
* 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.
* The uri parameter will be normalized with
* {@link Uri#normalizeScheme} 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.
*
* Reference specification: NFCForum-TS-RTD_URI_1.0
*
* @param uri URI to encode.
* @return an NDEF Record containing the URI
* @throws IllegalArugmentException if the uri is empty or invalid
*/
public static NdefRecord createUri(Uri uri) {
if (uri == null) throw new NullPointerException("uri is null");
uri = uri.normalizeScheme();
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++) {
if (uriString.startsWith(URI_PREFIX_MAP[i])) {
prefix = (byte) i;
uriString = uriString.substring(URI_PREFIX_MAP[i].length());
break;
}
}
byte[] uriBytes = uriString.getBytes(StandardCharsets.UTF_8);
byte[] recordBytes = new byte[uriBytes.length + 1];
recordBytes[0] = prefix;
System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.length);
return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, null, recordBytes);
}
/**
* Create a new NDEF Record containing a URI.
* Use this method to encode a URI (or URL) into an NDEF Record.
* 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.
* The uriString parameter will be normalized with
* {@link Uri#normalizeScheme} 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.
*
* 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.
* Use this method to encode MIME-typed data into an NDEF Record,
* such as "text/plain", or "image/jpeg".
* 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.
*
* For efficiency, This method might not make an internal copy of the
* mimeData byte array, so take care not
* to modify the mimeData byte array while still using the returned
* NdefRecord.
*
* @param mimeType a valid MIME type
* @param mimeData MIME data as bytes
* @return an NDEF Record containing the MIME-typed data
* @throws IllegalArugmentException if the mimeType is empty or invalid
*
*/
public static NdefRecord createMime(String mimeType, byte[] mimeData) {
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
// MIME RFCs suggest ASCII encoding for content-type
byte[] typeBytes = mimeType.getBytes(StandardCharsets.US_ASCII);
return new NdefRecord(TNF_MIME_MEDIA, typeBytes, null, mimeData);
}
/**
* Create a new NDEF Record containing external (application-specific) data.
* Use this method to encode application specific data into an NDEF Record.
* 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.
* 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.
* 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.
* There are no such restrictions on the payload data.
* For efficiency, This method might not make an internal copy of the
* data byte array, so take care not
* 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 either domain or type are empty or invalid
*/
public static NdefRecord createExternal(String domain, String type, byte[] data) {
if (domain == null) throw new NullPointerException("domain is null");
if (type == null) throw new NullPointerException("type is null");
domain = domain.trim().toLowerCase(Locale.ROOT);
type = type.trim().toLowerCase(Locale.ROOT);
if (domain.length() == 0) throw new IllegalArgumentException("domain is empty");
if (type.length() == 0) throw new IllegalArgumentException("type is empty");
byte[] byteDomain = domain.getBytes(StandardCharsets.UTF_8);
byte[] byteType = type.getBytes(StandardCharsets.UTF_8);
byte[] b = new byte[byteDomain.length + 1 + byteType.length];
System.arraycopy(byteDomain, 0, b, 0, byteDomain.length);
b[byteDomain.length] = ':';
System.arraycopy(byteType, 0, b, byteDomain.length + 1, byteType.length);
return new NdefRecord(TNF_EXTERNAL_TYPE, b, null, data);
}
/**
* Create a new NDEF record containing UTF-8 text data.
*
* The caller can either specify the language code for the provided text,
* or otherwise the language code corresponding to the current default
* locale will be used.
*
* Reference specification: NFCForum-TS-RTD_Text_1.0
* @param languageCode The languageCode for the record. If locale is empty or null,
* the language code of the current default locale will be used.
* @param text The text to be encoded in the record. Will be represented in UTF-8 format.
* @throws IllegalArgumentException if text is null
*/
public static NdefRecord createTextRecord(String languageCode, String text) {
if (text == null) throw new NullPointerException("text is null");
byte[] textBytes = text.getBytes(StandardCharsets.UTF_8);
byte[] languageCodeBytes = null;
if (languageCode != null && !languageCode.isEmpty()) {
languageCodeBytes = languageCode.getBytes(StandardCharsets.US_ASCII);
} else {
languageCodeBytes = Locale.getDefault().getLanguage().
getBytes(StandardCharsets.US_ASCII);
}
// We only have 6 bits to indicate ISO/IANA language code.
if (languageCodeBytes.length >= 64) {
throw new IllegalArgumentException("language code is too long, must be <64 bytes.");
}
ByteBuffer buffer = ByteBuffer.allocate(1 + languageCodeBytes.length + textBytes.length);
byte status = (byte) (languageCodeBytes.length & 0xFF);
buffer.put(status);
buffer.put(languageCodeBytes);
buffer.put(textBytes);
return new NdefRecord(TNF_WELL_KNOWN, RTD_TEXT, null, buffer.array());
}
/**
* Construct an NDEF Record from its component fields.
* Recommend to use helpers such as {#createUri} or
* {{@link #createExternal} where possible, since they perform
* stricter validation that the record is correctly formatted
* as per NDEF specifications. However if you know what you are
* doing then this constructor offers the most flexibility.
* An {@link NdefRecord} represents a logical (complete)
* record, and cannot represent NDEF Record chunks.
* Basic validation of the tnf, type, id and payload is performed
* as per the following rules:
*
* - The tnf paramter must be a 3-bit value.
* - Records with a tnf of {@link #TNF_EMPTY} cannot have a type,
* id or payload.
* - Records with a tnf of {@link #TNF_UNKNOWN} or {@literal 0x07}
* cannot have a type.
* - Records with a tnf of {@link #TNF_UNCHANGED} are not allowed
* since this class only represents complete (unchunked) records.
*
* This minimal validation is specified by
* NFCForum-TS-NDEF_1.0 section 3.2.6 (Type Name Format).
* If any of the above validation
* steps fail then {@link IllegalArgumentException} is thrown.
* Deep inspection of the type, id and payload fields is not
* performed, so it is possible to create NDEF Records
* that conform to section 3.2.6
* but fail other more strict NDEF specification requirements. For
* example, the payload may be invalid given the tnf and type.
*
* To omit a type, id or payload field, set the parameter to an
* empty byte array or null.
*
* @param tnf a 3-bit TNF constant
* @param type byte array, containing zero to 255 bytes, or null
* @param id byte array, containing zero to 255 bytes, or null
* @param payload byte array, containing zero to (2 ** 32 - 1) bytes,
* or null
* @throws IllegalArugmentException if a valid record cannot be created
*/
public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) {
/* convert nulls */
if (type == null) type = EMPTY_BYTE_ARRAY;
if (id == null) id = EMPTY_BYTE_ARRAY;
if (payload == null) payload = EMPTY_BYTE_ARRAY;
String message = validateTnf(tnf, type, id, payload);
if (message != null) {
throw new IllegalArgumentException(message);
}
mTnf = tnf;
mType = type;
mId = id;
mPayload = payload;
}
/**
* Construct an NDEF Record from raw bytes.
* This method is deprecated, use {@link NdefMessage#NdefMessage(byte[])}
* instead. This is because it does not make sense to parse a record:
* the NDEF binary format is only defined for a message, and the
* record flags MB and ME do not make sense outside of the context of
* an entire message.
* This implementation will attempt to parse a single record by ignoring
* the MB and ME flags, and otherwise following the rules of
* {@link NdefMessage#NdefMessage(byte[])}.
*
* @param data raw bytes to parse
* @throws FormatException if the data cannot be parsed into a valid record
* @deprecated use {@link NdefMessage#NdefMessage(byte[])} instead.
*/
@Deprecated
public NdefRecord(byte[] data) throws FormatException {
ByteBuffer buffer = ByteBuffer.wrap(data);
NdefRecord[] rs = parse(buffer, true);
if (buffer.remaining() > 0) {
throw new FormatException("data too long");
}
mTnf = rs[0].mTnf;
mType = rs[0].mType;
mId = rs[0].mId;
mPayload = rs[0].mPayload;
}
/**
* Returns the 3-bit TNF.
*
* TNF is the top-level type.
*/
public short getTnf() {
return mTnf;
}
/**
* Returns the variable length Type field.
*
* This should be used in conjunction with the TNF field to determine the
* payload format.
*
* Returns an empty byte array if this record
* does not have a type field.
*/
public byte[] getType() {
return mType.clone();
}
/**
* Returns the variable length ID.
*
* Returns an empty byte array if this record
* does not have an id field.
*/
public byte[] getId() {
return mId.clone();
}
/**
* Returns the variable length payload.
*
* Returns an empty byte array if this record
* does not have a payload field.
*/
public byte[] getPayload() {
return mPayload.clone();
}
/**
* Return this NDEF Record as a byte array.
* This method is deprecated, use {@link NdefMessage#toByteArray}
* instead. This is because the NDEF binary format is not defined for
* a record outside of the context of a message: the MB and ME flags
* cannot be set without knowing the location inside a message.
* This implementation will attempt to serialize a single record by
* always setting the MB and ME flags (in other words, assume this
* is a single-record NDEF Message).
*
* @deprecated use {@link NdefMessage#toByteArray()} instead
*/
@Deprecated
public byte[] toByteArray() {
ByteBuffer buffer = ByteBuffer.allocate(getByteLength());
writeToByteBuffer(buffer, true, true);
return buffer.array();
}
/**
* Map this record to a MIME type, or return null if it cannot be mapped.
* 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.
* 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.
* The returned MIME type will by normalized to lower-case using
* {@link Intent#normalizeMimeType}.
* 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 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, StandardCharsets.US_ASCII);
return Intent.normalizeMimeType(mimeType);
}
return null;
}
/**
* Map this record to a URI, or return null if it cannot be mapped.
* Currently this method considers the following to be URI records:
*
* - {@link #TNF_ABSOLUTE_URI} records.
* - {@link #TNF_WELL_KNOWN} with a type of {@link #RTD_URI}.
* - {@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.
*
* - {@link #TNF_EXTERNAL_TYPE} records.
*
* If this is not a URI record by the above rules, then null is returned.
* 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.
* The returned URI will be normalized to have a lower case scheme
* using {@link Uri#normalizeScheme}.
*
* @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)) {
Uri wktUri = parseWktUri();
return (wktUri != null ? wktUri.normalizeScheme() : null);
}
break;
case TNF_ABSOLUTE_URI:
Uri uri = Uri.parse(new String(mType, StandardCharsets.UTF_8));
return uri.normalizeScheme();
case TNF_EXTERNAL_TYPE:
if (inSmartPoster) {
break;
}
return Uri.parse("vnd.android.nfc://ext/" + new String(mType, StandardCharsets.US_ASCII));
}
return null;
}
/**
* 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;
}
// 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;
}
String prefix = URI_PREFIX_MAP[prefixIndex];
String suffix = new String(Arrays.copyOfRange(mPayload, 1, mPayload.length),
StandardCharsets.UTF_8);
return Uri.parse(prefix + suffix);
}
/**
* Main record parsing method.
* Expects NdefMessage to begin immediately, allows trailing data.
* 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
* possible to encourage well-formatted NDEF.
* Always returns 1 or more NdefRecord's, or throws FormatException.
*
* @param buffer ByteBuffer to read from
* @param ignoreMbMe ignore MB and ME flags, and read only 1 complete record
* @return one or more records
* @throws FormatException on any parsing error
*/
static NdefRecord[] parse(ByteBuffer buffer, boolean ignoreMbMe) throws FormatException {
List records = new ArrayList();
try {
byte[] type = null;
byte[] id = null;
byte[] payload = null;
ArrayList chunks = new ArrayList();
boolean inChunk = false;
short chunkTnf = -1;
boolean me = false;
while (!me) {
byte flag = buffer.get();
boolean mb = (flag & NdefRecord.FLAG_MB) != 0;
me = (flag & NdefRecord.FLAG_ME) != 0;
boolean cf = (flag & NdefRecord.FLAG_CF) != 0;
boolean sr = (flag & NdefRecord.FLAG_SR) != 0;
boolean il = (flag & NdefRecord.FLAG_IL) != 0;
short tnf = (short)(flag & 0x07);
if (!mb && records.size() == 0 && !inChunk && !ignoreMbMe) {
throw new FormatException("expected MB flag");
} else if (mb && (records.size() != 0 || inChunk) && !ignoreMbMe) {
throw new FormatException("unexpected MB flag");
} else if (inChunk && il) {
throw new FormatException("unexpected IL flag in non-leading chunk");
} else if (cf && me) {
throw new FormatException("unexpected ME flag in non-trailing chunk");
} else if (inChunk && tnf != NdefRecord.TNF_UNCHANGED) {
throw new FormatException("expected TNF_UNCHANGED in non-leading chunk");
} else if (!inChunk && tnf == NdefRecord.TNF_UNCHANGED) {
throw new FormatException("" +
"unexpected TNF_UNCHANGED in first chunk or unchunked record");
}
int typeLength = buffer.get() & 0xFF;
long payloadLength = sr ? (buffer.get() & 0xFF) : (buffer.getInt() & 0xFFFFFFFFL);
int idLength = il ? (buffer.get() & 0xFF) : 0;
if (inChunk && typeLength != 0) {
throw new FormatException("expected zero-length type in non-leading chunk");
}
if (!inChunk) {
type = (typeLength > 0 ? new byte[typeLength] : EMPTY_BYTE_ARRAY);
id = (idLength > 0 ? new byte[idLength] : EMPTY_BYTE_ARRAY);
buffer.get(type);
buffer.get(id);
}
ensureSanePayloadSize(payloadLength);
payload = (payloadLength > 0 ? new byte[(int)payloadLength] : EMPTY_BYTE_ARRAY);
buffer.get(payload);
if (cf && !inChunk) {
// first chunk
if (typeLength == 0 && tnf != NdefRecord.TNF_UNKNOWN) {
throw new FormatException("expected non-zero type length in first chunk");
}
chunks.clear();
chunkTnf = tnf;
}
if (cf || inChunk) {
// any chunk
chunks.add(payload);
}
if (!cf && inChunk) {
// last chunk, flatten the payload
payloadLength = 0;
for (byte[] p : chunks) {
payloadLength += p.length;
}
ensureSanePayloadSize(payloadLength);
payload = new byte[(int)payloadLength];
int i = 0;
for (byte[] p : chunks) {
System.arraycopy(p, 0, payload, i, p.length);
i += p.length;
}
tnf = chunkTnf;
}
if (cf) {
// more chunks to come
inChunk = true;
continue;
} else {
inChunk = false;
}
String error = validateTnf(tnf, type, id, payload);
if (error != null) {
throw new FormatException(error);
}
records.add(new NdefRecord(tnf, type, id, payload));
if (ignoreMbMe) { // for parsing a single NdefRecord
break;
}
}
} catch (BufferUnderflowException e) {
throw new FormatException("expected more data", e);
}
return records.toArray(new NdefRecord[records.size()]);
}
private static void ensureSanePayloadSize(long size) throws FormatException {
if (size > MAX_PAYLOAD_SIZE) {
throw new FormatException(
"payload above max limit: " + size + " > " + MAX_PAYLOAD_SIZE);
}
}
/**
* Perform simple validation that the tnf is valid.
* Validates the requirements of NFCForum-TS-NDEF_1.0 section
* 3.2.6 (Type Name Format). This just validates that the tnf
* is valid, and that the relevant type, id and payload
* fields are present (or empty) for this tnf. It does not
* perform any deep inspection of the type, id and payload fields.
* Also does not allow TNF_UNCHANGED since this class is only used
* to present logical (unchunked) records.
*
* @return null if valid, or a string error if invalid.
*/
static String validateTnf(short tnf, byte[] type, byte[] id, byte[] payload) {
switch (tnf) {
case TNF_EMPTY:
if (type.length != 0 || id.length != 0 || payload.length != 0) {
return "unexpected data in TNF_EMPTY record";
}
return null;
case TNF_WELL_KNOWN:
case TNF_MIME_MEDIA:
case TNF_ABSOLUTE_URI:
case TNF_EXTERNAL_TYPE:
return null;
case TNF_UNKNOWN:
case TNF_RESERVED:
if (type.length != 0) {
return "unexpected type field in TNF_UNKNOWN or TNF_RESERVEd record";
}
return null;
case TNF_UNCHANGED:
return "unexpected TNF_UNCHANGED in first chunk or logical record";
default:
return String.format("unexpected tnf value: 0x%02x", tnf);
}
}
/**
* Serialize record for network transmission.
* Uses specified MB and ME flags.
* Does not chunk records.
*/
void writeToByteBuffer(ByteBuffer buffer, boolean mb, boolean me) {
boolean sr = mPayload.length < 256;
boolean il = mTnf == TNF_EMPTY ? true : mId.length > 0;
byte flags = (byte)((mb ? FLAG_MB : 0) | (me ? FLAG_ME : 0) |
(sr ? FLAG_SR : 0) | (il ? FLAG_IL : 0) | mTnf);
buffer.put(flags);
buffer.put((byte)mType.length);
if (sr) {
buffer.put((byte)mPayload.length);
} else {
buffer.putInt(mPayload.length);
}
if (il) {
buffer.put((byte)mId.length);
}
buffer.put(mType);
buffer.put(mId);
buffer.put(mPayload);
}
/**
* Get byte length of serialized record.
*/
int getByteLength() {
int length = 3 + mType.length + mId.length + mPayload.length;
boolean sr = mPayload.length < 256;
boolean il = mTnf == TNF_EMPTY ? true : mId.length > 0;
if (!sr) length += 3;
if (il) length += 1;
return length;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mTnf);
dest.writeInt(mType.length);
dest.writeByteArray(mType);
dest.writeInt(mId.length);
dest.writeByteArray(mId);
dest.writeInt(mPayload.length);
dest.writeByteArray(mPayload);
}
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
@Override
public NdefRecord createFromParcel(Parcel in) {
short tnf = (short)in.readInt();
int typeLength = in.readInt();
byte[] type = new byte[typeLength];
in.readByteArray(type);
int idLength = in.readInt();
byte[] id = new byte[idLength];
in.readByteArray(id);
int payloadLength = in.readInt();
byte[] payload = new byte[payloadLength];
in.readByteArray(payload);
return new NdefRecord(tnf, type, id, payload);
}
@Override
public NdefRecord[] newArray(int size) {
return new NdefRecord[size];
}
};
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(mId);
result = prime * result + Arrays.hashCode(mPayload);
result = prime * result + mTnf;
result = prime * result + Arrays.hashCode(mType);
return result;
}
/**
* Returns true if the specified NDEF Record contains
* identical tnf, type, id and payload fields.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
NdefRecord other = (NdefRecord) obj;
if (!Arrays.equals(mId, other.mId)) return false;
if (!Arrays.equals(mPayload, other.mPayload)) return false;
if (mTnf != other.mTnf) return false;
return Arrays.equals(mType, other.mType);
}
@Override
public String toString() {
StringBuilder b = new StringBuilder(String.format("NdefRecord tnf=%X", mTnf));
if (mType.length > 0) b.append(" type=").append(bytesToString(mType));
if (mId.length > 0) b.append(" id=").append(bytesToString(mId));
if (mPayload.length > 0) b.append(" payload=").append(bytesToString(mPayload));
return b.toString();
}
private static StringBuilder bytesToString(byte[] bs) {
StringBuilder s = new StringBuilder();
for (byte b : bs) {
s.append(String.format("%02X", b));
}
return s;
}
}