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