content: /*
   * Copyright (C) 2010 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.nfc;
  
  import android.annotation.UnsupportedAppUsage;
  import android.content.Intent;
  import android.net.Uri;
  import android.os.Parcel;
  import android.os.Parcelable;
  
  import java.nio.BufferUnderflowException;
  import java.nio.ByteBuffer;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Locale;
  
  /**
   * Represents an immutable NDEF Record.
   * <p>
   * NDEF (NFC Data Exchange Format) is a light-weight binary format,
   * used to encapsulate typed data. It is specified by the NFC Forum,
   * for transmission and storage with NFC, however it is transport agnostic.
   * <p>
   * NDEF defines messages and records. An NDEF Record contains
   * typed data, such as MIME-type media, a URI, or a custom
   * application payload. An NDEF Message is a container for
   * one or more NDEF Records.
   * <p>
   * This class represents logical (complete) NDEF Records, and can not be
   * used to represent chunked (partial) NDEF Records. However
   * {@link NdefMessage#NdefMessage(byte[])} can be used to parse a message
   * containing chunked records, and will return a message with unchunked
   * (complete) records.
   * <p>
   * A logical NDEF Record always contains a 3-bit TNF (Type Name Field)
   * that provides high level typing for the rest of the record. The
   * remaining fields are variable length and not always present:
   * <ul>
   * <li><em>type</em>: detailed typing for the payload</li>
   * <li><em>id</em>: identifier meta-data, not commonly used</li>
   * <li><em>payload</em>: the actual payload</li>
   * </ul>
   * <p>
   * Helpers such as {@link NdefRecord#createUri}, {@link NdefRecord#createMime}
   * and {@link NdefRecord#createExternal} are included to create well-formatted
   * NDEF Records with correctly set tnf, type, id and payload fields, please
   * use these helpers whenever possible.
   * <p>
   * Use the constructor {@link #NdefRecord(short, byte[], byte[], byte[])}
   * if you know what you are doing and what to set the fields individually.
   * Only basic validation is performed with this constructor, so it is possible
   * to create records that do not confirm to the strict NFC Forum
   * specifications.
   * <p>
   * The binary representation of an NDEF Record includes additional flags to
   * indicate location with an NDEF message, provide support for chunking of
   * NDEF records, and to pack optional fields. This class does not expose
   * those details. To write an NDEF Record as binary you must first put it
   * into an {@link NdefMessage}, then call {@link NdefMessage#toByteArray()}.
   * <p class="note">
   * {@link NdefMessage} and {@link NdefRecord} implementations are
   * always available, even on Android devices that do not have NFC hardware.
   * <p class="note">
   * {@link NdefRecord}s are intended to be immutable (and thread-safe),
   * however they may contain mutable fields. So take care not to modify
   * mutable fields passed into constructors, or modify mutable fields
   * obtained by getter methods, unless such modification is explicitly
   * marked as safe.
   *
   * @see NfcAdapter#ACTION_NDEF_DISCOVERED
   * @see NdefMessage
   */
  public final class NdefRecord implements Parcelable {
      /**
       * Indicates the record is empty.<p>
       * Type, id and payload fields are empty in a {@literal TNF_EMPTY} record.
       */
      public static final short TNF_EMPTY = 0x00;
  
      /**
       * Indicates the type field contains a well-known RTD type name.<p>
       * Use this tnf with RTD types such as {@link #RTD_TEXT}, {@link #RTD_URI}.
       * <p>
       * The RTD type name format is specified in NFCForum-TS-RTD_1.0.
       *
       * @see #RTD_URI
       * @see #RTD_TEXT
       * @see #RTD_SMART_POSTER
       * @see #createUri
       */
      public static final short TNF_WELL_KNOWN = 0x01;
  
      /**
       * Indicates the type field contains a media-type BNF
       * construct, defined by RFC 2046.<p>
       * Use this with MIME type names such as {@literal "image/jpeg"}, or
       * using the helper {@link #createMime}.
       *
       * @see #createMime
       */
      public static final short TNF_MIME_MEDIA = 0x02;
  
      /**
       * Indicates the type field contains an absolute-URI
       * BNF construct defined by RFC 3986.<p>
       * When creating new records prefer {@link #createUri},
       * since it offers more compact URI encoding
       * ({@literal #RTD_URI} allows compression of common URI prefixes).
       *
       * @see #createUri
       */
      public static final short TNF_ABSOLUTE_URI = 0x03;
  
      /**
       * Indicates the type field contains an external type name.<p>
       * Used to encode custom payloads. When creating new records
       * use the helper {@link #createExternal}.<p>
       * The external-type RTD format is specified in NFCForum-TS-RTD_1.0.<p>
       * <p>
       * Note this TNF should not be used with RTD_TEXT or RTD_URI constants.
       * Those are well known RTD constants, not external RTD constants.
       *
       * @see #createExternal
       */
      public static final short TNF_EXTERNAL_TYPE = 0x04;
  
      /**
       * Indicates the payload type is unknown.<p>
       * NFC Forum explains this should be treated similarly to the
       * "application/octet-stream" MIME type. The payload
       * type is not explicitly encoded within the record.
       * <p>
       * The type field is empty in an {@literal TNF_UNKNOWN} record.
       */
      public static final short TNF_UNKNOWN = 0x05;
  
      /**
       * Indicates the payload is an intermediate or final chunk of a chunked
       * NDEF Record.<p>
       * {@literal TNF_UNCHANGED} can not be used with this class
       * since all {@link NdefRecord}s are already unchunked, however they
       * may appear in the binary format.
       */
      public static final short TNF_UNCHANGED = 0x06;
  
      /**
       * Reserved TNF type.
       * <p>
       * The NFC Forum NDEF Specification v1.0 suggests for NDEF parsers to treat this
       * value like TNF_UNKNOWN.
       * @hide
       */
      public static final short TNF_RESERVED = 0x07;
  
      /**
       * RTD Text type. For use with {@literal TNF_WELL_KNOWN}.
       * @see #TNF_WELL_KNOWN
       */
      public static final byte[] RTD_TEXT = {0x54};  // "T"
  
      /**
       * RTD URI type. For use with {@literal TNF_WELL_KNOWN}.
       * @see #TNF_WELL_KNOWN
       */
      public static final byte[] RTD_URI = {0x55};   // "U"
  
      /**
       * RTD Smart Poster type. For use with {@literal TNF_WELL_KNOWN}.
       * @see #TNF_WELL_KNOWN
       */
      public static final byte[] RTD_SMART_POSTER = {0x53, 0x70};  // "Sp"
  
      /**
       * RTD Alternative Carrier type. For use with {@literal TNF_WELL_KNOWN}.
       * @see #TNF_WELL_KNOWN
       */
      public static final byte[] RTD_ALTERNATIVE_CARRIER = {0x61, 0x63};  // "ac"
  
      /**
       * RTD Handover Carrier type. For use with {@literal TNF_WELL_KNOWN}.
       * @see #TNF_WELL_KNOWN
       */
      public static final byte[] RTD_HANDOVER_CARRIER = {0x48, 0x63};  // "Hc"
  
      /**
       * RTD Handover Request type. For use with {@literal TNF_WELL_KNOWN}.
       * @see #TNF_WELL_KNOWN
       */
      public static final byte[] RTD_HANDOVER_REQUEST = {0x48, 0x72};  // "Hr"
  
      /**
       * RTD Handover Select type. For use with {@literal TNF_WELL_KNOWN}.
       * @see #TNF_WELL_KNOWN
       */
      public static final byte[] RTD_HANDOVER_SELECT = {0x48, 0x73}; // "Hs"
  
      /**
       * RTD Android app type. For use with {@literal TNF_EXTERNAL}.
       * <p>
       * The payload of a record with type RTD_ANDROID_APP
       * should be the package name identifying an application.
       * Multiple RTD_ANDROID_APP records may be included
       * in a single {@link NdefMessage}.
       * <p>
       * Use {@link #createApplicationRecord(String)} to create
       * RTD_ANDROID_APP records.
       * @hide
       */
      public static final byte[] RTD_ANDROID_APP = "android.com:pkg".getBytes();
  
      private static final byte FLAG_MB = (byte) 0x80;
      private static final byte FLAG_ME = (byte) 0x40;
      private static final byte FLAG_CF = (byte) 0x20;
      private static final byte FLAG_SR = (byte) 0x10;
      private static final byte FLAG_IL = (byte) 0x08;
  
      /**
       * NFC Forum "URI Record Type Definition"<p>
       * This is a mapping of "URI Identifier Codes" to URI string prefixes,
       * per section 3.2.2 of the NFC Forum URI Record Type Definition document.
       */
      private static final String[] URI_PREFIX_MAP = new String[] {
              "", // 0x00
              "http://www.", // 0x01
              "https://www.", // 0x02
              "http://", // 0x03
              "https://", // 0x04
              "tel:", // 0x05
              "mailto:", // 0x06
              "ftp://anonymous:anonymous@", // 0x07
              "ftp://ftp.", // 0x08
              "ftps://", // 0x09
              "sftp://", // 0x0A
              "smb://", // 0x0B
              "nfs://", // 0x0C
              "ftp://", // 0x0D
              "dav://", // 0x0E
              "news:", // 0x0F
              "telnet://", // 0x10
              "imap:", // 0x11
              "rtsp://", // 0x12
              "urn:", // 0x13
              "pop:", // 0x14
              "sip:", // 0x15
              "sips:", // 0x16
              "tftp:", // 0x17
              "btspp://", // 0x18
              "btl2cap://", // 0x19
              "btgoep://", // 0x1A
              "tcpobex://", // 0x1B
              "irdaobex://", // 0x1C
              "file://", // 0x1D
              "urn:epc:id:", // 0x1E
              "urn:epc:tag:", // 0x1F
              "urn:epc:pat:", // 0x20
              "urn:epc:raw:", // 0x21
              "urn:epc:", // 0x22
              "urn:nfc:", // 0x23
      };
  
      private static final int MAX_PAYLOAD_SIZE = 10 * (1 << 20);  // 10 MB payload limit
  
      private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
  
      private final short mTnf;
      private final byte[] mType;
      @UnsupportedAppUsage
      private final byte[] mId;
      private final byte[] mPayload;
  
      /**
       * Create a new Android Application Record (AAR).
       * <p>
       * This record indicates to other Android devices the package
       * that should be used to handle the entire NDEF message.
       * You can embed this record anywhere into your message
       * to ensure that the intended package receives the message.
       * <p>
       * When an Android device dispatches an {@link NdefMessage}
       * containing one or more Android application records,
       * the applications contained in those records will be the
       * preferred target for the {@link NfcAdapter#ACTION_NDEF_DISCOVERED}
       * intent, in the order in which they appear in the message.
       * This dispatch behavior was first added to Android in
       * Ice Cream Sandwich.
       * <p>
       * If none of the applications have a are installed on the device,
       * a Market link will be opened to the first application.
       * <p>
       * Note that Android application records do not overrule
       * applications that have called
       * {@link NfcAdapter#enableForegroundDispatch}.
       *
       * @param packageName Android package name
       * @return Android application NDEF record
       */
      public static NdefRecord createApplicationRecord(String packageName) {
          if (packageName == null) throw new NullPointerException("packageName is null");
          if (packageName.length() == 0) throw new IllegalArgumentException("packageName is empty");
  
          return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, null,
                  packageName.getBytes(StandardCharsets.UTF_8));
      }
  
      /**
       * Create a new NDEF Record containing a URI.<p>
       * Use this method to encode a URI (or URL) into an NDEF Record.<p>
       * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN}
       * and {@link #RTD_URI}. This is the most efficient encoding
       * of a URI into NDEF.<p>
       * The uri parameter will be normalized with
       * {@link Uri#normalizeScheme} to set the scheme to lower case to
       * follow Android best practices for intent filtering.
       * However the unchecked exception
       * {@link IllegalArgumentException} may be thrown if the uri
       * parameter has serious problems, for example if it is empty, so always
       * catch this exception if you are passing user-generated data into this
       * method.<p>
       *
       * Reference specification: NFCForum-TS-RTD_URI_1.0
       *
       * @param uri URI to encode.
       * @return an NDEF Record containing the URI
       * @throws IllegalArugmentException if the uri is empty or invalid
       */
      public static NdefRecord createUri(Uri uri) {
          if (uri == null) throw new NullPointerException("uri is null");
  
          uri = uri.normalizeScheme();
          String uriString = uri.toString();
          if (uriString.length() == 0) throw new IllegalArgumentException("uri is empty");
  
          byte prefix = 0;
          for (int i = 1; i < URI_PREFIX_MAP.length; i++) {
              if (uriString.startsWith(URI_PREFIX_MAP[i])) {
                  prefix = (byte) i;
                  uriString = uriString.substring(URI_PREFIX_MAP[i].length());
                  break;
              }
          }
          byte[] uriBytes = uriString.getBytes(StandardCharsets.UTF_8);
          byte[] recordBytes = new byte[uriBytes.length + 1];
          recordBytes[0] = prefix;
          System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.length);
          return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, null, recordBytes);
      }
  
      /**
       * Create a new NDEF Record containing a URI.<p>
       * Use this method to encode a URI (or URL) into an NDEF Record.<p>
       * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN}
       * and {@link #RTD_URI}. This is the most efficient encoding
       * of a URI into NDEF.<p>
        * The uriString parameter will be normalized with
       * {@link Uri#normalizeScheme} to set the scheme to lower case to
       * follow Android best practices for intent filtering.
       * However the unchecked exception
       * {@link IllegalArgumentException} may be thrown if the uriString
       * parameter has serious problems, for example if it is empty, so always
       * catch this exception if you are passing user-generated data into this
       * method.<p>
       *
       * Reference specification: NFCForum-TS-RTD_URI_1.0
       *
       * @param uriString string URI to encode.
       * @return an NDEF Record containing the URI
       * @throws IllegalArugmentException if the uriString is empty or invalid
       */
      public static NdefRecord createUri(String uriString) {
          return createUri(Uri.parse(uriString));
      }
  
      /**
       * Create a new NDEF Record containing MIME data.<p>
       * Use this method to encode MIME-typed data into an NDEF Record,
       * such as "text/plain", or "image/jpeg".<p>
       * The mimeType parameter will be normalized with
       * {@link Intent#normalizeMimeType} to follow Android best
       * practices for intent filtering, for example to force lower-case.
       * However the unchecked exception
       * {@link IllegalArgumentException} may be thrown
       * if the mimeType parameter has serious problems,
       * for example if it is empty, so always catch this
       * exception if you are passing user-generated data into this method.
       * <p>
       * For efficiency, This method might not make an internal copy of the
       * mimeData byte array, so take care not
       * to modify the mimeData byte array while still using the returned
       * NdefRecord.
       *
       * @param mimeType a valid MIME type
       * @param mimeData MIME data as bytes
       * @return an NDEF Record containing the MIME-typed data
       * @throws IllegalArugmentException if the mimeType is empty or invalid
       *
       */
      public static NdefRecord createMime(String mimeType, byte[] mimeData) {
          if (mimeType == null) throw new NullPointerException("mimeType is null");
  
          // We only do basic MIME type validation: trying to follow the
          // RFCs strictly only ends in tears, since there are lots of MIME
          // types in common use that are not strictly valid as per RFC rules
          mimeType = Intent.normalizeMimeType(mimeType);
          if (mimeType.length() == 0) throw new IllegalArgumentException("mimeType is empty");
          int slashIndex = mimeType.indexOf('/');
          if (slashIndex == 0) throw new IllegalArgumentException("mimeType must have major type");
          if (slashIndex == mimeType.length() - 1) {
              throw new IllegalArgumentException("mimeType must have minor type");
          }
          // missing '/' is allowed
  
          // MIME RFCs suggest ASCII encoding for content-type
          byte[] typeBytes = mimeType.getBytes(StandardCharsets.US_ASCII);
          return new NdefRecord(TNF_MIME_MEDIA, typeBytes, null, mimeData);
      }
  
      /**
       * Create a new NDEF Record containing external (application-specific) data.<p>
       * Use this method to encode application specific data into an NDEF Record.
       * The data is typed by a domain name (usually your Android package name) and
       * a domain-specific type. This data is packaged into a "NFC Forum External
       * Type" NDEF Record.<p>
       * NFC Forum requires that the domain and type used in an external record
       * are treated as case insensitive, however Android intent filtering is
       * always case sensitive. So this method will force the domain and type to
       * lower-case before creating the NDEF Record.<p>
       * The unchecked exception {@link IllegalArgumentException} will be thrown
       * if the domain and type have serious problems, for example if either field
       * is empty, so always catch this
       * exception if you are passing user-generated data into this method.<p>
       * There are no such restrictions on the payload data.<p>
       * For efficiency, This method might not make an internal copy of the
       * data byte array, so take care not
       * to modify the data byte array while still using the returned
       * NdefRecord.
       *
       * Reference specification: NFCForum-TS-RTD_1.0
       * @param domain domain-name of issuing organization
       * @param type domain-specific type of data
       * @param data payload as bytes
       * @throws IllegalArugmentException if either domain or type are empty or invalid
       */
      public static NdefRecord createExternal(String domain, String type, byte[] data) {
          if (domain == null) throw new NullPointerException("domain is null");
          if (type == null) throw new NullPointerException("type is null");
  
          domain = domain.trim().toLowerCase(Locale.ROOT);
          type = type.trim().toLowerCase(Locale.ROOT);
  
          if (domain.length() == 0) throw new IllegalArgumentException("domain is empty");
          if (type.length() == 0) throw new IllegalArgumentException("type is empty");
  
          byte[] byteDomain = domain.getBytes(StandardCharsets.UTF_8);
          byte[] byteType = type.getBytes(StandardCharsets.UTF_8);
          byte[] b = new byte[byteDomain.length + 1 + byteType.length];
          System.arraycopy(byteDomain, 0, b, 0, byteDomain.length);
          b[byteDomain.length] = ':';
          System.arraycopy(byteType, 0, b, byteDomain.length + 1, byteType.length);
  
          return new NdefRecord(TNF_EXTERNAL_TYPE, b, null, data);
      }
  
      /**
       * Create a new NDEF record containing UTF-8 text data.<p>
       *
       * The caller can either specify the language code for the provided text,
       * or otherwise the language code corresponding to the current default
       * locale will be used.
       *
       * Reference specification: NFCForum-TS-RTD_Text_1.0
       * @param languageCode The languageCode for the record. If locale is empty or null,
       *                     the language code of the current default locale will be used.
       * @param text   The text to be encoded in the record. Will be represented in UTF-8 format.
       * @throws IllegalArgumentException if text is null
       */
      public static NdefRecord createTextRecord(String languageCode, String text) {
          if (text == null) throw new NullPointerException("text is null");
  
          byte[] textBytes = text.getBytes(StandardCharsets.UTF_8);
  
          byte[] languageCodeBytes = null;
          if (languageCode != null && !languageCode.isEmpty()) {
              languageCodeBytes = languageCode.getBytes(StandardCharsets.US_ASCII);
          } else {
              languageCodeBytes = Locale.getDefault().getLanguage().
                      getBytes(StandardCharsets.US_ASCII);
          }
          // We only have 6 bits to indicate ISO/IANA language code.
          if (languageCodeBytes.length >= 64) {
              throw new IllegalArgumentException("language code is too long, must be <64 bytes.");
          }
          ByteBuffer buffer = ByteBuffer.allocate(1 + languageCodeBytes.length + textBytes.length);
  
          byte status = (byte) (languageCodeBytes.length & 0xFF);
          buffer.put(status);
          buffer.put(languageCodeBytes);
          buffer.put(textBytes);
  
          return new NdefRecord(TNF_WELL_KNOWN, RTD_TEXT, null, buffer.array());
      }
  
      /**
       * Construct an NDEF Record from its component fields.<p>
       * Recommend to use helpers such as {#createUri} or
       * {{@link #createExternal} where possible, since they perform
       * stricter validation that the record is correctly formatted
       * as per NDEF specifications. However if you know what you are
       * doing then this constructor offers the most flexibility.<p>
       * An {@link NdefRecord} represents a logical (complete)
       * record, and cannot represent NDEF Record chunks.<p>
       * Basic validation of the tnf, type, id and payload is performed
       * as per the following rules:
       * <ul>
       * <li>The tnf paramter must be a 3-bit value.</li>
       * <li>Records with a tnf of {@link #TNF_EMPTY} cannot have a type,
       * id or payload.</li>
       * <li>Records with a tnf of {@link #TNF_UNKNOWN} or {@literal 0x07}
       * cannot have a type.</li>
       * <li>Records with a tnf of {@link #TNF_UNCHANGED} are not allowed
       * since this class only represents complete (unchunked) records.</li>
       * </ul>
       * This minimal validation is specified by
       * NFCForum-TS-NDEF_1.0 section 3.2.6 (Type Name Format).<p>
       * If any of the above validation
       * steps fail then {@link IllegalArgumentException} is thrown.<p>
       * Deep inspection of the type, id and payload fields is not
       * performed, so it is possible to create NDEF Records
       * that conform to section 3.2.6
       * but fail other more strict NDEF specification requirements. For
       * example, the payload may be invalid given the tnf and type.
       * <p>
       * To omit a type, id or payload field, set the parameter to an
       * empty byte array or null.
       *
       * @param tnf  a 3-bit TNF constant
       * @param type byte array, containing zero to 255 bytes, or null
       * @param id   byte array, containing zero to 255 bytes, or null
       * @param payload byte array, containing zero to (2 ** 32 - 1) bytes,
       *                or null
       * @throws IllegalArugmentException if a valid record cannot be created
       */
      public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) {
          /* convert nulls */
          if (type == null) type = EMPTY_BYTE_ARRAY;
          if (id == null) id = EMPTY_BYTE_ARRAY;
          if (payload == null) payload = EMPTY_BYTE_ARRAY;
  
          String message = validateTnf(tnf, type, id, payload);
          if (message != null) {
              throw new IllegalArgumentException(message);
          }
  
          mTnf = tnf;
          mType = type;
          mId = id;
          mPayload = payload;
      }
  
      /**
       * Construct an NDEF Record from raw bytes.<p>
       * This method is deprecated, use {@link NdefMessage#NdefMessage(byte[])}
       * instead. This is because it does not make sense to parse a record:
       * the NDEF binary format is only defined for a message, and the
       * record flags MB and ME do not make sense outside of the context of
       * an entire message.<p>
       * This implementation will attempt to parse a single record by ignoring
       * the MB and ME flags, and otherwise following the rules of
       * {@link NdefMessage#NdefMessage(byte[])}.<p>
       *
       * @param data raw bytes to parse
       * @throws FormatException if the data cannot be parsed into a valid record
       * @deprecated use {@link NdefMessage#NdefMessage(byte[])} instead.
       */
      @Deprecated
      public NdefRecord(byte[] data) throws FormatException {
          ByteBuffer buffer = ByteBuffer.wrap(data);
          NdefRecord[] rs = parse(buffer, true);
  
          if (buffer.remaining() > 0) {
              throw new FormatException("data too long");
          }
  
          mTnf = rs[0].mTnf;
          mType = rs[0].mType;
          mId = rs[0].mId;
          mPayload = rs[0].mPayload;
      }
  
      /**
       * Returns the 3-bit TNF.
       * <p>
       * TNF is the top-level type.
       */
      public short getTnf() {
          return mTnf;
      }
  
      /**
       * Returns the variable length Type field.
       * <p>
       * This should be used in conjunction with the TNF field to determine the
       * payload format.
       * <p>
       * Returns an empty byte array if this record
       * does not have a type field.
       */
      public byte[] getType() {
          return mType.clone();
      }
  
      /**
       * Returns the variable length ID.
       * <p>
       * Returns an empty byte array if this record
       * does not have an id field.
       */
      public byte[] getId() {
          return mId.clone();
      }
  
      /**
       * Returns the variable length payload.
       * <p>
       * Returns an empty byte array if this record
       * does not have a payload field.
       */
      public byte[] getPayload() {
          return mPayload.clone();
      }
  
      /**
       * Return this NDEF Record as a byte array.<p>
       * This method is deprecated, use {@link NdefMessage#toByteArray}
       * instead. This is because the NDEF binary format is not defined for
       * a record outside of the context of a message: the MB and ME flags
       * cannot be set without knowing the location inside a message.<p>
       * This implementation will attempt to serialize a single record by
       * always setting the MB and ME flags (in other words, assume this
       * is a single-record NDEF Message).<p>
       *
       * @deprecated use {@link NdefMessage#toByteArray()} instead
       */
      @Deprecated
      public byte[] toByteArray() {
          ByteBuffer buffer = ByteBuffer.allocate(getByteLength());
          writeToByteBuffer(buffer, true, true);
          return buffer.array();
      }
  
      /**
       * Map this record to a MIME type, or return null if it cannot be mapped.<p>
       * Currently this method considers all {@link #TNF_MIME_MEDIA} records to
       * be MIME records, as well as some {@link #TNF_WELL_KNOWN} records such as
       * {@link #RTD_TEXT}. If this is a MIME record then the MIME type as string
       * is returned, otherwise null is returned.<p>
       * This method does not perform validation that the MIME type is
       * actually valid. It always attempts to
       * return a string containing the type if this is a MIME record.<p>
       * The returned MIME type will by normalized to lower-case using
       * {@link Intent#normalizeMimeType}.<p>
       * The MIME payload can be obtained using {@link #getPayload}.
       *
       * @return MIME type as a string, or null if this is not a MIME record
       */
      public String toMimeType() {
          switch (mTnf) {
              case NdefRecord.TNF_WELL_KNOWN:
                  if (Arrays.equals(mType, NdefRecord.RTD_TEXT)) {
                      return "text/plain";
                  }
                  break;
              case NdefRecord.TNF_MIME_MEDIA:
                  String mimeType = new String(mType, StandardCharsets.US_ASCII);
                  return Intent.normalizeMimeType(mimeType);
          }
          return null;
      }
  
      /**
       * Map this record to a URI, or return null if it cannot be mapped.<p>
       * Currently this method considers the following to be URI records:
       * <ul>
       * <li>{@link #TNF_ABSOLUTE_URI} records.</li>
       * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_URI}.</li>
       * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_SMART_POSTER}
       * and containing a URI record in the NDEF message nested in the payload.
       * </li>
       * <li>{@link #TNF_EXTERNAL_TYPE} records.</li>
       * </ul>
       * If this is not a URI record by the above rules, then null is returned.<p>
       * This method does not perform validation that the URI is
       * actually valid: it always attempts to create and return a URI if
       * this record appears to be a URI record by the above rules.<p>
       * The returned URI will be normalized to have a lower case scheme
       * using {@link Uri#normalizeScheme}.<p>
       *
       * @return URI, or null if this is not a URI record
       */
      public Uri toUri() {
          return toUri(false);
      }
  
      private Uri toUri(boolean inSmartPoster) {
          switch (mTnf) {
              case TNF_WELL_KNOWN:
                  if (Arrays.equals(mType, RTD_SMART_POSTER) && !inSmartPoster) {
                      try {
                          // check payload for a nested NDEF Message containing a URI
                          NdefMessage nestedMessage = new NdefMessage(mPayload);
                          for (NdefRecord nestedRecord : nestedMessage.getRecords()) {
                              Uri uri = nestedRecord.toUri(true);
                              if (uri != null) {
                                  return uri;
                              }
                          }
                      } catch (FormatException e) {  }
                  } else if (Arrays.equals(mType, RTD_URI)) {
                      Uri wktUri = parseWktUri();
                      return (wktUri != null ? wktUri.normalizeScheme() : null);
                  }
                  break;
  
              case TNF_ABSOLUTE_URI:
                  Uri uri = Uri.parse(new String(mType, StandardCharsets.UTF_8));
                  return uri.normalizeScheme();
  
              case TNF_EXTERNAL_TYPE:
                  if (inSmartPoster) {
                      break;
                  }
                  return Uri.parse("vnd.android.nfc://ext/" + new String(mType, StandardCharsets.US_ASCII));
          }
          return null;
      }
  
      /**
       * Return complete URI of {@link #TNF_WELL_KNOWN}, {@link #RTD_URI} records.
       * @return complete URI, or null if invalid
       */
      private Uri parseWktUri() {
          if (mPayload.length < 2) {
              return null;
          }
  
          // payload[0] contains the URI Identifier Code, as per
          // NFC Forum "URI Record Type Definition" section 3.2.2.
          int prefixIndex = (mPayload[0] & (byte)0xFF);
          if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) {
              return null;
          }
          String prefix = URI_PREFIX_MAP[prefixIndex];
          String suffix = new String(Arrays.copyOfRange(mPayload, 1, mPayload.length),
                  StandardCharsets.UTF_8);
          return Uri.parse(prefix + suffix);
      }
  
      /**
       * Main record parsing method.<p>
       * Expects NdefMessage to begin immediately, allows trailing data.<p>
       * Currently has strict validation of all fields as per NDEF 1.0
       * specification section 2.5. We will attempt to keep this as strict as
       * possible to encourage well-formatted NDEF.<p>
       * Always returns 1 or more NdefRecord's, or throws FormatException.
       *
       * @param buffer ByteBuffer to read from
       * @param ignoreMbMe ignore MB and ME flags, and read only 1 complete record
       * @return one or more records
       * @throws FormatException on any parsing error
       */
      static NdefRecord[] parse(ByteBuffer buffer, boolean ignoreMbMe) throws FormatException {
          List<NdefRecord> records = new ArrayList<NdefRecord>();
  
          try {
              byte[] type = null;
              byte[] id = null;
              byte[] payload = null;
              ArrayList<byte[]> chunks = new ArrayList<byte[]>();
              boolean inChunk = false;
              short chunkTnf = -1;
              boolean me = false;
  
              while (!me) {
                  byte flag = buffer.get();
  
                  boolean mb = (flag & NdefRecord.FLAG_MB) != 0;
                  me = (flag & NdefRecord.FLAG_ME) != 0;
                  boolean cf = (flag & NdefRecord.FLAG_CF) != 0;
                  boolean sr = (flag & NdefRecord.FLAG_SR) != 0;
                  boolean il = (flag & NdefRecord.FLAG_IL) != 0;
                  short tnf = (short)(flag & 0x07);
  
                  if (!mb && records.size() == 0 && !inChunk && !ignoreMbMe) {
                      throw new FormatException("expected MB flag");
                  } else if (mb && (records.size() != 0 || inChunk) && !ignoreMbMe) {
                      throw new FormatException("unexpected MB flag");
                  } else if (inChunk && il) {
                      throw new FormatException("unexpected IL flag in non-leading chunk");
                  } else if (cf && me) {
                      throw new FormatException("unexpected ME flag in non-trailing chunk");
                  } else if (inChunk && tnf != NdefRecord.TNF_UNCHANGED) {
                      throw new FormatException("expected TNF_UNCHANGED in non-leading chunk");
                  } else if (!inChunk && tnf == NdefRecord.TNF_UNCHANGED) {
                      throw new FormatException("" +
                              "unexpected TNF_UNCHANGED in first chunk or unchunked record");
                  }
  
                  int typeLength = buffer.get() & 0xFF;
                  long payloadLength = sr ? (buffer.get() & 0xFF) : (buffer.getInt() & 0xFFFFFFFFL);
                  int idLength = il ? (buffer.get() & 0xFF) : 0;
  
                  if (inChunk && typeLength != 0) {
                      throw new FormatException("expected zero-length type in non-leading chunk");
                  }
  
                  if (!inChunk) {
                      type = (typeLength > 0 ? new byte[typeLength] : EMPTY_BYTE_ARRAY);
                      id = (idLength > 0 ? new byte[idLength] : EMPTY_BYTE_ARRAY);
                      buffer.get(type);
                      buffer.get(id);
                  }
  
                  ensureSanePayloadSize(payloadLength);
                  payload = (payloadLength > 0 ? new byte[(int)payloadLength] : EMPTY_BYTE_ARRAY);
                  buffer.get(payload);
  
                  if (cf && !inChunk) {
                      // first chunk
                      if (typeLength == 0 && tnf != NdefRecord.TNF_UNKNOWN) {
                          throw new FormatException("expected non-zero type length in first chunk");
                      }
                      chunks.clear();
                      chunkTnf = tnf;
                  }
                  if (cf || inChunk) {
                      // any chunk
                      chunks.add(payload);
                  }
                  if (!cf && inChunk) {
                      // last chunk, flatten the payload
                      payloadLength = 0;
                      for (byte[] p : chunks) {
                          payloadLength += p.length;
                      }
                      ensureSanePayloadSize(payloadLength);
                      payload = new byte[(int)payloadLength];
                      int i = 0;
                      for (byte[] p : chunks) {
                          System.arraycopy(p, 0, payload, i, p.length);
                          i += p.length;
                      }
                      tnf = chunkTnf;
                  }
                  if (cf) {
                      // more chunks to come
                      inChunk = true;
                      continue;
                  } else {
                      inChunk = false;
                  }
  
                  String error = validateTnf(tnf, type, id, payload);
                  if (error != null) {
                      throw new FormatException(error);
                  }
                  records.add(new NdefRecord(tnf, type, id, payload));
                  if (ignoreMbMe) {  // for parsing a single NdefRecord
                      break;
                  }
              }
          } catch (BufferUnderflowException e) {
              throw new FormatException("expected more data", e);
          }
          return records.toArray(new NdefRecord[records.size()]);
      }
  
      private static void ensureSanePayloadSize(long size) throws FormatException {
          if (size > MAX_PAYLOAD_SIZE) {
              throw new FormatException(
                      "payload above max limit: " + size + " > " + MAX_PAYLOAD_SIZE);
          }
      }
  
      /**
       * Perform simple validation that the tnf is valid.<p>
       * Validates the requirements of NFCForum-TS-NDEF_1.0 section
       * 3.2.6 (Type Name Format). This just validates that the tnf
       * is valid, and that the relevant type, id and payload
       * fields are present (or empty) for this tnf. It does not
       * perform any deep inspection of the type, id and payload fields.<p>
       * Also does not allow TNF_UNCHANGED since this class is only used
       * to present logical (unchunked) records.
       *
       * @return null if valid, or a string error if invalid.
       */
      static String validateTnf(short tnf, byte[] type, byte[] id, byte[] payload) {
          switch (tnf) {
              case TNF_EMPTY:
                  if (type.length != 0 || id.length != 0 || payload.length != 0) {
                      return "unexpected data in TNF_EMPTY record";
                  }
                  return null;
              case TNF_WELL_KNOWN:
              case TNF_MIME_MEDIA:
              case TNF_ABSOLUTE_URI:
              case TNF_EXTERNAL_TYPE:
                  return null;
              case TNF_UNKNOWN:
              case TNF_RESERVED:
                  if (type.length != 0) {
                      return "unexpected type field in TNF_UNKNOWN or TNF_RESERVEd record";
                  }
                  return null;
              case TNF_UNCHANGED:
                  return "unexpected TNF_UNCHANGED in first chunk or logical record";
              default:
                  return String.format("unexpected tnf value: 0x%02x", tnf);
          }
      }
  
      /**
       * Serialize record for network transmission.<p>
       * Uses specified MB and ME flags.<p>
       * Does not chunk records.
       */
      void writeToByteBuffer(ByteBuffer buffer, boolean mb, boolean me) {
          boolean sr = mPayload.length < 256;
          boolean il = mTnf == TNF_EMPTY ? true : mId.length > 0;
  
          byte flags = (byte)((mb ? FLAG_MB : 0) | (me ? FLAG_ME : 0) |
                  (sr ? FLAG_SR : 0) | (il ? FLAG_IL : 0) | mTnf);
          buffer.put(flags);
  
          buffer.put((byte)mType.length);
          if (sr) {
              buffer.put((byte)mPayload.length);
          } else {
              buffer.putInt(mPayload.length);
          }
          if (il) {
              buffer.put((byte)mId.length);
          }
  
          buffer.put(mType);
          buffer.put(mId);
          buffer.put(mPayload);
      }
  
      /**
       * Get byte length of serialized record.
       */
      int getByteLength() {
          int length = 3 + mType.length + mId.length + mPayload.length;
  
          boolean sr = mPayload.length < 256;
          boolean il = mTnf == TNF_EMPTY ? true : mId.length > 0;
  
          if (!sr) length += 3;
          if (il) length += 1;
  
          return length;
      }
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel dest, int flags) {
          dest.writeInt(mTnf);
          dest.writeInt(mType.length);
          dest.writeByteArray(mType);
          dest.writeInt(mId.length);
          dest.writeByteArray(mId);
          dest.writeInt(mPayload.length);
          dest.writeByteArray(mPayload);
      }
  
      public static final @android.annotation.NonNull Parcelable.Creator<NdefRecord> CREATOR =
              new Parcelable.Creator<NdefRecord>() {
          @Override
          public NdefRecord createFromParcel(Parcel in) {
              short tnf = (short)in.readInt();
              int typeLength = in.readInt();
              byte[] type = new byte[typeLength];
              in.readByteArray(type);
              int idLength = in.readInt();
              byte[] id = new byte[idLength];
              in.readByteArray(id);
              int payloadLength = in.readInt();
              byte[] payload = new byte[payloadLength];
              in.readByteArray(payload);
  
              return new NdefRecord(tnf, type, id, payload);
          }
          @Override
          public NdefRecord[] newArray(int size) {
              return new NdefRecord[size];
          }
      };
  
      @Override
      public int hashCode() {
          final int prime = 31;
          int result = 1;
          result = prime * result + Arrays.hashCode(mId);
          result = prime * result + Arrays.hashCode(mPayload);
          result = prime * result + mTnf;
          result = prime * result + Arrays.hashCode(mType);
          return result;
      }
  
      /**
       * Returns true if the specified NDEF Record contains
       * identical tnf, type, id and payload fields.
       */
      @Override
      public boolean equals(Object obj) {
          if (this == obj) return true;
          if (obj == null) return false;
          if (getClass() != obj.getClass()) return false;
          NdefRecord other = (NdefRecord) obj;
          if (!Arrays.equals(mId, other.mId)) return false;
          if (!Arrays.equals(mPayload, other.mPayload)) return false;
          if (mTnf != other.mTnf) return false;
          return Arrays.equals(mType, other.mType);
      }
  
      @Override
      public String toString() {
          StringBuilder b = new StringBuilder(String.format("NdefRecord tnf=%X", mTnf));
          if (mType.length > 0) b.append(" type=").append(bytesToString(mType));
          if (mId.length > 0) b.append(" id=").append(bytesToString(mId));
          if (mPayload.length > 0) b.append(" payload=").append(bytesToString(mPayload));
          return b.toString();
      }
  
      private static StringBuilder bytesToString(byte[] bs) {
          StringBuilder s = new StringBuilder();
          for (byte b : bs) {
              s.append(String.format("%02X", b));
          }
          return s;
      }
  }
  