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.Charsets; 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(Charsets.UTF_8)); 315 } 316 317 /** 318 * Create a new NDEF Record containing a URI.<p> 319 * Use this method to encode a URI (or URL) into an NDEF Record.<p> 320 * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} 321 * and {@link #RTD_URI}. This is the most efficient encoding 322 * of a URI into NDEF.<p> 323 * The uri parameter will be normalized with 324 * {@link Uri#normalizeScheme} to set the scheme to lower case to 325 * follow Android best practices for intent filtering. 326 * However the unchecked exception 327 * {@link IllegalArgumentException} may be thrown if the uri 328 * parameter has serious problems, for example if it is empty, so always 329 * catch this exception if you are passing user-generated data into this 330 * method.<p> 331 * 332 * Reference specification: NFCForum-TS-RTD_URI_1.0 333 * 334 * @param uri URI to encode. 335 * @return an NDEF Record containing the URI 336 * @throws IllegalArugmentException if the uri is empty or invalid 337 */ 338 public static NdefRecord createUri(Uri uri) { 339 if (uri == null) throw new NullPointerException("uri is null"); 340 341 uri = uri.normalizeScheme(); 342 String uriString = uri.toString(); 343 if (uriString.length() == 0) throw new IllegalArgumentException("uri is empty"); 344 345 byte prefix = 0; 346 for (int i = 1; i < URI_PREFIX_MAP.length; i++) { 347 if (uriString.startsWith(URI_PREFIX_MAP[i])) { 348 prefix = (byte) i; 349 uriString = uriString.substring(URI_PREFIX_MAP[i].length()); 350 break; 351 } 352 } 353 byte[] uriBytes = uriString.getBytes(Charsets.UTF_8); 354 byte[] recordBytes = new byte[uriBytes.length + 1]; 355 recordBytes[0] = prefix; 356 System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.length); 357 return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, null, recordBytes); 358 } 359 360 /** 361 * Create a new NDEF Record containing a URI.<p> 362 * Use this method to encode a URI (or URL) into an NDEF Record.<p> 363 * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} 364 * and {@link #RTD_URI}. This is the most efficient encoding 365 * of a URI into NDEF.<p> 366 * The uriString parameter will be normalized with 367 * {@link Uri#normalizeScheme} to set the scheme to lower case to 368 * follow Android best practices for intent filtering. 369 * However the unchecked exception 370 * {@link IllegalArgumentException} may be thrown if the uriString 371 * parameter has serious problems, for example if it is empty, so always 372 * catch this exception if you are passing user-generated data into this 373 * method.<p> 374 * 375 * Reference specification: NFCForum-TS-RTD_URI_1.0 376 * 377 * @param uriString string URI to encode. 378 * @return an NDEF Record containing the URI 379 * @throws IllegalArugmentException if the uriString is empty or invalid 380 */ 381 public static NdefRecord createUri(String uriString) { 382 return createUri(Uri.parse(uriString)); 383 } 384 385 /** 386 * Create a new NDEF Record containing MIME data.<p> 387 * Use this method to encode MIME-typed data into an NDEF Record, 388 * such as "text/plain", or "image/jpeg".<p> 389 * The mimeType parameter will be normalized with 390 * {@link Intent#normalizeMimeType} to follow Android best 391 * practices for intent filtering, for example to force lower-case. 392 * However the unchecked exception 393 * {@link IllegalArgumentException} may be thrown 394 * if the mimeType parameter has serious problems, 395 * for example if it is empty, so always catch this 396 * exception if you are passing user-generated data into this method. 397 * <p> 398 * For efficiency, This method might not make an internal copy of the 399 * mimeData byte array, so take care not 400 * to modify the mimeData byte array while still using the returned 401 * NdefRecord. 402 * 403 * @param mimeType a valid MIME type 404 * @param mimeData MIME data as bytes 405 * @return an NDEF Record containing the MIME-typed data 406 * @throws IllegalArugmentException if the mimeType is empty or invalid 407 * 408 */ 409 public static NdefRecord createMime(String mimeType, byte[] mimeData) { 410 if (mimeType == null) throw new NullPointerException("mimeType is null"); 411 412 // We only do basic MIME type validation: trying to follow the 413 // RFCs strictly only ends in tears, since there are lots of MIME 414 // types in common use that are not strictly valid as per RFC rules 415 mimeType = Intent.normalizeMimeType(mimeType); 416 if (mimeType.length() == 0) throw new IllegalArgumentException("mimeType is empty"); 417 int slashIndex = mimeType.indexOf('/'); 418 if (slashIndex == 0) throw new IllegalArgumentException("mimeType must have major type"); 419 if (slashIndex == mimeType.length() - 1) { 420 throw new IllegalArgumentException("mimeType must have minor type"); 421 } 422 // missing '/' is allowed 423 424 // MIME RFCs suggest ASCII encoding for content-type 425 byte[] typeBytes = mimeType.getBytes(Charsets.US_ASCII); 426 return new NdefRecord(TNF_MIME_MEDIA, typeBytes, null, mimeData); 427 } 428 429 /** 430 * Create a new NDEF Record containing external (application-specific) data.<p> 431 * Use this method to encode application specific data into an NDEF Record. 432 * The data is typed by a domain name (usually your Android package name) and 433 * a domain-specific type. This data is packaged into a "NFC Forum External 434 * Type" NDEF Record.<p> 435 * NFC Forum requires that the domain and type used in an external record 436 * are treated as case insensitive, however Android intent filtering is 437 * always case sensitive. So this method will force the domain and type to 438 * lower-case before creating the NDEF Record.<p> 439 * The unchecked exception {@link IllegalArgumentException} will be thrown 440 * if the domain and type have serious problems, for example if either field 441 * is empty, so always catch this 442 * exception if you are passing user-generated data into this method.<p> 443 * There are no such restrictions on the payload data.<p> 444 * For efficiency, This method might not make an internal copy of the 445 * data byte array, so take care not 446 * to modify the data byte array while still using the returned 447 * NdefRecord. 448 * 449 * Reference specification: NFCForum-TS-RTD_1.0 450 * @param domain domain-name of issuing organization 451 * @param type domain-specific type of data 452 * @param data payload as bytes 453 * @throws IllegalArugmentException if either domain or type are empty or invalid 454 */ 455 public static NdefRecord createExternal(String domain, String type, byte[] data) { 456 if (domain == null) throw new NullPointerException("domain is null"); 457 if (type == null) throw new NullPointerException("type is null"); 458 459 domain = domain.trim().toLowerCase(Locale.US); 460 type = type.trim().toLowerCase(Locale.US); 461 462 if (domain.length() == 0) throw new IllegalArgumentException("domain is empty"); 463 if (type.length() == 0) throw new IllegalArgumentException("type is empty"); 464 465 byte[] byteDomain = domain.getBytes(Charsets.UTF_8); 466 byte[] byteType = type.getBytes(Charsets.UTF_8); 467 byte[] b = new byte[byteDomain.length + 1 + byteType.length]; 468 System.arraycopy(byteDomain, 0, b, 0, byteDomain.length); 469 b[byteDomain.length] = ':'; 470 System.arraycopy(byteType, 0, b, byteDomain.length + 1, byteType.length); 471 472 return new NdefRecord(TNF_EXTERNAL_TYPE, b, null, data); 473 } 474 475 /** 476 * Construct an NDEF Record from its component fields.<p> 477 * Recommend to use helpers such as {#createUri} or 478 * {{@link #createExternal} where possible, since they perform 479 * stricter validation that the record is correctly formatted 480 * as per NDEF specifications. However if you know what you are 481 * doing then this constructor offers the most flexibility.<p> 482 * An {@link NdefRecord} represents a logical (complete) 483 * record, and cannot represent NDEF Record chunks.<p> 484 * Basic validation of the tnf, type, id and payload is performed 485 * as per the following rules: 486 * <ul> 487 * <li>The tnf paramter must be a 3-bit value.</li> 488 * <li>Records with a tnf of {@link #TNF_EMPTY} cannot have a type, 489 * id or payload.</li> 490 * <li>Records with a tnf of {@link #TNF_UNKNOWN} or {@literal 0x07} 491 * cannot have a type.</li> 492 * <li>Records with a tnf of {@link #TNF_UNCHANGED} are not allowed 493 * since this class only represents complete (unchunked) records.</li> 494 * </ul> 495 * This minimal validation is specified by 496 * NFCForum-TS-NDEF_1.0 section 3.2.6 (Type Name Format).<p> 497 * If any of the above validation 498 * steps fail then {@link IllegalArgumentException} is thrown.<p> 499 * Deep inspection of the type, id and payload fields is not 500 * performed, so it is possible to create NDEF Records 501 * that conform to section 3.2.6 502 * but fail other more strict NDEF specification requirements. For 503 * example, the payload may be invalid given the tnf and type. 504 * <p> 505 * To omit a type, id or payload field, set the parameter to an 506 * empty byte array or null. 507 * 508 * @param tnf a 3-bit TNF constant 509 * @param type byte array, containing zero to 255 bytes, or null 510 * @param id byte array, containing zero to 255 bytes, or null 511 * @param payload byte array, containing zero to (2 ** 32 - 1) bytes, 512 * or null 513 * @throws IllegalArugmentException if a valid record cannot be created 514 */ 515 public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) { 516 /* convert nulls */ 517 if (type == null) type = EMPTY_BYTE_ARRAY; 518 if (id == null) id = EMPTY_BYTE_ARRAY; 519 if (payload == null) payload = EMPTY_BYTE_ARRAY; 520 521 String message = validateTnf(tnf, type, id, payload); 522 if (message != null) { 523 throw new IllegalArgumentException(message); 524 } 525 526 mTnf = tnf; 527 mType = type; 528 mId = id; 529 mPayload = payload; 530 } 531 532 /** 533 * Construct an NDEF Record from raw bytes.<p> 534 * This method is deprecated, use {@link NdefMessage#NdefMessage(byte[])} 535 * instead. This is because it does not make sense to parse a record: 536 * the NDEF binary format is only defined for a message, and the 537 * record flags MB and ME do not make sense outside of the context of 538 * an entire message.<p> 539 * This implementation will attempt to parse a single record by ignoring 540 * the MB and ME flags, and otherwise following the rules of 541 * {@link NdefMessage#NdefMessage(byte[])}.<p> 542 * 543 * @param data raw bytes to parse 544 * @throws FormatException if the data cannot be parsed into a valid record 545 * @deprecated use {@link NdefMessage#NdefMessage(byte[])} instead. 546 */ 547 @Deprecated 548 public NdefRecord(byte[] data) throws FormatException { 549 ByteBuffer buffer = ByteBuffer.wrap(data); 550 NdefRecord[] rs = parse(buffer, true); 551 552 if (buffer.remaining() > 0) { 553 throw new FormatException("data too long"); 554 } 555 556 mTnf = rs[0].mTnf; 557 mType = rs[0].mType; 558 mId = rs[0].mId; 559 mPayload = rs[0].mPayload; 560 } 561 562 /** 563 * Returns the 3-bit TNF. 564 * <p> 565 * TNF is the top-level type. 566 */ 567 public short getTnf() { 568 return mTnf; 569 } 570 571 /** 572 * Returns the variable length Type field. 573 * <p> 574 * This should be used in conjunction with the TNF field to determine the 575 * payload format. 576 * <p> 577 * Returns an empty byte array if this record 578 * does not have a type field. 579 */ 580 public byte[] getType() { 581 return mType.clone(); 582 } 583 584 /** 585 * Returns the variable length ID. 586 * <p> 587 * Returns an empty byte array if this record 588 * does not have an id field. 589 */ 590 public byte[] getId() { 591 return mId.clone(); 592 } 593 594 /** 595 * Returns the variable length payload. 596 * <p> 597 * Returns an empty byte array if this record 598 * does not have a payload field. 599 */ 600 public byte[] getPayload() { 601 return mPayload.clone(); 602 } 603 604 /** 605 * Return this NDEF Record as a byte array.<p> 606 * This method is deprecated, use {@link NdefMessage#toByteArray} 607 * instead. This is because the NDEF binary format is not defined for 608 * a record outside of the context of a message: the MB and ME flags 609 * cannot be set without knowing the location inside a message.<p> 610 * This implementation will attempt to serialize a single record by 611 * always setting the MB and ME flags (in other words, assume this 612 * is a single-record NDEF Message).<p> 613 * 614 * @deprecated use {@link NdefMessage#toByteArray()} instead 615 */ 616 @Deprecated 617 public byte[] toByteArray() { 618 ByteBuffer buffer = ByteBuffer.allocate(getByteLength()); 619 writeToByteBuffer(buffer, true, true); 620 return buffer.array(); 621 } 622 623 /** 624 * Map this record to a MIME type, or return null if it cannot be mapped.<p> 625 * Currently this method considers all {@link #TNF_MIME_MEDIA} records to 626 * be MIME records, as well as some {@link #TNF_WELL_KNOWN} records such as 627 * {@link #RTD_TEXT}. If this is a MIME record then the MIME type as string 628 * is returned, otherwise null is returned.<p> 629 * This method does not perform validation that the MIME type is 630 * actually valid. It always attempts to 631 * return a string containing the type if this is a MIME record.<p> 632 * The returned MIME type will by normalized to lower-case using 633 * {@link Intent#normalizeMimeType}.<p> 634 * The MIME payload can be obtained using {@link #getPayload}. 635 * 636 * @return MIME type as a string, or null if this is not a MIME record 637 */ 638 public String toMimeType() { 639 switch (mTnf) { 640 case NdefRecord.TNF_WELL_KNOWN: 641 if (Arrays.equals(mType, NdefRecord.RTD_TEXT)) { 642 return "text/plain"; 643 } 644 break; 645 case NdefRecord.TNF_MIME_MEDIA: 646 String mimeType = new String(mType, Charsets.US_ASCII); 647 return Intent.normalizeMimeType(mimeType); 648 } 649 return null; 650 } 651 652 /** 653 * Map this record to a URI, or return null if it cannot be mapped.<p> 654 * Currently this method considers the following to be URI records: 655 * <ul> 656 * <li>{@link #TNF_ABSOLUTE_URI} records.</li> 657 * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_URI}.</li> 658 * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_SMART_POSTER} 659 * and containing a URI record in the NDEF message nested in the payload. 660 * </li> 661 * <li>{@link #TNF_EXTERNAL_TYPE} records.</li> 662 * </ul> 663 * If this is not a URI record by the above rules, then null is returned.<p> 664 * This method does not perform validation that the URI is 665 * actually valid: it always attempts to create and return a URI if 666 * this record appears to be a URI record by the above rules.<p> 667 * The returned URI will be normalized to have a lower case scheme 668 * using {@link Uri#normalizeScheme}.<p> 669 * 670 * @return URI, or null if this is not a URI record 671 */ 672 public Uri toUri() { 673 return toUri(false); 674 } 675 676 private Uri toUri(boolean inSmartPoster) { 677 switch (mTnf) { 678 case TNF_WELL_KNOWN: 679 if (Arrays.equals(mType, RTD_SMART_POSTER) && !inSmartPoster) { 680 try { 681 // check payload for a nested NDEF Message containing a URI 682 NdefMessage nestedMessage = new NdefMessage(mPayload); 683 for (NdefRecord nestedRecord : nestedMessage.getRecords()) { 684 Uri uri = nestedRecord.toUri(true); 685 if (uri != null) { 686 return uri; 687 } 688 } 689 } catch (FormatException e) { } 690 } else if (Arrays.equals(mType, RTD_URI)) { 691 return parseWktUri().normalizeScheme(); 692 } 693 break; 694 695 case TNF_ABSOLUTE_URI: 696 Uri uri = Uri.parse(new String(mType, Charsets.UTF_8)); 697 return uri.normalizeScheme(); 698 699 case TNF_EXTERNAL_TYPE: 700 if (inSmartPoster) { 701 break; 702 } 703 return Uri.parse("vnd.android.nfc://ext/" + new String(mType, Charsets.US_ASCII)); 704 } 705 return null; 706 } 707 708 /** 709 * Return complete URI of {@link #TNF_WELL_KNOWN}, {@link #RTD_URI} records. 710 * @return complete URI, or null if invalid 711 */ 712 private Uri parseWktUri() { 713 if (mPayload.length < 2) { 714 return null; 715 } 716 717 // payload[0] contains the URI Identifier Code, as per 718 // NFC Forum "URI Record Type Definition" section 3.2.2. 719 int prefixIndex = (mPayload[0] & (byte)0xFF); 720 if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) { 721 return null; 722 } 723 String prefix = URI_PREFIX_MAP[prefixIndex]; 724 String suffix = new String(Arrays.copyOfRange(mPayload, 1, mPayload.length), 725 Charsets.UTF_8); 726 return Uri.parse(prefix + suffix); 727 } 728 729 /** 730 * Main record parsing method.<p> 731 * Expects NdefMessage to begin immediately, allows trailing data.<p> 732 * Currently has strict validation of all fields as per NDEF 1.0 733 * specification section 2.5. We will attempt to keep this as strict as 734 * possible to encourage well-formatted NDEF.<p> 735 * Always returns 1 or more NdefRecord's, or throws FormatException. 736 * 737 * @param buffer ByteBuffer to read from 738 * @param ignoreMbMe ignore MB and ME flags, and read only 1 complete record 739 * @return one or more records 740 * @throws FormatException on any parsing error 741 */ 742 static NdefRecord[] parse(ByteBuffer buffer, boolean ignoreMbMe) throws FormatException { 743 List<NdefRecord> records = new ArrayList<NdefRecord>(); 744 745 try { 746 byte[] type = null; 747 byte[] id = null; 748 byte[] payload = null; 749 ArrayList<byte[]> chunks = new ArrayList<byte[]>(); 750 boolean inChunk = false; 751 short chunkTnf = -1; 752 boolean me = false; 753 754 while (!me) { 755 byte flag = buffer.get(); 756 757 boolean mb = (flag & NdefRecord.FLAG_MB) != 0; 758 me = (flag & NdefRecord.FLAG_ME) != 0; 759 boolean cf = (flag & NdefRecord.FLAG_CF) != 0; 760 boolean sr = (flag & NdefRecord.FLAG_SR) != 0; 761 boolean il = (flag & NdefRecord.FLAG_IL) != 0; 762 short tnf = (short)(flag & 0x07); 763 764 if (!mb && records.size() == 0 && !inChunk && !ignoreMbMe) { 765 throw new FormatException("expected MB flag"); 766 } else if (mb && records.size() != 0 && !ignoreMbMe) { 767 throw new FormatException("unexpected MB flag"); 768 } else if (inChunk && il) { 769 throw new FormatException("unexpected IL flag in non-leading chunk"); 770 } else if (cf && me) { 771 throw new FormatException("unexpected ME flag in non-trailing chunk"); 772 } else if (inChunk && tnf != NdefRecord.TNF_UNCHANGED) { 773 throw new FormatException("expected TNF_UNCHANGED in non-leading chunk"); 774 } else if (!inChunk && tnf == NdefRecord.TNF_UNCHANGED) { 775 throw new FormatException("" + 776 "unexpected TNF_UNCHANGED in first chunk or unchunked record"); 777 } 778 779 int typeLength = buffer.get() & 0xFF; 780 long payloadLength = sr ? (buffer.get() & 0xFF) : (buffer.getInt() & 0xFFFFFFFFL); 781 int idLength = il ? (buffer.get() & 0xFF) : 0; 782 783 if (inChunk && typeLength != 0) { 784 throw new FormatException("expected zero-length type in non-leading chunk"); 785 } 786 787 if (!inChunk) { 788 type = (typeLength > 0 ? new byte[typeLength] : EMPTY_BYTE_ARRAY); 789 id = (idLength > 0 ? new byte[idLength] : EMPTY_BYTE_ARRAY); 790 buffer.get(type); 791 buffer.get(id); 792 } 793 794 ensureSanePayloadSize(payloadLength); 795 payload = (payloadLength > 0 ? new byte[(int)payloadLength] : EMPTY_BYTE_ARRAY); 796 buffer.get(payload); 797 798 if (cf && !inChunk) { 799 // first chunk 800 chunks.clear(); 801 chunkTnf = tnf; 802 } 803 if (cf || inChunk) { 804 // any chunk 805 chunks.add(payload); 806 } 807 if (!cf && inChunk) { 808 // last chunk, flatten the payload 809 payloadLength = 0; 810 for (byte[] p : chunks) { 811 payloadLength += p.length; 812 } 813 ensureSanePayloadSize(payloadLength); 814 payload = new byte[(int)payloadLength]; 815 int i = 0; 816 for (byte[] p : chunks) { 817 System.arraycopy(p, 0, payload, i, p.length); 818 i += p.length; 819 } 820 tnf = chunkTnf; 821 } 822 if (cf) { 823 // more chunks to come 824 inChunk = true; 825 continue; 826 } else { 827 inChunk = false; 828 } 829 830 String error = validateTnf(tnf, type, id, payload); 831 if (error != null) { 832 throw new FormatException(error); 833 } 834 records.add(new NdefRecord(tnf, type, id, payload)); 835 if (ignoreMbMe) { // for parsing a single NdefRecord 836 break; 837 } 838 } 839 } catch (BufferUnderflowException e) { 840 throw new FormatException("expected more data", e); 841 } 842 return records.toArray(new NdefRecord[records.size()]); 843 } 844 845 private static void ensureSanePayloadSize(long size) throws FormatException { 846 if (size > MAX_PAYLOAD_SIZE) { 847 throw new FormatException( 848 "payload above max limit: " + size + " > " + MAX_PAYLOAD_SIZE); 849 } 850 } 851 852 /** 853 * Perform simple validation that the tnf is valid.<p> 854 * Validates the requirements of NFCForum-TS-NDEF_1.0 section 855 * 3.2.6 (Type Name Format). This just validates that the tnf 856 * is valid, and that the relevant type, id and payload 857 * fields are present (or empty) for this tnf. It does not 858 * perform any deep inspection of the type, id and payload fields.<p> 859 * Also does not allow TNF_UNCHANGED since this class is only used 860 * to present logical (unchunked) records. 861 * 862 * @return null if valid, or a string error if invalid. 863 */ 864 static String validateTnf(short tnf, byte[] type, byte[] id, byte[] payload) { 865 switch (tnf) { 866 case TNF_EMPTY: 867 if (type.length != 0 || id.length != 0 || payload.length != 0) { 868 return "unexpected data in TNF_EMPTY record"; 869 } 870 return null; 871 case TNF_WELL_KNOWN: 872 case TNF_MIME_MEDIA: 873 case TNF_ABSOLUTE_URI: 874 case TNF_EXTERNAL_TYPE: 875 return null; 876 case TNF_UNKNOWN: 877 case TNF_RESERVED: 878 if (type.length != 0) { 879 return "unexpected type field in TNF_UNKNOWN or TNF_RESERVEd record"; 880 } 881 return null; 882 case TNF_UNCHANGED: 883 return "unexpected TNF_UNCHANGED in first chunk or logical record"; 884 default: 885 return String.format("unexpected tnf value: 0x%02x", tnf); 886 } 887 } 888 889 /** 890 * Serialize record for network transmission.<p> 891 * Uses specified MB and ME flags.<p> 892 * Does not chunk records. 893 */ 894 void writeToByteBuffer(ByteBuffer buffer, boolean mb, boolean me) { 895 boolean sr = mPayload.length < 256; 896 boolean il = mId.length > 0; 897 898 byte flags = (byte)((mb ? FLAG_MB : 0) | (me ? FLAG_ME : 0) | 899 (sr ? FLAG_SR : 0) | (il ? FLAG_IL : 0) | mTnf); 900 buffer.put(flags); 901 902 buffer.put((byte)mType.length); 903 if (sr) { 904 buffer.put((byte)mPayload.length); 905 } else { 906 buffer.putInt(mPayload.length); 907 } 908 if (il) { 909 buffer.put((byte)mId.length); 910 } 911 912 buffer.put(mType); 913 buffer.put(mId); 914 buffer.put(mPayload); 915 } 916 917 /** 918 * Get byte length of serialized record. 919 */ 920 int getByteLength() { 921 int length = 3 + mType.length + mId.length + mPayload.length; 922 923 boolean sr = mPayload.length < 256; 924 boolean il = mId.length > 0; 925 926 if (!sr) length += 3; 927 if (il) length += 1; 928 929 return length; 930 } 931 932 @Override 933 public int describeContents() { 934 return 0; 935 } 936 937 @Override 938 public void writeToParcel(Parcel dest, int flags) { 939 dest.writeInt(mTnf); 940 dest.writeInt(mType.length); 941 dest.writeByteArray(mType); 942 dest.writeInt(mId.length); 943 dest.writeByteArray(mId); 944 dest.writeInt(mPayload.length); 945 dest.writeByteArray(mPayload); 946 } 947 948 public static final Parcelable.Creator<NdefRecord> CREATOR = 949 new Parcelable.Creator<NdefRecord>() { 950 @Override 951 public NdefRecord createFromParcel(Parcel in) { 952 short tnf = (short)in.readInt(); 953 int typeLength = in.readInt(); 954 byte[] type = new byte[typeLength]; 955 in.readByteArray(type); 956 int idLength = in.readInt(); 957 byte[] id = new byte[idLength]; 958 in.readByteArray(id); 959 int payloadLength = in.readInt(); 960 byte[] payload = new byte[payloadLength]; 961 in.readByteArray(payload); 962 963 return new NdefRecord(tnf, type, id, payload); 964 } 965 @Override 966 public NdefRecord[] newArray(int size) { 967 return new NdefRecord[size]; 968 } 969 }; 970 971 @Override 972 public int hashCode() { 973 final int prime = 31; 974 int result = 1; 975 result = prime * result + Arrays.hashCode(mId); 976 result = prime * result + Arrays.hashCode(mPayload); 977 result = prime * result + mTnf; 978 result = prime * result + Arrays.hashCode(mType); 979 return result; 980 } 981 982 /** 983 * Returns true if the specified NDEF Record contains 984 * identical tnf, type, id and payload fields. 985 */ 986 @Override 987 public boolean equals(Object obj) { 988 if (this == obj) return true; 989 if (obj == null) return false; 990 if (getClass() != obj.getClass()) return false; 991 NdefRecord other = (NdefRecord) obj; 992 if (!Arrays.equals(mId, other.mId)) return false; 993 if (!Arrays.equals(mPayload, other.mPayload)) return false; 994 if (mTnf != other.mTnf) return false; 995 return Arrays.equals(mType, other.mType); 996 } 997 998 @Override 999 public String toString() { 1000 StringBuilder b = new StringBuilder(String.format("NdefRecord tnf=%X", mTnf)); 1001 if (mType.length > 0) b.append(" type=").append(bytesToString(mType)); 1002 if (mId.length > 0) b.append(" id=").append(bytesToString(mId)); 1003 if (mPayload.length > 0) b.append(" payload=").append(bytesToString(mPayload)); 1004 return b.toString(); 1005 } 1006 1007 private static StringBuilder bytesToString(byte[] bs) { 1008 StringBuilder s = new StringBuilder(); 1009 for (byte b : bs) { 1010 s.append(String.format("%02X", b)); 1011 } 1012 return s; 1013 } 1014 } 1015