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