Home | History | Annotate | Download | only in impl
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.layoutlib.bridge.impl;
     18 
     19 import com.android.SdkConstants;
     20 import com.android.ide.common.rendering.api.DensityBasedResourceValue;
     21 import com.android.ide.common.rendering.api.LayoutLog;
     22 import com.android.ide.common.rendering.api.RenderResources;
     23 import com.android.ide.common.rendering.api.ResourceValue;
     24 import com.android.internal.util.XmlUtils;
     25 import com.android.layoutlib.bridge.Bridge;
     26 import com.android.layoutlib.bridge.android.BridgeContext;
     27 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
     28 import com.android.ninepatch.NinePatch;
     29 import com.android.ninepatch.NinePatchChunk;
     30 import com.android.resources.Density;
     31 
     32 import org.xmlpull.v1.XmlPullParser;
     33 import org.xmlpull.v1.XmlPullParserException;
     34 
     35 import android.annotation.NonNull;
     36 import android.content.res.ColorStateList;
     37 import android.content.res.Resources.Theme;
     38 import android.graphics.Bitmap;
     39 import android.graphics.Bitmap_Delegate;
     40 import android.graphics.NinePatch_Delegate;
     41 import android.graphics.Rect;
     42 import android.graphics.drawable.BitmapDrawable;
     43 import android.graphics.drawable.ColorDrawable;
     44 import android.graphics.drawable.Drawable;
     45 import android.graphics.drawable.NinePatchDrawable;
     46 import android.util.TypedValue;
     47 
     48 import java.io.File;
     49 import java.io.FileInputStream;
     50 import java.io.IOException;
     51 import java.io.InputStream;
     52 import java.net.MalformedURLException;
     53 import java.util.regex.Matcher;
     54 import java.util.regex.Pattern;
     55 
     56 /**
     57  * Helper class to provide various conversion method used in handling android resources.
     58  */
     59 public final class ResourceHelper {
     60 
     61     private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)");
     62     private final static float[] sFloatOut = new float[1];
     63 
     64     private final static TypedValue mValue = new TypedValue();
     65 
     66     /**
     67      * Returns the color value represented by the given string value
     68      * @param value the color value
     69      * @return the color as an int
     70      * @throws NumberFormatException if the conversion failed.
     71      */
     72     public static int getColor(String value) {
     73         if (value != null) {
     74             if (!value.startsWith("#")) {
     75                 if (value.startsWith(SdkConstants.PREFIX_THEME_REF)) {
     76                     throw new NumberFormatException(String.format(
     77                             "Attribute '%s' not found. Are you using the right theme?", value));
     78                 }
     79                 throw new NumberFormatException(
     80                         String.format("Color value '%s' must start with #", value));
     81             }
     82 
     83             value = value.substring(1);
     84 
     85             // make sure it's not longer than 32bit
     86             if (value.length() > 8) {
     87                 throw new NumberFormatException(String.format(
     88                         "Color value '%s' is too long. Format is either" +
     89                         "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
     90                         value));
     91             }
     92 
     93             if (value.length() == 3) { // RGB format
     94                 char[] color = new char[8];
     95                 color[0] = color[1] = 'F';
     96                 color[2] = color[3] = value.charAt(0);
     97                 color[4] = color[5] = value.charAt(1);
     98                 color[6] = color[7] = value.charAt(2);
     99                 value = new String(color);
    100             } else if (value.length() == 4) { // ARGB format
    101                 char[] color = new char[8];
    102                 color[0] = color[1] = value.charAt(0);
    103                 color[2] = color[3] = value.charAt(1);
    104                 color[4] = color[5] = value.charAt(2);
    105                 color[6] = color[7] = value.charAt(3);
    106                 value = new String(color);
    107             } else if (value.length() == 6) {
    108                 value = "FF" + value;
    109             }
    110 
    111             // this is a RRGGBB or AARRGGBB value
    112 
    113             // Integer.parseInt will fail to parse strings like "ff191919", so we use
    114             // a Long, but cast the result back into an int, since we know that we're only
    115             // dealing with 32 bit values.
    116             return (int)Long.parseLong(value, 16);
    117         }
    118 
    119         throw new NumberFormatException();
    120     }
    121 
    122     public static ColorStateList getColorStateList(ResourceValue resValue, BridgeContext context) {
    123         String value = resValue.getValue();
    124         if (value != null && !RenderResources.REFERENCE_NULL.equals(value)) {
    125             // first check if the value is a file (xml most likely)
    126             File f = new File(value);
    127             if (f.isFile()) {
    128                 try {
    129                     // let the framework inflate the ColorStateList from the XML file, by
    130                     // providing an XmlPullParser
    131                     XmlPullParser parser = ParserFactory.create(f);
    132 
    133                     BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
    134                             parser, context, resValue.isFramework());
    135                     try {
    136                         return ColorStateList.createFromXml(context.getResources(), blockParser);
    137                     } finally {
    138                         blockParser.ensurePopped();
    139                     }
    140                 } catch (XmlPullParserException e) {
    141                     Bridge.getLog().error(LayoutLog.TAG_BROKEN,
    142                             "Failed to configure parser for " + value, e, null /*data*/);
    143                     // we'll return null below.
    144                 } catch (Exception e) {
    145                     // this is an error and not warning since the file existence is
    146                     // checked before attempting to parse it.
    147                     Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
    148                             "Failed to parse file " + value, e, null /*data*/);
    149 
    150                     return null;
    151                 }
    152             } else {
    153                 // try to load the color state list from an int
    154                 try {
    155                     int color = ResourceHelper.getColor(value);
    156                     return ColorStateList.valueOf(color);
    157                 } catch (NumberFormatException e) {
    158                     Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
    159                             "Failed to convert " + value + " into a ColorStateList", e,
    160                             null /*data*/);
    161                     return null;
    162                 }
    163             }
    164         }
    165 
    166         return null;
    167     }
    168 
    169     /**
    170      * Returns a drawable from the given value.
    171      * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable,
    172      * or an hexadecimal color
    173      * @param context the current context
    174      */
    175     public static Drawable getDrawable(ResourceValue value, BridgeContext context) {
    176         return getDrawable(value, context, null);
    177     }
    178 
    179     /**
    180      * Returns a drawable from the given value.
    181      * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable,
    182      * or an hexadecimal color
    183      * @param context the current context
    184      * @param theme the theme to be used to inflate the drawable.
    185      */
    186     public static Drawable getDrawable(ResourceValue value, BridgeContext context, Theme theme) {
    187         if (value == null) {
    188             return null;
    189         }
    190         String stringValue = value.getValue();
    191         if (RenderResources.REFERENCE_NULL.equals(stringValue)) {
    192             return null;
    193         }
    194 
    195         String lowerCaseValue = stringValue.toLowerCase();
    196 
    197         Density density = Density.MEDIUM;
    198         if (value instanceof DensityBasedResourceValue) {
    199             density =
    200                 ((DensityBasedResourceValue)value).getResourceDensity();
    201         }
    202 
    203 
    204         if (lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) {
    205             File file = new File(stringValue);
    206             if (file.isFile()) {
    207                 try {
    208                     return getNinePatchDrawable(
    209                             new FileInputStream(file), density, value.isFramework(),
    210                             stringValue, context);
    211                 } catch (IOException e) {
    212                     // failed to read the file, we'll return null below.
    213                     Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
    214                             "Failed lot load " + file.getAbsolutePath(), e, null /*data*/);
    215                 }
    216             }
    217 
    218             return null;
    219         } else if (lowerCaseValue.endsWith(".xml")) {
    220             // create a block parser for the file
    221             File f = new File(stringValue);
    222             if (f.isFile()) {
    223                 try {
    224                     // let the framework inflate the Drawable from the XML file.
    225                     XmlPullParser parser = ParserFactory.create(f);
    226 
    227                     BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
    228                             parser, context, value.isFramework());
    229                     try {
    230                         return Drawable.createFromXml(context.getResources(), blockParser, theme);
    231                     } finally {
    232                         blockParser.ensurePopped();
    233                     }
    234                 } catch (Exception e) {
    235                     // this is an error and not warning since the file existence is checked before
    236                     // attempting to parse it.
    237                     Bridge.getLog().error(null, "Failed to parse file " + stringValue,
    238                             e, null /*data*/);
    239                 }
    240             } else {
    241                 Bridge.getLog().error(LayoutLog.TAG_BROKEN,
    242                         String.format("File %s does not exist (or is not a file)", stringValue),
    243                         null /*data*/);
    244             }
    245 
    246             return null;
    247         } else {
    248             File bmpFile = new File(stringValue);
    249             if (bmpFile.isFile()) {
    250                 try {
    251                     Bitmap bitmap = Bridge.getCachedBitmap(stringValue,
    252                             value.isFramework() ? null : context.getProjectKey());
    253 
    254                     if (bitmap == null) {
    255                         bitmap = Bitmap_Delegate.createBitmap(bmpFile, false /*isMutable*/,
    256                                 density);
    257                         Bridge.setCachedBitmap(stringValue, bitmap,
    258                                 value.isFramework() ? null : context.getProjectKey());
    259                     }
    260 
    261                     return new BitmapDrawable(context.getResources(), bitmap);
    262                 } catch (IOException e) {
    263                     // we'll return null below
    264                     Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
    265                             "Failed lot load " + bmpFile.getAbsolutePath(), e, null /*data*/);
    266                 }
    267             } else {
    268                 // attempt to get a color from the value
    269                 try {
    270                     int color = getColor(stringValue);
    271                     return new ColorDrawable(color);
    272                 } catch (NumberFormatException e) {
    273                     // we'll return null below.
    274                     Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
    275                             "Failed to convert " + stringValue + " into a drawable", e,
    276                             null /*data*/);
    277                 }
    278             }
    279         }
    280 
    281         return null;
    282     }
    283 
    284     private static Drawable getNinePatchDrawable(InputStream inputStream, Density density,
    285             boolean isFramework, String cacheKey, BridgeContext context) throws IOException {
    286         // see if we still have both the chunk and the bitmap in the caches
    287         NinePatchChunk chunk = Bridge.getCached9Patch(cacheKey,
    288                 isFramework ? null : context.getProjectKey());
    289         Bitmap bitmap = Bridge.getCachedBitmap(cacheKey,
    290                 isFramework ? null : context.getProjectKey());
    291 
    292         // if either chunk or bitmap is null, then we reload the 9-patch file.
    293         if (chunk == null || bitmap == null) {
    294             try {
    295                 NinePatch ninePatch = NinePatch.load(inputStream, true /*is9Patch*/,
    296                         false /* convert */);
    297                 if (ninePatch != null) {
    298                     if (chunk == null) {
    299                         chunk = ninePatch.getChunk();
    300 
    301                         Bridge.setCached9Patch(cacheKey, chunk,
    302                                 isFramework ? null : context.getProjectKey());
    303                     }
    304 
    305                     if (bitmap == null) {
    306                         bitmap = Bitmap_Delegate.createBitmap(ninePatch.getImage(),
    307                                 false /*isMutable*/,
    308                                 density);
    309 
    310                         Bridge.setCachedBitmap(cacheKey, bitmap,
    311                                 isFramework ? null : context.getProjectKey());
    312                     }
    313                 }
    314             } catch (MalformedURLException e) {
    315                 // URL is wrong, we'll return null below
    316             }
    317         }
    318 
    319         if (chunk != null && bitmap != null) {
    320             int[] padding = chunk.getPadding();
    321             Rect paddingRect = new Rect(padding[0], padding[1], padding[2], padding[3]);
    322 
    323             return new NinePatchDrawable(context.getResources(), bitmap,
    324                     NinePatch_Delegate.serialize(chunk),
    325                     paddingRect, null);
    326         }
    327 
    328         return null;
    329     }
    330 
    331     /**
    332      * Looks for an attribute in the current theme.
    333      *
    334      * @param resources the render resources
    335      * @param name the name of the attribute
    336      * @param defaultValue the default value.
    337      * @param isFrameworkAttr if the attribute is in android namespace
    338      * @return the value of the attribute or the default one if not found.
    339      */
    340     public static boolean getBooleanThemeValue(@NonNull RenderResources resources, String name,
    341             boolean isFrameworkAttr, boolean defaultValue) {
    342         ResourceValue value = resources.findItemInTheme(name, isFrameworkAttr);
    343         value = resources.resolveResValue(value);
    344         if (value == null) {
    345             return defaultValue;
    346         }
    347         return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue);
    348     }
    349 
    350     // ------- TypedValue stuff
    351     // This is taken from //device/libs/utils/ResourceTypes.cpp
    352 
    353     private static final class UnitEntry {
    354         String name;
    355         int type;
    356         int unit;
    357         float scale;
    358 
    359         UnitEntry(String name, int type, int unit, float scale) {
    360             this.name = name;
    361             this.type = type;
    362             this.unit = unit;
    363             this.scale = scale;
    364         }
    365     }
    366 
    367     private final static UnitEntry[] sUnitNames = new UnitEntry[] {
    368         new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f),
    369         new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
    370         new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
    371         new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f),
    372         new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f),
    373         new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f),
    374         new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f),
    375         new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100),
    376         new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100),
    377     };
    378 
    379     /**
    380      * Returns the raw value from the given attribute float-type value string.
    381      * This object is only valid until the next call on to {@link ResourceHelper}.
    382      */
    383     public static TypedValue getValue(String attribute, String value, boolean requireUnit) {
    384         if (parseFloatAttribute(attribute, value, mValue, requireUnit)) {
    385             return mValue;
    386         }
    387 
    388         return null;
    389     }
    390 
    391     /**
    392      * Parse a float attribute and return the parsed value into a given TypedValue.
    393      * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false.
    394      * @param value the string value of the attribute
    395      * @param outValue the TypedValue to receive the parsed value
    396      * @param requireUnit whether the value is expected to contain a unit.
    397      * @return true if success.
    398      */
    399     public static boolean parseFloatAttribute(String attribute, @NonNull String value,
    400             TypedValue outValue, boolean requireUnit) {
    401         assert !requireUnit || attribute != null;
    402 
    403         // remove the space before and after
    404         value = value.trim();
    405         int len = value.length();
    406 
    407         if (len <= 0) {
    408             return false;
    409         }
    410 
    411         // check that there's no non ascii characters.
    412         char[] buf = value.toCharArray();
    413         for (int i = 0 ; i < len ; i++) {
    414             if (buf[i] > 255) {
    415                 return false;
    416             }
    417         }
    418 
    419         // check the first character
    420         if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') {
    421             return false;
    422         }
    423 
    424         // now look for the string that is after the float...
    425         Matcher m = sFloatPattern.matcher(value);
    426         if (m.matches()) {
    427             String f_str = m.group(1);
    428             String end = m.group(2);
    429 
    430             float f;
    431             try {
    432                 f = Float.parseFloat(f_str);
    433             } catch (NumberFormatException e) {
    434                 // this shouldn't happen with the regexp above.
    435                 return false;
    436             }
    437 
    438             if (end.length() > 0 && end.charAt(0) != ' ') {
    439                 // Might be a unit...
    440                 if (parseUnit(end, outValue, sFloatOut)) {
    441                     computeTypedValue(outValue, f, sFloatOut[0]);
    442                     return true;
    443                 }
    444                 return false;
    445             }
    446 
    447             // make sure it's only spaces at the end.
    448             end = end.trim();
    449 
    450             if (end.length() == 0) {
    451                 if (outValue != null) {
    452                     if (!requireUnit) {
    453                         outValue.type = TypedValue.TYPE_FLOAT;
    454                         outValue.data = Float.floatToIntBits(f);
    455                     } else {
    456                         // no unit when required? Use dp and out an error.
    457                         applyUnit(sUnitNames[1], outValue, sFloatOut);
    458                         computeTypedValue(outValue, f, sFloatOut[0]);
    459 
    460                         Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE,
    461                                 String.format(
    462                                         "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!",
    463                                         value, attribute),
    464                                 null);
    465                     }
    466                     return true;
    467                 }
    468             }
    469         }
    470 
    471         return false;
    472     }
    473 
    474     private static void computeTypedValue(TypedValue outValue, float value, float scale) {
    475         value *= scale;
    476         boolean neg = value < 0;
    477         if (neg) {
    478             value = -value;
    479         }
    480         long bits = (long)(value*(1<<23)+.5f);
    481         int radix;
    482         int shift;
    483         if ((bits&0x7fffff) == 0) {
    484             // Always use 23p0 if there is no fraction, just to make
    485             // things easier to read.
    486             radix = TypedValue.COMPLEX_RADIX_23p0;
    487             shift = 23;
    488         } else if ((bits&0xffffffffff800000L) == 0) {
    489             // Magnitude is zero -- can fit in 0 bits of precision.
    490             radix = TypedValue.COMPLEX_RADIX_0p23;
    491             shift = 0;
    492         } else if ((bits&0xffffffff80000000L) == 0) {
    493             // Magnitude can fit in 8 bits of precision.
    494             radix = TypedValue.COMPLEX_RADIX_8p15;
    495             shift = 8;
    496         } else if ((bits&0xffffff8000000000L) == 0) {
    497             // Magnitude can fit in 16 bits of precision.
    498             radix = TypedValue.COMPLEX_RADIX_16p7;
    499             shift = 16;
    500         } else {
    501             // Magnitude needs entire range, so no fractional part.
    502             radix = TypedValue.COMPLEX_RADIX_23p0;
    503             shift = 23;
    504         }
    505         int mantissa = (int)(
    506             (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK);
    507         if (neg) {
    508             mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK;
    509         }
    510         outValue.data |=
    511             (radix<<TypedValue.COMPLEX_RADIX_SHIFT)
    512             | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT);
    513     }
    514 
    515     private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) {
    516         str = str.trim();
    517 
    518         for (UnitEntry unit : sUnitNames) {
    519             if (unit.name.equals(str)) {
    520                 applyUnit(unit, outValue, outScale);
    521                 return true;
    522             }
    523         }
    524 
    525         return false;
    526     }
    527 
    528     private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) {
    529         outValue.type = unit.type;
    530         // COMPLEX_UNIT_SHIFT is 0 and hence intelliJ complains about it. Suppress the warning.
    531         //noinspection PointlessBitwiseExpression
    532         outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT;
    533         outScale[0] = unit.scale;
    534     }
    535 }
    536 
    537