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