ResourceHelper.java revision 282e181b58cf72b6ca770dc7ca5f91f135444502
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
17package com.android.layoutlib.bridge.impl;
18
19import com.android.ide.common.rendering.api.DensityBasedResourceValue;
20import com.android.ide.common.rendering.api.LayoutLog;
21import com.android.ide.common.rendering.api.RenderResources;
22import com.android.ide.common.rendering.api.ResourceValue;
23import com.android.layoutlib.bridge.Bridge;
24import com.android.layoutlib.bridge.android.BridgeContext;
25import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
26import com.android.ninepatch.NinePatch;
27import com.android.ninepatch.NinePatchChunk;
28import com.android.resources.Density;
29
30import org.xmlpull.v1.XmlPullParser;
31import org.xmlpull.v1.XmlPullParserException;
32
33import android.content.res.ColorStateList;
34import android.graphics.Bitmap;
35import android.graphics.Bitmap_Delegate;
36import android.graphics.NinePatch_Delegate;
37import android.graphics.Rect;
38import android.graphics.drawable.BitmapDrawable;
39import android.graphics.drawable.ColorDrawable;
40import android.graphics.drawable.Drawable;
41import android.graphics.drawable.NinePatchDrawable;
42import android.util.TypedValue;
43
44import java.io.File;
45import java.io.FileInputStream;
46import java.io.IOException;
47import java.io.InputStream;
48import java.net.MalformedURLException;
49import java.util.regex.Matcher;
50import java.util.regex.Pattern;
51
52/**
53 * Helper class to provide various conversion method used in handling android resources.
54 */
55public 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