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