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