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.SdkConstants;
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.internal.util.XmlUtils;
25import com.android.layoutlib.bridge.Bridge;
26import com.android.layoutlib.bridge.android.BridgeContext;
27import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
28import com.android.layoutlib.bridge.android.RenderParamsFlags;
29import com.android.ninepatch.NinePatch;
30import com.android.ninepatch.NinePatchChunk;
31import com.android.resources.Density;
32
33import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35
36import android.annotation.NonNull;
37import android.annotation.Nullable;
38import android.content.res.ColorStateList;
39import android.content.res.ComplexColor;
40import android.content.res.ComplexColor_Accessor;
41import android.content.res.GradientColor;
42import android.content.res.Resources.Theme;
43import android.graphics.Bitmap;
44import android.graphics.Bitmap_Delegate;
45import android.graphics.NinePatch_Delegate;
46import android.graphics.Rect;
47import android.graphics.drawable.BitmapDrawable;
48import android.graphics.drawable.ColorDrawable;
49import android.graphics.drawable.Drawable;
50import android.graphics.drawable.NinePatchDrawable;
51import android.util.TypedValue;
52
53import java.io.File;
54import java.io.FileInputStream;
55import java.io.FileNotFoundException;
56import java.io.IOException;
57import java.io.InputStream;
58import java.net.MalformedURLException;
59import java.util.regex.Matcher;
60import java.util.regex.Pattern;
61
62/**
63 * Helper class to provide various conversion method used in handling android resources.
64 */
65public final class ResourceHelper {
66
67    private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)");
68    private final static float[] sFloatOut = new float[1];
69
70    private final static TypedValue mValue = new TypedValue();
71
72    /**
73     * Returns the color value represented by the given string value
74     * @param value the color value
75     * @return the color as an int
76     * @throws NumberFormatException if the conversion failed.
77     */
78    public static int getColor(String value) {
79        if (value != null) {
80            if (!value.startsWith("#")) {
81                if (value.startsWith(SdkConstants.PREFIX_THEME_REF)) {
82                    throw new NumberFormatException(String.format(
83                            "Attribute '%s' not found. Are you using the right theme?", value));
84                }
85                throw new NumberFormatException(
86                        String.format("Color value '%s' must start with #", value));
87            }
88
89            value = value.substring(1);
90
91            // make sure it's not longer than 32bit
92            if (value.length() > 8) {
93                throw new NumberFormatException(String.format(
94                        "Color value '%s' is too long. Format is either" +
95                        "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
96                        value));
97            }
98
99            if (value.length() == 3) { // RGB format
100                char[] color = new char[8];
101                color[0] = color[1] = 'F';
102                color[2] = color[3] = value.charAt(0);
103                color[4] = color[5] = value.charAt(1);
104                color[6] = color[7] = value.charAt(2);
105                value = new String(color);
106            } else if (value.length() == 4) { // ARGB format
107                char[] color = new char[8];
108                color[0] = color[1] = value.charAt(0);
109                color[2] = color[3] = value.charAt(1);
110                color[4] = color[5] = value.charAt(2);
111                color[6] = color[7] = value.charAt(3);
112                value = new String(color);
113            } else if (value.length() == 6) {
114                value = "FF" + value;
115            }
116
117            // this is a RRGGBB or AARRGGBB value
118
119            // Integer.parseInt will fail to parse strings like "ff191919", so we use
120            // a Long, but cast the result back into an int, since we know that we're only
121            // dealing with 32 bit values.
122            return (int)Long.parseLong(value, 16);
123        }
124
125        throw new NumberFormatException();
126    }
127
128    /**
129     * Returns a {@link ComplexColor} from the given {@link ResourceValue}
130     *
131     * @param resValue the value containing a color value or a file path to a complex color
132     * definition
133     * @param context the current context
134     * @param theme the theme to use when resolving the complex color
135     * @param allowGradients when false, only {@link ColorStateList} will be returned. If a {@link
136     * GradientColor} is found, null will be returned.
137     */
138    @Nullable
139    private static ComplexColor getInternalComplexColor(@NonNull ResourceValue resValue,
140            @NonNull BridgeContext context, @Nullable Theme theme, boolean allowGradients) {
141        String value = resValue.getValue();
142        if (value == null || RenderResources.REFERENCE_NULL.equals(value)) {
143            return null;
144        }
145
146        XmlPullParser parser = null;
147        // first check if the value is a file (xml most likely)
148        Boolean psiParserSupport = context.getLayoutlibCallback().getFlag(
149                RenderParamsFlags.FLAG_KEY_XML_FILE_PARSER_SUPPORT);
150        if (psiParserSupport != null && psiParserSupport) {
151            parser = context.getLayoutlibCallback().getXmlFileParser(value);
152        }
153        if (parser == null) {
154            File f = new File(value);
155            if (f.isFile()) {
156                // let the framework inflate the color from the XML file, by
157                // providing an XmlPullParser
158                try {
159                    parser = ParserFactory.create(f);
160                } catch (XmlPullParserException | FileNotFoundException e) {
161                    Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
162                            "Failed to parse file " + value, e, null /*data*/);
163                }
164            }
165        }
166
167        if (parser != null) {
168            try {
169                BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
170                        parser, context, resValue.isFramework());
171                try {
172                    // Advance the parser to the first element so we can detect if it's a
173                    // color list or a gradient color
174                    int type;
175                    //noinspection StatementWithEmptyBody
176                    while ((type = blockParser.next()) != XmlPullParser.START_TAG
177                            && type != XmlPullParser.END_DOCUMENT) {
178                        // Seek parser to start tag.
179                    }
180
181                    if (type != XmlPullParser.START_TAG) {
182                        throw new XmlPullParserException("No start tag found");
183                    }
184
185                    final String name = blockParser.getName();
186                    if (allowGradients && "gradient".equals(name)) {
187                        return ComplexColor_Accessor.createGradientColorFromXmlInner(
188                                context.getResources(),
189                                blockParser, blockParser,
190                                theme);
191                    } else if ("selector".equals(name)) {
192                        return ComplexColor_Accessor.createColorStateListFromXmlInner(
193                                context.getResources(),
194                                blockParser, blockParser,
195                                theme);
196                    }
197                } finally {
198                    blockParser.ensurePopped();
199                }
200            } catch (XmlPullParserException e) {
201                Bridge.getLog().error(LayoutLog.TAG_BROKEN,
202                        "Failed to configure parser for " + value, e, null /*data*/);
203                // we'll return null below.
204            } catch (Exception e) {
205                // this is an error and not warning since the file existence is
206                // checked before attempting to parse it.
207                Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
208                        "Failed to parse file " + value, e, null /*data*/);
209
210                return null;
211            }
212        } else {
213            // try to load the color state list from an int
214            try {
215                int color = getColor(value);
216                return ColorStateList.valueOf(color);
217            } catch (NumberFormatException e) {
218                Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
219                        "Failed to convert " + value + " into a ColorStateList", e,
220                        null /*data*/);
221            }
222        }
223
224        return null;
225    }
226
227    /**
228     * Returns a {@link ColorStateList} from the given {@link ResourceValue}
229     *
230     * @param resValue the value containing a color value or a file path to a complex color
231     * definition
232     * @param context the current context
233     */
234    @Nullable
235    public static ColorStateList getColorStateList(@NonNull ResourceValue resValue,
236            @NonNull BridgeContext context) {
237        return (ColorStateList) getInternalComplexColor(resValue, context, context.getTheme(),
238                false);
239    }
240
241    /**
242     * Returns a {@link ComplexColor} from the given {@link ResourceValue}
243     *
244     * @param resValue the value containing a color value or a file path to a complex color
245     * definition
246     * @param context the current context
247     */
248    @Nullable
249    public static ComplexColor getComplexColor(@NonNull ResourceValue resValue,
250            @NonNull BridgeContext context) {
251        return getInternalComplexColor(resValue, context, context.getTheme(), true);
252    }
253
254    /**
255     * Returns a drawable from the given value.
256     * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable,
257     * or an hexadecimal color
258     * @param context the current context
259     */
260    public static Drawable getDrawable(ResourceValue value, BridgeContext context) {
261        return getDrawable(value, context, null);
262    }
263
264    /**
265     * Returns a drawable from the given value.
266     * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable,
267     * or an hexadecimal color
268     * @param context the current context
269     * @param theme the theme to be used to inflate the drawable.
270     */
271    public static Drawable getDrawable(ResourceValue value, BridgeContext context, Theme theme) {
272        if (value == null) {
273            return null;
274        }
275        String stringValue = value.getValue();
276        if (RenderResources.REFERENCE_NULL.equals(stringValue)) {
277            return null;
278        }
279
280        String lowerCaseValue = stringValue.toLowerCase();
281
282        Density density = Density.MEDIUM;
283        if (value instanceof DensityBasedResourceValue) {
284            density =
285                ((DensityBasedResourceValue)value).getResourceDensity();
286        }
287
288
289        if (lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) {
290            File file = new File(stringValue);
291            if (file.isFile()) {
292                try {
293                    return getNinePatchDrawable(
294                            new FileInputStream(file), density, value.isFramework(),
295                            stringValue, context);
296                } catch (IOException e) {
297                    // failed to read the file, we'll return null below.
298                    Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
299                            "Failed lot load " + file.getAbsolutePath(), e, null /*data*/);
300                }
301            }
302
303            return null;
304        } else if (lowerCaseValue.endsWith(".xml")) {
305            // create a block parser for the file
306            File f = new File(stringValue);
307            if (f.isFile()) {
308                try {
309                    // let the framework inflate the Drawable from the XML file.
310                    XmlPullParser parser = ParserFactory.create(f);
311
312                    BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(
313                            parser, context, value.isFramework());
314                    try {
315                        return Drawable.createFromXml(context.getResources(), blockParser, theme);
316                    } finally {
317                        blockParser.ensurePopped();
318                    }
319                } catch (Exception e) {
320                    // this is an error and not warning since the file existence is checked before
321                    // attempting to parse it.
322                    Bridge.getLog().error(null, "Failed to parse file " + stringValue,
323                            e, null /*data*/);
324                }
325            } else {
326                Bridge.getLog().error(LayoutLog.TAG_BROKEN,
327                        String.format("File %s does not exist (or is not a file)", stringValue),
328                        null /*data*/);
329            }
330
331            return null;
332        } else {
333            File bmpFile = new File(stringValue);
334            if (bmpFile.isFile()) {
335                try {
336                    Bitmap bitmap = Bridge.getCachedBitmap(stringValue,
337                            value.isFramework() ? null : context.getProjectKey());
338
339                    if (bitmap == null) {
340                        bitmap = Bitmap_Delegate.createBitmap(bmpFile, false /*isMutable*/,
341                                density);
342                        Bridge.setCachedBitmap(stringValue, bitmap,
343                                value.isFramework() ? null : context.getProjectKey());
344                    }
345
346                    return new BitmapDrawable(context.getResources(), bitmap);
347                } catch (IOException e) {
348                    // we'll return null below
349                    Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
350                            "Failed lot load " + bmpFile.getAbsolutePath(), e, null /*data*/);
351                }
352            } else {
353                // attempt to get a color from the value
354                try {
355                    int color = getColor(stringValue);
356                    return new ColorDrawable(color);
357                } catch (NumberFormatException e) {
358                    // we'll return null below.
359                    Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
360                            "Failed to convert " + stringValue + " into a drawable", e,
361                            null /*data*/);
362                }
363            }
364        }
365
366        return null;
367    }
368
369    private static Drawable getNinePatchDrawable(InputStream inputStream, Density density,
370            boolean isFramework, String cacheKey, BridgeContext context) throws IOException {
371        // see if we still have both the chunk and the bitmap in the caches
372        NinePatchChunk chunk = Bridge.getCached9Patch(cacheKey,
373                isFramework ? null : context.getProjectKey());
374        Bitmap bitmap = Bridge.getCachedBitmap(cacheKey,
375                isFramework ? null : context.getProjectKey());
376
377        // if either chunk or bitmap is null, then we reload the 9-patch file.
378        if (chunk == null || bitmap == null) {
379            try {
380                NinePatch ninePatch = NinePatch.load(inputStream, true /*is9Patch*/,
381                        false /* convert */);
382                if (ninePatch != null) {
383                    if (chunk == null) {
384                        chunk = ninePatch.getChunk();
385
386                        Bridge.setCached9Patch(cacheKey, chunk,
387                                isFramework ? null : context.getProjectKey());
388                    }
389
390                    if (bitmap == null) {
391                        bitmap = Bitmap_Delegate.createBitmap(ninePatch.getImage(),
392                                false /*isMutable*/,
393                                density);
394
395                        Bridge.setCachedBitmap(cacheKey, bitmap,
396                                isFramework ? null : context.getProjectKey());
397                    }
398                }
399            } catch (MalformedURLException e) {
400                // URL is wrong, we'll return null below
401            }
402        }
403
404        if (chunk != null && bitmap != null) {
405            int[] padding = chunk.getPadding();
406            Rect paddingRect = new Rect(padding[0], padding[1], padding[2], padding[3]);
407
408            return new NinePatchDrawable(context.getResources(), bitmap,
409                    NinePatch_Delegate.serialize(chunk),
410                    paddingRect, null);
411        }
412
413        return null;
414    }
415
416    /**
417     * Looks for an attribute in the current theme.
418     *
419     * @param resources the render resources
420     * @param name the name of the attribute
421     * @param defaultValue the default value.
422     * @param isFrameworkAttr if the attribute is in android namespace
423     * @return the value of the attribute or the default one if not found.
424     */
425    public static boolean getBooleanThemeValue(@NonNull RenderResources resources, String name,
426            boolean isFrameworkAttr, boolean defaultValue) {
427        ResourceValue value = resources.findItemInTheme(name, isFrameworkAttr);
428        value = resources.resolveResValue(value);
429        if (value == null) {
430            return defaultValue;
431        }
432        return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue);
433    }
434
435    // ------- TypedValue stuff
436    // This is taken from //device/libs/utils/ResourceTypes.cpp
437
438    private static final class UnitEntry {
439        String name;
440        int type;
441        int unit;
442        float scale;
443
444        UnitEntry(String name, int type, int unit, float scale) {
445            this.name = name;
446            this.type = type;
447            this.unit = unit;
448            this.scale = scale;
449        }
450    }
451
452    private final static UnitEntry[] sUnitNames = new UnitEntry[] {
453        new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f),
454        new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
455        new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
456        new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f),
457        new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f),
458        new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f),
459        new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f),
460        new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100),
461        new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100),
462    };
463
464    /**
465     * Returns the raw value from the given attribute float-type value string.
466     * This object is only valid until the next call on to {@link ResourceHelper}.
467     */
468    public static TypedValue getValue(String attribute, String value, boolean requireUnit) {
469        if (parseFloatAttribute(attribute, value, mValue, requireUnit)) {
470            return mValue;
471        }
472
473        return null;
474    }
475
476    /**
477     * Parse a float attribute and return the parsed value into a given TypedValue.
478     * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false.
479     * @param value the string value of the attribute
480     * @param outValue the TypedValue to receive the parsed value
481     * @param requireUnit whether the value is expected to contain a unit.
482     * @return true if success.
483     */
484    public static boolean parseFloatAttribute(String attribute, @NonNull String value,
485            TypedValue outValue, boolean requireUnit) {
486        assert !requireUnit || attribute != null;
487
488        // remove the space before and after
489        value = value.trim();
490        int len = value.length();
491
492        if (len <= 0) {
493            return false;
494        }
495
496        // check that there's no non ascii characters.
497        char[] buf = value.toCharArray();
498        for (int i = 0 ; i < len ; i++) {
499            if (buf[i] > 255) {
500                return false;
501            }
502        }
503
504        // check the first character
505        if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') {
506            return false;
507        }
508
509        // now look for the string that is after the float...
510        Matcher m = sFloatPattern.matcher(value);
511        if (m.matches()) {
512            String f_str = m.group(1);
513            String end = m.group(2);
514
515            float f;
516            try {
517                f = Float.parseFloat(f_str);
518            } catch (NumberFormatException e) {
519                // this shouldn't happen with the regexp above.
520                return false;
521            }
522
523            if (end.length() > 0 && end.charAt(0) != ' ') {
524                // Might be a unit...
525                if (parseUnit(end, outValue, sFloatOut)) {
526                    computeTypedValue(outValue, f, sFloatOut[0]);
527                    return true;
528                }
529                return false;
530            }
531
532            // make sure it's only spaces at the end.
533            end = end.trim();
534
535            if (end.length() == 0) {
536                if (outValue != null) {
537                    if (!requireUnit) {
538                        outValue.type = TypedValue.TYPE_FLOAT;
539                        outValue.data = Float.floatToIntBits(f);
540                    } else {
541                        // no unit when required? Use dp and out an error.
542                        applyUnit(sUnitNames[1], outValue, sFloatOut);
543                        computeTypedValue(outValue, f, sFloatOut[0]);
544
545                        Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE,
546                                String.format(
547                                        "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!",
548                                        value, attribute),
549                                null);
550                    }
551                    return true;
552                }
553            }
554        }
555
556        return false;
557    }
558
559    private static void computeTypedValue(TypedValue outValue, float value, float scale) {
560        value *= scale;
561        boolean neg = value < 0;
562        if (neg) {
563            value = -value;
564        }
565        long bits = (long)(value*(1<<23)+.5f);
566        int radix;
567        int shift;
568        if ((bits&0x7fffff) == 0) {
569            // Always use 23p0 if there is no fraction, just to make
570            // things easier to read.
571            radix = TypedValue.COMPLEX_RADIX_23p0;
572            shift = 23;
573        } else if ((bits&0xffffffffff800000L) == 0) {
574            // Magnitude is zero -- can fit in 0 bits of precision.
575            radix = TypedValue.COMPLEX_RADIX_0p23;
576            shift = 0;
577        } else if ((bits&0xffffffff80000000L) == 0) {
578            // Magnitude can fit in 8 bits of precision.
579            radix = TypedValue.COMPLEX_RADIX_8p15;
580            shift = 8;
581        } else if ((bits&0xffffff8000000000L) == 0) {
582            // Magnitude can fit in 16 bits of precision.
583            radix = TypedValue.COMPLEX_RADIX_16p7;
584            shift = 16;
585        } else {
586            // Magnitude needs entire range, so no fractional part.
587            radix = TypedValue.COMPLEX_RADIX_23p0;
588            shift = 23;
589        }
590        int mantissa = (int)(
591            (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK);
592        if (neg) {
593            mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK;
594        }
595        outValue.data |=
596            (radix<<TypedValue.COMPLEX_RADIX_SHIFT)
597            | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT);
598    }
599
600    private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) {
601        str = str.trim();
602
603        for (UnitEntry unit : sUnitNames) {
604            if (unit.name.equals(str)) {
605                applyUnit(unit, outValue, outScale);
606                return true;
607            }
608        }
609
610        return false;
611    }
612
613    private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) {
614        outValue.type = unit.type;
615        // COMPLEX_UNIT_SHIFT is 0 and hence intelliJ complains about it. Suppress the warning.
616        //noinspection PointlessBitwiseExpression
617        outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT;
618        outScale[0] = unit.scale;
619    }
620}
621
622