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