1 /* 2 * Copyright (C) 2014 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 com.android.server.hdmi; 18 19 import android.annotation.Nullable; 20 import android.hardware.hdmi.HdmiDeviceInfo; 21 import android.util.Slog; 22 import android.util.SparseArray; 23 import android.util.Xml; 24 25 import com.android.internal.util.HexDump; 26 import com.android.internal.util.IndentingPrintWriter; 27 import com.android.server.hdmi.Constants.AudioCodec; 28 29 import org.xmlpull.v1.XmlPullParser; 30 import org.xmlpull.v1.XmlPullParserException; 31 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Collections; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Objects; 40 41 /** 42 * Various utilities to handle HDMI CEC messages. 43 */ 44 final class HdmiUtils { 45 46 private static final String TAG = "HdmiUtils"; 47 48 private static final int[] ADDRESS_TO_TYPE = { 49 HdmiDeviceInfo.DEVICE_TV, // ADDR_TV 50 HdmiDeviceInfo.DEVICE_RECORDER, // ADDR_RECORDER_1 51 HdmiDeviceInfo.DEVICE_RECORDER, // ADDR_RECORDER_2 52 HdmiDeviceInfo.DEVICE_TUNER, // ADDR_TUNER_1 53 HdmiDeviceInfo.DEVICE_PLAYBACK, // ADDR_PLAYBACK_1 54 HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM, // ADDR_AUDIO_SYSTEM 55 HdmiDeviceInfo.DEVICE_TUNER, // ADDR_TUNER_2 56 HdmiDeviceInfo.DEVICE_TUNER, // ADDR_TUNER_3 57 HdmiDeviceInfo.DEVICE_PLAYBACK, // ADDR_PLAYBACK_2 58 HdmiDeviceInfo.DEVICE_RECORDER, // ADDR_RECORDER_3 59 HdmiDeviceInfo.DEVICE_TUNER, // ADDR_TUNER_4 60 HdmiDeviceInfo.DEVICE_PLAYBACK, // ADDR_PLAYBACK_3 61 HdmiDeviceInfo.DEVICE_RESERVED, 62 HdmiDeviceInfo.DEVICE_RESERVED, 63 HdmiDeviceInfo.DEVICE_TV, // ADDR_SPECIFIC_USE 64 }; 65 66 private static final String[] DEFAULT_NAMES = { 67 "TV", 68 "Recorder_1", 69 "Recorder_2", 70 "Tuner_1", 71 "Playback_1", 72 "AudioSystem", 73 "Tuner_2", 74 "Tuner_3", 75 "Playback_2", 76 "Recorder_3", 77 "Tuner_4", 78 "Playback_3", 79 "Reserved_1", 80 "Reserved_2", 81 "Secondary_TV", 82 }; 83 84 /** 85 * Return value of {@link #getLocalPortFromPhysicalAddress(int, int)} 86 */ 87 static final int TARGET_NOT_UNDER_LOCAL_DEVICE = -1; 88 static final int TARGET_SAME_PHYSICAL_ADDRESS = 0; 89 HdmiUtils()90 private HdmiUtils() { /* cannot be instantiated */ } 91 92 /** 93 * Check if the given logical address is valid. A logical address is valid 94 * if it is one allocated for an actual device which allows communication 95 * with other logical devices. 96 * 97 * @param address logical address 98 * @return true if the given address is valid 99 */ isValidAddress(int address)100 static boolean isValidAddress(int address) { 101 return (Constants.ADDR_TV <= address && address <= Constants.ADDR_SPECIFIC_USE); 102 } 103 104 /** 105 * Return the device type for the given logical address. 106 * 107 * @param address logical address 108 * @return device type for the given logical address; DEVICE_INACTIVE 109 * if the address is not valid. 110 */ getTypeFromAddress(int address)111 static int getTypeFromAddress(int address) { 112 if (isValidAddress(address)) { 113 return ADDRESS_TO_TYPE[address]; 114 } 115 return HdmiDeviceInfo.DEVICE_INACTIVE; 116 } 117 118 /** 119 * Return the default device name for a logical address. This is the name 120 * by which the logical device is known to others until a name is 121 * set explicitly using HdmiCecService.setOsdName. 122 * 123 * @param address logical address 124 * @return default device name; empty string if the address is not valid 125 */ getDefaultDeviceName(int address)126 static String getDefaultDeviceName(int address) { 127 if (isValidAddress(address)) { 128 return DEFAULT_NAMES[address]; 129 } 130 return ""; 131 } 132 133 /** 134 * Verify if the given address is for the given device type. If not it will throw 135 * {@link IllegalArgumentException}. 136 * 137 * @param logicalAddress the logical address to verify 138 * @param deviceType the device type to check 139 * @throws IllegalArgumentException 140 */ verifyAddressType(int logicalAddress, int deviceType)141 static void verifyAddressType(int logicalAddress, int deviceType) { 142 int actualDeviceType = getTypeFromAddress(logicalAddress); 143 if (actualDeviceType != deviceType) { 144 throw new IllegalArgumentException("Device type missmatch:[Expected:" + deviceType 145 + ", Actual:" + actualDeviceType); 146 } 147 } 148 149 /** 150 * Check if the given CEC message come from the given address. 151 * 152 * @param cmd the CEC message to check 153 * @param expectedAddress the expected source address of the given message 154 * @param tag the tag of caller module (for log message) 155 * @return true if the CEC message comes from the given address 156 */ checkCommandSource(HdmiCecMessage cmd, int expectedAddress, String tag)157 static boolean checkCommandSource(HdmiCecMessage cmd, int expectedAddress, String tag) { 158 int src = cmd.getSource(); 159 if (src != expectedAddress) { 160 Slog.w(tag, "Invalid source [Expected:" + expectedAddress + ", Actual:" + src + "]"); 161 return false; 162 } 163 return true; 164 } 165 166 /** 167 * Parse the parameter block of CEC message as [System Audio Status]. 168 * 169 * @param cmd the CEC message to parse 170 * @return true if the given parameter has [ON] value 171 */ parseCommandParamSystemAudioStatus(HdmiCecMessage cmd)172 static boolean parseCommandParamSystemAudioStatus(HdmiCecMessage cmd) { 173 return cmd.getParams()[0] == Constants.SYSTEM_AUDIO_STATUS_ON; 174 } 175 176 /** 177 * Parse the <Report Audio Status> message and check if it is mute 178 * 179 * @param cmd the CEC message to parse 180 * @return true if the given parameter has [MUTE] 181 */ isAudioStatusMute(HdmiCecMessage cmd)182 static boolean isAudioStatusMute(HdmiCecMessage cmd) { 183 byte params[] = cmd.getParams(); 184 return (params[0] & 0x80) == 0x80; 185 } 186 187 /** 188 * Parse the <Report Audio Status> message and extract the volume 189 * 190 * @param cmd the CEC message to parse 191 * @return device's volume. Constants.UNKNOWN_VOLUME in case it is out of range 192 */ getAudioStatusVolume(HdmiCecMessage cmd)193 static int getAudioStatusVolume(HdmiCecMessage cmd) { 194 byte params[] = cmd.getParams(); 195 int volume = params[0] & 0x7F; 196 if (volume < 0x00 || 0x64 < volume) { 197 volume = Constants.UNKNOWN_VOLUME; 198 } 199 return volume; 200 } 201 202 /** 203 * Convert integer array to list of {@link Integer}. 204 * 205 * <p>The result is immutable. 206 * 207 * @param is integer array 208 * @return {@link List} instance containing the elements in the given array 209 */ asImmutableList(final int[] is)210 static List<Integer> asImmutableList(final int[] is) { 211 ArrayList<Integer> list = new ArrayList<>(is.length); 212 for (int type : is) { 213 list.add(type); 214 } 215 return Collections.unmodifiableList(list); 216 } 217 218 /** 219 * Assemble two bytes into single integer value. 220 * 221 * @param data to be assembled 222 * @return assembled value 223 */ twoBytesToInt(byte[] data)224 static int twoBytesToInt(byte[] data) { 225 return ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); 226 } 227 228 /** 229 * Assemble two bytes into single integer value. 230 * 231 * @param data to be assembled 232 * @param offset offset to the data to convert in the array 233 * @return assembled value 234 */ twoBytesToInt(byte[] data, int offset)235 static int twoBytesToInt(byte[] data, int offset) { 236 return ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF); 237 } 238 239 /** 240 * Assemble three bytes into single integer value. 241 * 242 * @param data to be assembled 243 * @return assembled value 244 */ threeBytesToInt(byte[] data)245 static int threeBytesToInt(byte[] data) { 246 return ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); 247 } 248 sparseArrayToList(SparseArray<T> array)249 static <T> List<T> sparseArrayToList(SparseArray<T> array) { 250 ArrayList<T> list = new ArrayList<>(); 251 for (int i = 0; i < array.size(); ++i) { 252 list.add(array.valueAt(i)); 253 } 254 return list; 255 } 256 mergeToUnmodifiableList(List<T> a, List<T> b)257 static <T> List<T> mergeToUnmodifiableList(List<T> a, List<T> b) { 258 if (a.isEmpty() && b.isEmpty()) { 259 return Collections.emptyList(); 260 } 261 if (a.isEmpty()) { 262 return Collections.unmodifiableList(b); 263 } 264 if (b.isEmpty()) { 265 return Collections.unmodifiableList(a); 266 } 267 List<T> newList = new ArrayList<>(); 268 newList.addAll(a); 269 newList.addAll(b); 270 return Collections.unmodifiableList(newList); 271 } 272 273 /** 274 * See if the new path is affecting the active path. 275 * 276 * @param activePath current active path 277 * @param newPath new path 278 * @return true if the new path changes the current active path 279 */ isAffectingActiveRoutingPath(int activePath, int newPath)280 static boolean isAffectingActiveRoutingPath(int activePath, int newPath) { 281 // The new path affects the current active path if the parent of the new path 282 // is an ancestor of the active path. 283 // (1.1.0.0, 2.0.0.0) -> true, new path alters the parent 284 // (1.1.0.0, 1.2.0.0) -> true, new path is a sibling 285 // (1.1.0.0, 1.2.1.0) -> false, new path is a descendant of a sibling 286 // (1.0.0.0, 3.2.0.0) -> false, in a completely different path 287 288 // Get the parent of the new path by clearing the least significant 289 // non-zero nibble. 290 for (int i = 0; i <= 12; i += 4) { 291 int nibble = (newPath >> i) & 0xF; 292 if (nibble != 0) { 293 int mask = 0xFFF0 << i; 294 newPath &= mask; 295 break; 296 } 297 } 298 if (newPath == 0x0000) { 299 return true; // Top path always affects the active path 300 } 301 return isInActiveRoutingPath(activePath, newPath); 302 } 303 304 /** 305 * See if the new path is in the active path. 306 * 307 * @param activePath current active path 308 * @param newPath new path 309 * @return true if the new path in the active routing path 310 */ isInActiveRoutingPath(int activePath, int newPath)311 static boolean isInActiveRoutingPath(int activePath, int newPath) { 312 // Check each nibble of the currently active path and the new path till the position 313 // where the active nibble is not zero. For (activePath, newPath), 314 // (1.1.0.0, 1.0.0.0) -> true, new path is a parent 315 // (1.2.1.0, 1.2.1.2) -> true, new path is a descendant 316 // (1.1.0.0, 1.2.0.0) -> false, new path is a sibling 317 // (1.0.0.0, 2.0.0.0) -> false, in a completely different path 318 for (int i = 12; i >= 0; i -= 4) { 319 int nibbleActive = (activePath >> i) & 0xF; 320 if (nibbleActive == 0) { 321 break; 322 } 323 int nibbleNew = (newPath >> i) & 0xF; 324 if (nibbleNew == 0) { 325 break; 326 } 327 if (nibbleActive != nibbleNew) { 328 return false; 329 } 330 } 331 return true; 332 } 333 334 /** 335 * Clone {@link HdmiDeviceInfo} with new power status. 336 */ cloneHdmiDeviceInfo(HdmiDeviceInfo info, int newPowerStatus)337 static HdmiDeviceInfo cloneHdmiDeviceInfo(HdmiDeviceInfo info, int newPowerStatus) { 338 return new HdmiDeviceInfo(info.getLogicalAddress(), 339 info.getPhysicalAddress(), info.getPortId(), info.getDeviceType(), 340 info.getVendorId(), info.getDisplayName(), newPowerStatus); 341 } 342 343 /** 344 * Dump a {@link SparseArray} to the print writer. 345 * 346 * <p>The dump is formatted: 347 * <pre> 348 * name: 349 * key = value 350 * key = value 351 * ... 352 * </pre> 353 */ dumpSparseArray(IndentingPrintWriter pw, String name, SparseArray<T> sparseArray)354 static <T> void dumpSparseArray(IndentingPrintWriter pw, String name, 355 SparseArray<T> sparseArray) { 356 printWithTrailingColon(pw, name); 357 pw.increaseIndent(); 358 int size = sparseArray.size(); 359 for (int i = 0; i < size; i++) { 360 int key = sparseArray.keyAt(i); 361 T value = sparseArray.get(key); 362 pw.printPair(Integer.toString(key), value); 363 pw.println(); 364 } 365 pw.decreaseIndent(); 366 } 367 printWithTrailingColon(IndentingPrintWriter pw, String name)368 private static void printWithTrailingColon(IndentingPrintWriter pw, String name) { 369 pw.println(name.endsWith(":") ? name : name.concat(":")); 370 } 371 372 /** 373 * Dump a {@link Map} to the print writer. 374 * 375 * <p>The dump is formatted: 376 * <pre> 377 * name: 378 * key = value 379 * key = value 380 * ... 381 * </pre> 382 */ dumpMap(IndentingPrintWriter pw, String name, Map<K, V> map)383 static <K, V> void dumpMap(IndentingPrintWriter pw, String name, Map<K, V> map) { 384 printWithTrailingColon(pw, name); 385 pw.increaseIndent(); 386 for (Map.Entry<K, V> entry: map.entrySet()) { 387 pw.printPair(entry.getKey().toString(), entry.getValue()); 388 pw.println(); 389 } 390 pw.decreaseIndent(); 391 } 392 393 /** 394 * Dump a {@link Map} to the print writer. 395 * 396 * <p>The dump is formatted: 397 * <pre> 398 * name: 399 * value 400 * value 401 * ... 402 * </pre> 403 */ dumpIterable(IndentingPrintWriter pw, String name, Iterable<T> values)404 static <T> void dumpIterable(IndentingPrintWriter pw, String name, Iterable<T> values) { 405 printWithTrailingColon(pw, name); 406 pw.increaseIndent(); 407 for (T value : values) { 408 pw.println(value); 409 } 410 pw.decreaseIndent(); 411 } 412 413 /** 414 * Method to parse target physical address to the port number on the current device. 415 * 416 * <p>This check assumes target address is valid. 417 * 418 * @param targetPhysicalAddress is the physical address of the target device 419 * @param myPhysicalAddress is the physical address of the current device 420 * @return 421 * If the target device is under the current device, return the port number of current device 422 * that the target device is connected to. This also applies to the devices that are indirectly 423 * connected to the current device. 424 * 425 * <p>If the target device has the same physical address as the current device, return 426 * {@link #TARGET_SAME_PHYSICAL_ADDRESS}. 427 * 428 * <p>If the target device is not under the current device, return 429 * {@link #TARGET_NOT_UNDER_LOCAL_DEVICE}. 430 */ getLocalPortFromPhysicalAddress( int targetPhysicalAddress, int myPhysicalAddress)431 public static int getLocalPortFromPhysicalAddress( 432 int targetPhysicalAddress, int myPhysicalAddress) { 433 if (myPhysicalAddress == targetPhysicalAddress) { 434 return TARGET_SAME_PHYSICAL_ADDRESS; 435 } 436 437 int mask = 0xF000; 438 int finalMask = 0xF000; 439 int maskedAddress = myPhysicalAddress; 440 441 while (maskedAddress != 0) { 442 maskedAddress = myPhysicalAddress & mask; 443 finalMask |= mask; 444 mask >>= 4; 445 } 446 447 int portAddress = targetPhysicalAddress & finalMask; 448 if ((portAddress & (finalMask << 4)) != myPhysicalAddress) { 449 return TARGET_NOT_UNDER_LOCAL_DEVICE; 450 } 451 452 mask <<= 4; 453 int port = portAddress & mask; 454 while ((port >> 4) != 0) { 455 port >>= 4; 456 } 457 return port; 458 } 459 460 public static class ShortAudioDescriptorXmlParser { 461 // We don't use namespaces 462 private static final String NS = null; 463 464 // return a list of devices config parse(InputStream in)465 public static List<DeviceConfig> parse(InputStream in) 466 throws XmlPullParserException, IOException { 467 XmlPullParser parser = Xml.newPullParser(); 468 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); 469 parser.setInput(in, null); 470 parser.nextTag(); 471 return readDevices(parser); 472 } 473 skip(XmlPullParser parser)474 private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { 475 if (parser.getEventType() != XmlPullParser.START_TAG) { 476 throw new IllegalStateException(); 477 } 478 int depth = 1; 479 while (depth != 0) { 480 switch (parser.next()) { 481 case XmlPullParser.END_TAG: 482 depth--; 483 break; 484 case XmlPullParser.START_TAG: 485 depth++; 486 break; 487 } 488 } 489 } 490 readDevices(XmlPullParser parser)491 private static List<DeviceConfig> readDevices(XmlPullParser parser) 492 throws XmlPullParserException, IOException { 493 List<DeviceConfig> devices = new ArrayList<>(); 494 495 parser.require(XmlPullParser.START_TAG, NS, "config"); 496 while (parser.next() != XmlPullParser.END_TAG) { 497 if (parser.getEventType() != XmlPullParser.START_TAG) { 498 continue; 499 } 500 String name = parser.getName(); 501 // Starts by looking for the device tag 502 if (name.equals("device")) { 503 String deviceType = parser.getAttributeValue(null, "type"); 504 DeviceConfig config = null; 505 if (deviceType != null) { 506 config = readDeviceConfig(parser, deviceType); 507 } 508 if (config != null) { 509 devices.add(config); 510 } 511 } else { 512 skip(parser); 513 } 514 } 515 return devices; 516 } 517 518 // Processes device tags in the config. 519 @Nullable readDeviceConfig(XmlPullParser parser, String deviceType)520 private static DeviceConfig readDeviceConfig(XmlPullParser parser, String deviceType) 521 throws XmlPullParserException, IOException { 522 List<CodecSad> codecSads = new ArrayList<>(); 523 int format; 524 byte[] descriptor; 525 526 parser.require(XmlPullParser.START_TAG, NS, "device"); 527 while (parser.next() != XmlPullParser.END_TAG) { 528 if (parser.getEventType() != XmlPullParser.START_TAG) { 529 continue; 530 } 531 String tagName = parser.getName(); 532 533 // Starts by looking for the supportedFormat tag 534 if (tagName.equals("supportedFormat")) { 535 String codecAttriValue = parser.getAttributeValue(null, "format"); 536 String sadAttriValue = parser.getAttributeValue(null, "descriptor"); 537 format = (codecAttriValue) == null 538 ? Constants.AUDIO_CODEC_NONE : formatNameToNum(codecAttriValue); 539 descriptor = readSad(sadAttriValue); 540 if (format != Constants.AUDIO_CODEC_NONE && descriptor != null) { 541 codecSads.add(new CodecSad(format, descriptor)); 542 } 543 parser.nextTag(); 544 parser.require(XmlPullParser.END_TAG, NS, "supportedFormat"); 545 } else { 546 skip(parser); 547 } 548 } 549 if (codecSads.size() == 0) { 550 return null; 551 } 552 return new DeviceConfig(deviceType, codecSads); 553 } 554 555 // Processes sad attribute in the supportedFormat. 556 @Nullable readSad(String sad)557 private static byte[] readSad(String sad) { 558 if (sad == null || sad.length() == 0) { 559 return null; 560 } 561 byte[] sadBytes = HexDump.hexStringToByteArray(sad); 562 if (sadBytes.length != 3) { 563 Slog.w(TAG, "SAD byte array length is not 3. Length = " + sadBytes.length); 564 return null; 565 } 566 return sadBytes; 567 } 568 569 @AudioCodec formatNameToNum(String codecAttriValue)570 private static int formatNameToNum(String codecAttriValue) { 571 switch (codecAttriValue) { 572 case "AUDIO_FORMAT_NONE": 573 return Constants.AUDIO_CODEC_NONE; 574 case "AUDIO_FORMAT_LPCM": 575 return Constants.AUDIO_CODEC_LPCM; 576 case "AUDIO_FORMAT_DD": 577 return Constants.AUDIO_CODEC_DD; 578 case "AUDIO_FORMAT_MPEG1": 579 return Constants.AUDIO_CODEC_MPEG1; 580 case "AUDIO_FORMAT_MP3": 581 return Constants.AUDIO_CODEC_MP3; 582 case "AUDIO_FORMAT_MPEG2": 583 return Constants.AUDIO_CODEC_MPEG2; 584 case "AUDIO_FORMAT_AAC": 585 return Constants.AUDIO_CODEC_AAC; 586 case "AUDIO_FORMAT_DTS": 587 return Constants.AUDIO_CODEC_DTS; 588 case "AUDIO_FORMAT_ATRAC": 589 return Constants.AUDIO_CODEC_ATRAC; 590 case "AUDIO_FORMAT_ONEBITAUDIO": 591 return Constants.AUDIO_CODEC_ONEBITAUDIO; 592 case "AUDIO_FORMAT_DDP": 593 return Constants.AUDIO_CODEC_DDP; 594 case "AUDIO_FORMAT_DTSHD": 595 return Constants.AUDIO_CODEC_DTSHD; 596 case "AUDIO_FORMAT_TRUEHD": 597 return Constants.AUDIO_CODEC_TRUEHD; 598 case "AUDIO_FORMAT_DST": 599 return Constants.AUDIO_CODEC_DST; 600 case "AUDIO_FORMAT_WMAPRO": 601 return Constants.AUDIO_CODEC_WMAPRO; 602 case "AUDIO_FORMAT_MAX": 603 return Constants.AUDIO_CODEC_MAX; 604 default: 605 return Constants.AUDIO_CODEC_NONE; 606 } 607 } 608 } 609 610 // Device configuration of its supported Codecs and their Short Audio Descriptors. 611 public static class DeviceConfig { 612 /** Name of the device. Should be {@link Constants.AudioDevice}. **/ 613 public final String name; 614 /** List of a {@link CodecSad}. **/ 615 public final List<CodecSad> supportedCodecs; 616 DeviceConfig(String name, List<CodecSad> supportedCodecs)617 public DeviceConfig(String name, List<CodecSad> supportedCodecs) { 618 this.name = name; 619 this.supportedCodecs = supportedCodecs; 620 } 621 622 @Override equals(Object obj)623 public boolean equals(Object obj) { 624 if (obj instanceof DeviceConfig) { 625 DeviceConfig that = (DeviceConfig) obj; 626 return that.name.equals(this.name) 627 && that.supportedCodecs.equals(this.supportedCodecs); 628 } 629 return false; 630 } 631 632 @Override hashCode()633 public int hashCode() { 634 return Objects.hash( 635 name, 636 supportedCodecs.hashCode()); 637 } 638 } 639 640 // Short Audio Descriptor of a specific Codec 641 public static class CodecSad { 642 /** Audio Codec. Should be {@link Constants.AudioCodec}. **/ 643 public final int audioCodec; 644 /** 645 * Three-byte Short Audio Descriptor. See HDMI Specification 1.4b CEC 13.15.3 and 646 * ANSI-CTA-861-F-FINAL 7.5.2 Audio Data Block for more details. 647 */ 648 public final byte[] sad; 649 CodecSad(int audioCodec, byte[] sad)650 public CodecSad(int audioCodec, byte[] sad) { 651 this.audioCodec = audioCodec; 652 this.sad = sad; 653 } 654 CodecSad(int audioCodec, String sad)655 public CodecSad(int audioCodec, String sad) { 656 this.audioCodec = audioCodec; 657 this.sad = HexDump.hexStringToByteArray(sad); 658 } 659 660 @Override equals(Object obj)661 public boolean equals(Object obj) { 662 if (obj instanceof CodecSad) { 663 CodecSad that = (CodecSad) obj; 664 return that.audioCodec == this.audioCodec 665 && Arrays.equals(that.sad, this.sad); 666 } 667 return false; 668 } 669 670 @Override hashCode()671 public int hashCode() { 672 return Objects.hash( 673 audioCodec, 674 Arrays.hashCode(sad)); 675 } 676 } 677 } 678