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