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.content.Intent; 20 import android.net.Uri; 21 import android.os.Parcel; 22 import android.os.Parcelable; 23 import java.nio.BufferUnderflowException; 24 import java.nio.ByteBuffer; 25 import java.nio.charset.StandardCharsets; 26 import java.util.ArrayList; 27 import java.util.Arrays; 28 import java.util.List; 29 import 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 */ 87 public 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(StandardCharsets.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(StandardCharsets.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(StandardCharsets.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.ROOT); 460 type = type.trim().toLowerCase(Locale.ROOT); 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(StandardCharsets.UTF_8); 466 byte[] byteType = type.getBytes(StandardCharsets.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, StandardCharsets.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, StandardCharsets.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, StandardCharsets.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 StandardCharsets.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