content: /*
   * Copyright (C) 2007 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.content.res;
  
  import android.annotation.ColorInt;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.pm.ActivityInfo.Config;
  import android.content.res.Resources.Theme;
  import android.graphics.Color;
  
  import com.android.internal.R;
  import com.android.internal.util.ArrayUtils;
  import com.android.internal.util.GrowingArrayUtils;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import android.annotation.UnsupportedAppUsage;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.MathUtils;
  import android.util.SparseArray;
  import android.util.StateSet;
  import android.util.Xml;
  import android.os.Parcel;
  import android.os.Parcelable;
  
  import java.io.IOException;
  import java.lang.ref.WeakReference;
  import java.util.Arrays;
  
  /**
   *
   * Lets you map {@link android.view.View} state sets to colors.
   * <p>
   * {@link android.content.res.ColorStateList}s are created from XML resource files defined in the
   * "color" subdirectory directory of an application's resource directory. The XML file contains
   * a single "selector" element with a number of "item" elements inside. For example:
   * <pre>
   * &lt;selector xmlns:android="http://schemas.android.com/apk/res/android"&gt;
   *   &lt;item android:state_focused="true"
   *           android:color="@color/sample_focused" /&gt;
   *   &lt;item android:state_pressed="true"
   *           android:state_enabled="false"
   *           android:color="@color/sample_disabled_pressed" /&gt;
   *   &lt;item android:state_enabled="false"
   *           android:color="@color/sample_disabled_not_pressed" /&gt;
   *   &lt;item android:color="@color/sample_default" /&gt;
   * &lt;/selector&gt;
   * </pre>
   *
   * This defines a set of state spec / color pairs where each state spec specifies a set of
   * states that a view must either be in or not be in and the color specifies the color associated
   * with that spec.
   *
   * <a name="StateSpec"></a>
   * <h3>State specs</h3>
   * <p>
   * Each item defines a set of state spec and color pairs, where the state spec is a series of
   * attributes set to either {@code true} or {@code false} to represent inclusion or exclusion. If
   * an attribute is not specified for an item, it may be any value.
   * <p>
   * For example, the following item will be matched whenever the focused state is set; any other
   * states may be set or unset:
   * <pre>
   * &lt;item android:state_focused="true"
   *         android:color="@color/sample_focused" /&gt;
   * </pre>
   * <p>
   * Typically, a color state list will reference framework-defined state attributes such as
   * {@link android.R.attr#state_focused android:state_focused} or
   * {@link android.R.attr#state_enabled android:state_enabled}; however, app-defined attributes may
   * also be used.
   * <p>
   * <strong>Note:</strong> The list of state specs will be matched against in the order that they
   * appear in the XML file. For this reason, more-specific items should be placed earlier in the
   * file. An item with no state spec is considered to match any set of states and is generally
   * useful as a final item to be used as a default.
   * <p>
   * If an item with no state spec is placed before other items, those items
   * will be ignored.
   *
   * <a name="ItemAttributes"></a>
   * <h3>Item attributes</h3>
   * <p>
   * Each item must define an {@link android.R.attr#color android:color} attribute, which may be
   * an HTML-style hex color, a reference to a color resource, or -- in API 23 and above -- a theme
   * attribute that resolves to a color.
   * <p>
   * Starting with API 23, items may optionally define an {@link android.R.attr#alpha android:alpha}
   * attribute to modify the base color's opacity. This attribute takes a either floating-point value
   * between 0 and 1 or a theme attribute that resolves as such. The item's overall color is
   * calculated by multiplying by the base color's alpha channel by the {@code alpha} value. For
   * example, the following item represents the theme's accent color at 50% opacity:
   * <pre>
   * &lt;item android:state_enabled="false"
   *         android:color="?android:attr/colorAccent"
   *         android:alpha="0.5" /&gt;
   * </pre>
   *
   * <a name="DeveloperGuide"></a>
   * <h3>Developer guide</h3>
   * <p>
   * For more information, see the guide to
   * <a href="{@docRoot}guide/topics/resources/color-list-resource.html">Color State
   * List Resource</a>.
   *
   * @attr ref android.R.styleable#ColorStateListItem_alpha
   * @attr ref android.R.styleable#ColorStateListItem_color
   */
  public class ColorStateList extends ComplexColor implements Parcelable {
      private static final String TAG = "ColorStateList";
  
      private static final int DEFAULT_COLOR = Color.RED;
      private static final int[][] EMPTY = new int[][] { new int[0] };
  
      /** Thread-safe cache of single-color ColorStateLists. */
      private static final SparseArray<WeakReference<ColorStateList>> sCache = new SparseArray<>();
  
      /** Lazily-created factory for this color state list. */
      @UnsupportedAppUsage
      private ColorStateListFactory mFactory;
  
      private int[][] mThemeAttrs;
      private @Config int mChangingConfigurations;
  
      @UnsupportedAppUsage
      private int[][] mStateSpecs;
      @UnsupportedAppUsage
      private int[] mColors;
      @UnsupportedAppUsage
      private int mDefaultColor;
      private boolean mIsOpaque;
  
      @UnsupportedAppUsage
      private ColorStateList() {
          // Not publicly instantiable.
      }
  
      /**
       * Creates a ColorStateList that returns the specified mapping from
       * states to colors.
       */
      public ColorStateList(int[][] states, @ColorInt int[] colors) {
          mStateSpecs = states;
          mColors = colors;
  
          onColorsChanged();
      }
  
      /**
       * @return A ColorStateList containing a single color.
       */
      @NonNull
      public static ColorStateList valueOf(@ColorInt int color) {
          synchronized (sCache) {
              final int index = sCache.indexOfKey(color);
              if (index >= 0) {
                  final ColorStateList cached = sCache.valueAt(index).get();
                  if (cached != null) {
                      return cached;
                  }
  
                  // Prune missing entry.
                  sCache.removeAt(index);
              }
  
              // Prune the cache before adding new items.
              final int N = sCache.size();
              for (int i = N - 1; i >= 0; i--) {
                  if (sCache.valueAt(i).get() == null) {
                      sCache.removeAt(i);
                  }
              }
  
              final ColorStateList csl = new ColorStateList(EMPTY, new int[] { color });
              sCache.put(color, new WeakReference<>(csl));
              return csl;
          }
      }
  
      /**
       * Creates a ColorStateList with the same properties as another
       * ColorStateList.
       * <p>
       * The properties of the new ColorStateList can be modified without
       * affecting the source ColorStateList.
       *
       * @param orig the source color state list
       */
      private ColorStateList(ColorStateList orig) {
          if (orig != null) {
              mChangingConfigurations = orig.mChangingConfigurations;
              mStateSpecs = orig.mStateSpecs;
              mDefaultColor = orig.mDefaultColor;
              mIsOpaque = orig.mIsOpaque;
  
              // Deep copy, these may change due to applyTheme().
              mThemeAttrs = orig.mThemeAttrs.clone();
              mColors = orig.mColors.clone();
          }
      }
  
      /**
       * Creates a ColorStateList from an XML document.
       *
       * @param r Resources against which the ColorStateList should be inflated.
       * @param parser Parser for the XML document defining the ColorStateList.
       * @return A new color state list.
       *
       * @deprecated Use #createFromXml(Resources, XmlPullParser parser, Theme)
       */
      @NonNull
      @Deprecated
      public static ColorStateList createFromXml(Resources r, XmlPullParser parser)
              throws XmlPullParserException, IOException {
          return createFromXml(r, parser, null);
      }
  
      /**
       * Creates a ColorStateList from an XML document using given a set of
       * {@link Resources} and a {@link Theme}.
       *
       * @param r Resources against which the ColorStateList should be inflated.
       * @param parser Parser for the XML document defining the ColorStateList.
       * @param theme Optional theme to apply to the color state list, may be
       *              {@code null}.
       * @return A new color state list.
       */
      @NonNull
      public static ColorStateList createFromXml(@NonNull Resources r, @NonNull XmlPullParser parser,
              @Nullable Theme theme) throws XmlPullParserException, IOException {
          final AttributeSet attrs = Xml.asAttributeSet(parser);
  
          int type;
          while ((type = parser.next()) != XmlPullParser.START_TAG
                     && type != XmlPullParser.END_DOCUMENT) {
              // Seek parser to start tag.
          }
  
          if (type != XmlPullParser.START_TAG) {
              throw new XmlPullParserException("No start tag found");
          }
  
          return createFromXmlInner(r, parser, attrs, theme);
      }
  
      /**
       * Create from inside an XML document. Called on a parser positioned at a
       * tag in an XML document, tries to create a ColorStateList from that tag.
       *
       * @throws XmlPullParserException if the current tag is not &lt;selector>
       * @return A new color state list for the current tag.
       */
      @NonNull
      static ColorStateList createFromXmlInner(@NonNull Resources r,
              @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)
              throws XmlPullParserException, IOException {
          final String name = parser.getName();
          if (!name.equals("selector")) {
              throw new XmlPullParserException(
                      parser.getPositionDescription() + ": invalid color state list tag " + name);
          }
  
          final ColorStateList colorStateList = new ColorStateList();
          colorStateList.inflate(r, parser, attrs, theme);
          return colorStateList;
      }
  
      /**
       * Creates a new ColorStateList that has the same states and colors as this
       * one but where each color has the specified alpha value (0-255).
       *
       * @param alpha The new alpha channel value (0-255).
       * @return A new color state list.
       */
      @NonNull
      public ColorStateList withAlpha(int alpha) {
          final int[] colors = new int[mColors.length];
          final int len = colors.length;
          for (int i = 0; i < len; i++) {
              colors[i] = (mColors[i] & 0xFFFFFF) | (alpha << 24);
          }
  
          return new ColorStateList(mStateSpecs, colors);
      }
  
      /**
       * Fill in this object based on the contents of an XML "selector" element.
       */
      private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
              @NonNull AttributeSet attrs, @Nullable Theme theme)
              throws XmlPullParserException, IOException {
          final int innerDepth = parser.getDepth()+1;
          int depth;
          int type;
  
          @Config int changingConfigurations = 0;
          int defaultColor = DEFAULT_COLOR;
  
          boolean hasUnresolvedAttrs = false;
  
          int[][] stateSpecList = ArrayUtils.newUnpaddedArray(int[].class, 20);
          int[][] themeAttrsList = new int[stateSpecList.length][];
          int[] colorList = new int[stateSpecList.length];
          int listSize = 0;
  
          while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
              if (type != XmlPullParser.START_TAG || depth > innerDepth
                      || !parser.getName().equals("item")) {
                  continue;
              }
  
              final TypedArray a = Resources.obtainAttributes(r, theme, attrs,
                      R.styleable.ColorStateListItem);
              final int[] themeAttrs = a.extractThemeAttrs();
              final int baseColor = a.getColor(R.styleable.ColorStateListItem_color, Color.MAGENTA);
              final float alphaMod = a.getFloat(R.styleable.ColorStateListItem_alpha, 1.0f);
  
              changingConfigurations |= a.getChangingConfigurations();
  
              a.recycle();
  
              // Parse all unrecognized attributes as state specifiers.
              int j = 0;
              final int numAttrs = attrs.getAttributeCount();
              int[] stateSpec = new int[numAttrs];
              for (int i = 0; i < numAttrs; i++) {
                  final int stateResId = attrs.getAttributeNameResource(i);
                  switch (stateResId) {
                      case R.attr.color:
                      case R.attr.alpha:
                          // Recognized attribute, ignore.
                          break;
                      default:
                          stateSpec[j++] = attrs.getAttributeBooleanValue(i, false)
                                  ? stateResId : -stateResId;
                  }
              }
              stateSpec = StateSet.trimStateSet(stateSpec, j);
  
              // Apply alpha modulation. If we couldn't resolve the color or
              // alpha yet, the default values leave us enough information to
              // modulate again during applyTheme().
              final int color = modulateColorAlpha(baseColor, alphaMod);
              if (listSize == 0 || stateSpec.length == 0) {
                  defaultColor = color;
              }
  
              if (themeAttrs != null) {
                  hasUnresolvedAttrs = true;
              }
  
              colorList = GrowingArrayUtils.append(colorList, listSize, color);
              themeAttrsList = GrowingArrayUtils.append(themeAttrsList, listSize, themeAttrs);
              stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec);
              listSize++;
          }
  
          mChangingConfigurations = changingConfigurations;
          mDefaultColor = defaultColor;
  
          if (hasUnresolvedAttrs) {
              mThemeAttrs = new int[listSize][];
              System.arraycopy(themeAttrsList, 0, mThemeAttrs, 0, listSize);
          } else {
              mThemeAttrs = null;
          }
  
          mColors = new int[listSize];
          mStateSpecs = new int[listSize][];
          System.arraycopy(colorList, 0, mColors, 0, listSize);
          System.arraycopy(stateSpecList, 0, mStateSpecs, 0, listSize);
  
          onColorsChanged();
      }
  
      /**
       * Returns whether a theme can be applied to this color state list, which
       * usually indicates that the color state list has unresolved theme
       * attributes.
       *
       * @return whether a theme can be applied to this color state list
       * @hide only for resource preloading
       */
      @Override
      @UnsupportedAppUsage
      public boolean canApplyTheme() {
          return mThemeAttrs != null;
      }
  
      /**
       * Applies a theme to this color state list.
       * <p>
       * <strong>Note:</strong> Applying a theme may affect the changing
       * configuration parameters of this color state list. After calling this
       * method, any dependent configurations must be updated by obtaining the
       * new configuration mask from {@link #getChangingConfigurations()}.
       *
       * @param t the theme to apply
       */
      private void applyTheme(Theme t) {
          if (mThemeAttrs == null) {
              return;
          }
  
          boolean hasUnresolvedAttrs = false;
  
          final int[][] themeAttrsList = mThemeAttrs;
          final int N = themeAttrsList.length;
          for (int i = 0; i < N; i++) {
              if (themeAttrsList[i] != null) {
                  final TypedArray a = t.resolveAttributes(themeAttrsList[i],
                          R.styleable.ColorStateListItem);
  
                  final float defaultAlphaMod;
                  if (themeAttrsList[i][R.styleable.ColorStateListItem_color] != 0) {
                      // If the base color hasn't been resolved yet, the current
                      // color's alpha channel is either full-opacity (if we
                      // haven't resolved the alpha modulation yet) or
                      // pre-modulated. Either is okay as a default value.
                      defaultAlphaMod = Color.alpha(mColors[i]) / 255.0f;
                  } else {
                      // Otherwise, the only correct default value is 1. Even if
                      // nothing is resolved during this call, we can apply this
                      // multiple times without losing of information.
                      defaultAlphaMod = 1.0f;
                  }
  
                  // Extract the theme attributes, if any, before attempting to
                  // read from the typed array. This prevents a crash if we have
                  // unresolved attrs.
                  themeAttrsList[i] = a.extractThemeAttrs(themeAttrsList[i]);
                  if (themeAttrsList[i] != null) {
                      hasUnresolvedAttrs = true;
                  }
  
                  final int baseColor = a.getColor(
                          R.styleable.ColorStateListItem_color, mColors[i]);
                  final float alphaMod = a.getFloat(
                          R.styleable.ColorStateListItem_alpha, defaultAlphaMod);
                  mColors[i] = modulateColorAlpha(baseColor, alphaMod);
  
                  // Account for any configuration changes.
                  mChangingConfigurations |= a.getChangingConfigurations();
  
                  a.recycle();
              }
          }
  
          if (!hasUnresolvedAttrs) {
              mThemeAttrs = null;
          }
  
          onColorsChanged();
      }
  
      /**
       * Returns an appropriately themed color state list.
       *
       * @param t the theme to apply
       * @return a copy of the color state list with the theme applied, or the
       *         color state list itself if there were no unresolved theme
       *         attributes
       * @hide only for resource preloading
       */
      @Override
      @UnsupportedAppUsage
      public ColorStateList obtainForTheme(Theme t) {
          if (t == null || !canApplyTheme()) {
              return this;
          }
  
          final ColorStateList clone = new ColorStateList(this);
          clone.applyTheme(t);
          return clone;
      }
  
      /**
       * Returns a mask of the configuration parameters for which this color
       * state list may change, requiring that it be re-created.
       *
       * @return a mask of the changing configuration parameters, as defined by
       *         {@link android.content.pm.ActivityInfo}
       *
       * @see android.content.pm.ActivityInfo
       */
      public @Config int getChangingConfigurations() {
          return super.getChangingConfigurations() | mChangingConfigurations;
      }
  
      private int modulateColorAlpha(int baseColor, float alphaMod) {
          if (alphaMod == 1.0f) {
              return baseColor;
          }
  
          final int baseAlpha = Color.alpha(baseColor);
          final int alpha = MathUtils.constrain((int) (baseAlpha * alphaMod + 0.5f), 0, 255);
          return (baseColor & 0xFFFFFF) | (alpha << 24);
      }
  
      /**
       * Indicates whether this color state list contains at least one state spec
       * and the first spec is not empty (e.g. match-all).
       *
       * @return True if this color state list changes color based on state, false
       *         otherwise.
       * @see #getColorForState(int[], int)
       */
      @Override
      public boolean isStateful() {
          return mStateSpecs.length >= 1 && mStateSpecs[0].length > 0;
      }
  
      /**
       * Return whether the state spec list has at least one item explicitly specifying
       * {@link android.R.attr#state_focused}.
       * @hide
       */
      public boolean hasFocusStateSpecified() {
          return StateSet.containsAttribute(mStateSpecs, R.attr.state_focused);
      }
  
      /**
       * Indicates whether this color state list is opaque, which means that every
       * color returned from {@link #getColorForState(int[], int)} has an alpha
       * value of 255.
       *
       * @return True if this color state list is opaque.
       */
      public boolean isOpaque() {
          return mIsOpaque;
      }
  
      /**
       * Return the color associated with the given set of
       * {@link android.view.View} states.
       *
       * @param stateSet an array of {@link android.view.View} states
       * @param defaultColor the color to return if there's no matching state
       *                     spec in this {@link ColorStateList} that matches the
       *                     stateSet.
       *
       * @return the color associated with that set of states in this {@link ColorStateList}.
       */
      public int getColorForState(@Nullable int[] stateSet, int defaultColor) {
          final int setLength = mStateSpecs.length;
          for (int i = 0; i < setLength; i++) {
              final int[] stateSpec = mStateSpecs[i];
              if (StateSet.stateSetMatches(stateSpec, stateSet)) {
                  return mColors[i];
              }
          }
          return defaultColor;
      }
  
      /**
       * Return the default color in this {@link ColorStateList}.
       *
       * @return the default color in this {@link ColorStateList}.
       */
      @ColorInt
      public int getDefaultColor() {
          return mDefaultColor;
      }
  
      /**
       * Return the states in this {@link ColorStateList}. The returned array
       * should not be modified.
       *
       * @return the states in this {@link ColorStateList}
       * @hide
       */
      @UnsupportedAppUsage
      public int[][] getStates() {
          return mStateSpecs;
      }
  
      /**
       * Return the colors in this {@link ColorStateList}. The returned array
       * should not be modified.
       *
       * @return the colors in this {@link ColorStateList}
       * @hide
       */
      @UnsupportedAppUsage
      public int[] getColors() {
          return mColors;
      }
  
      /**
       * Returns whether the specified state is referenced in any of the state
       * specs contained within this ColorStateList.
       * <p>
       * Any reference, either positive or negative {ex. ~R.attr.state_enabled},
       * will cause this method to return {@code true}. Wildcards are not counted
       * as references.
       *
       * @param state the state to search for
       * @return {@code true} if the state if referenced, {@code false} otherwise
       * @hide Use only as directed. For internal use only.
       */
      public boolean hasState(int state) {
          final int[][] stateSpecs = mStateSpecs;
          final int specCount = stateSpecs.length;
          for (int specIndex = 0; specIndex < specCount; specIndex++) {
              final int[] states = stateSpecs[specIndex];
              final int stateCount = states.length;
              for (int stateIndex = 0; stateIndex < stateCount; stateIndex++) {
                  if (states[stateIndex] == state || states[stateIndex] == ~state) {
                      return true;
                  }
              }
          }
          return false;
      }
  
      @Override
      public String toString() {
          return "ColorStateList{" +
                 "mThemeAttrs=" + Arrays.deepToString(mThemeAttrs) +
                 "mChangingConfigurations=" + mChangingConfigurations +
                 "mStateSpecs=" + Arrays.deepToString(mStateSpecs) +
                 "mColors=" + Arrays.toString(mColors) +
                 "mDefaultColor=" + mDefaultColor + '}';
      }
  
      /**
       * Updates the default color and opacity.
       */
      @UnsupportedAppUsage
      private void onColorsChanged() {
          int defaultColor = DEFAULT_COLOR;
          boolean isOpaque = true;
  
          final int[][] states = mStateSpecs;
          final int[] colors = mColors;
          final int N = states.length;
          if (N > 0) {
              defaultColor = colors[0];
  
              for (int i = N - 1; i > 0; i--) {
                  if (states[i].length == 0) {
                      defaultColor = colors[i];
                      break;
                  }
              }
  
              for (int i = 0; i < N; i++) {
                  if (Color.alpha(colors[i]) != 0xFF) {
                      isOpaque = false;
                      break;
                  }
              }
          }
  
          mDefaultColor = defaultColor;
          mIsOpaque = isOpaque;
      }
  
      /**
       * @return a factory that can create new instances of this ColorStateList
       * @hide only for resource preloading
       */
      public ConstantState<ComplexColor> getConstantState() {
          if (mFactory == null) {
              mFactory = new ColorStateListFactory(this);
          }
          return mFactory;
      }
  
      private static class ColorStateListFactory extends ConstantState<ComplexColor> {
          private final ColorStateList mSrc;
  
          @UnsupportedAppUsage
          public ColorStateListFactory(ColorStateList src) {
              mSrc = src;
          }
  
          @Override
          public @Config int getChangingConfigurations() {
              return mSrc.mChangingConfigurations;
          }
  
          @Override
          public ColorStateList newInstance() {
              return mSrc;
          }
  
          @Override
          public ColorStateList newInstance(Resources res, Theme theme) {
              return (ColorStateList) mSrc.obtainForTheme(theme);
          }
      }
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel dest, int flags) {
          if (canApplyTheme()) {
              Log.w(TAG, "Wrote partially-resolved ColorStateList to parcel!");
          }
          final int N = mStateSpecs.length;
          dest.writeInt(N);
          for (int i = 0; i < N; i++) {
              dest.writeIntArray(mStateSpecs[i]);
          }
          dest.writeIntArray(mColors);
      }
  
      public static final @android.annotation.NonNull Parcelable.Creator<ColorStateList> CREATOR =
              new Parcelable.Creator<ColorStateList>() {
          @Override
          public ColorStateList[] newArray(int size) {
              return new ColorStateList[size];
          }
  
          @Override
          public ColorStateList createFromParcel(Parcel source) {
              final int N = source.readInt();
              final int[][] stateSpecs = new int[N][];
              for (int i = 0; i < N; i++) {
                  stateSpecs[i] = source.createIntArray();
              }
              final int[] colors = source.createIntArray();
              return new ColorStateList(stateSpecs, colors);
          }
      };
  }
  