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