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