/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.layoutlib.bridge; import com.android.internal.util.XmlUtils; import com.android.layoutlib.api.IResourceValue; import com.android.layoutlib.api.IStyleResourceValue; import org.kxml2.io.KXmlParser; import org.xmlpull.v1.XmlPullParser; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.ViewGroup.LayoutParams; import java.io.File; import java.io.FileReader; import java.util.Map; /** * TODO: describe. */ public final class BridgeTypedArray extends TypedArray { @SuppressWarnings("hiding") private BridgeResources mResources; private BridgeContext mContext; @SuppressWarnings("hiding") private IResourceValue[] mData; private String[] mNames; private final boolean mPlatformFile; public BridgeTypedArray(BridgeResources resources, BridgeContext context, int len, boolean platformFile) { super(null, null, null, 0); mResources = resources; mContext = context; mPlatformFile = platformFile; mData = new IResourceValue[len]; mNames = new String[len]; } /** A bridge-specific method that sets a value in the type array */ public void bridgeSetValue(int index, String name, IResourceValue value) { mData[index] = value; mNames[index] = name; } /** * Seals the array after all calls to {@link #bridgeSetValue(int, String, IResourceValue)} have * been done. * <p/>This allows to compute the list of non default values, permitting * {@link #getIndexCount()} to return the proper value. */ public void sealArray() { // fills TypedArray.mIndices which is used to implement getIndexCount/getIndexAt // first count the array size int count = 0; for (IResourceValue data : mData) { if (data != null) { count++; } } // allocate the table with an extra to store the size mIndices = new int[count+1]; mIndices[0] = count; // fill the array with the indices. int index = 1; for (int i = 0 ; i < mData.length ; i++) { if (mData[i] != null) { mIndices[index++] = i; } } } /** * Return the number of values in this array. */ @Override public int length() { return mData.length; } /** * Return the Resources object this array was loaded from. */ @Override public Resources getResources() { return mResources; } /** * Retrieve the styled string value for the attribute at <var>index</var>. * * @param index Index of attribute to retrieve. * * @return CharSequence holding string data. May be styled. Returns * null if the attribute is not defined. */ @Override public CharSequence getText(int index) { if (mData[index] != null) { // FIXME: handle styled strings! return mData[index].getValue(); } return null; } /** * Retrieve the string value for the attribute at <var>index</var>. * * @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. */ @Override public String getString(int index) { if (mData[index] != null) { return mData[index].getValue(); } return null; } /** * Retrieve the boolean value for the attribute at <var>index</var>. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined. * * @return Attribute boolean value, or defValue if not defined. */ @Override public boolean getBoolean(int index, boolean defValue) { if (mData[index] == null) { return defValue; } String s = mData[index].getValue(); if (s != null) { return XmlUtils.convertValueToBoolean(s, defValue); } return defValue; } /** * Retrieve the integer value for the attribute at <var>index</var>. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined. * * @return Attribute int value, or defValue if not defined. */ @Override public int getInt(int index, int defValue) { if (mData[index] == null) { return defValue; } String s = mData[index].getValue(); try { return (s == null) ? defValue : XmlUtils.convertValueToInt(s, defValue); } catch (NumberFormatException e) { // pass } // Field is not null and is not an integer. // Check for possible constants and try to find them. // Get the map of attribute-constant -> IntegerValue Map<String, Integer> map = Bridge.getEnumValues(mNames[index]); if (map != null) { // accumulator to store the value of the 1+ constants. int result = 0; // split the value in case this is a mix of several flags. String[] keywords = s.split("\\|"); for (String keyword : keywords) { Integer i = map.get(keyword.trim()); if (i != null) { result |= i.intValue(); } else { mContext.getLogger().warning(String.format( "Unknown constant \"%s\" in attribute \"%2$s\"", keyword, mNames[index])); } } return result; } return defValue; } /** * Retrieve the float value for the attribute at <var>index</var>. * * @param index Index of attribute to retrieve. * * @return Attribute float value, or defValue if not defined.. */ @Override public float getFloat(int index, float defValue) { if (mData[index] == null) { return defValue; } String s = mData[index].getValue(); if (s != null) { try { return Float.parseFloat(s); } catch (NumberFormatException e) { mContext.getLogger().warning(String.format( "Unable to convert \"%s\" into a float in attribute \"%2$s\"", s, mNames[index])); // we'll return the default value below. } } return defValue; } /** * Retrieve the color value for the attribute at <var>index</var>. If * the attribute references a color resource holding a complex * {@link android.content.res.ColorStateList}, then the default color from * the set is returned. * * @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. */ @Override public int getColor(int index, int defValue) { if (mData[index] == null) { return defValue; } String s = mData[index].getValue(); try { return ResourceHelper.getColor(s); } catch (NumberFormatException e) { mContext.getLogger().warning(String.format( "Unable to convert \"%s\" into a color in attribute \"%2$s\"", s, mNames[index])); // we'll return the default value below. } return defValue; } /** * Retrieve the ColorStateList for the attribute at <var>index</var>. * The value may be either a single solid color or a reference to * a color or complex {@link android.content.res.ColorStateList} description. * * @param index Index of attribute to retrieve. * * @return ColorStateList for the attribute, or null if not defined. */ @Override public ColorStateList getColorStateList(int index) { if (mData[index] == null) { return null; } String value = mData[index].getValue(); if (value == null) { return null; } try { int color = ResourceHelper.getColor(value); return ColorStateList.valueOf(color); } catch (NumberFormatException e) { // if it's not a color value, we'll attempt to read the xml based color below. } // let the framework inflate the ColorStateList from the XML file. try { File f = new File(value); if (f.isFile()) { KXmlParser parser = new KXmlParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); parser.setInput(new FileReader(f)); ColorStateList colorStateList = ColorStateList.createFromXml( mContext.getResources(), // FIXME: we need to know if this resource is platform or not new BridgeXmlBlockParser(parser, mContext, false)); return colorStateList; } } catch (Exception e) { // this is an error and not warning since the file existence is checked before // attempting to parse it. mContext.getLogger().error(e); // return null below. } // looks like were unable to resolve the color value. mContext.getLogger().warning(String.format( "Unable to resolve color value \"%1$s\" in attribute \"%2$s\"", value, mNames[index])); return null; } /** * Retrieve the integer value for the attribute at <var>index</var>. * * @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. */ @Override public int getInteger(int index, int defValue) { if (mData[index] == null) { return defValue; } String s = mData[index].getValue(); if (s != null) { try { return Integer.parseInt(s); } catch (NumberFormatException e) { mContext.getLogger().warning(String.format( "Unable to convert \"%s\" into a integer in attribute \"%2$s\"", s, mNames[index])); // The default value is returned below. } } return defValue; } /** * Retrieve a dimensional unit attribute at <var>index</var>. Unit * conversions are based on the current {@link DisplayMetrics} * associated with the resources this {@link TypedArray} object * came from. * * @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. * * @see #getDimensionPixelOffset * @see #getDimensionPixelSize */ @Override public float getDimension(int index, float defValue) { if (mData[index] == null) { return defValue; } String s = mData[index].getValue(); if (s == null) { return defValue; } else if (s.equals(BridgeConstants.FILL_PARENT)) { return LayoutParams.FILL_PARENT; } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { return LayoutParams.WRAP_CONTENT; } if (ResourceHelper.stringToFloat(s, mValue)) { return mValue.getDimension(mResources.mMetrics); } // looks like we were unable to resolve the dimension value mContext.getLogger().warning(String.format( "Unable to resolve dimension value \"%1$s\" in attribute \"%2$s\"", s, mNames[index])); return defValue; } /** * Retrieve a dimensional unit attribute at <var>index</var> for use * as an offset in raw pixels. This is the same as * {@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. * * @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. * * @see #getDimension * @see #getDimensionPixelSize */ @Override public int getDimensionPixelOffset(int index, int defValue) { return (int) getDimension(index, defValue); } /** * Retrieve a dimensional unit attribute at <var>index</var> for use * as a size in raw pixels. This is the same as * {@link #getDimension}, except the returned value is converted to * 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. * * @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. * * @see #getDimension * @see #getDimensionPixelOffset */ @Override public int getDimensionPixelSize(int index, int defValue) { if (mData[index] == null) { return defValue; } String s = mData[index].getValue(); if (s == null) { return defValue; } else if (s.equals(BridgeConstants.FILL_PARENT)) { return LayoutParams.FILL_PARENT; } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { return LayoutParams.WRAP_CONTENT; } // FIXME huh? float f = getDimension(index, defValue); final int res = (int)(f+0.5f); if (res != 0) return res; if (f == 0) return 0; if (f > 0) return 1; throw new UnsupportedOperationException("Can't convert to dimension: " + Integer.toString(index)); } /** * Special version of {@link #getDimensionPixelSize} for retrieving * {@link android.view.ViewGroup}'s layout_width and layout_height * attributes. This is only here for performance reasons; applications * should use {@link #getDimensionPixelSize}. * * @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. */ @Override public int getLayoutDimension(int index, String name) { return getDimensionPixelSize(index, 0); } /** * Retrieve a fractional unit attribute at <var>index</var>. * * @param index Index of attribute to retrieve. * @param base The base value of this fraction. In other words, a * standard fraction is multiplied by this value. * @param pbase The parent base value of this fraction. In other * words, a parent fraction (nn%p) is multiplied by this * value. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute fractional value multiplied by the appropriate * base value, or defValue if not defined. */ @Override public float getFraction(int index, int base, int pbase, float defValue) { if (mData[index] == null) { return defValue; } String value = mData[index].getValue(); if (value == null) { return defValue; } if (ResourceHelper.stringToFloat(value, mValue)) { return mValue.getFraction(base, pbase); } // looks like we were unable to resolve the fraction value mContext.getLogger().warning(String.format( "Unable to resolve fraction value \"%1$s\" in attribute \"%2$s\"", value, mNames[index])); return defValue; } /** * Retrieve the resource identifier for the attribute at * <var>index</var>. 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 * final resource value that was found, <em>not</em> necessarily the * original resource that was specified by the attribute. * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. * * @return Attribute resource identifier, or defValue if not defined. */ @Override public int getResourceId(int index, int defValue) { // get the IResource for this index IResourceValue resValue = mData[index]; // no data, return the default value. if (resValue == null) { return defValue; } // check if this is a style resource if (resValue instanceof IStyleResourceValue) { // get the id that will represent this style. return mContext.getDynamicIdByStyle((IStyleResourceValue)resValue); } // if the attribute was a reference to an id, and not a declaration of an id (@+id), then // the xml attribute value was "resolved" which leads us to a IResourceValue with // getType() returning "id" and getName() returning the id name // (and getValue() returning null!). We need to handle this! if (resValue.getType() != null && resValue.getType().equals(BridgeConstants.RES_ID)) { // if this is a framework id if (mPlatformFile || resValue.isFramework()) { // look for idName in the android R classes return mContext.getFrameworkIdValue(resValue.getName(), defValue); } // look for idName in the project R class. return mContext.getProjectIdValue(resValue.getName(), defValue); } // else, try to get the value, and resolve it somehow. String value = resValue.getValue(); if (value == null) { return defValue; } // if the value is just an integer, return it. try { int i = Integer.parseInt(value); if (Integer.toString(i).equals(value)) { return i; } } catch (NumberFormatException e) { // pass } // Handle the @id/<name>, @+id/<name> and @android:id/<name> // We need to return the exact value that was compiled (from the various R classes), // as these values can be reused internally with calls to findViewById(). // There's a trick with platform layouts that not use "android:" but their IDs are in // fact in the android.R and com.android.internal.R classes. // The field mPlatformFile will indicate that all IDs are to be looked up in the android R // classes exclusively. // if this is a reference to an id, find it. if (value.startsWith("@id/") || value.startsWith("@+") || value.startsWith("@android:id/")) { int pos = value.indexOf('/'); String idName = value.substring(pos + 1); // if this is a framework id if (mPlatformFile || value.startsWith("@android") || value.startsWith("@+android")) { // look for idName in the android R classes return mContext.getFrameworkIdValue(idName, defValue); } // look for idName in the project R class. return mContext.getProjectIdValue(idName, defValue); } // not a direct id valid reference? resolve it Integer idValue = null; if (resValue.isFramework()) { idValue = Bridge.getResourceValue(resValue.getType(), resValue.getName()); } else { idValue = mContext.getProjectCallback().getResourceValue( resValue.getType(), resValue.getName()); } if (idValue != null) { return idValue.intValue(); } mContext.getLogger().warning(String.format( "Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index])); return defValue; } /** * Retrieve the Drawable for the attribute at <var>index</var>. 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. * * @param index Index of attribute to retrieve. * * @return Drawable for the attribute, or null if not defined. */ @Override public Drawable getDrawable(int index) { if (mData[index] == null) { return null; } String value = mData[index].getValue(); if (value == null || BridgeConstants.REFERENCE_NULL.equals(value)) { return null; } Drawable d = ResourceHelper.getDrawable(value, mContext, mData[index].isFramework()); if (d != null) { return d; } // looks like we were unable to resolve the drawable mContext.getLogger().warning(String.format( "Unable to resolve drawable \"%1$s\" in attribute \"%2$s\"", value, mNames[index])); return null; } /** * Retrieve the CharSequence[] for the attribute at <var>index</var>. * 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[]. * * @param index Index of attribute to retrieve. * * @return CharSequence[] for the attribute, or null if not defined. */ @Override public CharSequence[] getTextArray(int index) { if (mData[index] == null) { return null; } String value = mData[index].getValue(); if (value != null) { return new CharSequence[] { value }; } mContext.getLogger().warning(String.format( String.format("Unknown value for getTextArray(%d) => %s", //DEBUG index, mData[index].getName()))); return null; } /** * Retrieve the raw TypedValue for the attribute at <var>index</var>. * * @param index Index of attribute to retrieve. * @param outValue TypedValue object in which to place the attribute's * data. * * @return Returns true if the value was retrieved, else false. */ @Override public boolean getValue(int index, TypedValue outValue) { if (mData[index] == null) { return false; } String s = mData[index].getValue(); return ResourceHelper.stringToFloat(s, outValue); } /** * Determines whether there is an attribute at <var>index</var>. * * @param index Index of attribute to retrieve. * * @return True if the attribute has a value, false otherwise. */ @Override public boolean hasValue(int index) { return mData[index] != null; } /** * Retrieve the raw TypedValue for the attribute at <var>index</var> * and return a temporary object holding its data. This object is only * valid until the next call on to {@link TypedArray}. * * @param index Index of attribute to retrieve. * * @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.) */ @Override public TypedValue peekValue(int index) { if (getValue(index, mValue)) { return mValue; } return null; } /** * Returns a message about the parser state suitable for printing error messages. */ @Override public String getPositionDescription() { return "<internal -- stub if needed>"; } /** * Give back a previously retrieved StyledAttributes, for later re-use. */ @Override public void recycle() { // pass } @Override public boolean getValueAt(int index, TypedValue outValue) { // pass return false; } @Override public String toString() { return mData.toString(); } }