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