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