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