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