From 279c95becb3f6c8d018d545fc015a41e9b444df5 Mon Sep 17 00:00:00 2001 From: Mis012 Date: Thu, 2 Jan 2025 22:25:05 +0100 Subject: [PATCH] borrow relevant parts of AOSP commit dbee9bb342cdfaa5155b1918f90262c05e2464cb Our SVG-based VectorDrawable implementation still relies on `getDefaultColor` but exceptions are no longer thrown when parsing VectorDrawable xml files which use gradients as colors. --- .../android_graphics_SweepGradient.h | 33 + .../android/content/res/AssetManager.java | 2 - .../android/content/res/ColorStateList.java | 576 ++++++++++--- .../android/content/res/ComplexColor.java | 55 ++ .../res/ConfigurationBoundResourceCache.java | 58 ++ .../android/content/res/ConstantState.java | 61 ++ .../android/content/res/GradientColor.java | 512 ++++++++++++ .../android/content/res/Resources.java | 570 +++++++++---- .../content/res/ThemedResourceCache.java | 230 ++++++ .../android/content/res/TypedArray.java | 765 ++++++++++++++---- src/api-impl/android/graphics/Shader.java | 6 + .../android/graphics/SweepGradient.java | 110 +++ src/api-impl/android/util/MathUtils.java | 299 +++++++ src/api-impl/android/util/Pools.java | 165 ++++ src/api-impl/android/util/TypedValue.java | 11 + .../internal/util/GrowingArrayUtils.java | 35 +- src/api-impl/meson.build | 6 + 17 files changed, 3074 insertions(+), 420 deletions(-) create mode 100644 src/api-impl-jni/generated_headers/android_graphics_SweepGradient.h create mode 100644 src/api-impl/android/content/res/ComplexColor.java create mode 100644 src/api-impl/android/content/res/ConfigurationBoundResourceCache.java create mode 100644 src/api-impl/android/content/res/ConstantState.java create mode 100644 src/api-impl/android/content/res/GradientColor.java create mode 100644 src/api-impl/android/content/res/ThemedResourceCache.java create mode 100644 src/api-impl/android/graphics/SweepGradient.java create mode 100644 src/api-impl/android/util/MathUtils.java create mode 100644 src/api-impl/android/util/Pools.java diff --git a/src/api-impl-jni/generated_headers/android_graphics_SweepGradient.h b/src/api-impl-jni/generated_headers/android_graphics_SweepGradient.h new file mode 100644 index 00000000..c4667f61 --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_graphics_SweepGradient.h @@ -0,0 +1,33 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class android_graphics_SweepGradient */ + +#ifndef _Included_android_graphics_SweepGradient +#define _Included_android_graphics_SweepGradient +#ifdef __cplusplus +extern "C" { +#endif +#undef android_graphics_SweepGradient_TYPE_COLORS_AND_POSITIONS +#define android_graphics_SweepGradient_TYPE_COLORS_AND_POSITIONS 1L +#undef android_graphics_SweepGradient_TYPE_COLOR_START_AND_COLOR_END +#define android_graphics_SweepGradient_TYPE_COLOR_START_AND_COLOR_END 2L +/* + * Class: android_graphics_SweepGradient + * Method: nativeCreate1 + * Signature: (FF[I[F)J + */ +JNIEXPORT jlong JNICALL Java_android_graphics_SweepGradient_nativeCreate1 + (JNIEnv *, jclass, jfloat, jfloat, jintArray, jfloatArray); + +/* + * Class: android_graphics_SweepGradient + * Method: nativeCreate2 + * Signature: (FFII)J + */ +JNIEXPORT jlong JNICALL Java_android_graphics_SweepGradient_nativeCreate2 + (JNIEnv *, jclass, jfloat, jfloat, jint, jint); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/api-impl/android/content/res/AssetManager.java b/src/api-impl/android/content/res/AssetManager.java index 59e6f871..bde3f320 100644 --- a/src/api-impl/android/content/res/AssetManager.java +++ b/src/api-impl/android/content/res/AssetManager.java @@ -817,8 +817,6 @@ public final class AssetManager { int defStyleRes, int[] inValues, int[] inAttrs, int[] outValues, int[] outIndices); - /*package*/ native final boolean retrieveAttributes(int xmlParser, int[] inAttrs, - int[] outValues, int[] outIndices); /*package*/ native final int getArraySize(int resource); /*package*/ native final int retrieveArray(int resource, int[] outValues); private native final int getStringBlockCount(); diff --git a/src/api-impl/android/content/res/ColorStateList.java b/src/api-impl/android/content/res/ColorStateList.java index 01d6cef6..9820f91c 100644 --- a/src/api-impl/android/content/res/ColorStateList.java +++ b/src/api-impl/android/content/res/ColorStateList.java @@ -16,12 +16,22 @@ package android.content.res; -import android.content.Context; +//import android.annotation.ColorInt; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.Resources.Theme; +import android.graphics.Color; +import android.os.Parcel; +import android.os.Parcelable; 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 com.android.internal.R; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Arrays; @@ -55,106 +65,174 @@ import org.xmlpull.v1.XmlPullParserException; * href="{@docRoot}guide/topics/resources/color-list-resource.html">Color State * List Resource.

