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