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 /*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 /*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 /*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                         return null;
354                     }
355 
356                     try {
357                         return Drawable.createFromXml(context.getResources(), blockParser, theme);
358                     } finally {
359                         visitedValues.remove(value);
360                         blockParser.ensurePopped();
361                     }
362                 }
363             } catch (Exception e) {
364                 // this is an error and not warning since the file existence is checked before
365                 // attempting to parse it.
366                 Bridge.getLog().error(null, "Failed to parse file " + stringValue, e,
367                         null /*data*/);
368             }
369 
370             return null;
371         } else {
372             AssetRepository repository = getAssetRepository(context);
373             if (repository.isFileResource(stringValue)) {
374                 try {
375                     Bitmap bitmap = Bridge.getCachedBitmap(stringValue,
376                             value.isFramework() ? null : context.getProjectKey());
377 
378                     if (bitmap == null) {
379                         InputStream stream;
380                         try {
381                             stream = repository.openNonAsset(0, stringValue, ACCESS_STREAMING);
382 
383                         } catch (FileNotFoundException e) {
384                             stream = null;
385                         }
386                         bitmap =
387                                 Bitmap_Delegate.createBitmap(stream, false /*isMutable*/, density);
388                         Bridge.setCachedBitmap(stringValue, bitmap,
389                                 value.isFramework() ? null : context.getProjectKey());
390                     }
391 
392                     return new BitmapDrawable(context.getResources(), bitmap);
393                 } catch (IOException e) {
394                     // we'll return null below
395                     Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
396                             "Failed to load " + stringValue, e, null /*data*/);
397                 }
398             }
399         }
400 
401         return null;
402     }
403 
getAssetRepository(@onNull BridgeContext context)404     private static AssetRepository getAssetRepository(@NonNull BridgeContext context) {
405         BridgeAssetManager assetManager = context.getAssets();
406         return assetManager.getAssetRepository();
407     }
408 
409     /**
410      * Returns a {@link Typeface} given a font name. The font name, can be a system font family
411      * (like sans-serif) or a full path if the font is to be loaded from resources.
412      */
getFont(String fontName, BridgeContext context, Theme theme, boolean isFramework)413     public static Typeface getFont(String fontName, BridgeContext context, Theme theme, boolean
414             isFramework) {
415         if (fontName == null) {
416             return null;
417         }
418 
419         if (Typeface_Accessor.isSystemFont(fontName)) {
420             // Shortcut for the case where we are asking for a system font name. Those are not
421             // loaded using external resources.
422             return null;
423         }
424 
425 
426         return Typeface_Delegate.createFromDisk(context, fontName, isFramework);
427     }
428 
429     /**
430      * Returns a {@link Typeface} given a font name. The font name, can be a system font family
431      * (like sans-serif) or a full path if the font is to be loaded from resources.
432      */
getFont(ResourceValue value, BridgeContext context, Theme theme)433     public static Typeface getFont(ResourceValue value, BridgeContext context, Theme theme) {
434         if (value == null) {
435             return null;
436         }
437 
438         return getFont(value.getValue(), context, theme, value.isFramework());
439     }
440 
getNinePatchDrawable(Density density, boolean isFramework, String path, BridgeContext context)441     private static Drawable getNinePatchDrawable(Density density, boolean isFramework,
442             String path, BridgeContext context) throws IOException {
443         // see if we still have both the chunk and the bitmap in the caches
444         NinePatchChunk chunk = Bridge.getCached9Patch(path,
445                 isFramework ? null : context.getProjectKey());
446         Bitmap bitmap = Bridge.getCachedBitmap(path,
447                 isFramework ? null : context.getProjectKey());
448 
449         // if either chunk or bitmap is null, then we reload the 9-patch file.
450         if (chunk == null || bitmap == null) {
451             try {
452                 AssetRepository repository = getAssetRepository(context);
453                 if (!repository.isFileResource(path)) {
454                     return null;
455                 }
456                 InputStream stream = repository.openNonAsset(0, path, ACCESS_STREAMING);
457                 NinePatch ninePatch = NinePatch.load(stream, true /*is9Patch*/,
458                         false /* convert */);
459                 if (ninePatch != null) {
460                     if (chunk == null) {
461                         chunk = ninePatch.getChunk();
462 
463                         Bridge.setCached9Patch(path, chunk,
464                                 isFramework ? null : context.getProjectKey());
465                     }
466 
467                     if (bitmap == null) {
468                         bitmap = Bitmap_Delegate.createBitmap(ninePatch.getImage(),
469                                 false /*isMutable*/,
470                                 density);
471 
472                         Bridge.setCachedBitmap(path, bitmap,
473                                 isFramework ? null : context.getProjectKey());
474                     }
475                 }
476             } catch (MalformedURLException e) {
477                 // URL is wrong, we'll return null below
478             }
479         }
480 
481         if (chunk != null && bitmap != null) {
482             int[] padding = chunk.getPadding();
483             Rect paddingRect = new Rect(padding[0], padding[1], padding[2], padding[3]);
484 
485             return new NinePatchDrawable(context.getResources(), bitmap,
486                     NinePatch_Delegate.serialize(chunk),
487                     paddingRect, null);
488         }
489 
490         return null;
491     }
492 
493     /**
494      * Looks for an attribute in the current theme.
495      *
496      * @param resources the render resources
497      * @param attr the attribute reference
498      * @param defaultValue the default value.
499      * @return the value of the attribute or the default one if not found.
500      */
getBooleanThemeValue(@onNull RenderResources resources, @NonNull ResourceReference attr, boolean defaultValue)501     public static boolean getBooleanThemeValue(@NonNull RenderResources resources,
502             @NonNull ResourceReference attr, boolean defaultValue) {
503         ResourceValue value = resources.findItemInTheme(attr);
504         value = resources.resolveResValue(value);
505         if (value == null) {
506             return defaultValue;
507         }
508         return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue);
509     }
510 
511     /**
512      * Looks for a framework attribute in the current theme.
513      *
514      * @param resources the render resources
515      * @param name the name of the attribute
516      * @param defaultValue the default value.
517      * @return the value of the attribute or the default one if not found.
518      */
getBooleanThemeFrameworkAttrValue(@onNull RenderResources resources, @NonNull String name, boolean defaultValue)519     public static boolean getBooleanThemeFrameworkAttrValue(@NonNull RenderResources resources,
520             @NonNull String name, boolean defaultValue) {
521         ResourceReference attrRef = BridgeContext.createFrameworkAttrReference(name);
522         return getBooleanThemeValue(resources, attrRef, defaultValue);
523     }
524 
525     // ------- TypedValue stuff
526     // This is taken from //device/libs/utils/ResourceTypes.cpp
527 
528     private static final class UnitEntry {
529         String name;
530         int type;
531         int unit;
532         float scale;
533 
UnitEntry(String name, int type, int unit, float scale)534         UnitEntry(String name, int type, int unit, float scale) {
535             this.name = name;
536             this.type = type;
537             this.unit = unit;
538             this.scale = scale;
539         }
540     }
541 
542     private static final UnitEntry[] sUnitNames = new UnitEntry[] {
543         new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f),
544         new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
545         new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
546         new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f),
547         new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f),
548         new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f),
549         new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f),
550         new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100),
551         new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100),
552     };
553 
554     /**
555      * Returns the raw value from the given attribute float-type value string.
556      * This object is only valid until the next call on to {@link ResourceHelper}.
557      */
getValue(String attribute, String value, boolean requireUnit)558     public static TypedValue getValue(String attribute, String value, boolean requireUnit) {
559         if (parseFloatAttribute(attribute, value, mValue, requireUnit)) {
560             return mValue;
561         }
562 
563         return null;
564     }
565 
566     /**
567      * Parse a float attribute and return the parsed value into a given TypedValue.
568      * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false.
569      * @param value the string value of the attribute
570      * @param outValue the TypedValue to receive the parsed value
571      * @param requireUnit whether the value is expected to contain a unit.
572      * @return true if success.
573      */
parseFloatAttribute(String attribute, @NonNull String value, TypedValue outValue, boolean requireUnit)574     public static boolean parseFloatAttribute(String attribute, @NonNull String value,
575             TypedValue outValue, boolean requireUnit) {
576         assert !requireUnit || attribute != null;
577 
578         // remove the space before and after
579         value = value.trim();
580         int len = value.length();
581 
582         if (len <= 0) {
583             return false;
584         }
585 
586         // check that there's no non ascii characters.
587         char[] buf = value.toCharArray();
588         for (int i = 0 ; i < len ; i++) {
589             if (buf[i] > 255) {
590                 return false;
591             }
592         }
593 
594         // check the first character
595         if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') {
596             return false;
597         }
598 
599         // now look for the string that is after the float...
600         Matcher m = sFloatPattern.matcher(value);
601         if (m.matches()) {
602             String f_str = m.group(1);
603             String end = m.group(2);
604 
605             float f;
606             try {
607                 f = Float.parseFloat(f_str);
608             } catch (NumberFormatException e) {
609                 // this shouldn't happen with the regexp above.
610                 return false;
611             }
612 
613             if (end.length() > 0 && end.charAt(0) != ' ') {
614                 // Might be a unit...
615                 if (parseUnit(end, outValue, sFloatOut)) {
616                     computeTypedValue(outValue, f, sFloatOut[0]);
617                     return true;
618                 }
619                 return false;
620             }
621 
622             // make sure it's only spaces at the end.
623             end = end.trim();
624 
625             if (end.length() == 0) {
626                 if (outValue != null) {
627                     if (!requireUnit) {
628                         outValue.type = TypedValue.TYPE_FLOAT;
629                         outValue.data = Float.floatToIntBits(f);
630                     } else {
631                         // no unit when required? Use dp and out an error.
632                         applyUnit(sUnitNames[1], outValue, sFloatOut);
633                         computeTypedValue(outValue, f, sFloatOut[0]);
634 
635                         Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE,
636                                 String.format(
637                                         "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!",
638                                         value, attribute),
639                                 null);
640                     }
641                     return true;
642                 }
643             }
644         }
645 
646         return false;
647     }
648 
computeTypedValue(TypedValue outValue, float value, float scale)649     private static void computeTypedValue(TypedValue outValue, float value, float scale) {
650         value *= scale;
651         boolean neg = value < 0;
652         if (neg) {
653             value = -value;
654         }
655         long bits = (long)(value*(1<<23)+.5f);
656         int radix;
657         int shift;
658         if ((bits&0x7fffff) == 0) {
659             // Always use 23p0 if there is no fraction, just to make
660             // things easier to read.
661             radix = TypedValue.COMPLEX_RADIX_23p0;
662             shift = 23;
663         } else if ((bits&0xffffffffff800000L) == 0) {
664             // Magnitude is zero -- can fit in 0 bits of precision.
665             radix = TypedValue.COMPLEX_RADIX_0p23;
666             shift = 0;
667         } else if ((bits&0xffffffff80000000L) == 0) {
668             // Magnitude can fit in 8 bits of precision.
669             radix = TypedValue.COMPLEX_RADIX_8p15;
670             shift = 8;
671         } else if ((bits&0xffffff8000000000L) == 0) {
672             // Magnitude can fit in 16 bits of precision.
673             radix = TypedValue.COMPLEX_RADIX_16p7;
674             shift = 16;
675         } else {
676             // Magnitude needs entire range, so no fractional part.
677             radix = TypedValue.COMPLEX_RADIX_23p0;
678             shift = 23;
679         }
680         int mantissa = (int)(
681             (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK);
682         if (neg) {
683             mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK;
684         }
685         outValue.data |=
686             (radix<<TypedValue.COMPLEX_RADIX_SHIFT)
687             | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT);
688     }
689 
690     private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) {
691         str = str.trim();
692 
693         for (UnitEntry unit : sUnitNames) {
694             if (unit.name.equals(str)) {
695                 applyUnit(unit, outValue, outScale);
696                 return true;
697             }
698         }
699 
700         return false;
701     }
702 
703     private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) {
704         outValue.type = unit.type;
705         // COMPLEX_UNIT_SHIFT is 0 and hence intelliJ complains about it. Suppress the warning.
706         //noinspection PointlessBitwiseExpression
707         outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT;
708         outScale[0] = unit.scale;
709     }
710 }
711 
712