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