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