*/ -public class ColorStateList { - - private int[][] mStateSpecs; // must be parallel to mColors - private int[] mColors; // must be parallel to mStateSpecs - private int mDefaultColor = 0xffff0000; +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]}; - private static final SparseArray> sCache = - new SparseArray>(); - private ColorStateList() {} + /** + * Thread-safe cache of single-color ColorStateLists. + */ + private static final SparseArray> sCache = new SparseArray<>(); + + /** + * Lazily-created factory for this color state list. + */ + private ColorStateListFactory mFactory; + + private int[][] mThemeAttrs; + private int mChangingConfigurations; + + private int[][] mStateSpecs; + private int[] mColors; + private int mDefaultColor; + private boolean mIsOpaque; + + private ColorStateList() { + // Not publicly instantiable. + } /** * Creates a ColorStateList that returns the specified mapping from * states to colors. */ - public ColorStateList(int[][] states, int[] colors) { + public ColorStateList(int[][] states, /*@ColorInt*/ int[] colors) { mStateSpecs = states; mColors = colors; - if (states.length > 0) { - mDefaultColor = colors[0]; - - for (int i = 0; i < states.length; i++) { - if (states[i].length == 0) { - mDefaultColor = colors[i]; - } - } - } + onColorsChanged(); } /** - * Creates or retrieves a ColorStateList that always returns a single color. + * @return A ColorStateList containing a single color. */ - public static ColorStateList valueOf(int color) { - // TODO: should we collect these eventually? + @NonNull + public static ColorStateList valueOf(/*@ColorInt*/ int color) { synchronized (sCache) { - WeakReference ref = sCache.get(color); - ColorStateList csl = ref != null ? ref.get() : null; + final int index = sCache.indexOfKey(color); + if (index >= 0) { + final ColorStateList cached = sCache.valueAt(index).get(); + if (cached != null) { + return cached; + } - if (csl != null) { - return csl; + // Prune missing entry. + sCache.removeAt(index); } - csl = new ColorStateList(EMPTY, new int[] {color}); - sCache.put(color, new WeakReference(csl)); + // 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; } } /** - * Create a ColorStateList from an XML document, given a set of {@link Resources}. + * Creates a ColorStateList with the same properties as another + * ColorStateList. + *

+ * 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); + } - AttributeSet attrs = Xml.asAttributeSet(parser); + /** + * 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); + 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. - * Returns null if the tag is not a valid ColorStateList. + /** + * 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 <selector> + * @return A new color state list for the current tag. */ - private static ColorStateList createFromXmlInner(Resources r, XmlPullParser parser, - AttributeSet attrs) throws XmlPullParserException, IOException { - - ColorStateList colorStateList; - + @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")) { - colorStateList = new ColorStateList(); - } else { + if (!name.equals("selector")) { throw new XmlPullParserException( - parser.getPositionDescription() + ": invalid drawable tag " + name); + parser.getPositionDescription() + ": invalid color state list tag " + name); } - colorStateList.inflate(r, parser, attrs); + 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). + * 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) { - int[] colors = new int[mColors.length]; - - int len = colors.length; + 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); } @@ -165,95 +243,254 @@ public class ColorStateList { /** * Fill in this object based on the contents of an XML "selector" element. */ - private void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) + private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, + @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { - - int type; - final int innerDepth = parser.getDepth() + 1; int depth; + int type; - int listAllocated = 20; + 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; - int[] colorList = new int[listAllocated]; - int[][] stateSpecList = new int[listAllocated][]; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { - if (type != XmlPullParser.START_TAG) { + if (type != XmlPullParser.START_TAG || depth > innerDepth || !parser.getName().equals("item")) { continue; } - if (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); - int color = 0xffff0000; + changingConfigurations |= a.getChangingConfigurations(); - int i; + a.recycle(); + + // Parse all unrecognized attributes as state specifiers. int j = 0; final int numAttrs = attrs.getAttributeCount(); int[] stateSpec = new int[numAttrs]; - TypedArray a = Context.this_application.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ColorStateListItem); - color = a.getColor(com.android.internal.R.styleable.ColorStateListItem_color, color); - a.recycle(); - for (i = 0; i < numAttrs; i++) { + for (int i = 0; i < numAttrs; i++) { final int stateResId = attrs.getAttributeNameResource(i); - if (stateResId == 0) - break; - if (stateResId == com.android.internal.R.attr.color) { - } else { - stateSpec[j++] = attrs.getAttributeBooleanValue(i, false) - ? stateResId - : -stateResId; + 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) { - mDefaultColor = color; + defaultColor = color; } - if (listSize + 1 >= listAllocated) { - listAllocated = ArrayUtils.idealIntArraySize(listSize + 1); - - int[] ncolor = new int[listAllocated]; - System.arraycopy(colorList, 0, ncolor, 0, listSize); - - int[][] nstate = new int[listAllocated][]; - System.arraycopy(stateSpecList, 0, nstate, 0, listSize); - - colorList = ncolor; - stateSpecList = nstate; + if (themeAttrs != null) { + hasUnresolvedAttrs = true; } - colorList[listSize] = color; - stateSpecList[listSize] = stateSpec; + 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 + public boolean canApplyTheme() { + return mThemeAttrs != null; + } + + /** + * Applies a theme to this color state list. + *

+ * Note: 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 + 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 int getChangingConfigurations() { + return 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 more than one state spec + * and will change color based on state. + * + * @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; } /** - * Return the color associated with the given set of {@link android.view.View} states. + * 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 not state spec in this - * {@link ColorStateList} that matches the stateSet. + * @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(int[] stateSet, int defaultColor) { + public int getColorForState(@Nullable int[] stateSet, int defaultColor) { final int setLength = mStateSpecs.length; for (int i = 0; i < setLength; i++) { - int[] stateSpec = mStateSpecs[i]; + final int[] stateSpec = mStateSpecs[i]; if (StateSet.stateSetMatches(stateSpec, stateSet)) { return mColors[i]; } @@ -266,19 +503,170 @@ public class 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 + */ + 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 + */ + public int[] getColors() { + return mColors; + } + + /** + * Returns whether the specified state is referenced in any of the state + * specs contained within this ColorStateList. + *

+ * 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. + */ + 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 getConstantState() { + if (mFactory == null) { + mFactory = new ColorStateListFactory(this); + } + return mFactory; + } + + private static class ColorStateListFactory extends ConstantState { + private final ColorStateList mSrc; + + public ColorStateListFactory(ColorStateList src) { + mSrc = src; + } + + @Override + public 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 Parcelable.Creator CREATOR = new Parcelable.Creator() { + @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); + } + };*/ } diff --git a/src/api-impl/android/content/res/ComplexColor.java b/src/api-impl/android/content/res/ComplexColor.java new file mode 100644 index 00000000..ad547e67 --- /dev/null +++ b/src/api-impl/android/content/res/ComplexColor.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 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.content.res.Resources.Theme; +import android.graphics.Color; + +/** + * Defines an abstract class for the complex color information, like + * {@link android.content.res.ColorStateList} or {@link android.content.res.GradientColor} + */ +public abstract class ComplexColor { + /** + * @return {@code true} if this ComplexColor changes color based on state, {@code false} + * otherwise. + */ + public boolean isStateful() { return false; } + + /** + * @return the default color. + */ + //@ColorInt + public abstract int getDefaultColor(); + + /** + * @hide only for resource preloading + * + */ + public abstract ConstantState getConstantState(); + + /** + * @hide only for resource preloading + */ + public abstract boolean canApplyTheme(); + + /** + * @hide only for resource preloading + */ + public abstract ComplexColor obtainForTheme(Theme t); +} diff --git a/src/api-impl/android/content/res/ConfigurationBoundResourceCache.java b/src/api-impl/android/content/res/ConfigurationBoundResourceCache.java new file mode 100644 index 00000000..5ad012aa --- /dev/null +++ b/src/api-impl/android/content/res/ConfigurationBoundResourceCache.java @@ -0,0 +1,58 @@ +/* + * 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.content.res; + +/** + * A Cache class which can be used to cache resource objects that are easy to clone but more + * expensive to inflate. + * + * @hide For internal use only. + */ +public class ConfigurationBoundResourceCache extends ThemedResourceCache> { + private final Resources mResources; + + /** + * Creates a cache for the given Resources instance. + * + * @param resources the resources to use when creating new instances + */ + public ConfigurationBoundResourceCache(Resources resources) { + mResources = resources; + } + + /** + * If the resource is cached, creates and returns a new instance of it. + * + * @param key a key that uniquely identifies the drawable resource + * @param theme the theme where the resource will be used + * @return a new instance of the resource, or {@code null} if not in + * the cache + */ + public T getInstance(long key, Resources.Theme theme) { + final ConstantState entry = get(key, theme); + if (entry != null) { + return entry.newInstance(mResources, theme); + } + + return null; + } + + @Override + public boolean shouldInvalidateEntry(ConstantState entry, int configChanges) { + return Configuration.needNewResources(configChanges, entry.getChangingConfigurations()); + } +} diff --git a/src/api-impl/android/content/res/ConstantState.java b/src/api-impl/android/content/res/ConstantState.java new file mode 100644 index 00000000..73bfc819 --- /dev/null +++ b/src/api-impl/android/content/res/ConstantState.java @@ -0,0 +1,61 @@ +/* + * 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.content.res; + +/** + * A cache class that can provide new instances of a particular resource which may change + * depending on the current {@link Resources.Theme} or {@link Configuration}. + *

+ * A constant state should be able to return a bitmask of changing configurations, which + * identifies the type of configuration changes that may invalidate this resource. These + * configuration changes can be obtained from {@link android.util.TypedValue}. Entities such as + * {@link android.animation.Animator} also provide a changing configuration method to include + * their dependencies (e.g. An AnimatorSet's changing configuration is the union of the + * changing configurations of each Animator in the set) + * @hide + */ +abstract public class ConstantState { + + /** + * Return a bit mask of configuration changes that will impact + * this resource (and thus require completely reloading it). + */ + abstract public int getChangingConfigurations(); + + /** + * Create a new instance without supplying resources the caller + * is running in. + */ + public abstract T newInstance(); + + /** + * Create a new instance from its constant state. This + * must be implemented for resources that change based on the target + * density of their caller (that is depending on whether it is + * in compatibility mode). + */ + public T newInstance(Resources res) { + return newInstance(); + } + + /** + * Create a new instance from its constant state. This must be + * implemented for resources that can have a theme applied. + */ + public T newInstance(Resources res, Resources.Theme theme) { + return newInstance(res); + } +} diff --git a/src/api-impl/android/content/res/GradientColor.java b/src/api-impl/android/content/res/GradientColor.java new file mode 100644 index 00000000..90808526 --- /dev/null +++ b/src/api-impl/android/content/res/GradientColor.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2016 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.res.Resources.Theme; +import android.graphics.LinearGradient; +import android.graphics.RadialGradient; +import android.graphics.Shader; +import android.graphics.SweepGradient; +import android.graphics.drawable.GradientDrawable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import com.android.internal.R; +import com.android.internal.util.GrowingArrayUtils; +import java.io.IOException; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public class GradientColor extends ComplexColor { + private static final String TAG = "GradientColor"; + + private static final boolean DBG_GRADIENT = false; + + /** + * Lazily-created factory for this GradientColor. + */ + private GradientColorFactory mFactory; + + private int mChangingConfigurations; + private int mDefaultColor; + + // After parsing all the attributes from XML, this shader is the ultimate result containing + // all the XML information. + private Shader mShader = null; + + // Below are the attributes at the root element + private int mGradientType = GradientDrawable.LINEAR_GRADIENT; + + private float mCenterX = 0f; + private float mCenterY = 0f; + + private float mStartX = 0f; + private float mStartY = 0f; + private float mEndX = 0f; + private float mEndY = 0f; + + private int mStartColor = 0; + private int mCenterColor = 0; + private int mEndColor = 0; + private boolean mHasCenterColor = false; + + private float mGradientRadius = 0f; + + // Below are the attributes for the element. + private int[] mItemColors; + private float[] mItemOffsets; + + // Theme attributes for the root and item elements. + private int[] mThemeAttrs; + private int[][] mItemsThemeAttrs; + + private GradientColor() { + } + + private GradientColor(GradientColor copy) { + if (copy != null) { + mChangingConfigurations = copy.mChangingConfigurations; + mDefaultColor = copy.mDefaultColor; + mShader = copy.mShader; + mGradientType = copy.mGradientType; + mCenterX = copy.mCenterX; + mCenterY = copy.mCenterY; + mStartX = copy.mStartX; + mStartY = copy.mStartY; + mEndX = copy.mEndX; + mEndY = copy.mEndY; + mStartColor = copy.mStartColor; + mCenterColor = copy.mCenterColor; + mEndColor = copy.mEndColor; + mHasCenterColor = copy.mHasCenterColor; + mGradientRadius = copy.mGradientRadius; + + if (copy.mItemColors != null) { + mItemColors = copy.mItemColors.clone(); + } + if (copy.mItemOffsets != null) { + mItemOffsets = copy.mItemOffsets.clone(); + } + + if (copy.mThemeAttrs != null) { + mThemeAttrs = copy.mThemeAttrs.clone(); + } + if (copy.mItemsThemeAttrs != null) { + mItemsThemeAttrs = copy.mItemsThemeAttrs.clone(); + } + } + } + + /** + * Update the root level's attributes, either for inflate or applyTheme. + */ + private void updateRootElementState(TypedArray a) { + // Extract the theme attributes, if any. + mThemeAttrs = a.extractThemeAttrs(); + + mStartX = a.getFloat( + R.styleable.GradientColor_startX, mStartX); + mStartY = a.getFloat( + R.styleable.GradientColor_startY, mStartY); + mEndX = a.getFloat( + R.styleable.GradientColor_endX, mEndX); + mEndY = a.getFloat( + R.styleable.GradientColor_endY, mEndY); + + mCenterX = a.getFloat( + R.styleable.GradientColor_centerX, mCenterX); + mCenterY = a.getFloat( + R.styleable.GradientColor_centerY, mCenterY); + + mGradientType = a.getInt( + R.styleable.GradientColor_type, mGradientType); + + mStartColor = a.getColor( + R.styleable.GradientColor_startColor, mStartColor); + mHasCenterColor |= a.hasValue( + R.styleable.GradientColor_centerColor); + mCenterColor = a.getColor( + R.styleable.GradientColor_centerColor, mCenterColor); + mEndColor = a.getColor( + R.styleable.GradientColor_endColor, mEndColor); + + if (DBG_GRADIENT) { + Log.v(TAG, "hasCenterColor is " + mHasCenterColor); + if (mHasCenterColor) { + Log.v(TAG, "centerColor:" + mCenterColor); + } + Log.v(TAG, "startColor: " + mStartColor); + Log.v(TAG, "endColor: " + mEndColor); + } + + mGradientRadius = a.getFloat(R.styleable.GradientColor_gradientRadius, + mGradientRadius); + } + + /** + * Check if the XML content is valid. + * + * @throws XmlPullParserException if errors were found. + */ + private void validateXmlContent() throws XmlPullParserException { + if (mGradientRadius <= 0 && mGradientType == GradientDrawable.RADIAL_GRADIENT) { + throw new XmlPullParserException( + " tag requires 'gradientRadius' " + + "attribute with radial type"); + } + } + + /** + * The shader information will be applied to the native VectorDrawable's path. + * @hide + */ + public Shader getShader() { + return mShader; + } + + /** + * A public method to create GradientColor from a XML resource. + */ + public static GradientColor createFromXml(Resources r, XmlResourceParser parser, 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 GradientColor from that tag. + * + * @return A new GradientColor for the current tag. + * @throws XmlPullParserException if the current tag is not <gradient> + */ + @NonNull + static GradientColor createFromXmlInner(@NonNull Resources r, + @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) + throws XmlPullParserException, IOException { + final String name = parser.getName(); + if (!name.equals("gradient")) { + throw new XmlPullParserException( + parser.getPositionDescription() + ": invalid gradient color tag " + name); + } + + final GradientColor gradientColor = new GradientColor(); + gradientColor.inflate(r, parser, attrs, theme); + return gradientColor; + } + + /** + * Fill in this object based on the contents of an XML "gradient" element. + */ + private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, + @NonNull AttributeSet attrs, @Nullable Theme theme) + throws XmlPullParserException, IOException { + final TypedArray a = Resources.obtainAttributes(r, theme, attrs, R.styleable.GradientColor); + updateRootElementState(a); + mChangingConfigurations |= a.getChangingConfigurations(); + a.recycle(); + + // Check correctness and throw exception if errors found. + validateXmlContent(); + + inflateChildElements(r, parser, attrs, theme); + + onColorsChange(); + } + + /** + * Inflates child elements "item"s for each color stop. + * + * Note that at root level, we need to save ThemeAttrs for theme applied later. + * Here similarly, at each child item, we need to save the theme's attributes, and apply theme + * later as applyItemsAttrsTheme(). + */ + private void inflateChildElements(@NonNull Resources r, @NonNull XmlPullParser parser, + @NonNull AttributeSet attrs, @NonNull Theme theme) + throws XmlPullParserException, IOException { + final int innerDepth = parser.getDepth() + 1; + int type; + int depth; + + // Pre-allocate the array with some size, for better performance. + float[] offsetList = new float[20]; + int[] colorList = new int[offsetList.length]; + int[][] themeAttrsList = new int[offsetList.length][]; + + int listSize = 0; + boolean hasUnresolvedAttrs = false; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + if (depth > innerDepth || !parser.getName().equals("item")) { + continue; + } + + final TypedArray a = Resources.obtainAttributes(r, theme, attrs, + R.styleable.GradientColorItem); + boolean hasColor = a.hasValue(R.styleable.GradientColorItem_color); + boolean hasOffset = a.hasValue(R.styleable.GradientColorItem_offset); + if (!hasColor || !hasOffset) { + throw new XmlPullParserException( + parser.getPositionDescription() + ": tag requires a 'color' attribute and a 'offset' " + + "attribute!"); + } + + final int[] themeAttrs = a.extractThemeAttrs(); + int color = a.getColor(R.styleable.GradientColorItem_color, 0); + float offset = a.getFloat(R.styleable.GradientColorItem_offset, 0); + + if (DBG_GRADIENT) { + Log.v(TAG, "new item color " + color + " " + Integer.toHexString(color)); + Log.v(TAG, "offset" + offset); + } + mChangingConfigurations |= a.getChangingConfigurations(); + a.recycle(); + + if (themeAttrs != null) { + hasUnresolvedAttrs = true; + } + + colorList = GrowingArrayUtils.append(colorList, listSize, color); + offsetList = GrowingArrayUtils.append(offsetList, listSize, offset); + themeAttrsList = GrowingArrayUtils.append(themeAttrsList, listSize, themeAttrs); + listSize++; + } + if (listSize > 0) { + if (hasUnresolvedAttrs) { + mItemsThemeAttrs = new int[listSize][]; + System.arraycopy(themeAttrsList, 0, mItemsThemeAttrs, 0, listSize); + } else { + mItemsThemeAttrs = null; + } + + mItemColors = new int[listSize]; + mItemOffsets = new float[listSize]; + System.arraycopy(colorList, 0, mItemColors, 0, listSize); + System.arraycopy(offsetList, 0, mItemOffsets, 0, listSize); + } + } + + /** + * Apply theme to all the items. + */ + private void applyItemsAttrsTheme(Theme t) { + if (mItemsThemeAttrs == null) { + return; + } + + boolean hasUnresolvedAttrs = false; + + final int[][] themeAttrsList = mItemsThemeAttrs; + 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.GradientColorItem); + + // 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; + } + + mItemColors[i] = a.getColor(R.styleable.GradientColorItem_color, mItemColors[i]); + mItemOffsets[i] = a.getFloat(R.styleable.GradientColorItem_offset, mItemOffsets[i]); + if (DBG_GRADIENT) { + Log.v(TAG, "applyItemsAttrsTheme Colors[i] " + i + " " + + Integer.toHexString(mItemColors[i])); + Log.v(TAG, "Offsets[i] " + i + " " + mItemOffsets[i]); + } + + // Account for any configuration changes. + mChangingConfigurations |= a.getChangingConfigurations(); + + a.recycle(); + } + } + + if (!hasUnresolvedAttrs) { + mItemsThemeAttrs = null; + } + } + + private void onColorsChange() { + int[] tempColors = null; + float[] tempOffsets = null; + + if (mItemColors != null) { + int length = mItemColors.length; + tempColors = new int[length]; + tempOffsets = new float[length]; + + for (int i = 0; i < length; i++) { + tempColors[i] = mItemColors[i]; + tempOffsets[i] = mItemOffsets[i]; + } + } else { + if (mHasCenterColor) { + tempColors = new int[3]; + tempColors[0] = mStartColor; + tempColors[1] = mCenterColor; + tempColors[2] = mEndColor; + + tempOffsets = new float[3]; + tempOffsets[0] = 0.0f; + // Since 0.5f is default value, try to take the one that isn't 0.5f + tempOffsets[1] = 0.5f; + tempOffsets[2] = 1f; + } else { + tempColors = new int[2]; + tempColors[0] = mStartColor; + tempColors[1] = mEndColor; + } + } + if (tempColors.length < 2) { + Log.w(TAG, " tag requires 2 color values specified!" + tempColors.length + " " + tempColors); + } + + if (mGradientType == GradientDrawable.LINEAR_GRADIENT) { + mShader = new LinearGradient(mStartX, mStartY, mEndX, mEndY, tempColors, tempOffsets, + Shader.TileMode.CLAMP); + } else { + if (mGradientType == GradientDrawable.RADIAL_GRADIENT) { + mShader = new RadialGradient(mCenterX, mCenterY, mGradientRadius, tempColors, + tempOffsets, Shader.TileMode.CLAMP); + } else { + mShader = new SweepGradient(mCenterX, mCenterY, tempColors, tempOffsets); + } + } + mDefaultColor = tempColors[0]; + } + + /** + * For Gradient color, the default color is not very useful, since the gradient will override + * the color information anyway. + */ + @Override + //@ColorInt + public int getDefaultColor() { + return mDefaultColor; + } + + /** + * Similar to ColorStateList, setup constant state and its factory. + * @hide only for resource preloading + */ + @Override + public ConstantState getConstantState() { + if (mFactory == null) { + mFactory = new GradientColorFactory(this); + } + return mFactory; + } + + private static class GradientColorFactory extends ConstantState { + private final GradientColor mSrc; + + public GradientColorFactory(GradientColor src) { + mSrc = src; + } + + @Override + public int getChangingConfigurations() { + return mSrc.mChangingConfigurations; + } + + @Override + public GradientColor newInstance() { + return mSrc; + } + + @Override + public GradientColor newInstance(Resources res, Theme theme) { + return mSrc.obtainForTheme(theme); + } + } + + /** + * Returns an appropriately themed gradient color. + * + * @param t the theme to apply + * @return a copy of the gradient color the theme applied, or the + * gradient itself if there were no unresolved theme + * attributes + * @hide only for resource preloading + */ + @Override + public GradientColor obtainForTheme(Theme t) { + if (t == null || !canApplyTheme()) { + return this; + } + + final GradientColor clone = new GradientColor(this); + clone.applyTheme(t); + return clone; + } + + private void applyTheme(Theme t) { + if (mThemeAttrs != null) { + applyRootAttrsTheme(t); + } + if (mItemsThemeAttrs != null) { + applyItemsAttrsTheme(t); + } + onColorsChange(); + } + + private void applyRootAttrsTheme(Theme t) { + final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.GradientColor); + // mThemeAttrs will be set to null if if there are no theme attributes in the + // typed array. + mThemeAttrs = a.extractThemeAttrs(mThemeAttrs); + // merging the attributes update inside the updateRootElementState(). + updateRootElementState(a); + + // Account for any configuration changes. + mChangingConfigurations |= a.getChangingConfigurations(); + a.recycle(); + } + + /** + * Returns whether a theme can be applied to this gradient color, which + * usually indicates that the gradient color has unresolved theme + * attributes. + * + * @return whether a theme can be applied to this gradient color. + * @hide only for resource preloading + */ + @Override + public boolean canApplyTheme() { + return mThemeAttrs != null || mItemsThemeAttrs != null; + } +} diff --git a/src/api-impl/android/content/res/Resources.java b/src/api-impl/android/content/res/Resources.java index 492695c6..099c66d1 100644 --- a/src/api-impl/android/content/res/Resources.java +++ b/src/api-impl/android/content/res/Resources.java @@ -16,6 +16,8 @@ package android.content.res; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.pm.ActivityInfo; import android.icu.text.PluralRules; @@ -33,6 +35,9 @@ import android.util.Log; import android.util.LongSparseArray; import android.util.Slog; import android.util.TypedValue; +import android.util.Xml; +import android.util.Pools.SynchronizedPool; +import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.XmlUtils; // import android.view.DisplayAdjustments; import java.io.IOException; @@ -91,7 +96,10 @@ public class Resources { // single-threaded, and after that these are immutable. private static final LongSparseArray[] sPreloadedDrawables; private static final LongSparseArray sPreloadedColorDrawables = new LongSparseArray(); - private static final LongSparseArray sPreloadedColorStateLists = new LongSparseArray(); + private static final LongSparseArray> sPreloadedComplexColors = new LongSparseArray<>(); + + // Pool of TypedArrays targeted to this Resources object. + final SynchronizedPool mTypedArrayPool = new SynchronizedPool<>(5); private static boolean sPreloaded; private static int sPreloadedDensity; @@ -102,7 +110,7 @@ public class Resources { /*package*/ final Configuration mTmpConfig = new Configuration(); /*package*/ TypedValue mTmpValue = new TypedValue(); /*package*/ final Map> mDrawableCache = new HashMap>(0); - /*package*/ final LongSparseArray> mColorStateListCache = new LongSparseArray>(0); + private final ConfigurationBoundResourceCache mComplexColorCache = new ConfigurationBoundResourceCache<>(this); /*package*/ final LongSparseArray> mColorDrawableCache = new LongSparseArray>(0); /*package*/ boolean mPreloading; @@ -807,6 +815,10 @@ public class Resources { * @return Returns a single color value in the form 0xAARRGGBB. */ public int getColor(int id) throws NotFoundException { + return getColor(id, null); + } + + public int getColor(int id, Theme theme) throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -823,7 +835,7 @@ public class Resources { } mTmpValue = null; } - ColorStateList csl = loadColorStateList(value, id); + ColorStateList csl = loadColorStateList(value, id, theme); synchronized (mAccessLock) { if (mTmpValue == null) { mTmpValue = value; @@ -832,11 +844,6 @@ public class Resources { return csl.getDefaultColor(); } - public int getColor(int id, Theme theme) throws NotFoundException { - // TODO fix it - return 0; - } - /** * Return a color state list associated with a particular resource ID. The * resource may contain either a single raw color value, or a complex @@ -867,7 +874,7 @@ public class Resources { } getValue(id, value, true); } - ColorStateList res = loadColorStateList(value, id); + ColorStateList res = loadColorStateList(value, id, theme); synchronized (mAccessLock) { if (mTmpValue == null) { mTmpValue = value; @@ -1418,6 +1425,36 @@ public class Resources { return array; } + /** + * Retrieve the values for a set of attributes in the Theme. The + * contents of the typed array are ultimately filled in by + * {@link Resources#getValue}. + * + * @param values The base set of attribute values, must be equal in + * length to {@code attrs}. All values must be of type + * {@link TypedValue#TYPE_ATTRIBUTE}. + * @param attrs The desired attributes to be retrieved. + * @return Returns a TypedArray holding an array of the attribute + * values. Be sure to call {@link TypedArray#recycle()} + * when done with it. + * @hide + */ + @NonNull + public TypedArray resolveAttributes(@NonNull int[] values, @NonNull int[] attrs) { + final int len = attrs.length; + if (values == null || len != values.length) { + throw new IllegalArgumentException( + "Base attribute values must the same length as attrs"); + } + + final TypedArray array = TypedArray.obtain(Resources.this, len); + AssetManager.resolveAttrs(theme, 0, 0, values, attrs, array.mData, array.mIndices); + array.mTheme = this; + array.mXml = null; + + return array; + } + /** * Retrieve the value of an attribute in the Theme. The contents of * outValue are ultimately filled in by @@ -1461,6 +1498,8 @@ public class Resources { mAssets.releaseTheme(theme); } + private final ThemeKey mKey = new ThemeKey(); + /*package*/ Theme() { mAssets = Resources.this.mAssets; theme = mAssets.createTheme(); @@ -1470,11 +1509,93 @@ public class Resources { return Resources.this; } + /*package*/ ThemeKey getKey() { + return mKey; + } + private final AssetManager mAssets; public void rebase() {} } + static class ThemeKey implements Cloneable { + int[] mResId; + boolean[] mForce; + int mCount; + + private int mHashCode = 0; + + public void append(int resId, boolean force) { + if (mResId == null) { + mResId = new int[4]; + } + + if (mForce == null) { + mForce = new boolean[4]; + } + + mResId = GrowingArrayUtils.append(mResId, mCount, resId); + mForce = GrowingArrayUtils.append(mForce, mCount, force); + mCount++; + + mHashCode = 31 * (31 * mHashCode + resId) + (force ? 1 : 0); + } + + /** + * Sets up this key as a deep copy of another key. + * + * @param other the key to deep copy into this key + */ + public void setTo(ThemeKey other) { + mResId = other.mResId == null ? null : other.mResId.clone(); + mForce = other.mForce == null ? null : other.mForce.clone(); + mCount = other.mCount; + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass() || hashCode() != o.hashCode()) { + return false; + } + + final ThemeKey t = (ThemeKey)o; + if (mCount != t.mCount) { + return false; + } + + final int N = mCount; + for (int i = 0; i < N; i++) { + if (mResId[i] != t.mResId[i] || mForce[i] != t.mForce[i]) { + return false; + } + } + + return true; + } + + /** + * @return a shallow copy of this key + */ + @Override + public ThemeKey clone() { + final ThemeKey other = new ThemeKey(); + other.mResId = mResId; + other.mForce = mForce; + other.mCount = mCount; + other.mHashCode = mHashCode; + return other; + } + } + /** * Generate a new Theme object for this set of Resources. It initially * starts out empty. @@ -1615,7 +1736,7 @@ public class Resources { // clearDrawableCacheLocked(mDrawableCache, configChanges); // clearDrawableCacheLocked(mColorDrawableCache, configChanges); - mColorStateListCache.clear(); + mComplexColorCache.onConfigurationChange(configChanges); flushLayoutCache(); } @@ -2023,8 +2144,11 @@ public class Resources { static private final int LAYOUT_DIR_CONFIG = ActivityInfo.activityInfoConfigToNative( ActivityInfo.CONFIG_LAYOUT_DIRECTION); - /*package*/ Drawable loadDrawable(TypedValue value, int id) - throws NotFoundException { /* + /*package*/ Drawable loadDrawable(TypedValue value, int id) throws NotFoundException { + return loadDrawable(value, id, null); + } + + /*package*/ Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException { /* if (TRACE_FOR_PRELOAD) { // Log only framework resources @@ -2189,7 +2313,80 @@ public class Resources { return null; } - /*package*/ ColorStateList loadColorStateList(TypedValue value, int id) + /** + * Given the value and id, we can get the XML filename as in value.data, based on that, we + * first try to load CSL from the cache. If not found, try to get from the constant state. + * Last, parse the XML and generate the CSL. + */ + private ComplexColor loadComplexColorFromName(Theme theme, TypedValue value, int id) { + final long key = (((long)value.assetCookie) << 32) | value.data; + final ConfigurationBoundResourceCache cache = mComplexColorCache; + ComplexColor complexColor = cache.getInstance(key, theme); + if (complexColor != null) { + return complexColor; + } + + final android.content.res.ConstantState factory = sPreloadedComplexColors.get(key); + + if (factory != null) { + complexColor = factory.newInstance(this, theme); + } + if (complexColor == null) { + complexColor = loadComplexColorForCookie(value, id, theme); + } + + if (complexColor != null) { + if (mPreloading) { + if (verifyPreloadConfig(value.changingConfigurations, 0, value.resourceId, "color")) { + sPreloadedComplexColors.put(key, complexColor.getConstantState()); + } + } else { + cache.put(key, theme, complexColor.getConstantState()); + } + } + return complexColor; + } + + @Nullable + public ComplexColor loadComplexColor(@NonNull TypedValue value, int id, Theme theme) { + if (TRACE_FOR_PRELOAD) { + // Log only framework resources + if ((id >>> 24) == 0x1) { + final String name = getResourceName(id); + if (name != null) + android.util.Log.d("loadComplexColor", name); + } + } + + final long key = (((long)value.assetCookie) << 32) | value.data; + + // Handle inline color definitions. + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getColorStateListFromInt(value, key); + } + + final String file = value.string.toString(); + + ComplexColor complexColor; + if (file.endsWith(".xml")) { + try { + complexColor = loadComplexColorFromName(theme, value, id); + } catch (Exception e) { + final NotFoundException rnf = new NotFoundException( + "File " + file + " from complex color resource ID #0x" + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + } else { + throw new NotFoundException( + "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id) + ": .xml extension required"); + } + + return complexColor; + } + + @Nullable + ColorStateList loadColorStateList(TypedValue value, int id, Theme theme) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources @@ -2202,99 +2399,112 @@ public class Resources { final long key = (((long)value.assetCookie) << 32) | value.data; + // Handle inline color definitions. + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getColorStateListFromInt(value, key); + } + + ComplexColor complexColor = loadComplexColorFromName(theme, value, id); + if (complexColor != null && complexColor instanceof ColorStateList) { + return (ColorStateList)complexColor; + } + + throw new NotFoundException( + "Can't find ColorStateList from drawable resource ID #0x" + Integer.toHexString(id)); + } + + @NonNull + private ColorStateList getColorStateListFromInt(@NonNull TypedValue value, long key) { ColorStateList csl; - - if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && - value.type <= TypedValue.TYPE_LAST_COLOR_INT) { - - csl = sPreloadedColorStateLists.get(key); - if (csl != null) { - return csl; - } - - csl = ColorStateList.valueOf(value.data); - if (mPreloading) { - if (verifyPreloadConfig(value.changingConfigurations, 0, value.resourceId, - "color")) { - sPreloadedColorStateLists.put(key, csl); - } - } - - return csl; + final android.content.res.ConstantState factory = + sPreloadedComplexColors.get(key); + if (factory != null) { + return (ColorStateList)factory.newInstance(); } - csl = getCachedColorStateList(key); - if (csl != null) { - return csl; - } + csl = ColorStateList.valueOf(value.data); - csl = sPreloadedColorStateLists.get(key); - if (csl != null) { - return csl; - } - - if (value.string == null) { - throw new NotFoundException( - "Resource is not a ColorStateList (color or path): " + value); - } - - String file = value.string.toString(); - - if (file.endsWith(".xml")) { - Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); - try { - XmlResourceParser rp = loadXmlResourceParser( - file, id, value.assetCookie, "colorstatelist"); - csl = ColorStateList.createFromXml(this, rp); - rp.close(); - } catch (Exception e) { - Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); - NotFoundException rnf = new NotFoundException( - "File " + file + " from color state list resource ID #0x" + Integer.toHexString(id)); - rnf.initCause(e); - throw rnf; - } - Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); - } else { - throw new NotFoundException( - "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id) + ": .xml extension required"); - } - - if (csl != null) { - if (mPreloading) { - if (verifyPreloadConfig(value.changingConfigurations, 0, value.resourceId, - "color")) { - sPreloadedColorStateLists.put(key, csl); - } - } else { - synchronized (mAccessLock) { - // Log.i(TAG, "Saving cached color state list @ #" + - // Integer.toHexString(key.intValue()) - // + " in " + this + ": " + csl); - mColorStateListCache.put(key, new WeakReference(csl)); - } + if (mPreloading) { + if (verifyPreloadConfig(value.changingConfigurations, 0, value.resourceId, + "color")) { + sPreloadedComplexColors.put(key, csl.getConstantState()); } } return csl; } - private ColorStateList getCachedColorStateList(long key) { - synchronized (mAccessLock) { - WeakReference wr = mColorStateListCache.get(key); - if (wr != null) { // we have the key - ColorStateList entry = wr.get(); - if (entry != null) { - // Log.i(TAG, "Returning cached color state list @ #" + - // Integer.toHexString(((Integer)key).intValue()) - // + " in " + this + ": " + entry); - return entry; - } else { // our entry has been purged - mColorStateListCache.delete(key); + /** + * Load a ComplexColor based on the XML file content. The result can be a GradientColor or + * ColorStateList. Note that pure color will be wrapped into a ColorStateList. + * + * We deferred the parser creation to this function b/c we need to differentiate b/t gradient + * and selector tag. + * + * @return a ComplexColor (GradientColor or ColorStateList) based on the XML file content. + */ + @Nullable + private ComplexColor loadComplexColorForCookie(TypedValue value, int id, Theme theme) { + if (value.string == null) { + throw new UnsupportedOperationException( + "Can't convert to ComplexColor: type=0x" + value.type); + } + + final String file = value.string.toString(); + + if (TRACE_FOR_MISS_PRELOAD) { + // Log only framework resources + if ((id >>> 24) == 0x1) { + final String name = getResourceName(id); + if (name != null) { + Log.d(TAG, "Loading framework ComplexColor #" + Integer.toHexString(id) + ": " + name + " at " + file); } } } - return null; + + if (DEBUG_LOAD) { + Log.v(TAG, "Loading ComplexColor for cookie " + value.assetCookie + ": " + file); + } + + ComplexColor complexColor = null; + + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); + if (file.endsWith(".xml")) { + try { + final XmlResourceParser parser = loadXmlResourceParser( + file, id, value.assetCookie, "ComplexColor"); + + 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"); + } + + final String name = parser.getName(); + if (name.equals("gradient")) { + complexColor = GradientColor.createFromXmlInner(this, parser, attrs, theme); + } else if (name.equals("selector")) { + complexColor = ColorStateList.createFromXmlInner(this, parser, attrs, theme); + } + parser.close(); + } catch (Exception e) { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + final NotFoundException rnf = new NotFoundException( + "File " + file + " from ComplexColor resource ID #0x" + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + } else { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + throw new NotFoundException( + "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id) + ": .xml extension required"); + } + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + + return complexColor; } /*package*/ XmlResourceParser loadXmlResourceParser(int id, String type) @@ -2321,95 +2531,111 @@ public class Resources { } catch (IOException e) { throw new RuntimeException(e); } -/* if (id != 0) { - try { - // These may be compiled... - synchronized (mCachedXmlBlockIds) { - // First see if this block is in our cache. - final int num = mCachedXmlBlockIds.length; - for (int i=0; i= fullLen) { + attrs.mLength = len; + int fullLen = len * AssetManager.STYLE_NUM_ENTRIES; + if (attrs.mData.length >= fullLen) { + return attrs; + } + attrs.mData = new int[fullLen]; + attrs.mIndices = new int[1 + len]; return attrs; } - attrs.mData = new int[fullLen]; - attrs.mIndices = new int[1 + len]; - return attrs; - } - if (DEBUG_ATTRIBUTES_CACHE) { - RuntimeException here = new RuntimeException("here"); - here.fillInStackTrace(); - if (mLastRetrievedAttrs != null) { - Log.i(TAG, "Allocated new TypedArray of " + len + " in " + this, here); - Log.i(TAG, "Last retrieved attributes here", mLastRetrievedAttrs); + if (DEBUG_ATTRIBUTES_CACHE) { + RuntimeException here = new RuntimeException("here"); + here.fillInStackTrace(); + if (mLastRetrievedAttrs != null) { + Log.i(TAG, "Allocated new TypedArray of " + len + " in " + this, here); + Log.i(TAG, "Last retrieved attributes here", mLastRetrievedAttrs); + } + mLastRetrievedAttrs = here; } - mLastRetrievedAttrs = here; + return new TypedArray(this, + new int[len * AssetManager.STYLE_NUM_ENTRIES], + new int[1 + len], len); } - return new TypedArray(this, - new int[len * AssetManager.STYLE_NUM_ENTRIES], - new int[1 + len], len); + } + + + /** + * Obtains styled attributes from the theme, if available, or unstyled + * resources if the theme is null. + * + * @hide + */ + public static TypedArray obtainAttributes( + Resources res, Theme theme, AttributeSet set, int[] attrs) { + if (theme == null) { + return res.obtainAttributes(set, attrs); + } + + return theme.obtainStyledAttributes(set, attrs, 0, 0); + } + + private Resources() { + mAssets = AssetManager.getSystem(); + // NOTE: Intentionally leaving this uninitialized (all values set + // to zero), so that anyone who tries to do something that requires + // metrics will get a very wrong value. + mConfiguration.setToDefaults(); + mMetrics.setToDefaults(); + updateConfiguration(null, null); + mAssets.ensureStringBlocks(); } } - -private Resources() { - mAssets = AssetManager.getSystem(); - // NOTE: Intentionally leaving this uninitialized (all values set - // to zero), so that anyone who tries to do something that requires - // metrics will get a very wrong value. - mConfiguration.setToDefaults(); - mMetrics.setToDefaults(); - updateConfiguration(null, null); - mAssets.ensureStringBlocks(); -} -} diff --git a/src/api-impl/android/content/res/ThemedResourceCache.java b/src/api-impl/android/content/res/ThemedResourceCache.java new file mode 100644 index 00000000..e4a9e7c7 --- /dev/null +++ b/src/api-impl/android/content/res/ThemedResourceCache.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2015 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.NonNull; +import android.annotation.Nullable; +import android.content.res.Resources.Theme; +import android.content.res.Resources.ThemeKey; +import android.util.ArrayMap; +import android.util.LongSparseArray; +import java.lang.ref.WeakReference; + +/** + * Data structure used for caching data against themes. + * + * @param type of data to cache + */ +abstract class ThemedResourceCache { + private ArrayMap>> mThemedEntries; + private LongSparseArray> mUnthemedEntries; + private LongSparseArray> mNullThemedEntries; + + /** + * Adds a new theme-dependent entry to the cache. + * + * @param key a key that uniquely identifies the entry + * @param theme the theme against which this entry was inflated, or + * {@code null} if the entry has no theme applied + * @param entry the entry to cache + */ + public void put(long key, @Nullable Theme theme, @NonNull T entry) { + put(key, theme, entry, true); + } + + /** + * Adds a new entry to the cache. + * + * @param key a key that uniquely identifies the entry + * @param theme the theme against which this entry was inflated, or + * {@code null} if the entry has no theme applied + * @param entry the entry to cache + * @param usesTheme {@code true} if the entry is affected theme changes, + * {@code false} otherwise + */ + public void put(long key, @Nullable Theme theme, @NonNull T entry, boolean usesTheme) { + if (entry == null) { + return; + } + + synchronized (this) { + final LongSparseArray> entries; + if (!usesTheme) { + entries = getUnthemedLocked(true); + } else { + entries = getThemedLocked(theme, true); + } + if (entries != null) { + entries.put(key, new WeakReference<>(entry)); + } + } + } + + /** + * Returns an entry from the cache. + * + * @param key a key that uniquely identifies the entry + * @param theme the theme where the entry will be used + * @return a cached entry, or {@code null} if not in the cache + */ + @Nullable + public T get(long key, @Nullable Theme theme) { + // The themed (includes null-themed) and unthemed caches are mutually + // exclusive, so we'll give priority to whichever one we think we'll + // hit first. Since most of the framework drawables are themed, that's + // probably going to be the themed cache. + synchronized (this) { + final LongSparseArray> themedEntries = getThemedLocked(theme, false); + if (themedEntries != null) { + final WeakReference themedEntry = themedEntries.get(key); + if (themedEntry != null) { + return themedEntry.get(); + } + } + + final LongSparseArray> unthemedEntries = getUnthemedLocked(false); + if (unthemedEntries != null) { + final WeakReference unthemedEntry = unthemedEntries.get(key); + if (unthemedEntry != null) { + return unthemedEntry.get(); + } + } + } + + return null; + } + + /** + * Prunes cache entries that have been invalidated by a configuration + * change. + * + * @param configChanges a bitmask of configuration changes + */ + public void onConfigurationChange(int configChanges) { + prune(configChanges); + } + + /** + * Returns whether a cached entry has been invalidated by a configuration + * change. + * + * @param entry a cached entry + * @param configChanges a non-zero bitmask of configuration changes + * @return {@code true} if the entry is invalid, {@code false} otherwise + */ + protected abstract boolean shouldInvalidateEntry(@NonNull T entry, int configChanges); + + /** + * Returns the cached data for the specified theme, optionally creating a + * new entry if one does not already exist. + * + * @param t the theme for which to return cached data + * @param create {@code true} to create an entry if one does not already + * exist, {@code false} otherwise + * @return the cached data for the theme, or {@code null} if the cache is + * empty and {@code create} was {@code false} + */ + @Nullable + private LongSparseArray> getThemedLocked(@Nullable Theme t, boolean create) { + if (t == null) { + if (mNullThemedEntries == null && create) { + mNullThemedEntries = new LongSparseArray<>(1); + } + return mNullThemedEntries; + } + + if (mThemedEntries == null) { + if (create) { + mThemedEntries = new ArrayMap<>(1); + } else { + return null; + } + } + + final ThemeKey key = t.getKey(); + LongSparseArray> cache = mThemedEntries.get(key); + if (cache == null && create) { + cache = new LongSparseArray<>(1); + + final ThemeKey keyClone = key.clone(); + mThemedEntries.put(keyClone, cache); + } + + return cache; + } + + /** + * Returns the theme-agnostic cached data. + * + * @param create {@code true} to create an entry if one does not already + * exist, {@code false} otherwise + * @return the theme-agnostic cached data, or {@code null} if the cache is + * empty and {@code create} was {@code false} + */ + @Nullable + private LongSparseArray> getUnthemedLocked(boolean create) { + if (mUnthemedEntries == null && create) { + mUnthemedEntries = new LongSparseArray<>(1); + } + return mUnthemedEntries; + } + + /** + * Prunes cache entries affected by configuration changes or where weak + * references have expired. + * + * @param configChanges a bitmask of configuration changes, or {@code 0} to + * simply prune missing weak references + * @return {@code true} if the cache is completely empty after pruning + */ + private boolean prune(int configChanges) { + synchronized (this) { + if (mThemedEntries != null) { + for (int i = mThemedEntries.size() - 1; i >= 0; i--) { + if (pruneEntriesLocked(mThemedEntries.valueAt(i), configChanges)) { + mThemedEntries.removeAt(i); + } + } + } + + pruneEntriesLocked(mNullThemedEntries, configChanges); + pruneEntriesLocked(mUnthemedEntries, configChanges); + + return mThemedEntries == null && mNullThemedEntries == null && mUnthemedEntries == null; + } + } + + private boolean pruneEntriesLocked(@Nullable LongSparseArray> entries, + int configChanges) { + if (entries == null) { + return true; + } + + for (int i = entries.size() - 1; i >= 0; i--) { + final WeakReference ref = entries.valueAt(i); + if (ref == null || pruneEntryLocked(ref.get(), configChanges)) { + entries.removeAt(i); + } + } + + return entries.size() == 0; + } + + private boolean pruneEntryLocked(@Nullable T entry, int configChanges) { + return entry == null || (configChanges != 0 && shouldInvalidateEntry(entry, configChanges)); + } +} diff --git a/src/api-impl/android/content/res/TypedArray.java b/src/api-impl/android/content/res/TypedArray.java index c5b6aaa8..5e25b137 100644 --- a/src/api-impl/android/content/res/TypedArray.java +++ b/src/api-impl/android/content/res/TypedArray.java @@ -16,13 +16,16 @@ package android.content.res; +//import android.annotation.AnyRes; +//import android.annotation.ColorInt; +import android.annotation.Nullable; +//import android.annotation.StyleableRes; import android.graphics.drawable.Drawable; +import android.os.StrictMode; import android.util.AttributeSet; import android.util.DisplayMetrics; -import android.util.Log; import android.util.TypedValue; import com.android.internal.util.XmlUtils; - import java.util.Arrays; /** @@ -35,57 +38,118 @@ import java.util.Arrays; * the positions of the attributes given to obtainStyledAttributes. */ public class TypedArray { + + static TypedArray obtain(Resources res, int len) { + final TypedArray attrs = res.mTypedArrayPool.acquire(); + if (attrs != null) { + attrs.mLength = len; + attrs.mRecycled = false; + + final int fullLen = len * AssetManager.STYLE_NUM_ENTRIES; + if (attrs.mData.length >= fullLen) { + return attrs; + } + + attrs.mData = new int[fullLen]; + attrs.mIndices = new int[1 + len]; + return attrs; + } + + return new TypedArray(res, + new int[len * AssetManager.STYLE_NUM_ENTRIES], + new int[1 + len], len); + } + private final Resources mResources; + private final DisplayMetrics mMetrics; + private final AssetManager mAssets; + + private boolean mRecycled; + + /*package*/ int[] mRsrcs; // TODO update Resources.java to not need this? + /*package*/ XmlResourceParser mXml; - /*package*/ int[] mRsrcs; + /*package*/ Resources.Theme mTheme; /*package*/ int[] mData; /*package*/ int[] mIndices; /*package*/ int mLength; /*package*/ TypedValue mValue = new TypedValue(); /** - * Return the number of values in this array. + * Returns the number of values in this array. + * + * @throws RuntimeException if the TypedArray has already been recycled. */ public int length() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mLength; } /** * Return the number of indices in the array that actually have data. + * + * @throws RuntimeException if the TypedArray has already been recycled. */ public int getIndexCount() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mIndices[0]; } /** - * Return an index in the array that has data. + * Returns an index in the array that has data. * * @param at The index you would like to returned, ranging from 0 to - * {@link #getIndexCount()}. + * {@link #getIndexCount()}. * * @return The index at the given offset, which can be used with - * {@link #getValue} and related APIs. + * {@link #getValue} and related APIs. + * @throws RuntimeException if the TypedArray has already been recycled. */ public int getIndex(int at) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mIndices[1 + at]; } /** - * Return the Resources object this array was loaded from. + * Returns the Resources object this array was loaded from. + * + * @throws RuntimeException if the TypedArray has already been recycled. */ public Resources getResources() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mResources; } /** - * Retrieve the styled string value for the attribute at index. + * Retrieves the styled string value for the attribute at index. + *

+ * If the attribute is not a string, this method will attempt to coerce + * it to a string. * * @param index Index of attribute to retrieve. * - * @return CharSequence holding string data. May be styled. Returns - * null if the attribute is not defined. + * @return CharSequence holding string data. May be styled. Returns + * {@code null} if the attribute is not defined or could not be + * coerced to a string. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public CharSequence getText(int index) { + public CharSequence getText(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -95,24 +159,34 @@ public class TypedArray { return loadStringValueAt(index); } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to string: " + v); return v.coerceToString(); } - Log.w(Resources.TAG, "getString of bad type: 0x" + Integer.toHexString(type)); - return null; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getText of bad type: 0x" + Integer.toHexString(type)); } /** - * Retrieve the string value for the attribute at index. + * Retrieves the string value for the attribute at index. + *

+ * If the attribute is not a string, this method will attempt to coerce + * it to a string. * * @param index Index of attribute to retrieve. * - * @return String holding string data. Any styling information is - * removed. Returns null if the attribute is not defined. + * @return String holding string data. Any styling information is removed. + * Returns {@code null} if the attribute is not defined or could + * not be coerced to a string. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public String getString(int index) { + @Nullable + public String getString(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -122,18 +196,18 @@ public class TypedArray { return loadStringValueAt(index).toString(); } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to string: " + v); - CharSequence cs = v.coerceToString(); + final CharSequence cs = v.coerceToString(); return cs != null ? cs.toString() : null; } - Log.w(Resources.TAG, "getString of bad type: 0x" + Integer.toHexString(type)); - return null; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getString of bad type: 0x" + Integer.toHexString(type)); } /** - * Retrieve the string value for the attribute at index, but + * Retrieves the string value for the attribute at index, but * only if that string comes from an immediate value in an XML file. That * is, this does not allow references to string resources, string * attributes, or conversions from other types. As such, this method @@ -142,38 +216,48 @@ public class TypedArray { * * @param index Index of attribute to retrieve. * - * @return String holding string data. Any styling information is - * removed. Returns null if the attribute is not defined or is not - * an immediate string value. + * @return String holding string data. Any styling information is removed. + * Returns {@code null} if the attribute is not defined or is not + * an immediate string value. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public String getNonResourceString(int index) { + public String getNonResourceString(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; if (type == TypedValue.TYPE_STRING) { final int cookie = data[index + AssetManager.STYLE_ASSET_COOKIE]; if (cookie < 0) { - CharSequence string = ((XmlBlock.Parser)mXml).getPooledString(data[index + AssetManager.STYLE_DATA]); - if (string != null) - return string.toString(); + return ((XmlBlock.Parser)mXml).getPooledString( + data[index + AssetManager.STYLE_DATA]) + .toString(); } } return null; } /** - * @hide - * Retrieve the string value for the attribute at index that is + * Retrieves the string value for the attribute at index that is * not allowed to change with the given configurations. * * @param index Index of attribute to retrieve. * @param allowedChangingConfigs Bit mask of configurations from - * {@link Configuration}.NATIVE_CONFIG_* that are allowed to change. + * {@link Configuration}.NATIVE_CONFIG_* that are allowed to change. * - * @return String holding string data. Any styling information is - * removed. Returns null if the attribute is not defined. + * @return String holding string data. Any styling information is removed. + * Returns {@code null} if the attribute is not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @hide */ - public String getNonConfigurationString(int index, int allowedChangingConfigs) { + public String getNonConfigurationString(/*@StyleableRes*/ int index, int allowedChangingConfigs) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -186,25 +270,37 @@ public class TypedArray { return loadStringValueAt(index).toString(); } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to string: " + v); - CharSequence cs = v.coerceToString(); + final CharSequence cs = v.coerceToString(); return cs != null ? cs.toString() : null; } - Log.w(Resources.TAG, "getString of bad type: 0x" + Integer.toHexString(type)); - return null; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getNonConfigurationString of bad type: 0x" + Integer.toHexString(type)); } /** * Retrieve the boolean value for the attribute at index. + *

+ * If the attribute is an integer value, this method will return whether + * it is equal to zero. If the attribute is not a boolean or integer value, + * this method will attempt to coerce it to an integer using + * {@link Integer#decode(String)} and return whether it is equal to zero. * * @param index Index of attribute to retrieve. - * @param defValue Value to return if the attribute is not defined. + * @param defValue Value to return if the attribute is not defined or + * cannot be coerced to an integer. * - * @return Attribute boolean value, or defValue if not defined. + * @return Boolean value of the attribute, or defValue if the attribute was + * not defined or could not be coerced to an integer. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public boolean getBoolean(int index, boolean defValue) { + public boolean getBoolean(/*@StyleableRes*/ int index, boolean defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -214,25 +310,35 @@ public class TypedArray { return data[index + AssetManager.STYLE_DATA] != 0; } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to boolean: " + v); - return XmlUtils.convertValueToBoolean( - v.coerceToString(), defValue); + //StrictMode.noteResourceMismatch(v); + return XmlUtils.convertValueToBoolean(v.coerceToString(), defValue); } - Log.w(Resources.TAG, "getBoolean of bad type: 0x" + Integer.toHexString(type)); - return defValue; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getBoolean of bad type: 0x" + Integer.toHexString(type)); } /** * Retrieve the integer value for the attribute at index. + *

+ * If the attribute is not an integer, this method will attempt to coerce + * it to an integer using {@link Integer#decode(String)}. * * @param index Index of attribute to retrieve. - * @param defValue Value to return if the attribute is not defined. + * @param defValue Value to return if the attribute is not defined or + * cannot be coerced to an integer. * - * @return Attribute int value, or defValue if not defined. + * @return Integer value of the attribute, or defValue if the attribute was + * not defined or could not be coerced to an integer. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public int getInt(int index, int defValue) { + public int getInt(/*@StyleableRes*/ int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -242,24 +348,33 @@ public class TypedArray { return data[index + AssetManager.STYLE_DATA]; } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to int: " + v); - return XmlUtils.convertValueToInt( - v.coerceToString(), defValue); + //StrictMode.noteResourceMismatch(v); + return XmlUtils.convertValueToInt(v.coerceToString(), defValue); } - Log.w(Resources.TAG, "getInt of bad type: 0x" + Integer.toHexString(type)); - return defValue; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getInt of bad type: 0x" + Integer.toHexString(type)); } /** * Retrieve the float value for the attribute at index. + *

+ * If the attribute is not a float or an integer, this method will attempt + * to coerce it to a float using {@link Float#parseFloat(String)}. * * @param index Index of attribute to retrieve. * - * @return Attribute float value, or defValue if not defined.. + * @return Attribute float value, or defValue if the attribute was + * not defined or could not be coerced to a float. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public float getFloat(int index, float defValue) { + public float getFloat(/*@StyleableRes*/ int index, float defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -271,16 +386,17 @@ public class TypedArray { return data[index + AssetManager.STYLE_DATA]; } - TypedValue v = mValue; + final TypedValue v = mValue; if (getValueAt(index, v)) { - Log.w(Resources.TAG, "Converting to float: " + v); - CharSequence str = v.coerceToString(); + final CharSequence str = v.coerceToString(); if (str != null) { + //StrictMode.noteResourceMismatch(v); return Float.parseFloat(str.toString()); } } - Log.w(Resources.TAG, "getFloat of bad type: 0x" + Integer.toHexString(type)); - return defValue; + + // We already checked for TYPE_NULL. This should never happen. + throw new RuntimeException("getFloat of bad type: 0x" + Integer.toHexString(type)); } /** @@ -288,14 +404,25 @@ public class TypedArray { * the attribute references a color resource holding a complex * {@link android.content.res.ColorStateList}, then the default color from * the set is returned. + *

+ * This method will throw an exception if the attribute is defined but is + * not an integer color or color state list. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute color value, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer color or color state list. */ - public int getColor(int index, int defValue) { + //@ColorInt + public int getColor(/*@StyleableRes*/ int index, /*@ColorInt*/ int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -306,48 +433,109 @@ public class TypedArray { } else if (type == TypedValue.TYPE_STRING) { final TypedValue value = mValue; if (getValueAt(index, value)) { - ColorStateList csl = mResources.loadColorStateList( - value, value.resourceId); - return csl.getDefaultColor(); + final ComplexColor cc = mResources.loadComplexColor( + value, value.resourceId, mTheme); + return cc.getDefaultColor(); } return defValue; + } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } - Log.e(Resources.TAG, "Can't convert to color: type=0x" + Integer.toHexString(type)); - return defValue; + throw new UnsupportedOperationException("Can't convert to color: type=0x" + Integer.toHexString(type)); + } + + /** + * Retrieve the ComplexColor for the attribute at index. + * The value may be either a {@link android.content.res.ColorStateList} which can wrap a simple + * color value or a {@link android.content.res.GradientColor} + *

+ * This method will return {@code null} if the attribute is not defined or + * is not an integer color, color state list or GradientColor. + * + * @param index Index of attribute to retrieve. + * + * @return ComplexColor for the attribute, or {@code null} if not defined. + * @throws RuntimeException if the attribute if the TypedArray has already + * been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer color, color state list or GradientColor. + */ + @Nullable + public ComplexColor getComplexColor(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + final TypedValue value = mValue; + if (getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value)) { + if (value.type == TypedValue.TYPE_ATTRIBUTE) { + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); + } + return mResources.loadComplexColor(value, value.resourceId, mTheme); + } + return null; } /** * Retrieve the ColorStateList for the attribute at index. * The value may be either a single solid color or a reference to - * a color or complex {@link android.content.res.ColorStateList} description. + * a color or complex {@link android.content.res.ColorStateList} + * description. + *

+ * This method will return {@code null} if the attribute is not defined or + * is not an integer color or color state list. * * @param index Index of attribute to retrieve. * - * @return ColorStateList for the attribute, or null if not defined. + * @return ColorStateList for the attribute, or {@code null} if not + * defined. + * @throws RuntimeException if the attribute if the TypedArray has already + * been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer color or color state list. */ - public ColorStateList getColorStateList(int index) { + @Nullable + public ColorStateList getColorStateList(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + final TypedValue value = mValue; if (getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value)) { - if (value.type == -1) - return null; - if (value.type == TypedValue.TYPE_REFERENCE && value.data == 0) - return null; - return mResources.loadColorStateList(value, value.resourceId); + if (value.type == TypedValue.TYPE_ATTRIBUTE) { + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); + } + return mResources.loadColorStateList(value, value.resourceId, mTheme); } return null; } /** * Retrieve the integer value for the attribute at index. + *

+ * Unlike {@link #getInt(int, int)}, this method will throw an exception if + * the attribute is defined but is not an integer. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute integer value, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer. */ - public int getInteger(int index, int defValue) { + public int getInteger(/*@StyleableRes*/ int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -355,28 +543,43 @@ public class TypedArray { return defValue; } else if (type >= TypedValue.TYPE_FIRST_INT && type <= TypedValue.TYPE_LAST_INT) { return data[index + AssetManager.STYLE_DATA]; + } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to integer: type=0x" + Integer.toHexString(type)); } /** - * Retrieve a dimensional unit attribute at index. Unit + * Retrieve a dimensional unit attribute at index. Unit * conversions are based on the current {@link DisplayMetrics} * associated with the resources this {@link TypedArray} object * came from. + *

+ * This method will throw an exception if the attribute is defined but is + * not a dimension. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute dimension value multiplied by the appropriate - * metric, or defValue if not defined. + * metric, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer. * * @see #getDimensionPixelOffset * @see #getDimensionPixelSize */ - public float getDimension(int index, float defValue) { + public float getDimension(/*@StyleableRes*/ int index, float defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -384,7 +587,12 @@ public class TypedArray { return defValue; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimension( - data[index + AssetManager.STYLE_DATA], mResources.mMetrics); + data[index + AssetManager.STYLE_DATA], mMetrics); + } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" + Integer.toHexString(type)); @@ -396,18 +604,28 @@ public class TypedArray { * {@link #getDimension}, except the returned value is converted to * integer pixels for you. An offset conversion involves simply * truncating the base value to an integer. + *

+ * This method will throw an exception if the attribute is defined but is + * not a dimension. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute dimension value multiplied by the appropriate - * metric and truncated to integer pixels, or defValue if not defined. + * metric and truncated to integer pixels, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not an integer. * * @see #getDimension * @see #getDimensionPixelSize */ - public int getDimensionPixelOffset(int index, int defValue) { + public int getDimensionPixelOffset(/*@StyleableRes*/ int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -415,7 +633,12 @@ public class TypedArray { return defValue; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelOffset( - data[index + AssetManager.STYLE_DATA], mResources.mMetrics); + data[index + AssetManager.STYLE_DATA], mMetrics); + } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" + Integer.toHexString(type)); @@ -428,18 +651,28 @@ public class TypedArray { * integer pixels for use as a size. A size conversion involves * rounding the base value, and ensuring that a non-zero base value * is at least one pixel in size. + *

+ * This method will throw an exception if the attribute is defined but is + * not a dimension. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute dimension value multiplied by the appropriate - * metric and truncated to integer pixels, or defValue if not defined. + * metric and truncated to integer pixels, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not a dimension. * * @see #getDimension * @see #getDimensionPixelOffset */ - public int getDimensionPixelSize(int index, int defValue) { + public int getDimensionPixelSize(/*@StyleableRes*/ int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -447,7 +680,12 @@ public class TypedArray { return defValue; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelSize( - data[index + AssetManager.STYLE_DATA], mResources.mMetrics); + data[index + AssetManager.STYLE_DATA], mMetrics); + } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" + Integer.toHexString(type)); @@ -458,14 +696,24 @@ public class TypedArray { * {@link android.view.ViewGroup}'s layout_width and layout_height * attributes. This is only here for performance reasons; applications * should use {@link #getDimensionPixelSize}. + *

+ * This method will throw an exception if the attribute is defined but is + * not a dimension or integer (enum). * * @param index Index of the attribute to retrieve. * @param name Textual name of attribute for error reporting. * * @return Attribute dimension value multiplied by the appropriate - * metric and truncated to integer pixels. + * metric and truncated to integer pixels. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not a dimension or integer (enum). */ - public int getLayoutDimension(int index, String name) { + public int getLayoutDimension(/*@StyleableRes*/ int index, String name) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -473,10 +721,15 @@ public class TypedArray { return data[index + AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelSize( - data[index + AssetManager.STYLE_DATA], mResources.mMetrics); + data[index + AssetManager.STYLE_DATA], mMetrics); + } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } - throw new RuntimeException(getPositionDescription() + ": You must supply a " + name + " attribute."); + throw new UnsupportedOperationException(getPositionDescription() + ": You must supply a " + name + " attribute."); } /** @@ -487,12 +740,17 @@ public class TypedArray { * * @param index Index of the attribute to retrieve. * @param defValue The default value to return if this attribute is not - * default or contains the wrong type of data. + * default or contains the wrong type of data. * * @return Attribute dimension value multiplied by the appropriate - * metric and truncated to integer pixels. + * metric and truncated to integer pixels. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public int getLayoutDimension(int index, int defValue) { + public int getLayoutDimension(/*@StyleableRes*/ int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -500,14 +758,14 @@ public class TypedArray { return data[index + AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelSize( - data[index + AssetManager.STYLE_DATA], mResources.mMetrics); + data[index + AssetManager.STYLE_DATA], mMetrics); } return defValue; } /** - * Retrieve a fractional unit attribute at index. + * Retrieves a fractional unit attribute at index. * * @param index Index of attribute to retrieve. * @param base The base value of this fraction. In other words, a @@ -519,9 +777,16 @@ public class TypedArray { * not a resource. * * @return Attribute fractional value multiplied by the appropriate - * base value, or defValue if not defined. + * base value, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not a fraction. */ - public float getFraction(int index, int base, int pbase, float defValue) { + public float getFraction(/*@StyleableRes*/ int index, int base, int pbase, float defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; @@ -530,13 +795,18 @@ public class TypedArray { } else if (type == TypedValue.TYPE_FRACTION) { return TypedValue.complexToFraction( data[index + AssetManager.STYLE_DATA], base, pbase); + } else if (type == TypedValue.TYPE_ATTRIBUTE) { + final TypedValue value = mValue; + getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value); + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } throw new UnsupportedOperationException("Can't convert to fraction: type=0x" + Integer.toHexString(type)); } /** - * Retrieve the resource identifier for the attribute at + * Retrieves the resource identifier for the attribute at * index. Note that attribute resource as resolved when * the overall {@link TypedArray} object is retrieved. As a * result, this function will return the resource identifier of the @@ -548,8 +818,14 @@ public class TypedArray { * not a resource. * * @return Attribute resource identifier, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public int getResourceId(int index, int defValue) { + //@AnyRes + public int getResourceId(/*@StyleableRes*/ int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; if (data[index + AssetManager.STYLE_TYPE] != TypedValue.TYPE_NULL) { @@ -562,24 +838,56 @@ public class TypedArray { } /** - * Retrieve the Drawable for the attribute at index. This - * gets the resource ID of the selected attribute, and uses - * {@link Resources#getDrawable Resources.getDrawable} of the owning - * Resources object to retrieve its Drawable. + * Retrieves the theme attribute resource identifier for the attribute at + * index. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or not a + * resource. + * + * @return Theme attribute resource identifier, or defValue if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @hide + */ + public int getThemeAttributeId(/*@StyleableRes*/ int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + if (data[index + AssetManager.STYLE_TYPE] == TypedValue.TYPE_ATTRIBUTE) { + return data[index + AssetManager.STYLE_DATA]; + } + return defValue; + } + + /** + * Retrieve the Drawable for the attribute at index. + *

+ * This method will throw an exception if the attribute is defined but is + * not a color or drawable resource. * * @param index Index of attribute to retrieve. * - * @return Drawable for the attribute, or null if not defined. + * @return Drawable for the attribute, or {@code null} if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not a color or drawable resource. */ - public Drawable getDrawable(int index) { + @Nullable + public Drawable getDrawable(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + final TypedValue value = mValue; if (getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value)) { - if (false) { - System.out.println("******************************************************************"); - System.out.println("Got drawable resource: type=" + value.type + " str=" + value.string + " int=0x" + Integer.toHexString(value.data) + " cookie=" + value.assetCookie); - System.out.println("******************************************************************"); + if (value.type == TypedValue.TYPE_ATTRIBUTE) { + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); } - return mResources.loadDrawable(value, value.resourceId); + return mResources.loadDrawable(value, value.resourceId, mTheme); } return null; } @@ -589,19 +897,23 @@ public class TypedArray { * This gets the resource ID of the selected attribute, and uses * {@link Resources#getTextArray Resources.getTextArray} of the owning * Resources object to retrieve its String[]. + *

+ * This method will throw an exception if the attribute is defined but is + * not a text array resource. * * @param index Index of attribute to retrieve. * - * @return CharSequence[] for the attribute, or null if not defined. + * @return CharSequence[] for the attribute, or {@code null} if not + * defined. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public CharSequence[] getTextArray(int index) { + public CharSequence[] getTextArray(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + final TypedValue value = mValue; if (getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value)) { - if (false) { - System.out.println("******************************************************************"); - System.out.println("Got drawable resource: type=" + value.type + " str=" + value.string + " int=0x" + Integer.toHexString(value.data) + " cookie=" + value.assetCookie); - System.out.println("******************************************************************"); - } return mResources.getTextArray(value.resourceId); } return null; @@ -614,26 +926,77 @@ public class TypedArray { * @param outValue TypedValue object in which to place the attribute's * data. * - * @return Returns true if the value was retrieved, else false. + * @return {@code true} if the value was retrieved, false otherwise. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public boolean getValue(int index, TypedValue outValue) { + public boolean getValue(/*@StyleableRes*/ int index, TypedValue outValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, outValue); } + /** + * Returns the type of attribute at the specified index. + * + * @param index Index of attribute whose type to retrieve. + * + * @return Attribute type. + * @throws RuntimeException if the TypedArray has already been recycled. + */ + public int getType(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + index *= AssetManager.STYLE_NUM_ENTRIES; + return mData[index + AssetManager.STYLE_TYPE]; + } + /** * Determines whether there is an attribute at index. + *

+ * Note: If the attribute was set to {@code @empty} or + * {@code @undefined}, this method returns {@code false}. * * @param index Index of attribute to retrieve. * * @return True if the attribute has a value, false otherwise. + * @throws RuntimeException if the TypedArray has already been recycled. */ - public boolean hasValue(int index) { + public boolean hasValue(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index + AssetManager.STYLE_TYPE]; return type != TypedValue.TYPE_NULL; } + /** + * Determines whether there is an attribute at index, returning + * {@code true} if the attribute was explicitly set to {@code @empty} and + * {@code false} only if the attribute was undefined. + * + * @param index Index of attribute to retrieve. + * + * @return True if the attribute has a value or is empty, false otherwise. + * @throws RuntimeException if the TypedArray has already been recycled. + */ + public boolean hasValueOrEmpty(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index + AssetManager.STYLE_TYPE]; + return type != TypedValue.TYPE_NULL || data[index + AssetManager.STYLE_DATA] == TypedValue.DATA_NULL_EMPTY; + } + /** * Retrieve the raw TypedValue for the attribute at index * and return a temporary object holding its data. This object is only @@ -644,8 +1007,13 @@ public class TypedArray { * @return Returns a TypedValue object if the attribute is defined, * containing its data; otherwise returns null. (You will not * receive a TypedValue whose type is TYPE_NULL.) + * @throws RuntimeException if the TypedArray has already been recycled. */ - public TypedValue peekValue(int index) { + public TypedValue peekValue(/*@StyleableRes*/ int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + final TypedValue value = mValue; if (getValueAt(index * AssetManager.STYLE_NUM_ENTRIES, value)) { return value; @@ -655,22 +1023,127 @@ public class TypedArray { /** * Returns a message about the parser state suitable for printing error messages. + * + * @return Human-readable description of current parser state. + * @throws RuntimeException if the TypedArray has already been recycled. */ public String getPositionDescription() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mXml != null ? mXml.getPositionDescription() : ""; } /** - * Give back a previously retrieved array, for later re-use. + * Recycles the TypedArray, to be re-used by a later caller. After calling + * this function you must not ever touch the typed array again. + * + * @throws RuntimeException if the TypedArray has already been recycled. */ public void recycle() { - synchronized (mResources.mAccessLock) { - TypedArray cached = mResources.mCachedStyledAttributes; - if (cached == null || cached.mData.length < mData.length) { - mXml = null; - mResources.mCachedStyledAttributes = this; - } + if (mRecycled) { + throw new RuntimeException(toString() + " recycled twice!"); } + + mRecycled = true; + + // These may have been set by the client. + mXml = null; + mTheme = null; + + mResources.mTypedArrayPool.release(this); + } + + /** + * Extracts theme attributes from a typed array for later resolution using + * {@link android.content.res.Resources.Theme#resolveAttributes(int[], int[])}. + * Removes the entries from the typed array so that subsequent calls to typed + * getters will return the default value without crashing. + * + * @return an array of length {@link #getIndexCount()} populated with theme + * attributes, or null if there are no theme attributes in the typed + * array + * @throws RuntimeException if the TypedArray has already been recycled. + * @hide + */ + @Nullable + public int[] extractThemeAttrs() { + return extractThemeAttrs(null); + } + + /** + * @hide + */ + @Nullable + public int[] extractThemeAttrs(@Nullable int[] scrap) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + int[] attrs = null; + + final int[] data = mData; + final int N = length(); + for (int i = 0; i < N; i++) { + final int index = i * AssetManager.STYLE_NUM_ENTRIES; + if (data[index + AssetManager.STYLE_TYPE] != TypedValue.TYPE_ATTRIBUTE) { + // Not an attribute, ignore. + continue; + } + + // Null the entry so that we can safely call getZzz(). + data[index + AssetManager.STYLE_TYPE] = TypedValue.TYPE_NULL; + + final int attr = data[index + AssetManager.STYLE_DATA]; + if (attr == 0) { + // Useless data, ignore. + continue; + } + + // Ensure we have a usable attribute array. + if (attrs == null) { + if (scrap != null && scrap.length == N) { + attrs = scrap; + Arrays.fill(attrs, 0); + } else { + attrs = new int[N]; + } + } + + attrs[i] = attr; + } + + return attrs; + } + + /** + * Return a mask of the configuration parameters for which the values in + * this typed array may change. + * + * @return Returns a mask of the changing configuration parameters, as + * defined by {@link android.content.pm.ActivityInfo}. + * @throws RuntimeException if the TypedArray has already been recycled. + * @see android.content.pm.ActivityInfo + */ + public int getChangingConfigurations() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + int changingConfig = 0; + + final int[] data = mData; + final int N = length(); + for (int i = 0; i < N; i++) { + final int index = i * AssetManager.STYLE_NUM_ENTRIES; + final int type = data[index + AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + continue; + } + changingConfig |= data[index + AssetManager.STYLE_CHANGING_CONFIGURATIONS]; + } + return changingConfig; } private boolean getValueAt(int index, TypedValue outValue) { @@ -694,32 +1167,26 @@ public class TypedArray { final int cookie = data[index + AssetManager.STYLE_ASSET_COOKIE]; if (cookie < 0) { if (mXml != null) { - CharSequence string = ((XmlBlock.Parser)mXml).getPooledString(data[index + AssetManager.STYLE_DATA]); - if (string != null) - return string; - } - if (data[index + AssetManager.STYLE_RESOURCE_ID] != 0) { - return mResources.mAssets.getResourceText(data[index + AssetManager.STYLE_RESOURCE_ID]); + return ((XmlBlock.Parser)mXml).getPooledString( + data[index + AssetManager.STYLE_DATA]); } + return null; } - // System.out.println("Getting pooled from: " + v); - return mResources.mAssets.getPooledString( - cookie, data[index + AssetManager.STYLE_DATA]); + return mAssets.getPooledString(cookie, data[index + AssetManager.STYLE_DATA]); } // FIXME public /*package*/ TypedArray(Resources resources, int[] data, int[] indices, int len) { mResources = resources; + mMetrics = mResources.mMetrics; + mAssets = mResources.mAssets; mData = data; mIndices = indices; mLength = len; } + @Override public String toString() { return Arrays.toString(mData); } - - public int getChangingConfigurations() { - return 0; - } } diff --git a/src/api-impl/android/graphics/Shader.java b/src/api-impl/android/graphics/Shader.java index 3c743c8b..06568ac7 100644 --- a/src/api-impl/android/graphics/Shader.java +++ b/src/api-impl/android/graphics/Shader.java @@ -8,5 +8,11 @@ public class Shader { REPEAT } + protected void init(long ni) { + } + public void setLocalMatrix(Matrix matrix) {} + + + protected void copyLocalMatrix(Shader dest) {} } diff --git a/src/api-impl/android/graphics/SweepGradient.java b/src/api-impl/android/graphics/SweepGradient.java new file mode 100644 index 00000000..bba12195 --- /dev/null +++ b/src/api-impl/android/graphics/SweepGradient.java @@ -0,0 +1,110 @@ +/* + * 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.graphics; + +public class SweepGradient extends Shader { + + private static final int TYPE_COLORS_AND_POSITIONS = 1; + private static final int TYPE_COLOR_START_AND_COLOR_END = 2; + + /** + * Type of the LinearGradient: can be either TYPE_COLORS_AND_POSITIONS or + * TYPE_COLOR_START_AND_COLOR_END. + */ + private int mType; + + private float mCx; + private float mCy; + private int[] mColors; + private float[] mPositions; + private int mColor0; + private int mColor1; + + /** + * A subclass of Shader that draws a sweep gradient around a center point. + * + * @param cx The x-coordinate of the center + * @param cy The y-coordinate of the center + * @param colors The colors to be distributed between around the center. + * There must be at least 2 colors in the array. + * @param positions May be NULL. The relative position of + * each corresponding color in the colors array, beginning + * with 0 and ending with 1.0. If the values are not + * monotonic, the drawing may produce unexpected results. + * If positions is NULL, then the colors are automatically + * spaced evenly. + */ + public SweepGradient(float cx, float cy, + int colors[], float positions[]) { + if (colors.length < 2) { + throw new IllegalArgumentException("needs >= 2 number of colors"); + } + if (positions != null && colors.length != positions.length) { + throw new IllegalArgumentException( + "color and position arrays must be of equal length"); + } + mType = TYPE_COLORS_AND_POSITIONS; + mCx = cx; + mCy = cy; + mColors = colors; + mPositions = positions; + init(nativeCreate1(cx, cy, colors, positions)); + } + + /** + * A subclass of Shader that draws a sweep gradient around a center point. + * + * @param cx The x-coordinate of the center + * @param cy The y-coordinate of the center + * @param color0 The color to use at the start of the sweep + * @param color1 The color to use at the end of the sweep + */ + public SweepGradient(float cx, float cy, int color0, int color1) { + mType = TYPE_COLOR_START_AND_COLOR_END; + mCx = cx; + mCy = cy; + mColor0 = color0; + mColor1 = color1; + init(nativeCreate2(cx, cy, color0, color1)); + } + + /** + * @hide + */ + //@Override + protected Shader copy() { + final SweepGradient copy; + switch (mType) { + case TYPE_COLORS_AND_POSITIONS: + copy = new SweepGradient(mCx, mCy, mColors.clone(), + mPositions != null ? mPositions.clone() : null); + break; + case TYPE_COLOR_START_AND_COLOR_END: + copy = new SweepGradient(mCx, mCy, mColor0, mColor1); + break; + default: + throw new IllegalArgumentException("SweepGradient should be created with either " + + + "colors and positions or start color and end color"); + } + copyLocalMatrix(copy); + return copy; + } + + private static native long nativeCreate1(float x, float y, int colors[], float positions[]); + private static native long nativeCreate2(float x, float y, int color0, int color1); +} diff --git a/src/api-impl/android/util/MathUtils.java b/src/api-impl/android/util/MathUtils.java new file mode 100644 index 00000000..ae6a15f0 --- /dev/null +++ b/src/api-impl/android/util/MathUtils.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2009 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.util; + +//import android.compat.annotation.UnsupportedAppUsage; +import android.graphics.Rect; + +/** + * A class that contains utility methods related to numbers. + * + * @hide Pending API council approval + */ +public final class MathUtils { + private static final float DEG_TO_RAD = 3.1415926f / 180.0f; + private static final float RAD_TO_DEG = 180.0f / 3.1415926f; + + private MathUtils() { + } + + //@UnsupportedAppUsage + public static float abs(float v) { + return v > 0 ? v : -v; + } + + //@UnsupportedAppUsage + public static int constrain(int amount, int low, int high) { + return amount < low ? low : (amount > high ? high : amount); + } + + public static long constrain(long amount, long low, long high) { + return amount < low ? low : (amount > high ? high : amount); + } + + //@UnsupportedAppUsage + public static float constrain(float amount, float low, float high) { + return amount < low ? low : (amount > high ? high : amount); + } + + public static float log(float a) { + return (float)Math.log(a); + } + + public static float exp(float a) { + return (float)Math.exp(a); + } + + public static float pow(float a, float b) { + return (float)Math.pow(a, b); + } + + public static float sqrt(float a) { + return (float)Math.sqrt(a); + } + + public static float max(float a, float b) { + return a > b ? a : b; + } + + //@UnsupportedAppUsage + public static float max(int a, int b) { + return a > b ? a : b; + } + + public static float max(float a, float b, float c) { + return a > b ? (a > c ? a : c) : (b > c ? b : c); + } + + public static float max(int a, int b, int c) { + return a > b ? (a > c ? a : c) : (b > c ? b : c); + } + + public static float min(float a, float b) { + return a < b ? a : b; + } + + public static float min(int a, int b) { + return a < b ? a : b; + } + + public static float min(float a, float b, float c) { + return a < b ? (a < c ? a : c) : (b < c ? b : c); + } + + public static float min(int a, int b, int c) { + return a < b ? (a < c ? a : c) : (b < c ? b : c); + } + + public static float dist(float x1, float y1, float x2, float y2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + return (float)Math.hypot(x, y); + } + + public static float dist(float x1, float y1, float z1, float x2, float y2, float z2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + final float z = (z2 - z1); + return (float)Math.sqrt(x * x + y * y + z * z); + } + + public static float mag(float a, float b) { + return (float)Math.hypot(a, b); + } + + public static float mag(float a, float b, float c) { + return (float)Math.sqrt(a * a + b * b + c * c); + } + + public static float sq(float v) { + return v * v; + } + + public static float dot(float v1x, float v1y, float v2x, float v2y) { + return v1x * v2x + v1y * v2y; + } + + public static float cross(float v1x, float v1y, float v2x, float v2y) { + return v1x * v2y - v1y * v2x; + } + + public static float radians(float degrees) { + return degrees * DEG_TO_RAD; + } + + public static float degrees(float radians) { + return radians * RAD_TO_DEG; + } + + public static float acos(float value) { + return (float)Math.acos(value); + } + + public static float asin(float value) { + return (float)Math.asin(value); + } + + public static float atan(float value) { + return (float)Math.atan(value); + } + + public static float atan2(float a, float b) { + return (float)Math.atan2(a, b); + } + + public static float tan(float angle) { + return (float)Math.tan(angle); + } + + //@UnsupportedAppUsage + public static float lerp(float start, float stop, float amount) { + return start + (stop - start) * amount; + } + + public static float lerp(int start, int stop, float amount) { + return lerp((float)start, (float)stop, amount); + } + + /** + * Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link + * #lerp}{@code (a, b, s)} + * + *

If {@code a == b}, then this function will return 0. + */ + public static float lerpInv(float a, float b, float value) { + return a != b ? ((value - a) / (b - a)) : 0.0f; + } + + /** + * Returns the single argument constrained between [0.0, 1.0]. + */ + public static float saturate(float value) { + return constrain(value, 0.0f, 1.0f); + } + + /** + * Returns the saturated (constrained between [0, 1]) result of {@link #lerpInv}. + */ + public static float lerpInvSat(float a, float b, float value) { + return saturate(lerpInv(a, b, value)); + } + + /** + * Returns an interpolated angle in degrees between a set of start and end + * angles. + *

+ * Unlike {@link #lerp(float, float, float)}, the direction and distance of + * travel is determined by the shortest angle between the start and end + * angles. For example, if the starting angle is 0 and the ending angle is + * 350, then the interpolated angle will be in the range [0,-10] rather + * than [0,350]. + * + * @param start the starting angle in degrees + * @param end the ending angle in degrees + * @param amount the position between start and end in the range [0,1] + * where 0 is the starting angle and 1 is the ending angle + * @return the interpolated angle in degrees + */ + public static float lerpDeg(float start, float end, float amount) { + final float minAngle = (((end - start) + 180) % 360) - 180; + return minAngle * amount + start; + } + + public static float norm(float start, float stop, float value) { + return (value - start) / (stop - start); + } + + public static float map(float minStart, float minStop, float maxStart, float maxStop, float value) { + return maxStart + (maxStop - maxStart) * ((value - minStart) / (minStop - minStart)); + } + + /** + * Calculates a value in [rangeMin, rangeMax] that maps value in [valueMin, valueMax] to + * returnVal in [rangeMin, rangeMax]. + *

+ * Always returns a constrained value in the range [rangeMin, rangeMax], even if value is + * outside [valueMin, valueMax]. + *

+ * Eg: + * constrainedMap(0f, 100f, 0f, 1f, 0.5f) = 50f + * constrainedMap(20f, 200f, 10f, 20f, 20f) = 200f + * constrainedMap(20f, 200f, 10f, 20f, 50f) = 200f + * constrainedMap(10f, 50f, 10f, 20f, 5f) = 10f + * + * @param rangeMin minimum of the range that should be returned. + * @param rangeMax maximum of the range that should be returned. + * @param valueMin minimum of range to map {@code value} to. + * @param valueMax maximum of range to map {@code value} to. + * @param value to map to the range [{@code valueMin}, {@code valueMax}]. Note, can be outside + * this range, resulting in a clamped value. + * @return the mapped value, constrained to [{@code rangeMin}, {@code rangeMax}. + */ + public static float constrainedMap( + float rangeMin, float rangeMax, float valueMin, float valueMax, float value) { + return lerp(rangeMin, rangeMax, lerpInvSat(valueMin, valueMax, value)); + } + + /** + * Perform Hermite interpolation between two values. + * Eg: + * smoothStep(0, 0.5f, 0.5f) = 1f + * smoothStep(0, 0.5f, 0.25f) = 0.5f + * + * @param start Left edge. + * @param end Right edge. + * @param x A value between {@code start} and {@code end}. + * @return A number between 0 and 1 representing where {@code x} is in the interpolation. + */ + public static float smoothStep(float start, float end, float x) { + return constrain((x - start) / (end - start), 0f, 1f); + } + + /** + * Returns the sum of the two parameters, or throws an exception if the resulting sum would + * cause an overflow or underflow. + * @throws IllegalArgumentException when overflow or underflow would occur. + */ + public static int addOrThrow(int a, int b) throws IllegalArgumentException { + if (b == 0) { + return a; + } + + if (b > 0 && a <= (Integer.MAX_VALUE - b)) { + return a + b; + } + + if (b < 0 && a >= (Integer.MIN_VALUE - b)) { + return a + b; + } + throw new IllegalArgumentException("Addition overflow: " + a + " + " + b); + } + + /** + * Resize a {@link Rect} so one size would be {@param largestSide}. + * + * @param outToResize Rectangle that will be resized. + * @param largestSide Size of the largest side. + */ + public static void fitRect(Rect outToResize, int largestSide) { + if (outToResize.isEmpty()) { + return; + } + float maxSize = Math.max(outToResize.width(), outToResize.height()); + outToResize.scale(largestSide / maxSize); + } +} diff --git a/src/api-impl/android/util/Pools.java b/src/api-impl/android/util/Pools.java new file mode 100644 index 00000000..d2410d63 --- /dev/null +++ b/src/api-impl/android/util/Pools.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2009 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.util; + +/** + * Helper class for crating pools of objects. An example use looks like this: + *

+ * public class MyPooledClass {
+ *
+ *     private static final SynchronizedPool sPool =
+ *             new SynchronizedPool(10);
+ *
+ *     public static MyPooledClass obtain() {
+ *         MyPooledClass instance = sPool.acquire();
+ *         return (instance != null) ? instance : new MyPooledClass();
+ *     }
+ *
+ *     public void recycle() {
+ *          // Clear state if needed.
+ *          sPool.release(this);
+ *     }
+ *
+ *     . . .
+ * }
+ * 
+ * + * @hide + */ +public final class Pools { + + /** + * Interface for managing a pool of objects. + * + * @param The pooled type. + */ + public static interface Pool { + + /** + * @return An instance from the pool if such, null otherwise. + */ + public T acquire(); + + /** + * Release an instance to the pool. + * + * @param instance The instance to release. + * @return Whether the instance was put in the pool. + * + * @throws IllegalStateException If the instance is already in the pool. + */ + public boolean release(T instance); + } + + private Pools() { + /* do nothing - hiding constructor */ + } + + /** + * Simple (non-synchronized) pool of objects. + * + * @param The pooled type. + */ + public static class SimplePool implements Pool { + private final Object[] mPool; + + private int mPoolSize; + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SimplePool(int maxPoolSize) { + if (maxPoolSize <= 0) { + throw new IllegalArgumentException("The max pool size must be > 0"); + } + mPool = new Object[maxPoolSize]; + } + + @Override + @SuppressWarnings("unchecked") + public T acquire() { + if (mPoolSize > 0) { + final int lastPooledIndex = mPoolSize - 1; + T instance = (T)mPool[lastPooledIndex]; + mPool[lastPooledIndex] = null; + mPoolSize--; + return instance; + } + return null; + } + + @Override + public boolean release(T instance) { + if (isInPool(instance)) { + throw new IllegalStateException("Already in the pool!"); + } + if (mPoolSize < mPool.length) { + mPool[mPoolSize] = instance; + mPoolSize++; + return true; + } + return false; + } + + private boolean isInPool(T instance) { + for (int i = 0; i < mPoolSize; i++) { + if (mPool[i] == instance) { + return true; + } + } + return false; + } + } + + /** + * Synchronized) pool of objects. + * + * @param The pooled type. + */ + public static class SynchronizedPool extends SimplePool { + private final Object mLock = new Object(); + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SynchronizedPool(int maxPoolSize) { + super(maxPoolSize); + } + + @Override + public T acquire() { + synchronized (mLock) { + return super.acquire(); + } + } + + @Override + public boolean release(T element) { + synchronized (mLock) { + return super.release(element); + } + } + } +} diff --git a/src/api-impl/android/util/TypedValue.java b/src/api-impl/android/util/TypedValue.java index baadb25b..e486ff29 100644 --- a/src/api-impl/android/util/TypedValue.java +++ b/src/api-impl/android/util/TypedValue.java @@ -210,6 +210,17 @@ public class TypedValue { /* ------------------------------------------------------------ */ + /** + * {@link #TYPE_NULL} data indicating the value was not specified. + */ + public static final int DATA_NULL_UNDEFINED = 0; + /** + * {@link #TYPE_NULL} data indicating the value was explicitly set to null. + */ + public static final int DATA_NULL_EMPTY = 1; + + /* ------------------------------------------------------------ */ + /** * If {@link #density} is equal to this value, then the density should be * treated as the system's default density value: {@link DisplayMetrics#DENSITY_DEFAULT}. diff --git a/src/api-impl/com/android/internal/util/GrowingArrayUtils.java b/src/api-impl/com/android/internal/util/GrowingArrayUtils.java index aee8ab4e..d6468f90 100644 --- a/src/api-impl/com/android/internal/util/GrowingArrayUtils.java +++ b/src/api-impl/com/android/internal/util/GrowingArrayUtils.java @@ -27,6 +27,7 @@ package com.android.internal.util; * @hide */ public final class GrowingArrayUtils { + /** * Appends an element to the end of the array, growing the array if there is no more room. * @param array The array to which to append the element. This must NOT be null. @@ -38,10 +39,11 @@ public final class GrowingArrayUtils { */ public static T[] append(T[] array, int currentSize, T element) { assert currentSize <= array.length; + if (currentSize + 1 > array.length) { @SuppressWarnings("unchecked") T[] newArray = ArrayUtils.newUnpaddedArray( - (Class) array.getClass().getComponentType(), growSize(currentSize)); + (Class)array.getClass().getComponentType(), growSize(currentSize)); System.arraycopy(array, 0, newArray, 0, currentSize); array = newArray; } @@ -54,6 +56,7 @@ public final class GrowingArrayUtils { */ public static int[] append(int[] array, int currentSize, int element) { assert currentSize <= array.length; + if (currentSize + 1 > array.length) { int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize)); System.arraycopy(array, 0, newArray, 0, currentSize); @@ -68,6 +71,7 @@ public final class GrowingArrayUtils { */ public static long[] append(long[] array, int currentSize, long element) { assert currentSize <= array.length; + if (currentSize + 1 > array.length) { long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize)); System.arraycopy(array, 0, newArray, 0, currentSize); @@ -82,6 +86,7 @@ public final class GrowingArrayUtils { */ public static boolean[] append(boolean[] array, int currentSize, boolean element) { assert currentSize <= array.length; + if (currentSize + 1 > array.length) { boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize)); System.arraycopy(array, 0, newArray, 0, currentSize); @@ -91,6 +96,21 @@ public final class GrowingArrayUtils { return array; } + /** + * Primitive float version of {@link #append(Object[], int, Object)}. + */ + public static float[] append(float[] array, int currentSize, float element) { + assert currentSize <= array.length; + + if (currentSize + 1 > array.length) { + float[] newArray = ArrayUtils.newUnpaddedFloatArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + /** * Inserts an element into the array at the specified index, growing the array if there is no * more room. @@ -104,14 +124,16 @@ public final class GrowingArrayUtils { */ public static T[] insert(T[] array, int currentSize, int index, T element) { assert currentSize <= array.length; + if (currentSize + 1 <= array.length) { System.arraycopy(array, index, array, index + 1, currentSize - index); array[index] = element; return array; } + @SuppressWarnings("unchecked") T[] newArray = ArrayUtils.newUnpaddedArray((Class)array.getClass().getComponentType(), - growSize(currentSize)); + growSize(currentSize)); System.arraycopy(array, 0, newArray, 0, index); newArray[index] = element; System.arraycopy(array, index, newArray, index + 1, array.length - index); @@ -123,11 +145,13 @@ public final class GrowingArrayUtils { */ public static int[] insert(int[] array, int currentSize, int index, int element) { assert currentSize <= array.length; + if (currentSize + 1 <= array.length) { System.arraycopy(array, index, array, index + 1, currentSize - index); array[index] = element; return array; } + int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize)); System.arraycopy(array, 0, newArray, 0, index); newArray[index] = element; @@ -140,11 +164,13 @@ public final class GrowingArrayUtils { */ public static long[] insert(long[] array, int currentSize, int index, long element) { assert currentSize <= array.length; + if (currentSize + 1 <= array.length) { System.arraycopy(array, index, array, index + 1, currentSize - index); array[index] = element; return array; } + long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize)); System.arraycopy(array, 0, newArray, 0, index); newArray[index] = element; @@ -157,11 +183,13 @@ public final class GrowingArrayUtils { */ public static boolean[] insert(boolean[] array, int currentSize, int index, boolean element) { assert currentSize <= array.length; + if (currentSize + 1 <= array.length) { System.arraycopy(array, index, array, index + 1, currentSize - index); array[index] = element; return array; } + boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize)); System.arraycopy(array, 0, newArray, 0, index); newArray[index] = element; @@ -177,6 +205,7 @@ public final class GrowingArrayUtils { public static int growSize(int currentSize) { return currentSize <= 4 ? 8 : currentSize * 2; } + // Uninstantiable private GrowingArrayUtils() {} -} \ No newline at end of file +} diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index 90146aa7..faf78abf 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -118,9 +118,13 @@ srcs = [ 'android/content/res/AssetManager.java', 'android/content/res/ColorStateList.java', 'android/content/res/CompatibilityInfo.java', + 'android/content/res/ComplexColor.java', 'android/content/res/Configuration.java', + 'android/content/res/ConfigurationBoundResourceCache.java', + 'android/content/res/GradientColor.java', 'android/content/res/Resources.java', 'android/content/res/StringBlock.java', + 'android/content/res/ThemedResourceCache.java', 'android/content/res/TypedArray.java', 'android/content/res/XmlBlock.java', 'android/content/res/XmlResourceParser.java', @@ -210,6 +214,7 @@ srcs = [ 'android/graphics/RectF.java', 'android/graphics/Region.java', 'android/graphics/Shader.java', + 'android/graphics/SweepGradient.java', 'android/graphics/Typeface.java', 'android/graphics/Xfermode.java', 'android/graphics/drawable/Animatable.java', @@ -426,6 +431,7 @@ srcs = [ 'android/util/JsonScope.java', 'android/util/JsonToken.java', 'android/util/JsonWriter.java', + 'android/util/MathUtils.java', 'android/util/LayoutDirection.java', 'android/util/Log.java', 'android/util/LongSparseArray.java',