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