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