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 android.media.tv;
  
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.StringRes;
  import android.annotation.SystemApi;
  import android.annotation.UnsupportedAppUsage;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.content.pm.ResolveInfo;
  import android.content.pm.ServiceInfo;
  import android.content.res.Resources;
  import android.content.res.TypedArray;
  import android.content.res.XmlResourceParser;
  import android.graphics.drawable.Drawable;
  import android.graphics.drawable.Icon;
  import android.hardware.hdmi.HdmiControlManager;
  import android.hardware.hdmi.HdmiDeviceInfo;
  import android.hardware.hdmi.HdmiUtils;
  import android.hardware.hdmi.HdmiUtils.HdmiAddressRelativePosition;
  import android.net.Uri;
  import android.os.Bundle;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.os.UserHandle;
  import android.provider.Settings;
  import android.text.TextUtils;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.SparseIntArray;
  import android.util.Xml;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  
  /**
   * This class is used to specify meta information of a TV input.
   */
  public final class TvInputInfo implements Parcelable {
      private static final boolean DEBUG = false;
      private static final String TAG = "TvInputInfo";
  
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef({TYPE_TUNER, TYPE_OTHER, TYPE_COMPOSITE, TYPE_SVIDEO, TYPE_SCART, TYPE_COMPONENT,
              TYPE_VGA, TYPE_DVI, TYPE_HDMI, TYPE_DISPLAY_PORT})
      public @interface Type {}
  
      // Should be in sync with frameworks/base/core/res/res/values/attrs.xml
      /**
       * TV input type: the TV input service is a tuner which provides channels.
       */
      public static final int TYPE_TUNER = 0;
      /**
       * TV input type: a generic hardware TV input type.
       */
      public static final int TYPE_OTHER = 1000;
      /**
       * TV input type: the TV input service represents a composite port.
       */
      public static final int TYPE_COMPOSITE = 1001;
      /**
       * TV input type: the TV input service represents a SVIDEO port.
       */
      public static final int TYPE_SVIDEO = 1002;
      /**
       * TV input type: the TV input service represents a SCART port.
       */
      public static final int TYPE_SCART = 1003;
      /**
       * TV input type: the TV input service represents a component port.
       */
      public static final int TYPE_COMPONENT = 1004;
      /**
       * TV input type: the TV input service represents a VGA port.
       */
      public static final int TYPE_VGA = 1005;
      /**
       * TV input type: the TV input service represents a DVI port.
       */
      public static final int TYPE_DVI = 1006;
      /**
       * TV input type: the TV input service is HDMI. (e.g. HDMI 1)
       */
      public static final int TYPE_HDMI = 1007;
      /**
       * TV input type: the TV input service represents a display port.
       */
      public static final int TYPE_DISPLAY_PORT = 1008;
  
      /**
       * Used as a String extra field in setup intents created by {@link #createSetupIntent()} to
       * supply the ID of a specific TV input to set up.
       */
      public static final String EXTRA_INPUT_ID = "android.media.tv.extra.INPUT_ID";
  
      private final ResolveInfo mService;
  
      private final String mId;
      private final int mType;
      private final boolean mIsHardwareInput;
  
      // TODO: Remove mIconUri when createTvInputInfo() is removed.
      private Uri mIconUri;
  
      private final CharSequence mLabel;
      private final int mLabelResId;
      private final Icon mIcon;
      private final Icon mIconStandby;
      private final Icon mIconDisconnected;
  
      // Attributes from XML meta data.
      private final String mSetupActivity;
      private final boolean mCanRecord;
      private final int mTunerCount;
  
      // Attributes specific to HDMI
      private final HdmiDeviceInfo mHdmiDeviceInfo;
      private final boolean mIsConnectedToHdmiSwitch;
      @HdmiAddressRelativePosition
      private final int mHdmiConnectionRelativePosition;
      private final String mParentId;
  
      private final Bundle mExtras;
  
      /**
       * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
       * ResolveInfo, and HdmiDeviceInfo.
       *
       * @param service The ResolveInfo returned from the package manager about this TV input service.
       * @param hdmiDeviceInfo The HdmiDeviceInfo for a HDMI CEC logical device.
       * @param parentId The ID of this TV input's parent input. {@code null} if none exists.
       * @param label The label of this TvInputInfo. If it is {@code null} or empty, {@code service}
       *            label will be loaded.
       * @param iconUri The {@link android.net.Uri} to load the icon image. See
       *            {@link android.content.ContentResolver#openInputStream}. If it is {@code null},
       *            the application icon of {@code service} will be loaded.
       * @hide
       * @deprecated Use {@link Builder} instead.
       */
      @Deprecated
      @SystemApi
      public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
              HdmiDeviceInfo hdmiDeviceInfo, String parentId, String label, Uri iconUri)
                      throws XmlPullParserException, IOException {
          TvInputInfo info = new TvInputInfo.Builder(context, service)
                  .setHdmiDeviceInfo(hdmiDeviceInfo)
                  .setParentId(parentId)
                  .setLabel(label)
                  .build();
          info.mIconUri = iconUri;
          return info;
      }
  
      /**
       * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
       * ResolveInfo, and HdmiDeviceInfo.
       *
       * @param service The ResolveInfo returned from the package manager about this TV input service.
       * @param hdmiDeviceInfo The HdmiDeviceInfo for a HDMI CEC logical device.
       * @param parentId The ID of this TV input's parent input. {@code null} if none exists.
       * @param labelRes The label resource ID of this TvInputInfo. If it is {@code 0},
       *            {@code service} label will be loaded.
       * @param icon The {@link android.graphics.drawable.Icon} to load the icon image. If it is
       *            {@code null}, the application icon of {@code service} will be loaded.
       * @hide
       * @deprecated Use {@link Builder} instead.
       */
      @Deprecated
      @SystemApi
      public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
              HdmiDeviceInfo hdmiDeviceInfo, String parentId, int labelRes, Icon icon)
              throws XmlPullParserException, IOException {
          return new TvInputInfo.Builder(context, service)
                  .setHdmiDeviceInfo(hdmiDeviceInfo)
                  .setParentId(parentId)
                  .setLabel(labelRes)
                  .setIcon(icon)
                  .build();
      }
  
      /**
       * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
       * ResolveInfo, and TvInputHardwareInfo.
       *
       * @param service The ResolveInfo returned from the package manager about this TV input service.
       * @param hardwareInfo The TvInputHardwareInfo for a TV input hardware device.
       * @param label The label of this TvInputInfo. If it is {@code null} or empty, {@code service}
       *            label will be loaded.
       * @param iconUri The {@link android.net.Uri} to load the icon image. See
       *            {@link android.content.ContentResolver#openInputStream}. If it is {@code null},
       *            the application icon of {@code service} will be loaded.
       * @hide
       * @deprecated Use {@link Builder} instead.
       */
      @Deprecated
      @SystemApi
      public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
              TvInputHardwareInfo hardwareInfo, String label, Uri iconUri)
                      throws XmlPullParserException, IOException {
          TvInputInfo info = new TvInputInfo.Builder(context, service)
                  .setTvInputHardwareInfo(hardwareInfo)
                  .setLabel(label)
                  .build();
          info.mIconUri = iconUri;
          return info;
      }
  
      /**
       * Create a new instance of the TvInputInfo class, instantiating it from the given Context,
       * ResolveInfo, and TvInputHardwareInfo.
       *
       * @param service The ResolveInfo returned from the package manager about this TV input service.
       * @param hardwareInfo The TvInputHardwareInfo for a TV input hardware device.
       * @param labelRes The label resource ID of this TvInputInfo. If it is {@code 0},
       *            {@code service} label will be loaded.
       * @param icon The {@link android.graphics.drawable.Icon} to load the icon image. If it is
       *            {@code null}, the application icon of {@code service} will be loaded.
       * @hide
       * @deprecated Use {@link Builder} instead.
       */
      @Deprecated
      @SystemApi
      public static TvInputInfo createTvInputInfo(Context context, ResolveInfo service,
              TvInputHardwareInfo hardwareInfo, int labelRes, Icon icon)
              throws XmlPullParserException, IOException {
          return new TvInputInfo.Builder(context, service)
                  .setTvInputHardwareInfo(hardwareInfo)
                  .setLabel(labelRes)
                  .setIcon(icon)
                  .build();
      }
  
      private TvInputInfo(ResolveInfo service, String id, int type, boolean isHardwareInput,
              CharSequence label, int labelResId, Icon icon, Icon iconStandby, Icon iconDisconnected,
              String setupActivity, boolean canRecord, int tunerCount, HdmiDeviceInfo hdmiDeviceInfo,
              boolean isConnectedToHdmiSwitch,
              @HdmiAddressRelativePosition int hdmiConnectionRelativePosition, String parentId,
              Bundle extras) {
          mService = service;
          mId = id;
          mType = type;
          mIsHardwareInput = isHardwareInput;
          mLabel = label;
          mLabelResId = labelResId;
          mIcon = icon;
          mIconStandby = iconStandby;
          mIconDisconnected = iconDisconnected;
          mSetupActivity = setupActivity;
          mCanRecord = canRecord;
          mTunerCount = tunerCount;
          mHdmiDeviceInfo = hdmiDeviceInfo;
          mIsConnectedToHdmiSwitch = isConnectedToHdmiSwitch;
          mHdmiConnectionRelativePosition = hdmiConnectionRelativePosition;
          mParentId = parentId;
          mExtras = extras;
      }
  
      /**
       * Returns a unique ID for this TV input. The ID is generated from the package and class name
       * implementing the TV input service.
       */
      public String getId() {
          return mId;
      }
  
      /**
       * Returns the parent input ID.
       *
       * <p>A TV input may have a parent input if the TV input is actually a logical representation of
       * a device behind the hardware port represented by the parent input.
       * For example, a HDMI CEC logical device, connected to a HDMI port, appears as another TV
       * input. In this case, the parent input of this logical device is the HDMI port.
       *
       * <p>Applications may group inputs by parent input ID to provide an easier access to inputs
       * sharing the same physical port. In the example of HDMI CEC, logical HDMI CEC devices behind
       * the same HDMI port have the same parent ID, which is the ID representing the port. Thus
       * applications can group the hardware HDMI port and the logical HDMI CEC devices behind it
       * together using this method.
       *
       * @return the ID of the parent input, if exists. Returns {@code null} if the parent input is
       *         not specified.
       */
      public String getParentId() {
          return mParentId;
      }
  
      /**
       * Returns the information of the service that implements this TV input.
       */
      public ServiceInfo getServiceInfo() {
          return mService.serviceInfo;
      }
  
      /**
       * Returns the component of the service that implements this TV input.
       * @hide
       */
      @UnsupportedAppUsage
      public ComponentName getComponent() {
          return new ComponentName(mService.serviceInfo.packageName, mService.serviceInfo.name);
      }
  
      /**
       * Returns an intent to start the setup activity for this TV input.
       */
      public Intent createSetupIntent() {
          if (!TextUtils.isEmpty(mSetupActivity)) {
              Intent intent = new Intent(Intent.ACTION_MAIN);
              intent.setClassName(mService.serviceInfo.packageName, mSetupActivity);
              intent.putExtra(EXTRA_INPUT_ID, getId());
              return intent;
          }
          return null;
      }
  
      /**
       * Returns an intent to start the settings activity for this TV input.
       *
       * @deprecated Use {@link #createSetupIntent()} instead. Settings activity is deprecated.
       *             Use setup activity instead to provide settings.
       */
      @Deprecated
      public Intent createSettingsIntent() {
          return null;
      }
  
      /**
       * Returns the type of this TV input.
       */
      @Type
      public int getType() {
          return mType;
      }
  
      /**
       * Returns the number of tuners this TV input has.
       *
       * <p>This method is valid only for inputs of type {@link #TYPE_TUNER}. For inputs of other
       * types, it returns 0.
       *
       * <p>Tuners correspond to physical/logical resources that allow reception of TV signal. Having
       * <i>N</i> tuners means that the TV input is capable of receiving <i>N</i> different channels
       * concurrently.
       */
      public int getTunerCount() {
          return mTunerCount;
      }
  
      /**
       * Returns {@code true} if this TV input can record TV programs, {@code false} otherwise.
       */
      public boolean canRecord() {
          return mCanRecord;
      }
  
      /**
       * Returns domain-specific extras associated with this TV input.
       */
      public Bundle getExtras() {
          return mExtras;
      }
  
      /**
       * Returns the HDMI device information of this TV input.
       * @hide
       */
      @SystemApi
      public HdmiDeviceInfo getHdmiDeviceInfo() {
          if (mType == TYPE_HDMI) {
              return mHdmiDeviceInfo;
          }
          return null;
      }
  
      /**
       * Returns {@code true} if this TV input is pass-though which does not have any real channels in
       * TvProvider. {@code false} otherwise.
       *
       * @see TvContract#buildChannelUriForPassthroughInput(String)
       */
      public boolean isPassthroughInput() {
          return mType != TYPE_TUNER;
      }
  
      /**
       * Returns {@code true} if this TV input represents a hardware device. (e.g. built-in tuner,
       * HDMI1) {@code false} otherwise.
       * @hide
       */
      @SystemApi
      public boolean isHardwareInput() {
          return mIsHardwareInput;
      }
  
      /**
       * Returns {@code true}, if a CEC device for this TV input is connected to an HDMI switch, i.e.,
       * the device isn't directly connected to a HDMI port.
       * TODO(b/110094868): add @Deprecated for Q
       * @hide
       */
      @SystemApi
      public boolean isConnectedToHdmiSwitch() {
          return mIsConnectedToHdmiSwitch;
      }
  
      /**
       * Returns the relative position of this HDMI input.
       * TODO(b/110094868): unhide for Q
       * @hide
       */
      @HdmiAddressRelativePosition
      public int getHdmiConnectionRelativePosition() {
          return mHdmiConnectionRelativePosition;
      }
  
      /**
       * Checks if this TV input is marked hidden by the user in the settings.
       *
       * @param context Supplies a {@link Context} used to check if this TV input is hidden.
       * @return {@code true} if the user marked this TV input hidden in settings. {@code false}
       *         otherwise.
       */
      public boolean isHidden(Context context) {
          return TvInputSettings.isHidden(context, mId, UserHandle.myUserId());
      }
  
      /**
       * Loads the user-displayed label for this TV input.
       *
       * @param context Supplies a {@link Context} used to load the label.
       * @return a CharSequence containing the TV input's label. If the TV input does not have
       *         a label, its name is returned.
       */
      public CharSequence loadLabel(@NonNull Context context) {
          if (mLabelResId != 0) {
              return context.getPackageManager().getText(mService.serviceInfo.packageName,
                      mLabelResId, null);
          } else if (!TextUtils.isEmpty(mLabel)) {
              return mLabel;
          }
          return mService.loadLabel(context.getPackageManager());
      }
  
      /**
       * Loads the custom label set by user in settings.
       *
       * @param context Supplies a {@link Context} used to load the custom label.
       * @return a CharSequence containing the TV input's custom label. {@code null} if there is no
       *         custom label.
       */
      public CharSequence loadCustomLabel(Context context) {
          return TvInputSettings.getCustomLabel(context, mId, UserHandle.myUserId());
      }
  
      /**
       * Loads the user-displayed icon for this TV input.
       *
       * @param context Supplies a {@link Context} used to load the icon.
       * @return a Drawable containing the TV input's icon. If the TV input does not have an icon,
       *         application's icon is returned. If it's unavailable too, {@code null} is returned.
       */
      public Drawable loadIcon(@NonNull Context context) {
          if (mIcon != null) {
              return mIcon.loadDrawable(context);
          } else if (mIconUri != null) {
              try (InputStream is = context.getContentResolver().openInputStream(mIconUri)) {
                  Drawable drawable = Drawable.createFromStream(is, null);
                  if (drawable != null) {
                      return drawable;
                  }
              } catch (IOException e) {
                  Log.w(TAG, "Loading the default icon due to a failure on loading " + mIconUri, e);
                  // Falls back.
              }
          }
          return loadServiceIcon(context);
      }
  
      /**
       * Loads the user-displayed icon for this TV input per input state.
       *
       * @param context Supplies a {@link Context} used to load the icon.
       * @param state The input state. Should be one of the followings.
       *              {@link TvInputManager#INPUT_STATE_CONNECTED},
       *              {@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY} and
       *              {@link TvInputManager#INPUT_STATE_DISCONNECTED}.
       * @return a Drawable containing the TV input's icon for the given state or {@code null} if such
       *         an icon is not defined.
       * @hide
       */
      @SystemApi
      public Drawable loadIcon(@NonNull Context context, int state) {
          if (state == TvInputManager.INPUT_STATE_CONNECTED) {
              return loadIcon(context);
          } else if (state == TvInputManager.INPUT_STATE_CONNECTED_STANDBY) {
              if (mIconStandby != null) {
                  return mIconStandby.loadDrawable(context);
              }
          } else if (state == TvInputManager.INPUT_STATE_DISCONNECTED) {
              if (mIconDisconnected != null) {
                  return mIconDisconnected.loadDrawable(context);
              }
          } else {
              throw new IllegalArgumentException("Unknown state: " + state);
          }
          return null;
      }
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public int hashCode() {
          return mId.hashCode();
      }
  
      @Override
      public boolean equals(Object o) {
          if (o == this) {
              return true;
          }
  
          if (!(o instanceof TvInputInfo)) {
              return false;
          }
  
          TvInputInfo obj = (TvInputInfo) o;
          return Objects.equals(mService, obj.mService)
                  && TextUtils.equals(mId, obj.mId)
                  && mType == obj.mType
                  && mIsHardwareInput == obj.mIsHardwareInput
                  && TextUtils.equals(mLabel, obj.mLabel)
                  && Objects.equals(mIconUri, obj.mIconUri)
                  && mLabelResId == obj.mLabelResId
                  && Objects.equals(mIcon, obj.mIcon)
                  && Objects.equals(mIconStandby, obj.mIconStandby)
                  && Objects.equals(mIconDisconnected, obj.mIconDisconnected)
                  && TextUtils.equals(mSetupActivity, obj.mSetupActivity)
                  && mCanRecord == obj.mCanRecord
                  && mTunerCount == obj.mTunerCount
                  && Objects.equals(mHdmiDeviceInfo, obj.mHdmiDeviceInfo)
                  && mIsConnectedToHdmiSwitch == obj.mIsConnectedToHdmiSwitch
                  && mHdmiConnectionRelativePosition == obj.mHdmiConnectionRelativePosition
                  && TextUtils.equals(mParentId, obj.mParentId)
                  && Objects.equals(mExtras, obj.mExtras);
      }
  
      @Override
      public String toString() {
          return "TvInputInfo{id=" + mId
                  + ", pkg=" + mService.serviceInfo.packageName
                  + ", service=" + mService.serviceInfo.name + "}";
      }
  
      /**
       * Used to package this object into a {@link Parcel}.
       *
       * @param dest The {@link Parcel} to be written.
       * @param flags The flags used for parceling.
       */
      @Override
      public void writeToParcel(@NonNull Parcel dest, int flags) {
          mService.writeToParcel(dest, flags);
          dest.writeString(mId);
          dest.writeInt(mType);
          dest.writeByte(mIsHardwareInput ? (byte) 1 : 0);
          TextUtils.writeToParcel(mLabel, dest, flags);
          dest.writeParcelable(mIconUri, flags);
          dest.writeInt(mLabelResId);
          dest.writeParcelable(mIcon, flags);
          dest.writeParcelable(mIconStandby, flags);
          dest.writeParcelable(mIconDisconnected, flags);
          dest.writeString(mSetupActivity);
          dest.writeByte(mCanRecord ? (byte) 1 : 0);
          dest.writeInt(mTunerCount);
          dest.writeParcelable(mHdmiDeviceInfo, flags);
          dest.writeByte(mIsConnectedToHdmiSwitch ? (byte) 1 : 0);
          dest.writeInt(mHdmiConnectionRelativePosition);
          dest.writeString(mParentId);
          dest.writeBundle(mExtras);
      }
  
      private Drawable loadServiceIcon(Context context) {
          if (mService.serviceInfo.icon == 0
                  && mService.serviceInfo.applicationInfo.icon == 0) {
              return null;
          }
          return mService.serviceInfo.loadIcon(context.getPackageManager());
      }
  
      public static final @android.annotation.NonNull Parcelable.Creator<TvInputInfo> CREATOR =
              new Parcelable.Creator<TvInputInfo>() {
          @Override
          public TvInputInfo createFromParcel(Parcel in) {
              return new TvInputInfo(in);
          }
  
          @Override
          public TvInputInfo[] newArray(int size) {
              return new TvInputInfo[size];
          }
      };
  
      private TvInputInfo(Parcel in) {
          mService = ResolveInfo.CREATOR.createFromParcel(in);
          mId = in.readString();
          mType = in.readInt();
          mIsHardwareInput = in.readByte() == 1;
          mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
          mIconUri = in.readParcelable(null);
          mLabelResId = in.readInt();
          mIcon = in.readParcelable(null);
          mIconStandby = in.readParcelable(null);
          mIconDisconnected = in.readParcelable(null);
          mSetupActivity = in.readString();
          mCanRecord = in.readByte() == 1;
          mTunerCount = in.readInt();
          mHdmiDeviceInfo = in.readParcelable(null);
          mIsConnectedToHdmiSwitch = in.readByte() == 1;
          mHdmiConnectionRelativePosition = in.readInt();
          mParentId = in.readString();
          mExtras = in.readBundle();
      }
  
      /**
       * A convenience builder for creating {@link TvInputInfo} objects.
       */
      public static final class Builder {
          private static final int LENGTH_HDMI_PHYSICAL_ADDRESS = 4;
          private static final int LENGTH_HDMI_DEVICE_ID = 2;
  
          private static final String XML_START_TAG_NAME = "tv-input";
          private static final String DELIMITER_INFO_IN_ID = "/";
          private static final String PREFIX_HDMI_DEVICE = "HDMI";
          private static final String PREFIX_HARDWARE_DEVICE = "HW";
  
          private static final SparseIntArray sHardwareTypeToTvInputType = new SparseIntArray();
          static {
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_OTHER_HARDWARE,
                      TYPE_OTHER);
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_TUNER, TYPE_TUNER);
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPOSITE,
                      TYPE_COMPOSITE);
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SVIDEO, TYPE_SVIDEO);
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_SCART, TYPE_SCART);
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_COMPONENT,
                      TYPE_COMPONENT);
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_VGA, TYPE_VGA);
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_DVI, TYPE_DVI);
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_HDMI, TYPE_HDMI);
              sHardwareTypeToTvInputType.put(TvInputHardwareInfo.TV_INPUT_TYPE_DISPLAY_PORT,
                      TYPE_DISPLAY_PORT);
          }
  
          private final Context mContext;
          private final ResolveInfo mResolveInfo;
          private CharSequence mLabel;
          private int mLabelResId;
          private Icon mIcon;
          private Icon mIconStandby;
          private Icon mIconDisconnected;
          private String mSetupActivity;
          private Boolean mCanRecord;
          private Integer mTunerCount;
          private TvInputHardwareInfo mTvInputHardwareInfo;
          private HdmiDeviceInfo mHdmiDeviceInfo;
          private String mParentId;
          private Bundle mExtras;
  
          /**
           * Constructs a new builder for {@link TvInputInfo}.
           *
           * @param context A Context of the application package implementing this class.
           * @param component The name of the application component to be used for the
           *            {@link TvInputService}.
           */
          public Builder(Context context, ComponentName component) {
              if (context == null) {
                  throw new IllegalArgumentException("context cannot be null.");
              }
              Intent intent = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component);
              mResolveInfo = context.getPackageManager().resolveService(intent,
                      PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
              if (mResolveInfo == null) {
                  throw new IllegalArgumentException("Invalid component. Can't find the service.");
              }
              mContext = context;
          }
  
          /**
           * Constructs a new builder for {@link TvInputInfo}.
           *
           * @param resolveInfo The ResolveInfo returned from the package manager about this TV input
           *            service.
           * @hide
           */
          public Builder(Context context, ResolveInfo resolveInfo) {
              if (context == null) {
                  throw new IllegalArgumentException("context cannot be null");
              }
              if (resolveInfo == null) {
                  throw new IllegalArgumentException("resolveInfo cannot be null");
              }
              mContext = context;
              mResolveInfo = resolveInfo;
          }
  
          /**
           * Sets the icon.
           *
           * @param icon The icon that represents this TV input.
           * @return This Builder object to allow for chaining of calls to builder methods.
           * @hide
           */
          @SystemApi
          public Builder setIcon(Icon icon) {
              this.mIcon = icon;
              return this;
          }
  
          /**
           * Sets the icon for a given input state.
           *
           * @param icon The icon that represents this TV input for the given state.
           * @param state The input state. Should be one of the followings.
           *              {@link TvInputManager#INPUT_STATE_CONNECTED},
           *              {@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY} and
           *              {@link TvInputManager#INPUT_STATE_DISCONNECTED}.
           * @return This Builder object to allow for chaining of calls to builder methods.
           * @hide
           */
          @SystemApi
          public Builder setIcon(Icon icon, int state) {
              if (state == TvInputManager.INPUT_STATE_CONNECTED) {
                  this.mIcon = icon;
              } else if (state == TvInputManager.INPUT_STATE_CONNECTED_STANDBY) {
                  this.mIconStandby = icon;
              } else if (state == TvInputManager.INPUT_STATE_DISCONNECTED) {
                  this.mIconDisconnected = icon;
              } else {
                  throw new IllegalArgumentException("Unknown state: " + state);
              }
              return this;
          }
  
          /**
           * Sets the label.
           *
           * @param label The text to be used as label.
           * @return This Builder object to allow for chaining of calls to builder methods.
           * @hide
           */
          @SystemApi
          public Builder setLabel(CharSequence label) {
              if (mLabelResId != 0) {
                  throw new IllegalStateException("Resource ID for label is already set.");
              }
              this.mLabel = label;
              return this;
          }
  
          /**
           * Sets the label.
           *
           * @param resId The resource ID of the text to use.
           * @return This Builder object to allow for chaining of calls to builder methods.
           * @hide
           */
          @SystemApi
          public Builder setLabel(@StringRes int resId) {
              if (mLabel != null) {
                  throw new IllegalStateException("Label text is already set.");
              }
              this.mLabelResId = resId;
              return this;
          }
  
          /**
           * Sets the HdmiDeviceInfo.
           *
           * @param hdmiDeviceInfo The HdmiDeviceInfo for a HDMI CEC logical device.
           * @return This Builder object to allow for chaining of calls to builder methods.
           * @hide
           */
          @SystemApi
          public Builder setHdmiDeviceInfo(HdmiDeviceInfo hdmiDeviceInfo) {
              if (mTvInputHardwareInfo != null) {
                  Log.w(TAG, "TvInputHardwareInfo will not be used to build this TvInputInfo");
                  mTvInputHardwareInfo = null;
              }
              this.mHdmiDeviceInfo = hdmiDeviceInfo;
              return this;
          }
  
          /**
           * Sets the parent ID.
           *
           * @param parentId The parent ID.
           * @return This Builder object to allow for chaining of calls to builder methods.
           * @hide
           */
          @SystemApi
          public Builder setParentId(String parentId) {
              this.mParentId = parentId;
              return this;
          }
  
          /**
           * Sets the TvInputHardwareInfo.
           *
           * @param tvInputHardwareInfo
           * @return This Builder object to allow for chaining of calls to builder methods.
           * @hide
           */
          @SystemApi
          public Builder setTvInputHardwareInfo(TvInputHardwareInfo tvInputHardwareInfo) {
              if (mHdmiDeviceInfo != null) {
                  Log.w(TAG, "mHdmiDeviceInfo will not be used to build this TvInputInfo");
                  mHdmiDeviceInfo = null;
              }
              this.mTvInputHardwareInfo = tvInputHardwareInfo;
              return this;
          }
  
          /**
           * Sets the tuner count. Valid only for {@link #TYPE_TUNER}.
           *
           * @param tunerCount The number of tuners this TV input has.
           * @return This Builder object to allow for chaining of calls to builder methods.
           */
          public Builder setTunerCount(int tunerCount) {
              this.mTunerCount = tunerCount;
              return this;
          }
  
          /**
           * Sets whether this TV input can record TV programs or not.
           *
           * @param canRecord Whether this TV input can record TV programs.
           * @return This Builder object to allow for chaining of calls to builder methods.
           */
          public Builder setCanRecord(boolean canRecord) {
              this.mCanRecord = canRecord;
              return this;
          }
  
          /**
           * Sets domain-specific extras associated with this TV input.
           *
           * @param extras Domain-specific extras associated with this TV input. Keys <em>must</em> be
           *            a scoped name, i.e. prefixed with a package name you own, so that different
           *            developers will not create conflicting keys.
           * @return This Builder object to allow for chaining of calls to builder methods.
           */
          public Builder setExtras(Bundle extras) {
              this.mExtras = extras;
              return this;
          }
  
          /**
           * Creates a {@link TvInputInfo} instance with the specified fields. Most of the information
           * is obtained by parsing the AndroidManifest and {@link TvInputService#SERVICE_META_DATA}
           * for the {@link TvInputService} this TV input implements.
           *
           * @return TvInputInfo containing information about this TV input.
           */
          public TvInputInfo build() {
              ComponentName componentName = new ComponentName(mResolveInfo.serviceInfo.packageName,
                      mResolveInfo.serviceInfo.name);
              String id;
              int type;
              boolean isHardwareInput = false;
              boolean isConnectedToHdmiSwitch = false;
              @HdmiAddressRelativePosition
              int hdmiConnectionRelativePosition = HdmiUtils.HDMI_RELATIVE_POSITION_UNKNOWN;
  
              if (mHdmiDeviceInfo != null) {
                  id = generateInputId(componentName, mHdmiDeviceInfo);
                  type = TYPE_HDMI;
                  isHardwareInput = true;
                  hdmiConnectionRelativePosition = getRelativePosition(mContext, mHdmiDeviceInfo);
                  isConnectedToHdmiSwitch =
                          hdmiConnectionRelativePosition
                                  != HdmiUtils.HDMI_RELATIVE_POSITION_DIRECTLY_BELOW;
              } else if (mTvInputHardwareInfo != null) {
                  id = generateInputId(componentName, mTvInputHardwareInfo);
                  type = sHardwareTypeToTvInputType.get(mTvInputHardwareInfo.getType(), TYPE_TUNER);
                  isHardwareInput = true;
              } else {
                  id = generateInputId(componentName);
                  type = TYPE_TUNER;
              }
              parseServiceMetadata(type);
              return new TvInputInfo(mResolveInfo, id, type, isHardwareInput, mLabel, mLabelResId,
                      mIcon, mIconStandby, mIconDisconnected, mSetupActivity,
                      mCanRecord == null ? false : mCanRecord, mTunerCount == null ? 0 : mTunerCount,
                      mHdmiDeviceInfo, isConnectedToHdmiSwitch, hdmiConnectionRelativePosition,
                      mParentId, mExtras);
          }
  
          private static String generateInputId(ComponentName name) {
              return name.flattenToShortString();
          }
  
          private static String generateInputId(ComponentName name, HdmiDeviceInfo hdmiDeviceInfo) {
              // Example of the format : "/HDMI%04X%02X"
              String format = DELIMITER_INFO_IN_ID + PREFIX_HDMI_DEVICE
                      + "%0" + LENGTH_HDMI_PHYSICAL_ADDRESS + "X"
                      + "%0" + LENGTH_HDMI_DEVICE_ID + "X";
              return name.flattenToShortString() + String.format(Locale.ENGLISH, format,
                      hdmiDeviceInfo.getPhysicalAddress(), hdmiDeviceInfo.getId());
          }
  
          private static String generateInputId(ComponentName name,
                  TvInputHardwareInfo tvInputHardwareInfo) {
              return name.flattenToShortString() + DELIMITER_INFO_IN_ID + PREFIX_HARDWARE_DEVICE
                      + tvInputHardwareInfo.getDeviceId();
          }
  
          private static int getRelativePosition(Context context, HdmiDeviceInfo info) {
              HdmiControlManager hcm =
                      (HdmiControlManager) context.getSystemService(Context.HDMI_CONTROL_SERVICE);
              if (hcm == null) {
                  return HdmiUtils.HDMI_RELATIVE_POSITION_UNKNOWN;
              }
              return HdmiUtils.getHdmiAddressRelativePosition(
                      info.getPhysicalAddress(), hcm.getPhysicalAddress());
          }
  
          private void parseServiceMetadata(int inputType) {
              ServiceInfo si = mResolveInfo.serviceInfo;
              PackageManager pm = mContext.getPackageManager();
              try (XmlResourceParser parser =
                           si.loadXmlMetaData(pm, TvInputService.SERVICE_META_DATA)) {
                  if (parser == null) {
                      throw new IllegalStateException("No " + TvInputService.SERVICE_META_DATA
                              + " meta-data found for " + si.name);
                  }
  
                  Resources res = pm.getResourcesForApplication(si.applicationInfo);
                  AttributeSet attrs = Xml.asAttributeSet(parser);
  
                  int type;
                  while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                          && type != XmlPullParser.START_TAG) {
                  }
  
                  String nodeName = parser.getName();
                  if (!XML_START_TAG_NAME.equals(nodeName)) {
                      throw new IllegalStateException("Meta-data does not start with "
                              + XML_START_TAG_NAME + " tag for " + si.name);
                  }
  
                  TypedArray sa = res.obtainAttributes(attrs,
                          com.android.internal.R.styleable.TvInputService);
                  mSetupActivity = sa.getString(
                          com.android.internal.R.styleable.TvInputService_setupActivity);
                  if (mCanRecord == null) {
                      mCanRecord = sa.getBoolean(
                              com.android.internal.R.styleable.TvInputService_canRecord, false);
                  }
                  if (mTunerCount == null && inputType == TYPE_TUNER) {
                      mTunerCount = sa.getInt(
                              com.android.internal.R.styleable.TvInputService_tunerCount, 1);
                  }
                  sa.recycle();
              } catch (IOException | XmlPullParserException e) {
                  throw new IllegalStateException("Failed reading meta-data for " + si.packageName, e);
              } catch (NameNotFoundException e) {
                  throw new IllegalStateException("No resources found for " + si.packageName, e);
              }
          }
      }
  
      /**
       * Utility class for putting and getting settings for TV input.
       *
       * @hide
       */
      @SystemApi
      public static final class TvInputSettings {
          private static final String TV_INPUT_SEPARATOR = ":";
          private static final String CUSTOM_NAME_SEPARATOR = ",";
  
          private TvInputSettings() { }
  
          private static boolean isHidden(Context context, String inputId, int userId) {
              return getHiddenTvInputIds(context, userId).contains(inputId);
          }
  
          private static String getCustomLabel(Context context, String inputId, int userId) {
              return getCustomLabels(context, userId).get(inputId);
          }
  
          /**
           * Returns a set of TV input IDs which are marked as hidden by user in the settings.
           *
           * @param context The application context
           * @param userId The user ID for the stored hidden input set
           * @hide
           */
          @SystemApi
          public static Set<String> getHiddenTvInputIds(Context context, int userId) {
              String hiddenIdsString = Settings.Secure.getStringForUser(
                      context.getContentResolver(), Settings.Secure.TV_INPUT_HIDDEN_INPUTS, userId);
              Set<String> set = new HashSet<>();
              if (TextUtils.isEmpty(hiddenIdsString)) {
                  return set;
              }
              String[] ids = hiddenIdsString.split(TV_INPUT_SEPARATOR);
              for (String id : ids) {
                  set.add(Uri.decode(id));
              }
              return set;
          }
  
          /**
           * Returns a map of TV input ID/custom label pairs set by the user in the settings.
           *
           * @param context The application context
           * @param userId The user ID for the stored hidden input map
           * @hide
           */
          @SystemApi
          public static Map<String, String> getCustomLabels(Context context, int userId) {
              String labelsString = Settings.Secure.getStringForUser(
                      context.getContentResolver(), Settings.Secure.TV_INPUT_CUSTOM_LABELS, userId);
              Map<String, String> map = new HashMap<>();
              if (TextUtils.isEmpty(labelsString)) {
                  return map;
              }
              String[] pairs = labelsString.split(TV_INPUT_SEPARATOR);
              for (String pairString : pairs) {
                  String[] pair = pairString.split(CUSTOM_NAME_SEPARATOR);
                  map.put(Uri.decode(pair[0]), Uri.decode(pair[1]));
              }
              return map;
          }
  
          /**
           * Stores a set of TV input IDs which are marked as hidden by user. This is expected to
           * be called from the settings app.
           *
           * @param context The application context
           * @param hiddenInputIds A set including all the hidden TV input IDs
           * @param userId The user ID for the stored hidden input set
           * @hide
           */
          @SystemApi
          public static void putHiddenTvInputs(Context context, Set<String> hiddenInputIds,
                  int userId) {
              StringBuilder builder = new StringBuilder();
              boolean firstItem = true;
              for (String inputId : hiddenInputIds) {
                  ensureValidField(inputId);
                  if (firstItem) {
                      firstItem = false;
                  } else {
                      builder.append(TV_INPUT_SEPARATOR);
                  }
                  builder.append(Uri.encode(inputId));
              }
              Settings.Secure.putStringForUser(context.getContentResolver(),
                      Settings.Secure.TV_INPUT_HIDDEN_INPUTS, builder.toString(), userId);
  
              // Notify of the TvInputInfo changes.
              TvInputManager tm = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
              for (String inputId : hiddenInputIds) {
                  TvInputInfo info = tm.getTvInputInfo(inputId);
                  if (info != null) {
                      tm.updateTvInputInfo(info);
                  }
              }
          }
  
          /**
           * Stores a map of TV input ID/custom label set by user. This is expected to be
           * called from the settings app.
           *
           * @param context The application context.
           * @param customLabels A map of TV input ID/custom label pairs
           * @param userId The user ID for the stored hidden input map
           * @hide
           */
          @SystemApi
          public static void putCustomLabels(Context context,
                  Map<String, String> customLabels, int userId) {
              StringBuilder builder = new StringBuilder();
              boolean firstItem = true;
              for (Map.Entry<String, String> entry: customLabels.entrySet()) {
                  ensureValidField(entry.getKey());
                  ensureValidField(entry.getValue());
                  if (firstItem) {
                      firstItem = false;
                  } else {
                      builder.append(TV_INPUT_SEPARATOR);
                  }
                  builder.append(Uri.encode(entry.getKey()));
                  builder.append(CUSTOM_NAME_SEPARATOR);
                  builder.append(Uri.encode(entry.getValue()));
              }
              Settings.Secure.putStringForUser(context.getContentResolver(),
                      Settings.Secure.TV_INPUT_CUSTOM_LABELS, builder.toString(), userId);
  
              // Notify of the TvInputInfo changes.
              TvInputManager tm = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
              for (String inputId : customLabels.keySet()) {
                  TvInputInfo info = tm.getTvInputInfo(inputId);
                  if (info != null) {
                      tm.updateTvInputInfo(info);
                  }
              }
          }
  
          private static void ensureValidField(String value) {
              if (TextUtils.isEmpty(value)) {
                  throw new IllegalArgumentException(value + " should not empty ");
              }
          }
      }
  }
  