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