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