1 /*
2  * Copyright (C) 2010 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 android.nfc;
18 
19 import android.content.Intent;
20 import android.net.Uri;
21 import android.os.Parcel;
22 import android.os.Parcelable;
23 
24 import java.nio.BufferUnderflowException;
25 import java.nio.ByteBuffer;
26 import java.nio.charset.StandardCharsets;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.List;
30 import java.util.Locale;
31 
32 /**
33  * Represents an immutable NDEF Record.
34  * <p>
35  * NDEF (NFC Data Exchange Format) is a light-weight binary format,
36  * used to encapsulate typed data. It is specified by the NFC Forum,
37  * for transmission and storage with NFC, however it is transport agnostic.
38  * <p>
39  * NDEF defines messages and records. An NDEF Record contains
40  * typed data, such as MIME-type media, a URI, or a custom
41  * application payload. An NDEF Message is a container for
42  * one or more NDEF Records.
43  * <p>
44  * This class represents logical (complete) NDEF Records, and can not be
45  * used to represent chunked (partial) NDEF Records. However
46  * {@link NdefMessage#NdefMessage(byte[])} can be used to parse a message
47  * containing chunked records, and will return a message with unchunked
48  * (complete) records.
49  * <p>
50  * A logical NDEF Record always contains a 3-bit TNF (Type Name Field)
51  * that provides high level typing for the rest of the record. The
52  * remaining fields are variable length and not always present:
53  * <ul>
54  * <li><em>type</em>: detailed typing for the payload</li>
55  * <li><em>id</em>: identifier meta-data, not commonly used</li>
56  * <li><em>payload</em>: the actual payload</li>
57  * </ul>
58  * <p>
59  * Helpers such as {@link NdefRecord#createUri}, {@link NdefRecord#createMime}
60  * and {@link NdefRecord#createExternal} are included to create well-formatted
61  * NDEF Records with correctly set tnf, type, id and payload fields, please
62  * use these helpers whenever possible.
63  * <p>
64  * Use the constructor {@link #NdefRecord(short, byte[], byte[], byte[])}
65  * if you know what you are doing and what to set the fields individually.
66  * Only basic validation is performed with this constructor, so it is possible
67  * to create records that do not confirm to the strict NFC Forum
68  * specifications.
69  * <p>
70  * The binary representation of an NDEF Record includes additional flags to
71  * indicate location with an NDEF message, provide support for chunking of
72  * NDEF records, and to pack optional fields. This class does not expose
73  * those details. To write an NDEF Record as binary you must first put it
74  * into an {@link NdefMessage}, then call {@link NdefMessage#toByteArray()}.
75  * <p class="note">
76  * {@link NdefMessage} and {@link NdefRecord} implementations are
77  * always available, even on Android devices that do not have NFC hardware.
78  * <p class="note">
79  * {@link NdefRecord}s are intended to be immutable (and thread-safe),
80  * however they may contain mutable fields. So take care not to modify
81  * mutable fields passed into constructors, or modify mutable fields
82  * obtained by getter methods, unless such modification is explicitly
83  * marked as safe.
84  *
85  * @see NfcAdapter#ACTION_NDEF_DISCOVERED
86  * @see NdefMessage
87  */
88 public final class NdefRecord implements Parcelable {
89     /**
90      * Indicates the record is empty.<p>
91      * Type, id and payload fields are empty in a {@literal TNF_EMPTY} record.
92      */
93     public static final short TNF_EMPTY = 0x00;
94 
95     /**
96      * Indicates the type field contains a well-known RTD type name.<p>
97      * Use this tnf with RTD types such as {@link #RTD_TEXT}, {@link #RTD_URI}.
98      * <p>
99      * The RTD type name format is specified in NFCForum-TS-RTD_1.0.
100      *
101      * @see #RTD_URI
102      * @see #RTD_TEXT
103      * @see #RTD_SMART_POSTER
104      * @see #createUri
105      */
106     public static final short TNF_WELL_KNOWN = 0x01;
107 
108     /**
109      * Indicates the type field contains a media-type BNF
110      * construct, defined by RFC 2046.<p>
111      * Use this with MIME type names such as {@literal "image/jpeg"}, or
112      * using the helper {@link #createMime}.
113      *
114      * @see #createMime
115      */
116     public static final short TNF_MIME_MEDIA = 0x02;
117 
118     /**
119      * Indicates the type field contains an absolute-URI
120      * BNF construct defined by RFC 3986.<p>
121      * When creating new records prefer {@link #createUri},
122      * since it offers more compact URI encoding
123      * ({@literal #RTD_URI} allows compression of common URI prefixes).
124      *
125      * @see #createUri
126      */
127     public static final short TNF_ABSOLUTE_URI = 0x03;
128 
129     /**
130      * Indicates the type field contains an external type name.<p>
131      * Used to encode custom payloads. When creating new records
132      * use the helper {@link #createExternal}.<p>
133      * The external-type RTD format is specified in NFCForum-TS-RTD_1.0.<p>
134      * <p>
135      * Note this TNF should not be used with RTD_TEXT or RTD_URI constants.
136      * Those are well known RTD constants, not external RTD constants.
137      *
138      * @see #createExternal
139      */
140     public static final short TNF_EXTERNAL_TYPE = 0x04;
141 
142     /**
143      * Indicates the payload type is unknown.<p>
144      * NFC Forum explains this should be treated similarly to the
145      * "application/octet-stream" MIME type. The payload
146      * type is not explicitly encoded within the record.
147      * <p>
148      * The type field is empty in an {@literal TNF_UNKNOWN} record.
149      */
150     public static final short TNF_UNKNOWN = 0x05;
151 
152     /**
153      * Indicates the payload is an intermediate or final chunk of a chunked
154      * NDEF Record.<p>
155      * {@literal TNF_UNCHANGED} can not be used with this class
156      * since all {@link NdefRecord}s are already unchunked, however they
157      * may appear in the binary format.
158      */
159     public static final short TNF_UNCHANGED = 0x06;
160 
161     /**
162      * Reserved TNF type.
163      * <p>
164      * The NFC Forum NDEF Specification v1.0 suggests for NDEF parsers to treat this
165      * value like TNF_UNKNOWN.
166      * @hide
167      */
168     public static final short TNF_RESERVED = 0x07;
169 
170     /**
171      * RTD Text type. For use with {@literal TNF_WELL_KNOWN}.
172      * @see #TNF_WELL_KNOWN
173      */
174     public static final byte[] RTD_TEXT = {0x54};  // "T"
175 
176     /**
177      * RTD URI type. For use with {@literal TNF_WELL_KNOWN}.
178      * @see #TNF_WELL_KNOWN
179      */
180     public static final byte[] RTD_URI = {0x55};   // "U"
181 
182     /**
183      * RTD Smart Poster type. For use with {@literal TNF_WELL_KNOWN}.
184      * @see #TNF_WELL_KNOWN
185      */
186     public static final byte[] RTD_SMART_POSTER = {0x53, 0x70};  // "Sp"
187 
188     /**
189      * RTD Alternative Carrier type. For use with {@literal TNF_WELL_KNOWN}.
190      * @see #TNF_WELL_KNOWN
191      */
192     public static final byte[] RTD_ALTERNATIVE_CARRIER = {0x61, 0x63};  // "ac"
193 
194     /**
195      * RTD Handover Carrier type. For use with {@literal TNF_WELL_KNOWN}.
196      * @see #TNF_WELL_KNOWN
197      */
198     public static final byte[] RTD_HANDOVER_CARRIER = {0x48, 0x63};  // "Hc"
199 
200     /**
201      * RTD Handover Request type. For use with {@literal TNF_WELL_KNOWN}.
202      * @see #TNF_WELL_KNOWN
203      */
204     public static final byte[] RTD_HANDOVER_REQUEST = {0x48, 0x72};  // "Hr"
205 
206     /**
207      * RTD Handover Select type. For use with {@literal TNF_WELL_KNOWN}.
208      * @see #TNF_WELL_KNOWN
209      */
210     public static final byte[] RTD_HANDOVER_SELECT = {0x48, 0x73}; // "Hs"
211 
212     /**
213      * RTD Android app type. For use with {@literal TNF_EXTERNAL}.
214      * <p>
215      * The payload of a record with type RTD_ANDROID_APP
216      * should be the package name identifying an application.
217      * Multiple RTD_ANDROID_APP records may be included
218      * in a single {@link NdefMessage}.
219      * <p>
220      * Use {@link #createApplicationRecord(String)} to create
221      * RTD_ANDROID_APP records.
222      * @hide
223      */
224     public static final byte[] RTD_ANDROID_APP = "android.com:pkg".getBytes();
225 
226     private static final byte FLAG_MB = (byte) 0x80;
227     private static final byte FLAG_ME = (byte) 0x40;
228     private static final byte FLAG_CF = (byte) 0x20;
229     private static final byte FLAG_SR = (byte) 0x10;
230     private static final byte FLAG_IL = (byte) 0x08;
231 
232     /**
233      * NFC Forum "URI Record Type Definition"<p>
234      * This is a mapping of "URI Identifier Codes" to URI string prefixes,
235      * per section 3.2.2 of the NFC Forum URI Record Type Definition document.
236      */
237     private static final String[] URI_PREFIX_MAP = new String[] {
238             "", // 0x00
239             "http://www.", // 0x01
240             "https://www.", // 0x02
241             "http://", // 0x03
242             "https://", // 0x04
243             "tel:", // 0x05
244             "mailto:", // 0x06
245             "ftp://anonymous:anonymous@", // 0x07
246             "ftp://ftp.", // 0x08
247             "ftps://", // 0x09
248             "sftp://", // 0x0A
249             "smb://", // 0x0B
250             "nfs://", // 0x0C
251             "ftp://", // 0x0D
252             "dav://", // 0x0E
253             "news:", // 0x0F
254             "telnet://", // 0x10
255             "imap:", // 0x11
256             "rtsp://", // 0x12
257             "urn:", // 0x13
258             "pop:", // 0x14
259             "sip:", // 0x15
260             "sips:", // 0x16
261             "tftp:", // 0x17
262             "btspp://", // 0x18
263             "btl2cap://", // 0x19
264             "btgoep://", // 0x1A
265             "tcpobex://", // 0x1B
266             "irdaobex://", // 0x1C
267             "file://", // 0x1D
268             "urn:epc:id:", // 0x1E
269             "urn:epc:tag:", // 0x1F
270             "urn:epc:pat:", // 0x20
271             "urn:epc:raw:", // 0x21
272             "urn:epc:", // 0x22
273             "urn:nfc:", // 0x23
274     };
275 
276     private static final int MAX_PAYLOAD_SIZE = 10 * (1 << 20);  // 10 MB payload limit
277 
278     private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
279 
280     private final short mTnf;
281     private final byte[] mType;
282     private final byte[] mId;
283     private final byte[] mPayload;
284 
285     /**
286      * Create a new Android Application Record (AAR).
287      * <p>
288      * This record indicates to other Android devices the package
289      * that should be used to handle the entire NDEF message.
290      * You can embed this record anywhere into your message
291      * to ensure that the intended package receives the message.
292      * <p>
293      * When an Android device dispatches an {@link NdefMessage}
294      * containing one or more Android application records,
295      * the applications contained in those records will be the
296      * preferred target for the {@link NfcAdapter#ACTION_NDEF_DISCOVERED}
297      * intent, in the order in which they appear in the message.
298      * This dispatch behavior was first added to Android in
299      * Ice Cream Sandwich.
300      * <p>
301      * If none of the applications have a are installed on the device,
302      * a Market link will be opened to the first application.
303      * <p>
304      * Note that Android application records do not overrule
305      * applications that have called
306      * {@link NfcAdapter#enableForegroundDispatch}.
307      *
308      * @param packageName Android package name
309      * @return Android application NDEF record
310      */
createApplicationRecord(String packageName)311     public static NdefRecord createApplicationRecord(String packageName) {
312         if (packageName == null) throw new NullPointerException("packageName is null");
313         if (packageName.length() == 0) throw new IllegalArgumentException("packageName is empty");
314 
315         return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, null,
316                 packageName.getBytes(StandardCharsets.UTF_8));
317     }
318 
319     /**
320      * Create a new NDEF Record containing a URI.<p>
321      * Use this method to encode a URI (or URL) into an NDEF Record.<p>
322      * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN}
323      * and {@link #RTD_URI}. This is the most efficient encoding
324      * of a URI into NDEF.<p>
325      * The uri parameter will be normalized with
326      * {@link Uri#normalizeScheme} to set the scheme to lower case to
327      * follow Android best practices for intent filtering.
328      * However the unchecked exception
329      * {@link IllegalArgumentException} may be thrown if the uri
330      * parameter has serious problems, for example if it is empty, so always
331      * catch this exception if you are passing user-generated data into this
332      * method.<p>
333      *
334      * Reference specification: NFCForum-TS-RTD_URI_1.0
335      *
336      * @param uri URI to encode.
337      * @return an NDEF Record containing the URI
338      * @throws IllegalArugmentException if the uri is empty or invalid
339      */
createUri(Uri uri)340     public static NdefRecord createUri(Uri uri) {
341         if (uri == null) throw new NullPointerException("uri is null");
342 
343         uri = uri.normalizeScheme();
344         String uriString = uri.toString();
345         if (uriString.length() == 0) throw new IllegalArgumentException("uri is empty");
346 
347         byte prefix = 0;
348         for (int i = 1; i < URI_PREFIX_MAP.length; i++) {
349             if (uriString.startsWith(URI_PREFIX_MAP[i])) {
350                 prefix = (byte) i;
351                 uriString = uriString.substring(URI_PREFIX_MAP[i].length());
352                 break;
353             }
354         }
355         byte[] uriBytes = uriString.getBytes(StandardCharsets.UTF_8);
356         byte[] recordBytes = new byte[uriBytes.length + 1];
357         recordBytes[0] = prefix;
358         System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.length);
359         return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, null, recordBytes);
360     }
361 
362     /**
363      * Create a new NDEF Record containing a URI.<p>
364      * Use this method to encode a URI (or URL) into an NDEF Record.<p>
365      * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN}
366      * and {@link #RTD_URI}. This is the most efficient encoding
367      * of a URI into NDEF.<p>
368       * The uriString parameter will be normalized with
369      * {@link Uri#normalizeScheme} to set the scheme to lower case to
370      * follow Android best practices for intent filtering.
371      * However the unchecked exception
372      * {@link IllegalArgumentException} may be thrown if the uriString
373      * parameter has serious problems, for example if it is empty, so always
374      * catch this exception if you are passing user-generated data into this
375      * method.<p>
376      *
377      * Reference specification: NFCForum-TS-RTD_URI_1.0
378      *
379      * @param uriString string URI to encode.
380      * @return an NDEF Record containing the URI
381      * @throws IllegalArugmentException if the uriString is empty or invalid
382      */
createUri(String uriString)383     public static NdefRecord createUri(String uriString) {
384         return createUri(Uri.parse(uriString));
385     }
386 
387     /**
388      * Create a new NDEF Record containing MIME data.<p>
389      * Use this method to encode MIME-typed data into an NDEF Record,
390      * such as "text/plain", or "image/jpeg".<p>
391      * The mimeType parameter will be normalized with
392      * {@link Intent#normalizeMimeType} to follow Android best
393      * practices for intent filtering, for example to force lower-case.
394      * However the unchecked exception
395      * {@link IllegalArgumentException} may be thrown
396      * if the mimeType parameter has serious problems,
397      * for example if it is empty, so always catch this
398      * exception if you are passing user-generated data into this method.
399      * <p>
400      * For efficiency, This method might not make an internal copy of the
401      * mimeData byte array, so take care not
402      * to modify the mimeData byte array while still using the returned
403      * NdefRecord.
404      *
405      * @param mimeType a valid MIME type
406      * @param mimeData MIME data as bytes
407      * @return an NDEF Record containing the MIME-typed data
408      * @throws IllegalArugmentException if the mimeType is empty or invalid
409      *
410      */
createMime(String mimeType, byte[] mimeData)411     public static NdefRecord createMime(String mimeType, byte[] mimeData) {
412         if (mimeType == null) throw new NullPointerException("mimeType is null");
413 
414         // We only do basic MIME type validation: trying to follow the
415         // RFCs strictly only ends in tears, since there are lots of MIME
416         // types in common use that are not strictly valid as per RFC rules
417         mimeType = Intent.normalizeMimeType(mimeType);
418         if (mimeType.length() == 0) throw new IllegalArgumentException("mimeType is empty");
419         int slashIndex = mimeType.indexOf('/');
420         if (slashIndex == 0) throw new IllegalArgumentException("mimeType must have major type");
421         if (slashIndex == mimeType.length() - 1) {
422             throw new IllegalArgumentException("mimeType must have minor type");
423         }
424         // missing '/' is allowed
425 
426         // MIME RFCs suggest ASCII encoding for content-type
427         byte[] typeBytes = mimeType.getBytes(StandardCharsets.US_ASCII);
428         return new NdefRecord(TNF_MIME_MEDIA, typeBytes, null, mimeData);
429     }
430 
431     /**
432      * Create a new NDEF Record containing external (application-specific) data.<p>
433      * Use this method to encode application specific data into an NDEF Record.
434      * The data is typed by a domain name (usually your Android package name) and
435      * a domain-specific type. This data is packaged into a "NFC Forum External
436      * Type" NDEF Record.<p>
437      * NFC Forum requires that the domain and type used in an external record
438      * are treated as case insensitive, however Android intent filtering is
439      * always case sensitive. So this method will force the domain and type to
440      * lower-case before creating the NDEF Record.<p>
441      * The unchecked exception {@link IllegalArgumentException} will be thrown
442      * if the domain and type have serious problems, for example if either field
443      * is empty, so always catch this
444      * exception if you are passing user-generated data into this method.<p>
445      * There are no such restrictions on the payload data.<p>
446      * For efficiency, This method might not make an internal copy of the
447      * data byte array, so take care not
448      * to modify the data byte array while still using the returned
449      * NdefRecord.
450      *
451      * Reference specification: NFCForum-TS-RTD_1.0
452      * @param domain domain-name of issuing organization
453      * @param type domain-specific type of data
454      * @param data payload as bytes
455      * @throws IllegalArugmentException if either domain or type are empty or invalid
456      */
createExternal(String domain, String type, byte[] data)457     public static NdefRecord createExternal(String domain, String type, byte[] data) {
458         if (domain == null) throw new NullPointerException("domain is null");
459         if (type == null) throw new NullPointerException("type is null");
460 
461         domain = domain.trim().toLowerCase(Locale.ROOT);
462         type = type.trim().toLowerCase(Locale.ROOT);
463 
464         if (domain.length() == 0) throw new IllegalArgumentException("domain is empty");
465         if (type.length() == 0) throw new IllegalArgumentException("type is empty");
466 
467         byte[] byteDomain = domain.getBytes(StandardCharsets.UTF_8);
468         byte[] byteType = type.getBytes(StandardCharsets.UTF_8);
469         byte[] b = new byte[byteDomain.length + 1 + byteType.length];
470         System.arraycopy(byteDomain, 0, b, 0, byteDomain.length);
471         b[byteDomain.length] = ':';
472         System.arraycopy(byteType, 0, b, byteDomain.length + 1, byteType.length);
473 
474         return new NdefRecord(TNF_EXTERNAL_TYPE, b, null, data);
475     }
476 
477     /**
478      * Create a new NDEF record containing UTF-8 text data.<p>
479      *
480      * The caller can either specify the language code for the provided text,
481      * or otherwise the language code corresponding to the current default
482      * locale will be used.
483      *
484      * Reference specification: NFCForum-TS-RTD_Text_1.0
485      * @param languageCode The languageCode for the record. If locale is empty or null,
486      *                     the language code of the current default locale will be used.
487      * @param text   The text to be encoded in the record. Will be represented in UTF-8 format.
488      * @throws IllegalArgumentException if text is null
489      */
createTextRecord(String languageCode, String text)490     public static NdefRecord createTextRecord(String languageCode, String text) {
491         if (text == null) throw new NullPointerException("text is null");
492 
493         byte[] textBytes = text.getBytes(StandardCharsets.UTF_8);
494 
495         byte[] languageCodeBytes = null;
496         if (languageCode != null && !languageCode.isEmpty()) {
497             languageCodeBytes = languageCode.getBytes(StandardCharsets.US_ASCII);
498         } else {
499             languageCodeBytes = Locale.getDefault().getLanguage().
500                     getBytes(StandardCharsets.US_ASCII);
501         }
502         // We only have 6 bits to indicate ISO/IANA language code.
503         if (languageCodeBytes.length >= 64) {
504             throw new IllegalArgumentException("language code is too long, must be <64 bytes.");
505         }
506         ByteBuffer buffer = ByteBuffer.allocate(1 + languageCodeBytes.length + textBytes.length);
507 
508         byte status = (byte) (languageCodeBytes.length & 0xFF);
509         buffer.put(status);
510         buffer.put(languageCodeBytes);
511         buffer.put(textBytes);
512 
513         return new NdefRecord(TNF_WELL_KNOWN, RTD_TEXT, null, buffer.array());
514     }
515 
516     /**
517      * Construct an NDEF Record from its component fields.<p>
518      * Recommend to use helpers such as {#createUri} or
519      * {{@link #createExternal} where possible, since they perform
520      * stricter validation that the record is correctly formatted
521      * as per NDEF specifications. However if you know what you are
522      * doing then this constructor offers the most flexibility.<p>
523      * An {@link NdefRecord} represents a logical (complete)
524      * record, and cannot represent NDEF Record chunks.<p>
525      * Basic validation of the tnf, type, id and payload is performed
526      * as per the following rules:
527      * <ul>
528      * <li>The tnf paramter must be a 3-bit value.</li>
529      * <li>Records with a tnf of {@link #TNF_EMPTY} cannot have a type,
530      * id or payload.</li>
531      * <li>Records with a tnf of {@link #TNF_UNKNOWN} or {@literal 0x07}
532      * cannot have a type.</li>
533      * <li>Records with a tnf of {@link #TNF_UNCHANGED} are not allowed
534      * since this class only represents complete (unchunked) records.</li>
535      * </ul>
536      * This minimal validation is specified by
537      * NFCForum-TS-NDEF_1.0 section 3.2.6 (Type Name Format).<p>
538      * If any of the above validation
539      * steps fail then {@link IllegalArgumentException} is thrown.<p>
540      * Deep inspection of the type, id and payload fields is not
541      * performed, so it is possible to create NDEF Records
542      * that conform to section 3.2.6
543      * but fail other more strict NDEF specification requirements. For
544      * example, the payload may be invalid given the tnf and type.
545      * <p>
546      * To omit a type, id or payload field, set the parameter to an
547      * empty byte array or null.
548      *
549      * @param tnf  a 3-bit TNF constant
550      * @param type byte array, containing zero to 255 bytes, or null
551      * @param id   byte array, containing zero to 255 bytes, or null
552      * @param payload byte array, containing zero to (2 ** 32 - 1) bytes,
553      *                or null
554      * @throws IllegalArugmentException if a valid record cannot be created
555      */
NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload)556     public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) {
557         /* convert nulls */
558         if (type == null) type = EMPTY_BYTE_ARRAY;
559         if (id == null) id = EMPTY_BYTE_ARRAY;
560         if (payload == null) payload = EMPTY_BYTE_ARRAY;
561 
562         String message = validateTnf(tnf, type, id, payload);
563         if (message != null) {
564             throw new IllegalArgumentException(message);
565         }
566 
567         mTnf = tnf;
568         mType = type;
569         mId = id;
570         mPayload = payload;
571     }
572 
573     /**
574      * Construct an NDEF Record from raw bytes.<p>
575      * This method is deprecated, use {@link NdefMessage#NdefMessage(byte[])}
576      * instead. This is because it does not make sense to parse a record:
577      * the NDEF binary format is only defined for a message, and the
578      * record flags MB and ME do not make sense outside of the context of
579      * an entire message.<p>
580      * This implementation will attempt to parse a single record by ignoring
581      * the MB and ME flags, and otherwise following the rules of
582      * {@link NdefMessage#NdefMessage(byte[])}.<p>
583      *
584      * @param data raw bytes to parse
585      * @throws FormatException if the data cannot be parsed into a valid record
586      * @deprecated use {@link NdefMessage#NdefMessage(byte[])} instead.
587      */
588     @Deprecated
NdefRecord(byte[] data)589     public NdefRecord(byte[] data) throws FormatException {
590         ByteBuffer buffer = ByteBuffer.wrap(data);
591         NdefRecord[] rs = parse(buffer, true);
592 
593         if (buffer.remaining() > 0) {
594             throw new FormatException("data too long");
595         }
596 
597         mTnf = rs[0].mTnf;
598         mType = rs[0].mType;
599         mId = rs[0].mId;
600         mPayload = rs[0].mPayload;
601     }
602 
603     /**
604      * Returns the 3-bit TNF.
605      * <p>
606      * TNF is the top-level type.
607      */
getTnf()608     public short getTnf() {
609         return mTnf;
610     }
611 
612     /**
613      * Returns the variable length Type field.
614      * <p>
615      * This should be used in conjunction with the TNF field to determine the
616      * payload format.
617      * <p>
618      * Returns an empty byte array if this record
619      * does not have a type field.
620      */
getType()621     public byte[] getType() {
622         return mType.clone();
623     }
624 
625     /**
626      * Returns the variable length ID.
627      * <p>
628      * Returns an empty byte array if this record
629      * does not have an id field.
630      */
getId()631     public byte[] getId() {
632         return mId.clone();
633     }
634 
635     /**
636      * Returns the variable length payload.
637      * <p>
638      * Returns an empty byte array if this record
639      * does not have a payload field.
640      */
getPayload()641     public byte[] getPayload() {
642         return mPayload.clone();
643     }
644 
645     /**
646      * Return this NDEF Record as a byte array.<p>
647      * This method is deprecated, use {@link NdefMessage#toByteArray}
648      * instead. This is because the NDEF binary format is not defined for
649      * a record outside of the context of a message: the MB and ME flags
650      * cannot be set without knowing the location inside a message.<p>
651      * This implementation will attempt to serialize a single record by
652      * always setting the MB and ME flags (in other words, assume this
653      * is a single-record NDEF Message).<p>
654      *
655      * @deprecated use {@link NdefMessage#toByteArray()} instead
656      */
657     @Deprecated
toByteArray()658     public byte[] toByteArray() {
659         ByteBuffer buffer = ByteBuffer.allocate(getByteLength());
660         writeToByteBuffer(buffer, true, true);
661         return buffer.array();
662     }
663 
664     /**
665      * Map this record to a MIME type, or return null if it cannot be mapped.<p>
666      * Currently this method considers all {@link #TNF_MIME_MEDIA} records to
667      * be MIME records, as well as some {@link #TNF_WELL_KNOWN} records such as
668      * {@link #RTD_TEXT}. If this is a MIME record then the MIME type as string
669      * is returned, otherwise null is returned.<p>
670      * This method does not perform validation that the MIME type is
671      * actually valid. It always attempts to
672      * return a string containing the type if this is a MIME record.<p>
673      * The returned MIME type will by normalized to lower-case using
674      * {@link Intent#normalizeMimeType}.<p>
675      * The MIME payload can be obtained using {@link #getPayload}.
676      *
677      * @return MIME type as a string, or null if this is not a MIME record
678      */
toMimeType()679     public String toMimeType() {
680         switch (mTnf) {
681             case NdefRecord.TNF_WELL_KNOWN:
682                 if (Arrays.equals(mType, NdefRecord.RTD_TEXT)) {
683                     return "text/plain";
684                 }
685                 break;
686             case NdefRecord.TNF_MIME_MEDIA:
687                 String mimeType = new String(mType, StandardCharsets.US_ASCII);
688                 return Intent.normalizeMimeType(mimeType);
689         }
690         return null;
691     }
692 
693     /**
694      * Map this record to a URI, or return null if it cannot be mapped.<p>
695      * Currently this method considers the following to be URI records:
696      * <ul>
697      * <li>{@link #TNF_ABSOLUTE_URI} records.</li>
698      * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_URI}.</li>
699      * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_SMART_POSTER}
700      * and containing a URI record in the NDEF message nested in the payload.
701      * </li>
702      * <li>{@link #TNF_EXTERNAL_TYPE} records.</li>
703      * </ul>
704      * If this is not a URI record by the above rules, then null is returned.<p>
705      * This method does not perform validation that the URI is
706      * actually valid: it always attempts to create and return a URI if
707      * this record appears to be a URI record by the above rules.<p>
708      * The returned URI will be normalized to have a lower case scheme
709      * using {@link Uri#normalizeScheme}.<p>
710      *
711      * @return URI, or null if this is not a URI record
712      */
toUri()713     public Uri toUri() {
714         return toUri(false);
715     }
716 
toUri(boolean inSmartPoster)717     private Uri toUri(boolean inSmartPoster) {
718         switch (mTnf) {
719             case TNF_WELL_KNOWN:
720                 if (Arrays.equals(mType, RTD_SMART_POSTER) && !inSmartPoster) {
721                     try {
722                         // check payload for a nested NDEF Message containing a URI
723                         NdefMessage nestedMessage = new NdefMessage(mPayload);
724                         for (NdefRecord nestedRecord : nestedMessage.getRecords()) {
725                             Uri uri = nestedRecord.toUri(true);
726                             if (uri != null) {
727                                 return uri;
728                             }
729                         }
730                     } catch (FormatException e) {  }
731                 } else if (Arrays.equals(mType, RTD_URI)) {
732                     Uri wktUri = parseWktUri();
733                     return (wktUri != null ? wktUri.normalizeScheme() : null);
734                 }
735                 break;
736 
737             case TNF_ABSOLUTE_URI:
738                 Uri uri = Uri.parse(new String(mType, StandardCharsets.UTF_8));
739                 return uri.normalizeScheme();
740 
741             case TNF_EXTERNAL_TYPE:
742                 if (inSmartPoster) {
743                     break;
744                 }
745                 return Uri.parse("vnd.android.nfc://ext/" + new String(mType, StandardCharsets.US_ASCII));
746         }
747         return null;
748     }
749 
750     /**
751      * Return complete URI of {@link #TNF_WELL_KNOWN}, {@link #RTD_URI} records.
752      * @return complete URI, or null if invalid
753      */
parseWktUri()754     private Uri parseWktUri() {
755         if (mPayload.length < 2) {
756             return null;
757         }
758 
759         // payload[0] contains the URI Identifier Code, as per
760         // NFC Forum "URI Record Type Definition" section 3.2.2.
761         int prefixIndex = (mPayload[0] & (byte)0xFF);
762         if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) {
763             return null;
764         }
765         String prefix = URI_PREFIX_MAP[prefixIndex];
766         String suffix = new String(Arrays.copyOfRange(mPayload, 1, mPayload.length),
767                 StandardCharsets.UTF_8);
768         return Uri.parse(prefix + suffix);
769     }
770 
771     /**
772      * Main record parsing method.<p>
773      * Expects NdefMessage to begin immediately, allows trailing data.<p>
774      * Currently has strict validation of all fields as per NDEF 1.0
775      * specification section 2.5. We will attempt to keep this as strict as
776      * possible to encourage well-formatted NDEF.<p>
777      * Always returns 1 or more NdefRecord's, or throws FormatException.
778      *
779      * @param buffer ByteBuffer to read from
780      * @param ignoreMbMe ignore MB and ME flags, and read only 1 complete record
781      * @return one or more records
782      * @throws FormatException on any parsing error
783      */
parse(ByteBuffer buffer, boolean ignoreMbMe)784     static NdefRecord[] parse(ByteBuffer buffer, boolean ignoreMbMe) throws FormatException {
785         List<NdefRecord> records = new ArrayList<NdefRecord>();
786 
787         try {
788             byte[] type = null;
789             byte[] id = null;
790             byte[] payload = null;
791             ArrayList<byte[]> chunks = new ArrayList<byte[]>();
792             boolean inChunk = false;
793             short chunkTnf = -1;
794             boolean me = false;
795 
796             while (!me) {
797                 byte flag = buffer.get();
798 
799                 boolean mb = (flag & NdefRecord.FLAG_MB) != 0;
800                 me = (flag & NdefRecord.FLAG_ME) != 0;
801                 boolean cf = (flag & NdefRecord.FLAG_CF) != 0;
802                 boolean sr = (flag & NdefRecord.FLAG_SR) != 0;
803                 boolean il = (flag & NdefRecord.FLAG_IL) != 0;
804                 short tnf = (short)(flag & 0x07);
805 
806                 if (!mb && records.size() == 0 && !inChunk && !ignoreMbMe) {
807                     throw new FormatException("expected MB flag");
808                 } else if (mb && records.size() != 0 && !ignoreMbMe) {
809                     throw new FormatException("unexpected MB flag");
810                 } else if (inChunk && il) {
811                     throw new FormatException("unexpected IL flag in non-leading chunk");
812                 } else if (cf && me) {
813                     throw new FormatException("unexpected ME flag in non-trailing chunk");
814                 } else if (inChunk && tnf != NdefRecord.TNF_UNCHANGED) {
815                     throw new FormatException("expected TNF_UNCHANGED in non-leading chunk");
816                 } else if (!inChunk && tnf == NdefRecord.TNF_UNCHANGED) {
817                     throw new FormatException("" +
818                             "unexpected TNF_UNCHANGED in first chunk or unchunked record");
819                 }
820 
821                 int typeLength = buffer.get() & 0xFF;
822                 long payloadLength = sr ? (buffer.get() & 0xFF) : (buffer.getInt() & 0xFFFFFFFFL);
823                 int idLength = il ? (buffer.get() & 0xFF) : 0;
824 
825                 if (inChunk && typeLength != 0) {
826                     throw new FormatException("expected zero-length type in non-leading chunk");
827                 }
828 
829                 if (!inChunk) {
830                     type = (typeLength > 0 ? new byte[typeLength] : EMPTY_BYTE_ARRAY);
831                     id = (idLength > 0 ? new byte[idLength] : EMPTY_BYTE_ARRAY);
832                     buffer.get(type);
833                     buffer.get(id);
834                 }
835 
836                 ensureSanePayloadSize(payloadLength);
837                 payload = (payloadLength > 0 ? new byte[(int)payloadLength] : EMPTY_BYTE_ARRAY);
838                 buffer.get(payload);
839 
840                 if (cf && !inChunk) {
841                     // first chunk
842                     chunks.clear();
843                     chunkTnf = tnf;
844                 }
845                 if (cf || inChunk) {
846                     // any chunk
847                     chunks.add(payload);
848                 }
849                 if (!cf && inChunk) {
850                     // last chunk, flatten the payload
851                     payloadLength = 0;
852                     for (byte[] p : chunks) {
853                         payloadLength += p.length;
854                     }
855                     ensureSanePayloadSize(payloadLength);
856                     payload = new byte[(int)payloadLength];
857                     int i = 0;
858                     for (byte[] p : chunks) {
859                         System.arraycopy(p, 0, payload, i, p.length);
860                         i += p.length;
861                     }
862                     tnf = chunkTnf;
863                 }
864                 if (cf) {
865                     // more chunks to come
866                     inChunk = true;
867                     continue;
868                 } else {
869                     inChunk = false;
870                 }
871 
872                 String error = validateTnf(tnf, type, id, payload);
873                 if (error != null) {
874                     throw new FormatException(error);
875                 }
876                 records.add(new NdefRecord(tnf, type, id, payload));
877                 if (ignoreMbMe) {  // for parsing a single NdefRecord
878                     break;
879                 }
880             }
881         } catch (BufferUnderflowException e) {
882             throw new FormatException("expected more data", e);
883         }
884         return records.toArray(new NdefRecord[records.size()]);
885     }
886 
ensureSanePayloadSize(long size)887     private static void ensureSanePayloadSize(long size) throws FormatException {
888         if (size > MAX_PAYLOAD_SIZE) {
889             throw new FormatException(
890                     "payload above max limit: " + size + " > " + MAX_PAYLOAD_SIZE);
891         }
892     }
893 
894     /**
895      * Perform simple validation that the tnf is valid.<p>
896      * Validates the requirements of NFCForum-TS-NDEF_1.0 section
897      * 3.2.6 (Type Name Format). This just validates that the tnf
898      * is valid, and that the relevant type, id and payload
899      * fields are present (or empty) for this tnf. It does not
900      * perform any deep inspection of the type, id and payload fields.<p>
901      * Also does not allow TNF_UNCHANGED since this class is only used
902      * to present logical (unchunked) records.
903      *
904      * @return null if valid, or a string error if invalid.
905      */
validateTnf(short tnf, byte[] type, byte[] id, byte[] payload)906     static String validateTnf(short tnf, byte[] type, byte[] id, byte[] payload) {
907         switch (tnf) {
908             case TNF_EMPTY:
909                 if (type.length != 0 || id.length != 0 || payload.length != 0) {
910                     return "unexpected data in TNF_EMPTY record";
911                 }
912                 return null;
913             case TNF_WELL_KNOWN:
914             case TNF_MIME_MEDIA:
915             case TNF_ABSOLUTE_URI:
916             case TNF_EXTERNAL_TYPE:
917                 return null;
918             case TNF_UNKNOWN:
919             case TNF_RESERVED:
920                 if (type.length != 0) {
921                     return "unexpected type field in TNF_UNKNOWN or TNF_RESERVEd record";
922                 }
923                 return null;
924             case TNF_UNCHANGED:
925                 return "unexpected TNF_UNCHANGED in first chunk or logical record";
926             default:
927                 return String.format("unexpected tnf value: 0x%02x", tnf);
928         }
929     }
930 
931     /**
932      * Serialize record for network transmission.<p>
933      * Uses specified MB and ME flags.<p>
934      * Does not chunk records.
935      */
writeToByteBuffer(ByteBuffer buffer, boolean mb, boolean me)936     void writeToByteBuffer(ByteBuffer buffer, boolean mb, boolean me) {
937         boolean sr = mPayload.length < 256;
938         boolean il = mId.length > 0;
939 
940         byte flags = (byte)((mb ? FLAG_MB : 0) | (me ? FLAG_ME : 0) |
941                 (sr ? FLAG_SR : 0) | (il ? FLAG_IL : 0) | mTnf);
942         buffer.put(flags);
943 
944         buffer.put((byte)mType.length);
945         if (sr) {
946             buffer.put((byte)mPayload.length);
947         } else {
948             buffer.putInt(mPayload.length);
949         }
950         if (il) {
951             buffer.put((byte)mId.length);
952         }
953 
954         buffer.put(mType);
955         buffer.put(mId);
956         buffer.put(mPayload);
957     }
958 
959     /**
960      * Get byte length of serialized record.
961      */
getByteLength()962     int getByteLength() {
963         int length = 3 + mType.length + mId.length + mPayload.length;
964 
965         boolean sr = mPayload.length < 256;
966         boolean il = mId.length > 0;
967 
968         if (!sr) length += 3;
969         if (il) length += 1;
970 
971         return length;
972     }
973 
974     @Override
describeContents()975     public int describeContents() {
976         return 0;
977     }
978 
979     @Override
writeToParcel(Parcel dest, int flags)980     public void writeToParcel(Parcel dest, int flags) {
981         dest.writeInt(mTnf);
982         dest.writeInt(mType.length);
983         dest.writeByteArray(mType);
984         dest.writeInt(mId.length);
985         dest.writeByteArray(mId);
986         dest.writeInt(mPayload.length);
987         dest.writeByteArray(mPayload);
988     }
989 
990     public static final Parcelable.Creator<NdefRecord> CREATOR =
991             new Parcelable.Creator<NdefRecord>() {
992         @Override
993         public NdefRecord createFromParcel(Parcel in) {
994             short tnf = (short)in.readInt();
995             int typeLength = in.readInt();
996             byte[] type = new byte[typeLength];
997             in.readByteArray(type);
998             int idLength = in.readInt();
999             byte[] id = new byte[idLength];
1000             in.readByteArray(id);
1001             int payloadLength = in.readInt();
1002             byte[] payload = new byte[payloadLength];
1003             in.readByteArray(payload);
1004 
1005             return new NdefRecord(tnf, type, id, payload);
1006         }
1007         @Override
1008         public NdefRecord[] newArray(int size) {
1009             return new NdefRecord[size];
1010         }
1011     };
1012 
1013     @Override
hashCode()1014     public int hashCode() {
1015         final int prime = 31;
1016         int result = 1;
1017         result = prime * result + Arrays.hashCode(mId);
1018         result = prime * result + Arrays.hashCode(mPayload);
1019         result = prime * result + mTnf;
1020         result = prime * result + Arrays.hashCode(mType);
1021         return result;
1022     }
1023 
1024     /**
1025      * Returns true if the specified NDEF Record contains
1026      * identical tnf, type, id and payload fields.
1027      */
1028     @Override
equals(Object obj)1029     public boolean equals(Object obj) {
1030         if (this == obj) return true;
1031         if (obj == null) return false;
1032         if (getClass() != obj.getClass()) return false;
1033         NdefRecord other = (NdefRecord) obj;
1034         if (!Arrays.equals(mId, other.mId)) return false;
1035         if (!Arrays.equals(mPayload, other.mPayload)) return false;
1036         if (mTnf != other.mTnf) return false;
1037         return Arrays.equals(mType, other.mType);
1038     }
1039 
1040     @Override
toString()1041     public String toString() {
1042         StringBuilder b = new StringBuilder(String.format("NdefRecord tnf=%X", mTnf));
1043         if (mType.length > 0) b.append(" type=").append(bytesToString(mType));
1044         if (mId.length > 0) b.append(" id=").append(bytesToString(mId));
1045         if (mPayload.length > 0) b.append(" payload=").append(bytesToString(mPayload));
1046         return b.toString();
1047     }
1048 
bytesToString(byte[] bs)1049     private static StringBuilder bytesToString(byte[] bs) {
1050         StringBuilder s = new StringBuilder();
1051         for (byte b : bs) {
1052             s.append(String.format("%02X", b));
1053         }
1054         return s;
1055     }
1056 }
1057