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