| /* |
| * 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 org.robolectric.shadows; |
| |
| import android.util.TypedValue; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Helper class to provide various conversion method used in handling android resources. |
| */ |
| public final class ResourceHelper { |
| |
| private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]*(?:\\.[0-9]+)?)(.*)"); |
| private final static float[] sFloatOut = new float[1]; |
| |
| private final static TypedValue mValue = new TypedValue(); |
| |
| private final static Class<?> androidInternalR; |
| |
| static { |
| try { |
| androidInternalR = Class.forName("com.android.internal.R$id"); |
| } catch (ClassNotFoundException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** |
| * Returns the color value represented by the given string value |
| * |
| * @param value the color value |
| * @return the color as an int |
| * @throws NumberFormatException if the conversion failed. |
| */ |
| public static int getColor(String value) { |
| if (value != null) { |
| if (value.startsWith("#") == false) { |
| throw new NumberFormatException( |
| String.format("Color value '%s' must start with #", value)); |
| } |
| |
| value = value.substring(1); |
| |
| // make sure it's not longer than 32bit |
| if (value.length() > 8) { |
| throw new NumberFormatException(String.format( |
| "Color value '%s' is too long. Format is either" + |
| "#AARRGGBB, #RRGGBB, #RGB, or #ARGB", |
| value)); |
| } |
| |
| if (value.length() == 3) { // RGB format |
| char[] color = new char[8]; |
| color[0] = color[1] = 'F'; |
| color[2] = color[3] = value.charAt(0); |
| color[4] = color[5] = value.charAt(1); |
| color[6] = color[7] = value.charAt(2); |
| value = new String(color); |
| } else if (value.length() == 4) { // ARGB format |
| char[] color = new char[8]; |
| color[0] = color[1] = value.charAt(0); |
| color[2] = color[3] = value.charAt(1); |
| color[4] = color[5] = value.charAt(2); |
| color[6] = color[7] = value.charAt(3); |
| value = new String(color); |
| } else if (value.length() == 6) { |
| value = "FF" + value; |
| } |
| |
| // this is a RRGGBB or AARRGGBB value |
| |
| // Integer.parseInt will fail to inferFromValue strings like "ff191919", so we use |
| // a Long, but cast the result back into an int, since we know that we're only |
| // dealing with 32 bit values. |
| return (int)Long.parseLong(value, 16); |
| } |
| |
| throw new NumberFormatException(); |
| } |
| |
| /** |
| * Returns the TypedValue color type represented by the given string value |
| * |
| * @param value the color value |
| * @return the color as an int. For backwards compatibility, will return a default of ARGB8 if |
| * value format is unrecognized. |
| */ |
| public static int getColorType(String value) { |
| if (value != null && value.startsWith("#")) { |
| switch (value.length()) { |
| case 4: |
| return TypedValue.TYPE_INT_COLOR_RGB4; |
| case 5: |
| return TypedValue.TYPE_INT_COLOR_ARGB4; |
| case 7: |
| return TypedValue.TYPE_INT_COLOR_RGB8; |
| case 9: |
| return TypedValue.TYPE_INT_COLOR_ARGB8; |
| } |
| } |
| return TypedValue.TYPE_INT_COLOR_ARGB8; |
| } |
| |
| public static int getInternalResourceId(String idName) { |
| try { |
| return (int) androidInternalR.getField(idName).get(null); |
| } catch (NoSuchFieldException | IllegalAccessException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| // ------- TypedValue stuff |
| // This is taken from //device/libs/utils/ResourceTypes.cpp |
| |
| private static final class UnitEntry { |
| String name; |
| int type; |
| int unit; |
| float scale; |
| |
| UnitEntry(String name, int type, int unit, float scale) { |
| this.name = name; |
| this.type = type; |
| this.unit = unit; |
| this.scale = scale; |
| } |
| } |
| |
| private final static UnitEntry[] sUnitNames = new UnitEntry[] { |
| new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), |
| new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), |
| new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), |
| new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), |
| new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), |
| new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), |
| new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), |
| new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), |
| new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), |
| }; |
| |
| /** |
| * Returns the raw value from the given attribute float-type value string. |
| * This object is only valid until the next call on to {@link ResourceHelper}. |
| * |
| * @param attribute Attribute name. |
| * @param value Attribute value. |
| * @param requireUnit whether the value is expected to contain a unit. |
| * @return The typed value. |
| */ |
| public static TypedValue getValue(String attribute, String value, boolean requireUnit) { |
| if (parseFloatAttribute(attribute, value, mValue, requireUnit)) { |
| return mValue; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Parse a float attribute and return the parsed value into a given TypedValue. |
| * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false. |
| * @param value the string value of the attribute |
| * @param outValue the TypedValue to receive the parsed value |
| * @param requireUnit whether the value is expected to contain a unit. |
| * @return true if success. |
| */ |
| public static boolean parseFloatAttribute(String attribute, String value, |
| TypedValue outValue, boolean requireUnit) { |
| assert requireUnit == false || attribute != null; |
| |
| // remove the space before and after |
| value = value.trim(); |
| int len = value.length(); |
| |
| if (len <= 0) { |
| return false; |
| } |
| |
| // check that there's no non ascii characters. |
| char[] buf = value.toCharArray(); |
| for (int i = 0 ; i < len ; i++) { |
| if (buf[i] > 255) { |
| return false; |
| } |
| } |
| |
| // check the first character |
| if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.' && buf[0] != '-') { |
| return false; |
| } |
| |
| // now look for the string that is after the float... |
| Matcher m = sFloatPattern.matcher(value); |
| if (m.matches()) { |
| String f_str = m.group(1); |
| String end = m.group(2); |
| |
| float f; |
| try { |
| f = Float.parseFloat(f_str); |
| } catch (NumberFormatException e) { |
| // this shouldn't happen with the regexp above. |
| return false; |
| } |
| |
| if (end.length() > 0 && end.charAt(0) != ' ') { |
| // Might be a unit... |
| if (parseUnit(end, outValue, sFloatOut)) { |
| computeTypedValue(outValue, f, sFloatOut[0]); |
| return true; |
| } |
| return false; |
| } |
| |
| // make sure it's only spaces at the end. |
| end = end.trim(); |
| |
| if (end.length() == 0) { |
| if (outValue != null) { |
| outValue.assetCookie = 0; |
| outValue.string = null; |
| |
| if (requireUnit == false) { |
| outValue.type = TypedValue.TYPE_FLOAT; |
| outValue.data = Float.floatToIntBits(f); |
| } else { |
| // no unit when required? Use dp and out an error. |
| applyUnit(sUnitNames[1], outValue, sFloatOut); |
| computeTypedValue(outValue, f, sFloatOut[0]); |
| |
| System.out.println(String.format( |
| "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!", |
| value, attribute)); |
| } |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| private static void computeTypedValue(TypedValue outValue, float value, float scale) { |
| value *= scale; |
| boolean neg = value < 0; |
| if (neg) { |
| value = -value; |
| } |
| long bits = (long)(value*(1<<23)+.5f); |
| int radix; |
| int shift; |
| if ((bits&0x7fffff) == 0) { |
| // Always use 23p0 if there is no fraction, just to make |
| // things easier to read. |
| radix = TypedValue.COMPLEX_RADIX_23p0; |
| shift = 23; |
| } else if ((bits&0xffffffffff800000L) == 0) { |
| // Magnitude is zero -- can fit in 0 bits of precision. |
| radix = TypedValue.COMPLEX_RADIX_0p23; |
| shift = 0; |
| } else if ((bits&0xffffffff80000000L) == 0) { |
| // Magnitude can fit in 8 bits of precision. |
| radix = TypedValue.COMPLEX_RADIX_8p15; |
| shift = 8; |
| } else if ((bits&0xffffff8000000000L) == 0) { |
| // Magnitude can fit in 16 bits of precision. |
| radix = TypedValue.COMPLEX_RADIX_16p7; |
| shift = 16; |
| } else { |
| // Magnitude needs entire range, so no fractional part. |
| radix = TypedValue.COMPLEX_RADIX_23p0; |
| shift = 23; |
| } |
| int mantissa = (int)( |
| (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); |
| if (neg) { |
| mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; |
| } |
| outValue.data |= |
| (radix<<TypedValue.COMPLEX_RADIX_SHIFT) |
| | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT); |
| } |
| |
| private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { |
| str = str.trim(); |
| |
| for (UnitEntry unit : sUnitNames) { |
| if (unit.name.equals(str)) { |
| applyUnit(unit, outValue, outScale); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) { |
| outValue.type = unit.type; |
| outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; |
| outScale[0] = unit.scale; |
| } |
| } |
| |