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