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