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