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 
17 package com.android.layoutlib.bridge.impl;
18 
19 import com.android.SdkConstants;
20 import com.android.ide.common.rendering.api.DensityBasedResourceValue;
21 import com.android.ide.common.rendering.api.LayoutLog;
22 import com.android.ide.common.rendering.api.RenderResources;
23 import com.android.ide.common.rendering.api.ResourceValue;
24 import com.android.internal.util.XmlUtils;
25 import com.android.layoutlib.bridge.Bridge;
26 import com.android.layoutlib.bridge.android.BridgeContext;
27 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
28 import com.android.layoutlib.bridge.android.RenderParamsFlags;
29 import com.android.ninepatch.NinePatch;
30 import com.android.ninepatch.NinePatchChunk;
31 import com.android.resources.Density;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 
36 import android.annotation.NonNull;
37 import android.annotation.Nullable;
38 import android.content.res.ColorStateList;
39 import android.content.res.ComplexColor;
40 import android.content.res.ComplexColor_Accessor;
41 import android.content.res.GradientColor;
42 import android.content.res.Resources.Theme;
43 import android.graphics.Bitmap;
44 import android.graphics.Bitmap_Delegate;
45 import android.graphics.NinePatch_Delegate;
46 import android.graphics.Rect;
47 import android.graphics.drawable.BitmapDrawable;
48 import android.graphics.drawable.ColorDrawable;
49 import android.graphics.drawable.Drawable;
50 import android.graphics.drawable.NinePatchDrawable;
51 import android.util.TypedValue;
52 
53 import java.io.File;
54 import java.io.FileInputStream;
55 import java.io.FileNotFoundException;
56 import java.io.IOException;
57 import java.io.InputStream;
58 import java.net.MalformedURLException;
59 import java.util.regex.Matcher;
60 import java.util.regex.Pattern;
61 
62 /**
63  * Helper class to provide various conversion method used in handling android resources.
64  */
65 public 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      */
getColor(String value)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
getInternalComplexColor(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Theme theme, boolean allowGradients)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
getColorStateList(@onNull ResourceValue resValue, @NonNull BridgeContext context)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
getComplexColor(@onNull ResourceValue resValue, @NonNull BridgeContext context)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      */
getDrawable(ResourceValue value, BridgeContext context)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      */
getDrawable(ResourceValue value, BridgeContext context, Theme theme)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 
getNinePatchDrawable(InputStream inputStream, Density density, boolean isFramework, String cacheKey, BridgeContext context)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      */
getBooleanThemeValue(@onNull RenderResources resources, String name, boolean isFrameworkAttr, boolean defaultValue)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 
UnitEntry(String name, int type, int unit, float scale)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      */
getValue(String attribute, String value, boolean requireUnit)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      */
parseFloatAttribute(String attribute, @NonNull String value, TypedValue outValue, boolean requireUnit)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 
computeTypedValue(TypedValue outValue, float value, float scale)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