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