• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ide.common.rendering.api.AndroidConstants;
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.GraphicsUtilities;
34 import com.android.ninepatch.NinePatch;
35 import com.android.resources.Density;
36 import com.android.resources.ResourceType;
37 
38 import org.ccil.cowan.tagsoup.HTMLSchema;
39 import org.ccil.cowan.tagsoup.Parser;
40 import org.xml.sax.Attributes;
41 import org.xml.sax.InputSource;
42 import org.xml.sax.SAXException;
43 import org.xml.sax.helpers.DefaultHandler;
44 import org.xmlpull.v1.XmlPullParser;
45 import org.xmlpull.v1.XmlPullParserException;
46 
47 import android.annotation.NonNull;
48 import android.annotation.Nullable;
49 import android.content.res.BridgeAssetManager;
50 import android.content.res.ColorStateList;
51 import android.content.res.ComplexColor;
52 import android.content.res.ComplexColor_Accessor;
53 import android.content.res.GradientColor;
54 import android.content.res.Resources;
55 import android.content.res.Resources.Theme;
56 import android.content.res.StringBlock;
57 import android.content.res.StringBlock.Height;
58 import android.graphics.Bitmap;
59 import android.graphics.Bitmap.Config;
60 import android.graphics.BitmapFactory;
61 import android.graphics.BitmapFactory.Options;
62 import android.graphics.Rect;
63 import android.graphics.Typeface;
64 import android.graphics.Typeface_Accessor;
65 import android.graphics.Typeface_Delegate;
66 import android.graphics.drawable.BitmapDrawable;
67 import android.graphics.drawable.ColorDrawable;
68 import android.graphics.drawable.Drawable;
69 import android.graphics.drawable.NinePatchDrawable;
70 import android.text.Annotation;
71 import android.text.Spannable;
72 import android.text.SpannableString;
73 import android.text.Spanned;
74 import android.text.SpannedString;
75 import android.text.TextUtils;
76 import android.text.style.AbsoluteSizeSpan;
77 import android.text.style.BulletSpan;
78 import android.text.style.RelativeSizeSpan;
79 import android.text.style.StrikethroughSpan;
80 import android.text.style.StyleSpan;
81 import android.text.style.SubscriptSpan;
82 import android.text.style.SuperscriptSpan;
83 import android.text.style.TypefaceSpan;
84 import android.text.style.URLSpan;
85 import android.text.style.UnderlineSpan;
86 import android.util.TypedValue;
87 
88 import java.awt.image.BufferedImage;
89 import java.io.FileNotFoundException;
90 import java.io.IOException;
91 import java.io.InputStream;
92 import java.io.StringReader;
93 import java.util.ArrayDeque;
94 import java.util.ArrayList;
95 import java.util.Deque;
96 import java.util.HashMap;
97 import java.util.HashSet;
98 import java.util.List;
99 import java.util.Map;
100 import java.util.Set;
101 import java.util.regex.Matcher;
102 import java.util.regex.Pattern;
103 
104 import com.google.common.base.Strings;
105 
106 import static android.content.res.AssetManager.ACCESS_STREAMING;
107 
108 /**
109  * Helper class to provide various conversion method used in handling android resources.
110  */
111 public final class ResourceHelper {
112     private static final Key<Set<ResourceValue>> KEY_GET_DRAWABLE =
113             Key.create("ResourceHelper.getDrawable");
114     private static final Pattern sFloatPattern = Pattern.compile("(-?[0-9]*(?:\\.[0-9]+)?)(.*)");
115     private static final float[] sFloatOut = new float[1];
116 
117     private static final TypedValue mValue = new TypedValue();
118 
119     /**
120      * Returns the color value represented by the given string value.
121      *
122      * @param value the color value
123      * @return the color as an int
124      * @throws NumberFormatException if the conversion failed.
125      */
getColor(@ullable String value)126     public static int getColor(@Nullable String value) {
127         if (value == null) {
128             throw new NumberFormatException("null value");
129         }
130 
131         value = value.trim();
132         int len = value.length();
133 
134         // make sure it's not longer than 32bit or smaller than the RGB format
135         if (len < 2 || len > 9) {
136             throw new NumberFormatException(String.format(
137                     "Color value '%s' has wrong size. Format is either" +
138                             "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
139                     value));
140         }
141 
142         if (value.charAt(0) != '#') {
143             if (value.startsWith(AndroidConstants.PREFIX_THEME_REF)) {
144                 throw new NumberFormatException(String.format(
145                         "Attribute '%s' not found. Are you using the right theme?", value));
146             }
147             throw new NumberFormatException(
148                     String.format("Color value '%s' must start with #", value));
149         }
150 
151         value = value.substring(1);
152 
153         if (len == 4) { // RGB format
154             char[] color = new char[8];
155             color[0] = color[1] = 'F';
156             color[2] = color[3] = value.charAt(0);
157             color[4] = color[5] = value.charAt(1);
158             color[6] = color[7] = value.charAt(2);
159             value = new String(color);
160         } else if (len == 5) { // ARGB format
161             char[] color = new char[8];
162             color[0] = color[1] = value.charAt(0);
163             color[2] = color[3] = value.charAt(1);
164             color[4] = color[5] = value.charAt(2);
165             color[6] = color[7] = value.charAt(3);
166             value = new String(color);
167         } else if (len == 7) {
168             value = "FF" + value;
169         }
170 
171         // this is a RRGGBB or AARRGGBB value
172 
173         // Integer.parseInt will fail to parse strings like "ff191919", so we use
174         // a Long, but cast the result back into an int, since we know that we're only
175         // dealing with 32 bit values.
176         return (int)Long.parseLong(value, 16);
177     }
178 
179     /**
180      * Returns a {@link ComplexColor} from the given {@link ResourceValue}
181      *
182      * @param resValue the value containing a color value or a file path to a complex color
183      * definition
184      * @param context the current context
185      * @param theme the theme to use when resolving the complex color
186      * @param allowGradients when false, only {@link ColorStateList} will be returned. If a {@link
187      * GradientColor} is found, null will be returned.
188      */
189     @Nullable
getInternalComplexColor(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Theme theme, boolean allowGradients)190     private static ComplexColor getInternalComplexColor(@NonNull ResourceValue resValue,
191             @NonNull BridgeContext context, @Nullable Theme theme, boolean allowGradients) {
192         String value = resValue.getValue();
193         if (value == null || RenderResources.REFERENCE_NULL.equals(value)) {
194             return null;
195         }
196 
197         // try to load the color state list from an int
198         if (value.trim().startsWith("#")) {
199             try {
200                 int color = getColor(value);
201                 return ColorStateList.valueOf(color);
202             } catch (NumberFormatException e) {
203                 Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT,
204                         String.format("\"%1$s\" cannot be interpreted as a color.", value),
205                         null, null);
206                 return null;
207             }
208         }
209 
210         try {
211             BridgeXmlBlockParser blockParser = getXmlBlockParser(context, resValue);
212             if (blockParser != null) {
213                 try {
214                     // Advance the parser to the first element so we can detect if it's a
215                     // color list or a gradient color
216                     int type;
217                     //noinspection StatementWithEmptyBody
218                     while ((type = blockParser.next()) != XmlPullParser.START_TAG
219                             && type != XmlPullParser.END_DOCUMENT) {
220                         // Seek parser to start tag.
221                     }
222 
223                     if (type != XmlPullParser.START_TAG) {
224                         assert false : "No start tag found";
225                         return null;
226                     }
227 
228                     final String name = blockParser.getName();
229                     if (allowGradients && "gradient".equals(name)) {
230                         return ComplexColor_Accessor.createGradientColorFromXmlInner(
231                                 context.getResources(),
232                                 blockParser, blockParser,
233                                 theme);
234                     } else if ("selector".equals(name)) {
235                         return ComplexColor_Accessor.createColorStateListFromXmlInner(
236                                 context.getResources(),
237                                 blockParser, blockParser,
238                                 theme);
239                     }
240                 } finally {
241                     blockParser.ensurePopped();
242                 }
243             }
244         } catch (XmlPullParserException e) {
245             Bridge.getLog().error(ILayoutLog.TAG_BROKEN,
246                     "Failed to configure parser for " + value, e, null,null /*data*/);
247             // we'll return null below.
248         } catch (Exception e) {
249             // this is an error and not warning since the file existence is
250             // checked before attempting to parse it.
251             Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ,
252                     "Failed to parse file " + value, e, null, null /*data*/);
253 
254             return null;
255         }
256 
257         return null;
258     }
259 
260     /**
261      * Returns a {@link ColorStateList} from the given {@link ResourceValue}
262      *
263      * @param resValue the value containing a color value or a file path to a complex color
264      * definition
265      * @param context the current context
266      */
267     @Nullable
getColorStateList(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Resources.Theme theme)268     public static ColorStateList getColorStateList(@NonNull ResourceValue resValue,
269             @NonNull BridgeContext context, @Nullable Resources.Theme theme) {
270         return (ColorStateList) getInternalComplexColor(resValue, context,
271                 theme != null ? theme : context.getTheme(),
272                 false);
273     }
274 
275     /**
276      * Returns a {@link ComplexColor} from the given {@link ResourceValue}
277      *
278      * @param resValue the value containing a color value or a file path to a complex color
279      * definition
280      * @param context the current context
281      */
282     @Nullable
getComplexColor(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Resources.Theme theme)283     public static ComplexColor getComplexColor(@NonNull ResourceValue resValue,
284             @NonNull BridgeContext context, @Nullable Resources.Theme theme) {
285         return getInternalComplexColor(resValue, context,
286                 theme != null ? theme : context.getTheme(),
287                 true);
288     }
289 
290     /**
291      * Returns a drawable from the given value.
292      *
293      * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable,
294      *     or an hexadecimal color
295      * @param context the current context
296      */
297     @Nullable
getDrawable(ResourceValue value, BridgeContext context)298     public static Drawable getDrawable(ResourceValue value, BridgeContext context) {
299         return getDrawable(value, context, null);
300     }
301 
302     /**
303      * Returns a {@link BridgeXmlBlockParser} to parse the given {@link ResourceValue}. The passed
304      * value must point to an XML resource.
305      */
306     @Nullable
getXmlBlockParser(@onNull BridgeContext context, @NonNull ResourceValue value)307     public static BridgeXmlBlockParser getXmlBlockParser(@NonNull BridgeContext context,
308             @NonNull ResourceValue value) throws XmlPullParserException {
309         String stringValue = value.getValue();
310         if (RenderResources.REFERENCE_NULL.equals(stringValue)) {
311             return null;
312         }
313 
314         XmlPullParser parser = null;
315         ResourceNamespace namespace;
316 
317         LayoutlibCallback layoutlibCallback = context.getLayoutlibCallback();
318         // Framework values never need a PSI parser. They do not change and the do not contain
319         // aapt:attr attributes.
320         if (!value.isFramework()) {
321             parser = layoutlibCallback.getParser(value);
322         }
323 
324         if (parser != null) {
325             namespace = ((ILayoutPullParser) parser).getLayoutNamespace();
326         } else {
327             parser = ParserFactory.create(stringValue);
328             namespace = value.getNamespace();
329         }
330 
331         return parser == null
332                 ? null
333                 : new BridgeXmlBlockParser(parser, context, namespace);
334     }
335 
336     /**
337      * Returns a drawable from the given value.
338      *
339      * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable,
340      *     or an hexadecimal color
341      * @param context the current context
342      * @param theme the theme to be used to inflate the drawable.
343      */
344     @Nullable
getDrawable(ResourceValue value, BridgeContext context, Theme theme)345     public static Drawable getDrawable(ResourceValue value, BridgeContext context, Theme theme) {
346         if (value == null) {
347             return null;
348         }
349         String stringValue = value.getValue();
350         if (RenderResources.REFERENCE_NULL.equals(stringValue)) {
351             return null;
352         }
353 
354         // try the simple case first. Attempt to get a color from the value
355         if (stringValue.trim().startsWith("#")) {
356             try {
357                 int color = getColor(stringValue);
358                 return new ColorDrawable(color);
359             } catch (NumberFormatException e) {
360                 Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT,
361                         String.format("\"%1$s\" cannot be interpreted as a color.", stringValue),
362                         null, null);
363                 return null;
364             }
365         }
366 
367         Density density = Density.MEDIUM;
368         if (value instanceof DensityBasedResourceValue) {
369             density = ((DensityBasedResourceValue) value).getResourceDensity();
370             if (density == Density.NODPI || density == Density.ANYDPI) {
371                 density = Density.getEnum(context.getConfiguration().densityDpi);
372             }
373         }
374 
375         String lowerCaseValue = stringValue.toLowerCase();
376         if (lowerCaseValue.endsWith(".xml") || value.getResourceType() == ResourceType.AAPT) {
377             // create a block parser for the file
378             try {
379                 BridgeXmlBlockParser blockParser = getXmlBlockParser(context, value);
380                 if (blockParser != null) {
381                     Set<ResourceValue> visitedValues = context.getUserData(KEY_GET_DRAWABLE);
382                     if (visitedValues == null) {
383                         visitedValues = new HashSet<>();
384                         context.putUserData(KEY_GET_DRAWABLE, visitedValues);
385                     }
386                     if (!visitedValues.add(value)) {
387                         Bridge.getLog().error(null, "Cyclic dependency in " + stringValue, null,
388                                 null);
389                         return null;
390                     }
391 
392                     try {
393                         return Drawable.createFromXml(context.getResources(), blockParser, theme);
394                     } finally {
395                         visitedValues.remove(value);
396                         blockParser.ensurePopped();
397                     }
398                 }
399             } catch (Exception e) {
400                 // this is an error and not warning since the file existence is checked before
401                 // attempting to parse it.
402                 Bridge.getLog().error(null, "Failed to parse file " + stringValue, e,
403                         null, null /*data*/);
404             }
405 
406             return null;
407         } else {
408             AssetRepository repository = getAssetRepository(context);
409             if (repository.isFileResource(stringValue)) {
410                 try {
411                     Bitmap bitmap = Bridge.getCachedBitmap(stringValue,
412                             value.isFramework() ? null : context.getProjectKey());
413 
414                     if (bitmap == null) {
415                         InputStream stream;
416                         try {
417                             stream = repository.openNonAsset(0, stringValue, ACCESS_STREAMING);
418 
419                         } catch (FileNotFoundException e) {
420                             stream = null;
421                         }
422                         Options options = new Options();
423                         options.inDensity = density.getDpiValue();
424                         bitmap = BitmapFactory.decodeStream(stream, null, options);
425                         if (bitmap != null && bitmap.getNinePatchChunk() == null &&
426                                 lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) {
427                             //We are dealing with a non-compiled nine patch.
428                             stream = repository.openNonAsset(0, stringValue, ACCESS_STREAMING);
429                             NinePatch ninePatch = NinePatch.load(stream, true /*is9Patch*/, false /* convert */);
430                             BufferedImage image = ninePatch.getImage();
431 
432                             // width and height of the nine patch without the special border.
433                             int width = image.getWidth();
434                             int height = image.getHeight();
435 
436                             // Get pixel data from image independently of its type.
437                             int[] imageData = GraphicsUtilities.getPixels(image, 0, 0, width,
438                                     height, null);
439 
440                             bitmap = Bitmap.createBitmap(imageData, width, height, Config.ARGB_8888);
441 
442                             bitmap.setDensity(options.inDensity);
443                             bitmap.setNinePatchChunk(ninePatch.getChunk().getSerializedChunk());
444                         }
445                         Bridge.setCachedBitmap(stringValue, bitmap,
446                                 value.isFramework() ? null : context.getProjectKey());
447                     }
448 
449                     if (bitmap != null && bitmap.getNinePatchChunk() != null) {
450                         return new NinePatchDrawable(context.getResources(), bitmap, bitmap
451                                 .getNinePatchChunk(), new Rect(), lowerCaseValue);
452                     } else {
453                         return new BitmapDrawable(context.getResources(), bitmap);
454                     }
455                 } catch (IOException e) {
456                     // we'll return null below
457                     Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ,
458                             "Failed to load " + stringValue, e, null, null /*data*/);
459                 }
460             }
461         }
462 
463         return null;
464     }
465 
getAssetRepository(@onNull BridgeContext context)466     private static AssetRepository getAssetRepository(@NonNull BridgeContext context) {
467         BridgeAssetManager assetManager = context.getAssets();
468         return assetManager.getAssetRepository();
469     }
470 
471     /**
472      * Returns a {@link Typeface} given a font name. The font name, can be a system font family
473      * (like sans-serif) or a full path if the font is to be loaded from resources.
474      */
getFont(String fontName, BridgeContext context, Theme theme, boolean isFramework)475     public static Typeface getFont(String fontName, BridgeContext context, Theme theme, boolean
476             isFramework) {
477         if (fontName == null) {
478             return null;
479         }
480 
481         if (Typeface_Accessor.isSystemFont(fontName)) {
482             // Shortcut for the case where we are asking for a system font name. Those are not
483             // loaded using external resources.
484             return null;
485         }
486 
487 
488         return Typeface_Delegate.createFromDisk(context, fontName, isFramework);
489     }
490 
491     /**
492      * Returns a {@link Typeface} given a font name. The font name, can be a system font family
493      * (like sans-serif) or a full path if the font is to be loaded from resources.
494      */
getFont(ResourceValue value, BridgeContext context, Theme theme)495     public static Typeface getFont(ResourceValue value, BridgeContext context, Theme theme) {
496         if (value == null) {
497             return null;
498         }
499 
500         return getFont(value.getValue(), context, theme, value.isFramework());
501     }
502 
503     /**
504      * Looks for an attribute in the current theme.
505      *
506      * @param resources the render resources
507      * @param attr the attribute reference
508      * @param defaultValue the default value.
509      * @return the value of the attribute or the default one if not found.
510      */
getBooleanThemeValue(@onNull RenderResources resources, @NonNull ResourceReference attr, boolean defaultValue)511     public static boolean getBooleanThemeValue(@NonNull RenderResources resources,
512             @NonNull ResourceReference attr, boolean defaultValue) {
513         ResourceValue value = resources.findItemInTheme(attr);
514         value = resources.resolveResValue(value);
515         if (value == null) {
516             return defaultValue;
517         }
518         return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue);
519     }
520 
521     /**
522      * Looks for a framework attribute in the current theme.
523      *
524      * @param resources the render resources
525      * @param name the name of the attribute
526      * @param defaultValue the default value.
527      * @return the value of the attribute or the default one if not found.
528      */
getBooleanThemeFrameworkAttrValue(@onNull RenderResources resources, @NonNull String name, boolean defaultValue)529     public static boolean getBooleanThemeFrameworkAttrValue(@NonNull RenderResources resources,
530             @NonNull String name, boolean defaultValue) {
531         ResourceReference attrRef = BridgeContext.createFrameworkAttrReference(name);
532         return getBooleanThemeValue(resources, attrRef, defaultValue);
533     }
534 
535     /**
536      * This takes a resource string containing HTML tags for styling,
537      * and returns it correctly formatted to be displayed.
538      */
parseHtml(String string)539     public static CharSequence parseHtml(String string) {
540         // The parser requires <li> tags to be surrounded by <ul> tags to handle whitespace
541         // correctly, though Android does not support <ul> tags.
542         String str = string.replaceAll("<li>", "<ul><li>")
543                 .replaceAll("</li>","</li></ul>");
544         int firstTagIndex = str.indexOf('<');
545         int lastTagIndex = str.lastIndexOf('>');
546         StringBuilder stringBuilder = new StringBuilder(str.substring(0, firstTagIndex));
547         List<Tag> tagList = new ArrayList<>();
548         Map<String, Deque<Tag>> startStacks = new HashMap<>();
549         Parser parser = new Parser();
550         parser.setContentHandler(new DefaultHandler() {
551             @Override
552             public void startElement(String uri, String localName, String qName,
553                     Attributes attributes) {
554                 if (!Strings.isNullOrEmpty(localName)) {
555                     Tag tag = new Tag(localName);
556                     tag.mStart = stringBuilder.length();
557                     tag.mAttributes = attributes;
558                     startStacks.computeIfAbsent(localName, key -> new ArrayDeque<>()).addFirst(tag);
559                 }
560             }
561 
562             @Override
563             public void endElement(String uri, String localName, String qName) {
564                 if (!Strings.isNullOrEmpty(localName)) {
565                     Tag tag = startStacks.get(localName).removeFirst();
566                     tag.mEnd = stringBuilder.length();
567                     tagList.add(tag);
568                 }
569             }
570 
571             @Override
572             public void characters(char[] ch, int start, int length) {
573                 stringBuilder.append(ch, start, length);
574             }
575         });
576         try {
577             parser.setProperty(Parser.schemaProperty, new HTMLSchema());
578             parser.parse(new InputSource(
579                     new StringReader(str.substring(firstTagIndex, lastTagIndex + 1))));
580         } catch (SAXException | IOException e) {
581             Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT,
582                     "The string " + str + " is not valid HTML", null, null);
583             return str;
584         }
585         stringBuilder.append(str.substring(lastTagIndex + 1));
586         return applyStyles(stringBuilder, tagList);
587     }
588 
589     /**
590      * This applies the styles from tagList that are supported by Android
591      * and returns a {@link SpannedString}.
592      * This should mirror {@link StringBlock#applyStyles}
593      */
594     @NonNull
applyStyles(@onNull StringBuilder stringBuilder, @NonNull List<Tag> tagList)595     private static SpannedString applyStyles(@NonNull StringBuilder stringBuilder,
596             @NonNull List<Tag> tagList) {
597         SpannableString spannableString = new SpannableString(stringBuilder);
598         for (Tag tag : tagList) {
599             int start = tag.mStart;
600             int end = tag.mEnd;
601             Attributes attrs = tag.mAttributes;
602             switch (tag.mLabel) {
603                 case "b":
604                     spannableString.setSpan(new StyleSpan(Typeface.BOLD), start, end,
605                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
606                     break;
607                 case "i":
608                     spannableString.setSpan(new StyleSpan(Typeface.ITALIC), start, end,
609                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
610                     break;
611                 case "u":
612                     spannableString.setSpan(new UnderlineSpan(), start, end,
613                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
614                     break;
615                 case "tt":
616                     spannableString.setSpan(new TypefaceSpan("monospace"), start, end,
617                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
618                     break;
619                 case "big":
620                     spannableString.setSpan(new RelativeSizeSpan(1.25f), start, end,
621                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
622                     break;
623                 case "small":
624                     spannableString.setSpan(new RelativeSizeSpan(0.8f), start, end,
625                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
626                     break;
627                 case "sup":
628                     spannableString.setSpan(new SuperscriptSpan(), start, end,
629                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
630                     break;
631                 case "sub":
632                     spannableString.setSpan(new SubscriptSpan(), start, end,
633                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
634                     break;
635                 case "strike":
636                     spannableString.setSpan(new StrikethroughSpan(), start, end,
637                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
638                     break;
639                 case "li":
640                     StringBlock.addParagraphSpan(spannableString, new BulletSpan(10), start, end);
641                     break;
642                 case "marquee":
643                     spannableString.setSpan(TextUtils.TruncateAt.MARQUEE, start, end,
644                             Spanned.SPAN_INCLUSIVE_INCLUSIVE);
645                     break;
646                 case "font":
647                     String heightAttr = attrs.getValue("height");
648                     if (heightAttr != null) {
649                         int height = Integer.parseInt(heightAttr);
650                         StringBlock.addParagraphSpan(spannableString, new Height(height), start,
651                                 end);
652                     }
653 
654                     String sizeAttr = attrs.getValue("size");
655                     if (sizeAttr != null) {
656                         int size = Integer.parseInt(sizeAttr);
657                         spannableString.setSpan(new AbsoluteSizeSpan(size, true), start, end,
658                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
659                     }
660 
661                     String fgcolorAttr = attrs.getValue("fgcolor");
662                     if (fgcolorAttr != null) {
663                         spannableString.setSpan(StringBlock.getColor(fgcolorAttr, true), start, end,
664                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
665                     }
666 
667                     String colorAttr = attrs.getValue("color");
668                     if (colorAttr != null) {
669                         spannableString.setSpan(StringBlock.getColor(colorAttr, true), start, end,
670                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
671                     }
672 
673                     String bgcolorAttr = attrs.getValue("bgcolor");
674                     if (bgcolorAttr != null) {
675                         spannableString.setSpan(StringBlock.getColor(bgcolorAttr, false), start,
676                                 end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
677                     }
678 
679                     String faceAttr = attrs.getValue("face");
680                     if (faceAttr != null) {
681                         spannableString.setSpan(new TypefaceSpan(faceAttr), start, end,
682                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
683                     }
684                     break;
685                 case "a":
686                     String href = tag.mAttributes.getValue("href");
687                     if (href != null) {
688                         spannableString.setSpan(new URLSpan(href), start, end,
689                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
690                     }
691                     break;
692                 case "annotation":
693                     for (int i = 0; i < attrs.getLength(); i++) {
694                         String key = attrs.getLocalName(i);
695                         String value = attrs.getValue(i);
696                         spannableString.setSpan(new Annotation(key, value), start, end,
697                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
698                     }
699             }
700         }
701         return new SpannedString(spannableString);
702     }
703 
704     // ------- TypedValue stuff
705     // This is taken from //device/libs/utils/ResourceTypes.cpp
706 
707     private static final class UnitEntry {
708         String name;
709         int type;
710         int unit;
711         float scale;
712 
UnitEntry(String name, int type, int unit, float scale)713         UnitEntry(String name, int type, int unit, float scale) {
714             this.name = name;
715             this.type = type;
716             this.unit = unit;
717             this.scale = scale;
718         }
719     }
720 
721     private static final UnitEntry[] sUnitNames = new UnitEntry[] {
722         new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f),
723         new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
724         new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
725         new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f),
726         new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f),
727         new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f),
728         new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f),
729         new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100),
730         new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100),
731     };
732 
733     /**
734      * Returns the raw value from the given attribute float-type value string.
735      * This object is only valid until the next call on to {@link ResourceHelper}.
736      */
getValue(String attribute, String value, boolean requireUnit)737     public static TypedValue getValue(String attribute, String value, boolean requireUnit) {
738         if (parseFloatAttribute(attribute, value, mValue, requireUnit)) {
739             return mValue;
740         }
741 
742         return null;
743     }
744 
745     /**
746      * Parse a float attribute and return the parsed value into a given TypedValue.
747      * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false.
748      * @param value the string value of the attribute
749      * @param outValue the TypedValue to receive the parsed value
750      * @param requireUnit whether the value is expected to contain a unit.
751      * @return true if success.
752      */
parseFloatAttribute(String attribute, @NonNull String value, TypedValue outValue, boolean requireUnit)753     public static boolean parseFloatAttribute(String attribute, @NonNull String value,
754             TypedValue outValue, boolean requireUnit) {
755         assert !requireUnit || attribute != null;
756 
757         // remove the space before and after
758         value = value.trim();
759         int len = value.length();
760 
761         if (len <= 0) {
762             return false;
763         }
764 
765         // check that there's no non ascii characters.
766         char[] buf = value.toCharArray();
767         for (int i = 0 ; i < len ; i++) {
768             if (buf[i] > 255) {
769                 return false;
770             }
771         }
772 
773         // check the first character
774         if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') {
775             return false;
776         }
777 
778         // now look for the string that is after the float...
779         Matcher m = sFloatPattern.matcher(value);
780         if (m.matches()) {
781             String f_str = m.group(1);
782             String end = m.group(2);
783 
784             float f;
785             try {
786                 f = Float.parseFloat(f_str);
787             } catch (NumberFormatException e) {
788                 // this shouldn't happen with the regexp above.
789                 return false;
790             }
791 
792             if (end.length() > 0 && end.charAt(0) != ' ') {
793                 // Might be a unit...
794                 if (parseUnit(end, outValue, sFloatOut)) {
795                     computeTypedValue(outValue, f, sFloatOut[0]);
796                     return true;
797                 }
798                 return false;
799             }
800 
801             // make sure it's only spaces at the end.
802             end = end.trim();
803 
804             if (end.length() == 0) {
805                 if (outValue != null) {
806                     if (!requireUnit) {
807                         outValue.type = TypedValue.TYPE_FLOAT;
808                         outValue.data = Float.floatToIntBits(f);
809                     } else {
810                         // no unit when required? Use dp and out an error.
811                         applyUnit(sUnitNames[1], outValue, sFloatOut);
812                         computeTypedValue(outValue, f, sFloatOut[0]);
813 
814                         Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_RESOLVE,
815                                 String.format(
816                                         "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!",
817                                         value, attribute),
818                                 null, null);
819                     }
820                     return true;
821                 }
822             }
823         }
824 
825         return false;
826     }
827 
computeTypedValue(TypedValue outValue, float value, float scale)828     private static void computeTypedValue(TypedValue outValue, float value, float scale) {
829         value *= scale;
830         boolean neg = value < 0;
831         if (neg) {
832             value = -value;
833         }
834         long bits = (long)(value*(1<<23)+.5f);
835         int radix;
836         int shift;
837         if ((bits&0x7fffff) == 0) {
838             // Always use 23p0 if there is no fraction, just to make
839             // things easier to read.
840             radix = TypedValue.COMPLEX_RADIX_23p0;
841             shift = 23;
842         } else if ((bits&0xffffffffff800000L) == 0) {
843             // Magnitude is zero -- can fit in 0 bits of precision.
844             radix = TypedValue.COMPLEX_RADIX_0p23;
845             shift = 0;
846         } else if ((bits&0xffffffff80000000L) == 0) {
847             // Magnitude can fit in 8 bits of precision.
848             radix = TypedValue.COMPLEX_RADIX_8p15;
849             shift = 8;
850         } else if ((bits&0xffffff8000000000L) == 0) {
851             // Magnitude can fit in 16 bits of precision.
852             radix = TypedValue.COMPLEX_RADIX_16p7;
853             shift = 16;
854         } else {
855             // Magnitude needs entire range, so no fractional part.
856             radix = TypedValue.COMPLEX_RADIX_23p0;
857             shift = 23;
858         }
859         int mantissa = (int)(
860             (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK);
861         if (neg) {
862             mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK;
863         }
864         outValue.data |=
865             (radix<<TypedValue.COMPLEX_RADIX_SHIFT)
866             | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT);
867     }
868 
869     private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) {
870         str = str.trim();
871 
872         for (UnitEntry unit : sUnitNames) {
873             if (unit.name.equals(str)) {
874                 applyUnit(unit, outValue, outScale);
875                 return true;
876             }
877         }
878 
879         return false;
880     }
881 
882     private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) {
883         outValue.type = unit.type;
884         // COMPLEX_UNIT_SHIFT is 0 and hence intelliJ complains about it. Suppress the warning.
885         //noinspection PointlessBitwiseExpression
886         outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT;
887         outScale[0] = unit.scale;
888     }
889 
890     private static class Tag {
891         private String mLabel;
892         private int mStart;
893         private int mEnd;
894         private Attributes mAttributes;
895 
896         private Tag(String label) {
897             mLabel = label;
898         }
899     }
900 }
901 
902