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