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