content: /*
   * Copyright (C) 2014 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.hdmi;
  
  import android.annotation.Nullable;
  import android.hardware.hdmi.HdmiDeviceInfo;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.Xml;
  
  import com.android.internal.util.HexDump;
  import com.android.internal.util.IndentingPrintWriter;
  import com.android.server.hdmi.Constants.AudioCodec;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  
  /**
   * Various utilities to handle HDMI CEC messages.
   */
  final class HdmiUtils {
  
      private static final String TAG = "HdmiUtils";
  
      private static final int[] ADDRESS_TO_TYPE = {
          HdmiDeviceInfo.DEVICE_TV,  // ADDR_TV
          HdmiDeviceInfo.DEVICE_RECORDER,  // ADDR_RECORDER_1
          HdmiDeviceInfo.DEVICE_RECORDER,  // ADDR_RECORDER_2
          HdmiDeviceInfo.DEVICE_TUNER,  // ADDR_TUNER_1
          HdmiDeviceInfo.DEVICE_PLAYBACK,  // ADDR_PLAYBACK_1
          HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM,  // ADDR_AUDIO_SYSTEM
          HdmiDeviceInfo.DEVICE_TUNER,  // ADDR_TUNER_2
          HdmiDeviceInfo.DEVICE_TUNER,  // ADDR_TUNER_3
          HdmiDeviceInfo.DEVICE_PLAYBACK,  // ADDR_PLAYBACK_2
          HdmiDeviceInfo.DEVICE_RECORDER,  // ADDR_RECORDER_3
          HdmiDeviceInfo.DEVICE_TUNER,  // ADDR_TUNER_4
          HdmiDeviceInfo.DEVICE_PLAYBACK,  // ADDR_PLAYBACK_3
          HdmiDeviceInfo.DEVICE_RESERVED,
          HdmiDeviceInfo.DEVICE_RESERVED,
          HdmiDeviceInfo.DEVICE_TV,  // ADDR_SPECIFIC_USE
      };
  
      private static final String[] DEFAULT_NAMES = {
          "TV",
          "Recorder_1",
          "Recorder_2",
          "Tuner_1",
          "Playback_1",
          "AudioSystem",
          "Tuner_2",
          "Tuner_3",
          "Playback_2",
          "Recorder_3",
          "Tuner_4",
          "Playback_3",
          "Reserved_1",
          "Reserved_2",
          "Secondary_TV",
      };
  
      /**
       * Return value of {@link #getLocalPortFromPhysicalAddress(int, int)}
       */
      static final int TARGET_NOT_UNDER_LOCAL_DEVICE = -1;
      static final int TARGET_SAME_PHYSICAL_ADDRESS = 0;
  
      private HdmiUtils() { /* cannot be instantiated */ }
  
      /**
       * Check if the given logical address is valid. A logical address is valid
       * if it is one allocated for an actual device which allows communication
       * with other logical devices.
       *
       * @param address logical address
       * @return true if the given address is valid
       */
      static boolean isValidAddress(int address) {
          return (Constants.ADDR_TV <= address && address <= Constants.ADDR_SPECIFIC_USE);
      }
  
      /**
       * Return the device type for the given logical address.
       *
       * @param address logical address
       * @return device type for the given logical address; DEVICE_INACTIVE
       *         if the address is not valid.
       */
      static int getTypeFromAddress(int address) {
          if (isValidAddress(address)) {
              return ADDRESS_TO_TYPE[address];
          }
          return HdmiDeviceInfo.DEVICE_INACTIVE;
      }
  
      /**
       * Return the default device name for a logical address. This is the name
       * by which the logical device is known to others until a name is
       * set explicitly using HdmiCecService.setOsdName.
       *
       * @param address logical address
       * @return default device name; empty string if the address is not valid
       */
      static String getDefaultDeviceName(int address) {
          if (isValidAddress(address)) {
              return DEFAULT_NAMES[address];
          }
          return "";
      }
  
      /**
       * Verify if the given address is for the given device type.  If not it will throw
       * {@link IllegalArgumentException}.
       *
       * @param logicalAddress the logical address to verify
       * @param deviceType the device type to check
       * @throws IllegalArgumentException
       */
      static void verifyAddressType(int logicalAddress, int deviceType) {
          int actualDeviceType = getTypeFromAddress(logicalAddress);
          if (actualDeviceType != deviceType) {
              throw new IllegalArgumentException("Device type missmatch:[Expected:" + deviceType
                      + ", Actual:" + actualDeviceType);
          }
      }
  
      /**
       * Check if the given CEC message come from the given address.
       *
       * @param cmd the CEC message to check
       * @param expectedAddress the expected source address of the given message
       * @param tag the tag of caller module (for log message)
       * @return true if the CEC message comes from the given address
       */
      static boolean checkCommandSource(HdmiCecMessage cmd, int expectedAddress, String tag) {
          int src = cmd.getSource();
          if (src != expectedAddress) {
              Slog.w(tag, "Invalid source [Expected:" + expectedAddress + ", Actual:" + src + "]");
              return false;
          }
          return true;
      }
  
      /**
       * Parse the parameter block of CEC message as [System Audio Status].
       *
       * @param cmd the CEC message to parse
       * @return true if the given parameter has [ON] value
       */
      static boolean parseCommandParamSystemAudioStatus(HdmiCecMessage cmd) {
          return cmd.getParams()[0] == Constants.SYSTEM_AUDIO_STATUS_ON;
      }
  
      /**
       * Parse the <Report Audio Status> message and check if it is mute
       *
       * @param cmd the CEC message to parse
       * @return true if the given parameter has [MUTE]
       */
      static boolean isAudioStatusMute(HdmiCecMessage cmd) {
          byte params[] = cmd.getParams();
          return (params[0] & 0x80) == 0x80;
      }
  
      /**
       * Parse the <Report Audio Status> message and extract the volume
       *
       * @param cmd the CEC message to parse
       * @return device's volume. Constants.UNKNOWN_VOLUME in case it is out of range
       */
      static int getAudioStatusVolume(HdmiCecMessage cmd) {
          byte params[] = cmd.getParams();
          int volume = params[0] & 0x7F;
          if (volume < 0x00 || 0x64 < volume) {
              volume = Constants.UNKNOWN_VOLUME;
          }
          return volume;
      }
  
      /**
       * Convert integer array to list of {@link Integer}.
       *
       * <p>The result is immutable.
       *
       * @param is integer array
       * @return {@link List} instance containing the elements in the given array
       */
      static List<Integer> asImmutableList(final int[] is) {
          ArrayList<Integer> list = new ArrayList<>(is.length);
          for (int type : is) {
              list.add(type);
          }
          return Collections.unmodifiableList(list);
      }
  
      /**
       * Assemble two bytes into single integer value.
       *
       * @param data to be assembled
       * @return assembled value
       */
      static int twoBytesToInt(byte[] data) {
          return ((data[0] & 0xFF) << 8) | (data[1] & 0xFF);
      }
  
      /**
       * Assemble two bytes into single integer value.
       *
       * @param data to be assembled
       * @param offset offset to the data to convert in the array
       * @return assembled value
       */
      static int twoBytesToInt(byte[] data, int offset) {
          return ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF);
      }
  
      /**
       * Assemble three bytes into single integer value.
       *
       * @param data to be assembled
       * @return assembled value
       */
      static int threeBytesToInt(byte[] data) {
          return ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF);
      }
  
      static <T> List<T> sparseArrayToList(SparseArray<T> array) {
          ArrayList<T> list = new ArrayList<>();
          for (int i = 0; i < array.size(); ++i) {
              list.add(array.valueAt(i));
          }
          return list;
      }
  
      static <T> List<T> mergeToUnmodifiableList(List<T> a, List<T> b) {
          if (a.isEmpty() && b.isEmpty()) {
              return Collections.emptyList();
          }
          if (a.isEmpty()) {
              return Collections.unmodifiableList(b);
          }
          if (b.isEmpty()) {
              return Collections.unmodifiableList(a);
          }
          List<T> newList = new ArrayList<>();
          newList.addAll(a);
          newList.addAll(b);
          return Collections.unmodifiableList(newList);
      }
  
      /**
       * See if the new path is affecting the active path.
       *
       * @param activePath current active path
       * @param newPath new path
       * @return true if the new path changes the current active path
       */
      static boolean isAffectingActiveRoutingPath(int activePath, int newPath) {
          // The new path affects the current active path if the parent of the new path
          // is an ancestor of the active path.
          // (1.1.0.0, 2.0.0.0) -> true, new path alters the parent
          // (1.1.0.0, 1.2.0.0) -> true, new path is a sibling
          // (1.1.0.0, 1.2.1.0) -> false, new path is a descendant of a sibling
          // (1.0.0.0, 3.2.0.0) -> false, in a completely different path
  
          // Get the parent of the new path by clearing the least significant
          // non-zero nibble.
          for (int i = 0; i <= 12; i += 4) {
              int nibble = (newPath >> i) & 0xF;
              if (nibble != 0) {
                  int mask = 0xFFF0 << i;
                  newPath &= mask;
                  break;
              }
          }
          if (newPath == 0x0000) {
              return true;  // Top path always affects the active path
          }
          return isInActiveRoutingPath(activePath, newPath);
      }
  
      /**
       * See if the new path is in the active path.
       *
       * @param activePath current active path
       * @param newPath new path
       * @return true if the new path in the active routing path
       */
      static boolean isInActiveRoutingPath(int activePath, int newPath) {
          // Check each nibble of the currently active path and the new path till the position
          // where the active nibble is not zero. For (activePath, newPath),
          // (1.1.0.0, 1.0.0.0) -> true, new path is a parent
          // (1.2.1.0, 1.2.1.2) -> true, new path is a descendant
          // (1.1.0.0, 1.2.0.0) -> false, new path is a sibling
          // (1.0.0.0, 2.0.0.0) -> false, in a completely different path
          for (int i = 12; i >= 0; i -= 4) {
              int nibbleActive = (activePath >> i) & 0xF;
              if (nibbleActive == 0) {
                  break;
              }
              int nibbleNew = (newPath >> i) & 0xF;
              if (nibbleNew == 0) {
                  break;
              }
              if (nibbleActive != nibbleNew) {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * Clone {@link HdmiDeviceInfo} with new power status.
       */
      static HdmiDeviceInfo cloneHdmiDeviceInfo(HdmiDeviceInfo info, int newPowerStatus) {
          return new HdmiDeviceInfo(info.getLogicalAddress(),
                  info.getPhysicalAddress(), info.getPortId(), info.getDeviceType(),
                  info.getVendorId(), info.getDisplayName(), newPowerStatus);
      }
  
      /**
       * Dump a {@link SparseArray} to the print writer.
       *
       * <p>The dump is formatted:
       * <pre>
       *     name:
       *        key = value
       *        key = value
       *        ...
       * </pre>
       */
      static <T> void dumpSparseArray(IndentingPrintWriter pw, String name,
              SparseArray<T> sparseArray) {
          printWithTrailingColon(pw, name);
          pw.increaseIndent();
          int size = sparseArray.size();
          for (int i = 0; i < size; i++) {
              int key = sparseArray.keyAt(i);
              T value = sparseArray.get(key);
              pw.printPair(Integer.toString(key), value);
              pw.println();
          }
          pw.decreaseIndent();
      }
  
      private static void printWithTrailingColon(IndentingPrintWriter pw, String name) {
          pw.println(name.endsWith(":") ? name : name.concat(":"));
      }
  
      /**
       * Dump a {@link Map} to the print writer.
       *
       * <p>The dump is formatted:
       * <pre>
       *     name:
       *        key = value
       *        key = value
       *        ...
       * </pre>
       */
      static <K, V> void dumpMap(IndentingPrintWriter pw, String name, Map<K, V> map) {
          printWithTrailingColon(pw, name);
          pw.increaseIndent();
          for (Map.Entry<K, V> entry: map.entrySet()) {
              pw.printPair(entry.getKey().toString(), entry.getValue());
              pw.println();
          }
          pw.decreaseIndent();
      }
  
      /**
       * Dump a {@link Map} to the print writer.
       *
       * <p>The dump is formatted:
       * <pre>
       *     name:
       *        value
       *        value
       *        ...
       * </pre>
       */
      static <T> void dumpIterable(IndentingPrintWriter pw, String name, Iterable<T> values) {
          printWithTrailingColon(pw, name);
          pw.increaseIndent();
          for (T value : values) {
              pw.println(value);
          }
          pw.decreaseIndent();
      }
  
      /**
       * Method to parse target physical address to the port number on the current device.
       *
       * <p>This check assumes target address is valid.
       *
       * @param targetPhysicalAddress is the physical address of the target device
       * @param myPhysicalAddress is the physical address of the current device
       * @return
       * If the target device is under the current device, return the port number of current device
       * that the target device is connected to. This also applies to the devices that are indirectly
       * connected to the current device.
       *
       * <p>If the target device has the same physical address as the current device, return
       * {@link #TARGET_SAME_PHYSICAL_ADDRESS}.
       *
       * <p>If the target device is not under the current device, return
       * {@link #TARGET_NOT_UNDER_LOCAL_DEVICE}.
       */
      public static int getLocalPortFromPhysicalAddress(
              int targetPhysicalAddress, int myPhysicalAddress) {
          if (myPhysicalAddress == targetPhysicalAddress) {
              return TARGET_SAME_PHYSICAL_ADDRESS;
          }
  
          int mask = 0xF000;
          int finalMask = 0xF000;
          int maskedAddress = myPhysicalAddress;
  
          while (maskedAddress != 0) {
              maskedAddress = myPhysicalAddress & mask;
              finalMask |= mask;
              mask >>= 4;
          }
  
          int portAddress = targetPhysicalAddress & finalMask;
          if ((portAddress & (finalMask << 4)) != myPhysicalAddress) {
              return TARGET_NOT_UNDER_LOCAL_DEVICE;
          }
  
          mask <<= 4;
          int port = portAddress & mask;
          while ((port >> 4) != 0) {
              port >>= 4;
          }
          return port;
      }
  
      public static class ShortAudioDescriptorXmlParser {
          // We don't use namespaces
          private static final String NS = null;
  
          // return a list of devices config
          public static List<DeviceConfig> parse(InputStream in)
                  throws XmlPullParserException, IOException {
              XmlPullParser parser = Xml.newPullParser();
              parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
              parser.setInput(in, null);
              parser.nextTag();
              return readDevices(parser);
          }
  
          private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
              if (parser.getEventType() != XmlPullParser.START_TAG) {
                  throw new IllegalStateException();
              }
              int depth = 1;
              while (depth != 0) {
                  switch (parser.next()) {
                      case XmlPullParser.END_TAG:
                          depth--;
                          break;
                      case XmlPullParser.START_TAG:
                          depth++;
                          break;
                  }
              }
          }
  
          private static List<DeviceConfig> readDevices(XmlPullParser parser)
                  throws XmlPullParserException, IOException {
              List<DeviceConfig> devices = new ArrayList<>();
  
              parser.require(XmlPullParser.START_TAG, NS, "config");
              while (parser.next() != XmlPullParser.END_TAG) {
                  if (parser.getEventType() != XmlPullParser.START_TAG) {
                      continue;
                  }
                  String name = parser.getName();
                  // Starts by looking for the device tag
                  if (name.equals("device")) {
                      String deviceType = parser.getAttributeValue(null, "type");
                      DeviceConfig config = null;
                      if (deviceType != null) {
                          config = readDeviceConfig(parser, deviceType);
                      }
                      if (config != null) {
                          devices.add(config);
                      }
                  } else {
                      skip(parser);
                  }
              }
              return devices;
          }
  
          // Processes device tags in the config.
          @Nullable
          private static DeviceConfig readDeviceConfig(XmlPullParser parser, String deviceType)
                  throws XmlPullParserException, IOException {
              List<CodecSad> codecSads = new ArrayList<>();
              int format;
              byte[] descriptor;
  
              parser.require(XmlPullParser.START_TAG, NS, "device");
              while (parser.next() != XmlPullParser.END_TAG) {
                  if (parser.getEventType() != XmlPullParser.START_TAG) {
                      continue;
                  }
                  String tagName = parser.getName();
  
                  // Starts by looking for the supportedFormat tag
                  if (tagName.equals("supportedFormat")) {
                      String codecAttriValue = parser.getAttributeValue(null, "format");
                      String sadAttriValue = parser.getAttributeValue(null, "descriptor");
                      format = (codecAttriValue) == null
                              ? Constants.AUDIO_CODEC_NONE : formatNameToNum(codecAttriValue);
                      descriptor = readSad(sadAttriValue);
                      if (format != Constants.AUDIO_CODEC_NONE && descriptor != null) {
                          codecSads.add(new CodecSad(format, descriptor));
                      }
                      parser.nextTag();
                      parser.require(XmlPullParser.END_TAG, NS, "supportedFormat");
                  } else {
                      skip(parser);
                  }
              }
              if (codecSads.size() == 0) {
                  return null;
              }
              return new DeviceConfig(deviceType, codecSads);
          }
  
          // Processes sad attribute in the supportedFormat.
          @Nullable
          private static byte[] readSad(String sad) {
              if (sad == null || sad.length() == 0) {
                  return null;
              }
              byte[] sadBytes = HexDump.hexStringToByteArray(sad);
              if (sadBytes.length != 3) {
                  Slog.w(TAG, "SAD byte array length is not 3. Length = " + sadBytes.length);
                  return null;
              }
              return sadBytes;
          }
  
          @AudioCodec
          private static int formatNameToNum(String codecAttriValue) {
              switch (codecAttriValue) {
                  case "AUDIO_FORMAT_NONE":
                      return Constants.AUDIO_CODEC_NONE;
                  case "AUDIO_FORMAT_LPCM":
                      return Constants.AUDIO_CODEC_LPCM;
                  case "AUDIO_FORMAT_DD":
                      return Constants.AUDIO_CODEC_DD;
                  case "AUDIO_FORMAT_MPEG1":
                      return Constants.AUDIO_CODEC_MPEG1;
                  case "AUDIO_FORMAT_MP3":
                      return Constants.AUDIO_CODEC_MP3;
                  case "AUDIO_FORMAT_MPEG2":
                      return Constants.AUDIO_CODEC_MPEG2;
                  case "AUDIO_FORMAT_AAC":
                      return Constants.AUDIO_CODEC_AAC;
                  case "AUDIO_FORMAT_DTS":
                      return Constants.AUDIO_CODEC_DTS;
                  case "AUDIO_FORMAT_ATRAC":
                      return Constants.AUDIO_CODEC_ATRAC;
                  case "AUDIO_FORMAT_ONEBITAUDIO":
                      return Constants.AUDIO_CODEC_ONEBITAUDIO;
                  case "AUDIO_FORMAT_DDP":
                      return Constants.AUDIO_CODEC_DDP;
                  case "AUDIO_FORMAT_DTSHD":
                      return Constants.AUDIO_CODEC_DTSHD;
                  case "AUDIO_FORMAT_TRUEHD":
                      return Constants.AUDIO_CODEC_TRUEHD;
                  case "AUDIO_FORMAT_DST":
                      return Constants.AUDIO_CODEC_DST;
                  case "AUDIO_FORMAT_WMAPRO":
                      return Constants.AUDIO_CODEC_WMAPRO;
                  case "AUDIO_FORMAT_MAX":
                      return Constants.AUDIO_CODEC_MAX;
                  default:
                      return Constants.AUDIO_CODEC_NONE;
              }
          }
      }
  
      // Device configuration of its supported Codecs and their Short Audio Descriptors.
      public static class DeviceConfig {
          /** Name of the device. Should be {@link Constants.AudioDevice}. **/
          public final String name;
          /** List of a {@link CodecSad}. **/
          public final List<CodecSad> supportedCodecs;
  
          public DeviceConfig(String name, List<CodecSad> supportedCodecs) {
              this.name = name;
              this.supportedCodecs = supportedCodecs;
          }
  
          @Override
          public boolean equals(Object obj) {
              if (obj instanceof DeviceConfig) {
                  DeviceConfig that = (DeviceConfig) obj;
                  return that.name.equals(this.name)
                      && that.supportedCodecs.equals(this.supportedCodecs);
              }
              return false;
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(
                  name,
                  supportedCodecs.hashCode());
          }
      }
  
      // Short Audio Descriptor of a specific Codec
      public static class CodecSad {
          /** Audio Codec. Should be {@link Constants.AudioCodec}. **/
          public final int audioCodec;
          /**
           * Three-byte Short Audio Descriptor. See HDMI Specification 1.4b CEC 13.15.3 and
           * ANSI-CTA-861-F-FINAL 7.5.2 Audio Data Block for more details.
           */
          public final byte[] sad;
  
          public CodecSad(int audioCodec, byte[] sad) {
              this.audioCodec = audioCodec;
              this.sad = sad;
          }
  
          public CodecSad(int audioCodec, String sad) {
              this.audioCodec = audioCodec;
              this.sad = HexDump.hexStringToByteArray(sad);
          }
  
          @Override
          public boolean equals(Object obj) {
              if (obj instanceof CodecSad) {
                  CodecSad that = (CodecSad) obj;
                  return that.audioCodec == this.audioCodec
                      && Arrays.equals(that.sad, this.sad);
              }
              return false;
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(
                  audioCodec,
                  Arrays.hashCode(sad));
          }
      }
  }
  