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