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