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.create(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                         Rect padding = new Rect();
425                         bitmap = BitmapFactory.decodeStream(stream, padding, options);
426                         if (bitmap != null && bitmap.getNinePatchChunk() == null &&
427                                 lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) {
428                             //We are dealing with a non-compiled nine patch.
429                             stream = repository.openNonAsset(0, stringValue, ACCESS_STREAMING);
430                             NinePatch ninePatch = NinePatch.load(stream, true /*is9Patch*/, false /* convert */);
431                             BufferedImage image = ninePatch.getImage();
432 
433                             // width and height of the nine patch without the special border.
434                             int width = image.getWidth();
435                             int height = image.getHeight();
436 
437                             // Get pixel data from image independently of its type.
438                             int[] imageData = GraphicsUtilities.getPixels(image, 0, 0, width,
439                                     height, null);
440 
441                             bitmap = Bitmap.createBitmap(imageData, width, height, Config.ARGB_8888);
442 
443                             bitmap.setDensity(options.inDensity);
444                             bitmap.setNinePatchChunk(ninePatch.getChunk().getSerializedChunk());
445                             int[] padArray = ninePatch.getChunk().getPadding();
446                             padding.set(padArray[0], padArray[1], padArray[2], padArray[3]);
447                         }
448                         Bridge.setCachedBitmapPadding(stringValue, padding,
449                                 value.isFramework() ? null : context.getProjectKey());
450                         Bridge.setCachedBitmap(stringValue, bitmap,
451                                 value.isFramework() ? null : context.getProjectKey());
452                     }
453 
454                     if (bitmap != null && bitmap.getNinePatchChunk() != null) {
455                         Rect padding = Bridge.getCachedBitmapPadding(stringValue,
456                                 value.isFramework() ? null : context.getProjectKey());
457                         return new NinePatchDrawable(context.getResources(), bitmap, bitmap
458                                 .getNinePatchChunk(), padding, lowerCaseValue);
459                     } else {
460                         return new BitmapDrawable(context.getResources(), bitmap);
461                     }
462                 } catch (IOException e) {
463                     // we'll return null below
464                     Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ,
465                             "Failed to load " + stringValue, e, null, null /*data*/);
466                 }
467             }
468         }
469 
470         return null;
471     }
472 
getAssetRepository(@onNull BridgeContext context)473     private static AssetRepository getAssetRepository(@NonNull BridgeContext context) {
474         BridgeAssetManager assetManager = context.getAssets();
475         return assetManager.getAssetRepository();
476     }
477 
478     /**
479      * Returns a {@link Typeface} given a font name. The font name, can be a system font family
480      * (like sans-serif) or a full path if the font is to be loaded from resources.
481      */
getFont(String fontName, BridgeContext context, Theme theme, boolean isFramework)482     public static Typeface getFont(String fontName, BridgeContext context, Theme theme, boolean
483             isFramework) {
484         if (fontName == null || fontName.isBlank()) {
485             return null;
486         }
487 
488         if (Typeface_Accessor.isSystemFont(fontName)) {
489             // Shortcut for the case where we are asking for a system font name. Those are not
490             // loaded using external resources.
491             return null;
492         }
493 
494 
495         return Typeface_Delegate.createFromDisk(context, fontName, isFramework);
496     }
497 
498     /**
499      * Returns a {@link Typeface} given a font name. The font name, can be a system font family
500      * (like sans-serif) or a full path if the font is to be loaded from resources.
501      */
getFont(ResourceValue value, BridgeContext context, Theme theme)502     public static Typeface getFont(ResourceValue value, BridgeContext context, Theme theme) {
503         if (value == null) {
504             return null;
505         }
506 
507         return getFont(value.getValue(), context, theme, value.isFramework());
508     }
509 
510     /**
511      * Looks for an attribute in the current theme.
512      *
513      * @param resources the render resources
514      * @param attr the attribute reference
515      * @param defaultValue the default value.
516      * @return the value of the attribute or the default one if not found.
517      */
getBooleanThemeValue(@onNull RenderResources resources, @NonNull ResourceReference attr, boolean defaultValue)518     public static boolean getBooleanThemeValue(@NonNull RenderResources resources,
519             @NonNull ResourceReference attr, boolean defaultValue) {
520         ResourceValue value = resources.findItemInTheme(attr);
521         value = resources.resolveResValue(value);
522         if (value == null) {
523             return defaultValue;
524         }
525         return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue);
526     }
527 
528     /**
529      * Looks for a framework attribute in the current theme.
530      *
531      * @param resources the render resources
532      * @param name the name of the attribute
533      * @param defaultValue the default value.
534      * @return the value of the attribute or the default one if not found.
535      */
getBooleanThemeFrameworkAttrValue(@onNull RenderResources resources, @NonNull String name, boolean defaultValue)536     public static boolean getBooleanThemeFrameworkAttrValue(@NonNull RenderResources resources,
537             @NonNull String name, boolean defaultValue) {
538         ResourceReference attrRef = BridgeContext.createFrameworkAttrReference(name);
539         return getBooleanThemeValue(resources, attrRef, defaultValue);
540     }
541 
542     /**
543      * This takes a resource string containing HTML tags for styling,
544      * and returns it correctly formatted to be displayed.
545      */
parseHtml(String string)546     public static CharSequence parseHtml(String string) {
547         // The parser requires <li> tags to be surrounded by <ul> tags to handle whitespace
548         // correctly, though Android does not support <ul> tags.
549         String str = string.replaceAll("<li>", "<ul><li>")
550                 .replaceAll("</li>","</li></ul>");
551         int firstTagIndex = str.indexOf('<');
552         if (firstTagIndex == -1) {
553             return string;
554         }
555         int lastTagIndex = str.lastIndexOf('>');
556         StringBuilder stringBuilder = new StringBuilder(str.substring(0, firstTagIndex));
557         List<Tag> tagList = new ArrayList<>();
558         Map<String, Deque<Tag>> startStacks = new HashMap<>();
559         Parser parser = new Parser();
560         parser.setContentHandler(new DefaultHandler() {
561             @Override
562             public void startElement(String uri, String localName, String qName,
563                     Attributes attributes) {
564                 if (!Strings.isNullOrEmpty(localName)) {
565                     Tag tag = new Tag(localName);
566                     tag.mStart = stringBuilder.length();
567                     tag.mAttributes = attributes;
568                     startStacks.computeIfAbsent(localName, key -> new ArrayDeque<>()).addFirst(tag);
569                 }
570             }
571 
572             @Override
573             public void endElement(String uri, String localName, String qName) {
574                 if (!Strings.isNullOrEmpty(localName)) {
575                     Tag tag = startStacks.get(localName).removeFirst();
576                     tag.mEnd = stringBuilder.length();
577                     tagList.add(tag);
578                 }
579             }
580 
581             @Override
582             public void characters(char[] ch, int start, int length) {
583                 stringBuilder.append(ch, start, length);
584             }
585         });
586         try {
587             parser.setProperty(Parser.schemaProperty, new HTMLSchema());
588             parser.parse(new InputSource(
589                     new StringReader(str.substring(firstTagIndex, lastTagIndex + 1))));
590         } catch (SAXException | IOException e) {
591             Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT,
592                     "The string " + str + " is not valid HTML", null, null);
593             return str;
594         }
595         stringBuilder.append(str.substring(lastTagIndex + 1));
596         return applyStyles(stringBuilder, tagList);
597     }
598 
599     /**
600      * This applies the styles from tagList that are supported by Android
601      * and returns a {@link SpannedString}.
602      * This should mirror {@link StringBlock#applyStyles}
603      */
604     @NonNull
applyStyles(@onNull StringBuilder stringBuilder, @NonNull List<Tag> tagList)605     private static SpannedString applyStyles(@NonNull StringBuilder stringBuilder,
606             @NonNull List<Tag> tagList) {
607         SpannableString spannableString = new SpannableString(stringBuilder);
608         for (Tag tag : tagList) {
609             int start = tag.mStart;
610             int end = tag.mEnd;
611             Attributes attrs = tag.mAttributes;
612             switch (tag.mLabel) {
613                 case "b":
614                     spannableString.setSpan(new StyleSpan(Typeface.BOLD), start, end,
615                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
616                     break;
617                 case "i":
618                     spannableString.setSpan(new StyleSpan(Typeface.ITALIC), start, end,
619                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
620                     break;
621                 case "u":
622                     spannableString.setSpan(new UnderlineSpan(), start, end,
623                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
624                     break;
625                 case "tt":
626                     spannableString.setSpan(new TypefaceSpan("monospace"), start, end,
627                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
628                     break;
629                 case "big":
630                     spannableString.setSpan(new RelativeSizeSpan(1.25f), start, end,
631                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
632                     break;
633                 case "small":
634                     spannableString.setSpan(new RelativeSizeSpan(0.8f), start, end,
635                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
636                     break;
637                 case "sup":
638                     spannableString.setSpan(new SuperscriptSpan(), start, end,
639                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
640                     break;
641                 case "sub":
642                     spannableString.setSpan(new SubscriptSpan(), start, end,
643                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
644                     break;
645                 case "strike":
646                     spannableString.setSpan(new StrikethroughSpan(), start, end,
647                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
648                     break;
649                 case "li":
650                     StringBlock.addParagraphSpan(spannableString, new BulletSpan(10), start, end);
651                     break;
652                 case "marquee":
653                     spannableString.setSpan(TextUtils.TruncateAt.MARQUEE, start, end,
654                             Spanned.SPAN_INCLUSIVE_INCLUSIVE);
655                     break;
656                 case "font":
657                     String heightAttr = attrs.getValue("height");
658                     if (heightAttr != null) {
659                         int height = Integer.parseInt(heightAttr);
660                         StringBlock.addParagraphSpan(spannableString, new Height(height), start,
661                                 end);
662                     }
663 
664                     String sizeAttr = attrs.getValue("size");
665                     if (sizeAttr != null) {
666                         int size = Integer.parseInt(sizeAttr);
667                         spannableString.setSpan(new AbsoluteSizeSpan(size, true), start, end,
668                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
669                     }
670 
671                     String fgcolorAttr = attrs.getValue("fgcolor");
672                     if (fgcolorAttr != null) {
673                         spannableString.setSpan(StringBlock.getColor(fgcolorAttr, true), start, end,
674                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
675                     }
676 
677                     String colorAttr = attrs.getValue("color");
678                     if (colorAttr != null) {
679                         spannableString.setSpan(StringBlock.getColor(colorAttr, true), start, end,
680                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
681                     }
682 
683                     String bgcolorAttr = attrs.getValue("bgcolor");
684                     if (bgcolorAttr != null) {
685                         spannableString.setSpan(StringBlock.getColor(bgcolorAttr, false), start,
686                                 end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
687                     }
688 
689                     String faceAttr = attrs.getValue("face");
690                     if (faceAttr != null) {
691                         spannableString.setSpan(new TypefaceSpan(faceAttr), start, end,
692                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
693                     }
694                     break;
695                 case "a":
696                     String href = tag.mAttributes.getValue("href");
697                     if (href != null) {
698                         spannableString.setSpan(new URLSpan(href), start, end,
699                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
700                     }
701                     break;
702                 case "annotation":
703                     for (int i = 0; i < attrs.getLength(); i++) {
704                         String key = attrs.getLocalName(i);
705                         String value = attrs.getValue(i);
706                         spannableString.setSpan(new Annotation(key, value), start, end,
707                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
708                     }
709             }
710         }
711         return new SpannedString(spannableString);
712     }
713 
714     // ------- TypedValue stuff
715     // This is taken from //device/libs/utils/ResourceTypes.cpp
716 
717     private static final class UnitEntry {
718         String name;
719         int type;
720         int unit;
721         float scale;
722 
UnitEntry(String name, int type, int unit, float scale)723         UnitEntry(String name, int type, int unit, float scale) {
724             this.name = name;
725             this.type = type;
726             this.unit = unit;
727             this.scale = scale;
728         }
729     }
730 
731     private static final UnitEntry[] sUnitNames = new UnitEntry[] {
732         new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f),
733         new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
734         new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
735         new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f),
736         new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f),
737         new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f),
738         new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f),
739         new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100),
740         new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100),
741     };
742 
743     /**
744      * Returns the raw value from the given attribute float-type value string.
745      * This object is only valid until the next call on to {@link ResourceHelper}.
746      */
getValue(String attribute, String value, boolean requireUnit)747     public static TypedValue getValue(String attribute, String value, boolean requireUnit) {
748         if (parseFloatAttribute(attribute, value, mValue, requireUnit)) {
749             return mValue;
750         }
751 
752         return null;
753     }
754 
755     /**
756      * Parse a float attribute and return the parsed value into a given TypedValue.
757      * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false.
758      * @param value the string value of the attribute
759      * @param outValue the TypedValue to receive the parsed value
760      * @param requireUnit whether the value is expected to contain a unit.
761      * @return true if success.
762      */
parseFloatAttribute(String attribute, @NonNull String value, TypedValue outValue, boolean requireUnit)763     public static boolean parseFloatAttribute(String attribute, @NonNull String value,
764             TypedValue outValue, boolean requireUnit) {
765         assert !requireUnit || attribute != null;
766 
767         // remove the space before and after
768         value = value.trim();
769         int len = value.length();
770 
771         if (len <= 0) {
772             return false;
773         }
774 
775         // check that there's no non ascii characters.
776         char[] buf = value.toCharArray();
777         for (int i = 0 ; i < len ; i++) {
778             if (buf[i] > 255) {
779                 return false;
780             }
781         }
782 
783         // check the first character
784         if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') {
785             return false;
786         }
787 
788         // now look for the string that is after the float...
789         Matcher m = sFloatPattern.matcher(value);
790         if (m.matches()) {
791             String f_str = m.group(1);
792             String end = m.group(2);
793 
794             float f;
795             try {
796                 f = Float.parseFloat(f_str);
797             } catch (NumberFormatException e) {
798                 // this shouldn't happen with the regexp above.
799                 return false;
800             }
801 
802             if (end.length() > 0 && end.charAt(0) != ' ') {
803                 // Might be a unit...
804                 if (parseUnit(end, outValue, sFloatOut)) {
805                     computeTypedValue(outValue, f, sFloatOut[0]);
806                     return true;
807                 }
808                 return false;
809             }
810 
811             // make sure it's only spaces at the end.
812             end = end.trim();
813 
814             if (end.length() == 0) {
815                 if (outValue != null) {
816                     if (!requireUnit) {
817                         outValue.type = TypedValue.TYPE_FLOAT;
818                         outValue.data = Float.floatToIntBits(f);
819                     } else {
820                         // no unit when required? Use dp and out an error.
821                         applyUnit(sUnitNames[1], outValue, sFloatOut);
822                         computeTypedValue(outValue, f, sFloatOut[0]);
823 
824                         Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_RESOLVE,
825                                 String.format(
826                                         "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!",
827                                         value, attribute),
828                                 null, null);
829                     }
830                     return true;
831                 }
832             }
833         }
834 
835         return false;
836     }
837 
computeTypedValue(TypedValue outValue, float value, float scale)838     private static void computeTypedValue(TypedValue outValue, float value, float scale) {
839         value *= scale;
840         boolean neg = value < 0;
841         if (neg) {
842             value = -value;
843         }
844         long bits = (long)(value*(1<<23)+.5f);
845         int radix;
846         int shift;
847         if ((bits&0x7fffff) == 0) {
848             // Always use 23p0 if there is no fraction, just to make
849             // things easier to read.
850             radix = TypedValue.COMPLEX_RADIX_23p0;
851             shift = 23;
852         } else if ((bits&0xffffffffff800000L) == 0) {
853             // Magnitude is zero -- can fit in 0 bits of precision.
854             radix = TypedValue.COMPLEX_RADIX_0p23;
855             shift = 0;
856         } else if ((bits&0xffffffff80000000L) == 0) {
857             // Magnitude can fit in 8 bits of precision.
858             radix = TypedValue.COMPLEX_RADIX_8p15;
859             shift = 8;
860         } else if ((bits&0xffffff8000000000L) == 0) {
861             // Magnitude can fit in 16 bits of precision.
862             radix = TypedValue.COMPLEX_RADIX_16p7;
863             shift = 16;
864         } else {
865             // Magnitude needs entire range, so no fractional part.
866             radix = TypedValue.COMPLEX_RADIX_23p0;
867             shift = 23;
868         }
869         int mantissa = (int)(
870             (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK);
871         if (neg) {
872             mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK;
873         }
874         outValue.data |=
875             (radix<<TypedValue.COMPLEX_RADIX_SHIFT)
876             | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT);
877     }
878 
879     private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) {
880         str = str.trim();
881 
882         for (UnitEntry unit : sUnitNames) {
883             if (unit.name.equals(str)) {
884                 applyUnit(unit, outValue, outScale);
885                 return true;
886             }
887         }
888 
889         return false;
890     }
891 
892     private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) {
893         outValue.type = unit.type;
894         // COMPLEX_UNIT_SHIFT is 0 and hence intelliJ complains about it. Suppress the warning.
895         //noinspection PointlessBitwiseExpression
896         outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT;
897         outScale[0] = unit.scale;
898     }
899 
900     private static class Tag {
901         private String mLabel;
902         private int mStart;
903         private int mEnd;
904         private Attributes mAttributes;
905 
906         private Tag(String label) {
907             mLabel = label;
908         }
909     }
910 }
911 
912