1 /* 2 * Copyright (C) 2006 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 17 package android.content.res; 18 19 import android.annotation.Nullable; 20 import android.app.ActivityThread; 21 import android.app.Application; 22 import android.compat.annotation.UnsupportedAppUsage; 23 import android.graphics.Color; 24 import android.graphics.Paint; 25 import android.graphics.Rect; 26 import android.graphics.Typeface; 27 import android.graphics.text.LineBreakConfig; 28 import android.ravenwood.annotation.RavenwoodClassLoadHook; 29 import android.ravenwood.annotation.RavenwoodKeepWholeClass; 30 import android.text.Annotation; 31 import android.text.Spannable; 32 import android.text.SpannableString; 33 import android.text.SpannedString; 34 import android.text.TextPaint; 35 import android.text.TextUtils; 36 import android.text.style.AbsoluteSizeSpan; 37 import android.text.style.BackgroundColorSpan; 38 import android.text.style.BulletSpan; 39 import android.text.style.CharacterStyle; 40 import android.text.style.ForegroundColorSpan; 41 import android.text.style.LineBreakConfigSpan; 42 import android.text.style.LineHeightSpan; 43 import android.text.style.RelativeSizeSpan; 44 import android.text.style.StrikethroughSpan; 45 import android.text.style.StyleSpan; 46 import android.text.style.SubscriptSpan; 47 import android.text.style.SuperscriptSpan; 48 import android.text.style.TextAppearanceSpan; 49 import android.text.style.TypefaceSpan; 50 import android.text.style.URLSpan; 51 import android.text.style.UnderlineSpan; 52 import android.util.Log; 53 import android.util.SparseArray; 54 55 import com.android.internal.annotations.GuardedBy; 56 57 import java.io.Closeable; 58 import java.util.Arrays; 59 60 /** 61 * Conveniences for retrieving data out of a compiled string resource. 62 * 63 * {@hide} 64 */ 65 @RavenwoodKeepWholeClass 66 @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) 67 public final class StringBlock implements Closeable { 68 private static final String TAG = "AssetManager"; 69 private static final boolean localLOGV = false; 70 71 private long mNative; // final, but gets modified when closed 72 private final boolean mUseSparse; 73 private final boolean mOwnsNative; 74 75 private CharSequence[] mStrings; 76 private SparseArray<CharSequence> mSparseStrings; 77 78 @GuardedBy("this") private boolean mOpen = true; 79 80 StyleIDs mStyleIDs = null; 81 StringBlock(byte[] data, boolean useSparse)82 public StringBlock(byte[] data, boolean useSparse) { 83 mNative = nativeCreate(data, 0, data.length); 84 mUseSparse = useSparse; 85 mOwnsNative = true; 86 if (localLOGV) Log.v(TAG, "Created string block " + this 87 + ": " + nativeGetSize(mNative)); 88 } 89 StringBlock(byte[] data, int offset, int size, boolean useSparse)90 public StringBlock(byte[] data, int offset, int size, boolean useSparse) { 91 mNative = nativeCreate(data, offset, size); 92 mUseSparse = useSparse; 93 mOwnsNative = true; 94 if (localLOGV) Log.v(TAG, "Created string block " + this 95 + ": " + nativeGetSize(mNative)); 96 } 97 98 /** 99 * @deprecated use {@link #getSequence(int)} which can return null when a string cannot be found 100 * due to incremental installation. 101 */ 102 @Deprecated 103 @UnsupportedAppUsage get(int idx)104 public CharSequence get(int idx) { 105 CharSequence seq = getSequence(idx); 106 return seq == null ? "" : seq; 107 } 108 109 @Nullable getSequence(int idx)110 public CharSequence getSequence(int idx) { 111 synchronized (this) { 112 if (mStrings != null) { 113 CharSequence res = mStrings[idx]; 114 if (res != null) { 115 return res; 116 } 117 } else if (mSparseStrings != null) { 118 CharSequence res = mSparseStrings.get(idx); 119 if (res != null) { 120 return res; 121 } 122 } else { 123 final int num = nativeGetSize(mNative); 124 if (mUseSparse && num > 250) { 125 mSparseStrings = new SparseArray<CharSequence>(); 126 } else { 127 mStrings = new CharSequence[num]; 128 } 129 } 130 String str = nativeGetString(mNative, idx); 131 if (str == null) { 132 return null; 133 } 134 CharSequence res = str; 135 int[] style = nativeGetStyle(mNative, idx); 136 if (localLOGV) Log.v(TAG, "Got string: " + str); 137 if (localLOGV) Log.v(TAG, "Got styles: " + Arrays.toString(style)); 138 if (style != null) { 139 if (mStyleIDs == null) { 140 mStyleIDs = new StyleIDs(); 141 } 142 143 // the style array is a flat array of <type, start, end> hence 144 // the magic constant 3. 145 for (int styleIndex = 0; styleIndex < style.length; styleIndex += 3) { 146 int styleId = style[styleIndex]; 147 148 if (styleId == mStyleIDs.boldId || styleId == mStyleIDs.italicId 149 || styleId == mStyleIDs.underlineId || styleId == mStyleIDs.ttId 150 || styleId == mStyleIDs.bigId || styleId == mStyleIDs.smallId 151 || styleId == mStyleIDs.subId || styleId == mStyleIDs.supId 152 || styleId == mStyleIDs.strikeId || styleId == mStyleIDs.listItemId 153 || styleId == mStyleIDs.marqueeId) { 154 // id already found skip to next style 155 continue; 156 } 157 158 String styleTag = nativeGetString(mNative, styleId); 159 if (styleTag == null) { 160 return null; 161 } 162 163 if (styleTag.equals("b")) { 164 mStyleIDs.boldId = styleId; 165 } else if (styleTag.equals("i")) { 166 mStyleIDs.italicId = styleId; 167 } else if (styleTag.equals("u")) { 168 mStyleIDs.underlineId = styleId; 169 } else if (styleTag.equals("tt")) { 170 mStyleIDs.ttId = styleId; 171 } else if (styleTag.equals("big")) { 172 mStyleIDs.bigId = styleId; 173 } else if (styleTag.equals("small")) { 174 mStyleIDs.smallId = styleId; 175 } else if (styleTag.equals("sup")) { 176 mStyleIDs.supId = styleId; 177 } else if (styleTag.equals("sub")) { 178 mStyleIDs.subId = styleId; 179 } else if (styleTag.equals("strike")) { 180 mStyleIDs.strikeId = styleId; 181 } else if (styleTag.equals("li")) { 182 mStyleIDs.listItemId = styleId; 183 } else if (styleTag.equals("marquee")) { 184 mStyleIDs.marqueeId = styleId; 185 } else if (styleTag.equals("nobreak")) { 186 mStyleIDs.mNoBreakId = styleId; 187 } else if (styleTag.equals("nohyphen")) { 188 mStyleIDs.mNoHyphenId = styleId; 189 } 190 } 191 192 res = applyStyles(str, style, mStyleIDs); 193 } 194 if (res != null) { 195 if (mStrings != null) mStrings[idx] = res; 196 else mSparseStrings.put(idx, res); 197 } 198 return res; 199 } 200 } 201 202 @Override finalize()203 protected void finalize() throws Throwable { 204 try { 205 super.finalize(); 206 } finally { 207 close(); 208 } 209 } 210 211 @Override close()212 public void close() { 213 synchronized (this) { 214 if (mOpen) { 215 mOpen = false; 216 217 if (mOwnsNative) { 218 nativeDestroy(mNative); 219 } 220 mNative = 0; 221 } 222 } 223 } 224 225 static final class StyleIDs { 226 private int boldId = -1; 227 private int italicId = -1; 228 private int underlineId = -1; 229 private int ttId = -1; 230 private int bigId = -1; 231 private int smallId = -1; 232 private int subId = -1; 233 private int supId = -1; 234 private int strikeId = -1; 235 private int listItemId = -1; 236 private int marqueeId = -1; 237 private int mNoBreakId = -1; 238 private int mNoHyphenId = -1; 239 } 240 241 @Nullable applyStyles(String str, int[] style, StyleIDs ids)242 private CharSequence applyStyles(String str, int[] style, StyleIDs ids) { 243 if (style.length == 0) 244 return str; 245 246 SpannableString buffer = new SpannableString(str); 247 int i=0; 248 while (i < style.length) { 249 int type = style[i]; 250 if (localLOGV) Log.v(TAG, "Applying style span id=" + type 251 + ", start=" + style[i+1] + ", end=" + style[i+2]); 252 253 254 if (type == ids.boldId) { 255 Application application = ActivityThread.currentApplication(); 256 int fontWeightAdjustment = 257 application.getResources().getConfiguration().fontWeightAdjustment; 258 buffer.setSpan(new StyleSpan(Typeface.BOLD, fontWeightAdjustment), 259 style[i+1], style[i+2]+1, 260 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 261 } else if (type == ids.italicId) { 262 buffer.setSpan(new StyleSpan(Typeface.ITALIC), 263 style[i+1], style[i+2]+1, 264 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 265 } else if (type == ids.underlineId) { 266 buffer.setSpan(new UnderlineSpan(), 267 style[i+1], style[i+2]+1, 268 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 269 } else if (type == ids.ttId) { 270 buffer.setSpan(new TypefaceSpan("monospace"), 271 style[i+1], style[i+2]+1, 272 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 273 } else if (type == ids.bigId) { 274 buffer.setSpan(new RelativeSizeSpan(1.25f), 275 style[i+1], style[i+2]+1, 276 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 277 } else if (type == ids.smallId) { 278 buffer.setSpan(new RelativeSizeSpan(0.8f), 279 style[i+1], style[i+2]+1, 280 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 281 } else if (type == ids.subId) { 282 buffer.setSpan(new SubscriptSpan(), 283 style[i+1], style[i+2]+1, 284 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 285 } else if (type == ids.supId) { 286 buffer.setSpan(new SuperscriptSpan(), 287 style[i+1], style[i+2]+1, 288 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 289 } else if (type == ids.strikeId) { 290 buffer.setSpan(new StrikethroughSpan(), 291 style[i+1], style[i+2]+1, 292 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 293 } else if (type == ids.listItemId) { 294 addParagraphSpan(buffer, new BulletSpan(10), 295 style[i+1], style[i+2]+1); 296 } else if (type == ids.marqueeId) { 297 buffer.setSpan(TextUtils.TruncateAt.MARQUEE, 298 style[i+1], style[i+2]+1, 299 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 300 } else if (type == ids.mNoBreakId) { 301 buffer.setSpan(LineBreakConfigSpan.createNoBreakSpan(), 302 style[i + 1], style[i + 2] + 1, 303 Spannable.SPAN_INCLUSIVE_EXCLUSIVE); 304 } else if (type == ids.mNoHyphenId) { 305 buffer.setSpan(LineBreakConfigSpan.createNoHyphenationSpan(), 306 style[i + 1], style[i + 2] + 1, 307 Spannable.SPAN_INCLUSIVE_EXCLUSIVE); 308 } else { 309 String tag = nativeGetString(mNative, type); 310 if (tag == null) { 311 return null; 312 } 313 if (tag.startsWith("font;")) { 314 String sub; 315 316 sub = subtag(tag, ";height="); 317 if (sub != null) { 318 int size = Integer.parseInt(sub); 319 addParagraphSpan(buffer, new Height(size), 320 style[i+1], style[i+2]+1); 321 } 322 323 sub = subtag(tag, ";size="); 324 if (sub != null) { 325 int size = Integer.parseInt(sub); 326 buffer.setSpan(new AbsoluteSizeSpan(size, true), 327 style[i+1], style[i+2]+1, 328 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 329 } 330 331 sub = subtag(tag, ";fgcolor="); 332 if (sub != null) { 333 buffer.setSpan(getColor(sub, true), 334 style[i+1], style[i+2]+1, 335 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 336 } 337 338 sub = subtag(tag, ";color="); 339 if (sub != null) { 340 buffer.setSpan(getColor(sub, true), 341 style[i+1], style[i+2]+1, 342 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 343 } 344 345 sub = subtag(tag, ";bgcolor="); 346 if (sub != null) { 347 buffer.setSpan(getColor(sub, false), 348 style[i+1], style[i+2]+1, 349 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 350 } 351 352 sub = subtag(tag, ";face="); 353 if (sub != null) { 354 buffer.setSpan(new TypefaceSpan(sub), 355 style[i+1], style[i+2]+1, 356 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 357 } 358 } else if (tag.startsWith("a;")) { 359 String sub; 360 361 sub = subtag(tag, ";href="); 362 if (sub != null) { 363 buffer.setSpan(new URLSpan(sub), 364 style[i+1], style[i+2]+1, 365 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 366 } 367 } else if (tag.startsWith("annotation;")) { 368 int len = tag.length(); 369 int next; 370 371 for (int t = tag.indexOf(';'); t < len; t = next) { 372 int eq = tag.indexOf('=', t); 373 if (eq < 0) { 374 break; 375 } 376 377 next = tag.indexOf(';', eq); 378 if (next < 0) { 379 next = len; 380 } 381 382 String key = tag.substring(t + 1, eq); 383 String value = tag.substring(eq + 1, next); 384 385 buffer.setSpan(new Annotation(key, value), 386 style[i+1], style[i+2]+1, 387 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 388 } 389 } else if (tag.startsWith("lineBreakConfig;")) { 390 String lbStyleStr = subtag(tag, ";style="); 391 int lbStyle = LineBreakConfig.LINE_BREAK_STYLE_UNSPECIFIED; 392 if (lbStyleStr != null) { 393 if (lbStyleStr.equals("none")) { 394 lbStyle = LineBreakConfig.LINE_BREAK_STYLE_NONE; 395 } else if (lbStyleStr.equals("normal")) { 396 lbStyle = LineBreakConfig.LINE_BREAK_STYLE_NORMAL; 397 } else if (lbStyleStr.equals("loose")) { 398 lbStyle = LineBreakConfig.LINE_BREAK_STYLE_LOOSE; 399 } else if (lbStyleStr.equals("strict")) { 400 lbStyle = LineBreakConfig.LINE_BREAK_STYLE_STRICT; 401 } else { 402 Log.w(TAG, "Unknown LineBreakConfig style: " + lbStyleStr); 403 } 404 } 405 406 String lbWordStyleStr = subtag(tag, ";wordStyle="); 407 int lbWordStyle = LineBreakConfig.LINE_BREAK_STYLE_UNSPECIFIED; 408 if (lbWordStyleStr != null) { 409 if (lbWordStyleStr.equals("none")) { 410 lbWordStyle = LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE; 411 } else if (lbWordStyleStr.equals("phrase")) { 412 lbWordStyle = LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE; 413 } else { 414 Log.w(TAG, "Unknown LineBreakConfig word style: " + lbWordStyleStr); 415 } 416 } 417 418 // Attach span only when the both lbStyle and lbWordStyle are valid. 419 if (lbStyle != LineBreakConfig.LINE_BREAK_STYLE_UNSPECIFIED 420 || lbWordStyle != LineBreakConfig.LINE_BREAK_WORD_STYLE_UNSPECIFIED) { 421 buffer.setSpan(new LineBreakConfigSpan( 422 new LineBreakConfig(lbStyle, lbWordStyle, 423 LineBreakConfig.HYPHENATION_UNSPECIFIED)), 424 style[i + 1], style[i + 2] + 1, 425 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 426 } 427 } 428 } 429 430 i += 3; 431 } 432 return new SpannedString(buffer); 433 } 434 435 /** 436 * Returns a span for the specified color string representation. 437 * If the specified string does not represent a color (null, empty, etc.) 438 * the color black is returned instead. 439 * 440 * @param color The color as a string. Can be a resource reference, 441 * hexadecimal, octal or a name 442 * @param foreground True if the color will be used as the foreground color, 443 * false otherwise 444 * 445 * @return A CharacterStyle 446 * 447 * @see Color#parseColor(String) 448 */ getColor(String color, boolean foreground)449 private static CharacterStyle getColor(String color, boolean foreground) { 450 int c = 0xff000000; 451 452 if (!TextUtils.isEmpty(color)) { 453 if (color.startsWith("@")) { 454 Resources res = Resources.getSystem(); 455 String name = color.substring(1); 456 int colorRes = res.getIdentifier(name, "color", "android"); 457 if (colorRes != 0) { 458 ColorStateList colors = res.getColorStateList(colorRes, null); 459 if (foreground) { 460 return new TextAppearanceSpan(null, 0, 0, colors, null); 461 } else { 462 c = colors.getDefaultColor(); 463 } 464 } 465 } else { 466 try { 467 c = Color.parseColor(color); 468 } catch (IllegalArgumentException e) { 469 c = Color.BLACK; 470 } 471 } 472 } 473 474 if (foreground) { 475 return new ForegroundColorSpan(c); 476 } else { 477 return new BackgroundColorSpan(c); 478 } 479 } 480 481 /** 482 * If a translator has messed up the edges of paragraph-level markup, 483 * fix it to actually cover the entire paragraph that it is attached to 484 * instead of just whatever range they put it on. 485 */ addParagraphSpan(Spannable buffer, Object what, int start, int end)486 private static void addParagraphSpan(Spannable buffer, Object what, 487 int start, int end) { 488 int len = buffer.length(); 489 490 if (start != 0 && start != len && buffer.charAt(start - 1) != '\n') { 491 for (start--; start > 0; start--) { 492 if (buffer.charAt(start - 1) == '\n') { 493 break; 494 } 495 } 496 } 497 498 if (end != 0 && end != len && buffer.charAt(end - 1) != '\n') { 499 for (end++; end < len; end++) { 500 if (buffer.charAt(end - 1) == '\n') { 501 break; 502 } 503 } 504 } 505 506 buffer.setSpan(what, start, end, Spannable.SPAN_PARAGRAPH); 507 } 508 subtag(String full, String attribute)509 private static String subtag(String full, String attribute) { 510 int start = full.indexOf(attribute); 511 if (start < 0) { 512 return null; 513 } 514 515 start += attribute.length(); 516 int end = full.indexOf(';', start); 517 518 if (end < 0) { 519 return full.substring(start); 520 } else { 521 return full.substring(start, end); 522 } 523 } 524 525 /** 526 * Forces the text line to be the specified height, shrinking/stretching 527 * the ascent if possible, or the descent if shrinking the ascent further 528 * will make the text unreadable. 529 */ 530 private static class Height implements LineHeightSpan.WithDensity { 531 private int mSize; 532 private static float sProportion = 0; 533 Height(int size)534 public Height(int size) { 535 mSize = size; 536 } 537 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm)538 public void chooseHeight(CharSequence text, int start, int end, 539 int spanstartv, int v, 540 Paint.FontMetricsInt fm) { 541 // Should not get called, at least not by StaticLayout. 542 chooseHeight(text, start, end, spanstartv, v, fm, null); 543 } 544 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm, TextPaint paint)545 public void chooseHeight(CharSequence text, int start, int end, 546 int spanstartv, int v, 547 Paint.FontMetricsInt fm, TextPaint paint) { 548 int size = mSize; 549 if (paint != null) { 550 size *= paint.density; 551 } 552 553 if (fm.bottom - fm.top < size) { 554 fm.top = fm.bottom - size; 555 fm.ascent = fm.ascent - size; 556 } else { 557 if (sProportion == 0) { 558 /* 559 * Calculate what fraction of the nominal ascent 560 * the height of a capital letter actually is, 561 * so that we won't reduce the ascent to less than 562 * that unless we absolutely have to. 563 */ 564 565 Paint p = new Paint(); 566 p.setTextSize(100); 567 Rect r = new Rect(); 568 p.getTextBounds("ABCDEFG", 0, 7, r); 569 570 sProportion = (r.top) / p.ascent(); 571 } 572 573 int need = (int) Math.ceil(-fm.top * sProportion); 574 575 if (size - fm.descent >= need) { 576 /* 577 * It is safe to shrink the ascent this much. 578 */ 579 580 fm.top = fm.bottom - size; 581 fm.ascent = fm.descent - size; 582 } else if (size >= need) { 583 /* 584 * We can't show all the descent, but we can at least 585 * show all the ascent. 586 */ 587 588 fm.top = fm.ascent = -need; 589 fm.bottom = fm.descent = fm.top + size; 590 } else { 591 /* 592 * Show as much of the ascent as we can, and no descent. 593 */ 594 595 fm.top = fm.ascent = -size; 596 fm.bottom = fm.descent = 0; 597 } 598 } 599 } 600 } 601 602 /** 603 * Create from an existing string block native object. This is 604 * -extremely- dangerous -- only use it if you absolutely know what you 605 * are doing! The given native object must exist for the entire lifetime 606 * of this newly creating StringBlock. 607 */ 608 @UnsupportedAppUsage StringBlock(long obj, boolean useSparse)609 public StringBlock(long obj, boolean useSparse) { 610 mNative = obj; 611 mUseSparse = useSparse; 612 mOwnsNative = false; 613 if (localLOGV) Log.v(TAG, "Created string block " + this 614 + ": " + nativeGetSize(mNative)); 615 } 616 nativeCreate(byte[] data, int offset, int size)617 private static native long nativeCreate(byte[] data, 618 int offset, 619 int size); nativeGetSize(long obj)620 private static native int nativeGetSize(long obj); nativeGetString(long obj, int idx)621 private static native String nativeGetString(long obj, int idx); nativeGetStyle(long obj, int idx)622 private static native int[] nativeGetStyle(long obj, int idx); nativeDestroy(long obj)623 private static native void nativeDestroy(long obj); 624 } 625