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