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