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