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