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