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 long mNative; // final, but gets modified when closed 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 mNative = 0; 211 } 212 } 213 } 214 215 static final class StyleIDs { 216 private int boldId = -1; 217 private int italicId = -1; 218 private int underlineId = -1; 219 private int ttId = -1; 220 private int bigId = -1; 221 private int smallId = -1; 222 private int subId = -1; 223 private int supId = -1; 224 private int strikeId = -1; 225 private int listItemId = -1; 226 private int marqueeId = -1; 227 } 228 229 @Nullable applyStyles(String str, int[] style, StyleIDs ids)230 private CharSequence applyStyles(String str, int[] style, StyleIDs ids) { 231 if (style.length == 0) 232 return str; 233 234 SpannableString buffer = new SpannableString(str); 235 int i=0; 236 while (i < style.length) { 237 int type = style[i]; 238 if (localLOGV) Log.v(TAG, "Applying style span id=" + type 239 + ", start=" + style[i+1] + ", end=" + style[i+2]); 240 241 242 if (type == ids.boldId) { 243 Application application = ActivityThread.currentApplication(); 244 int fontWeightAdjustment = 245 application.getResources().getConfiguration().fontWeightAdjustment; 246 buffer.setSpan(new StyleSpan(Typeface.BOLD, fontWeightAdjustment), 247 style[i+1], style[i+2]+1, 248 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 249 } else if (type == ids.italicId) { 250 buffer.setSpan(new StyleSpan(Typeface.ITALIC), 251 style[i+1], style[i+2]+1, 252 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 253 } else if (type == ids.underlineId) { 254 buffer.setSpan(new UnderlineSpan(), 255 style[i+1], style[i+2]+1, 256 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 257 } else if (type == ids.ttId) { 258 buffer.setSpan(new TypefaceSpan("monospace"), 259 style[i+1], style[i+2]+1, 260 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 261 } else if (type == ids.bigId) { 262 buffer.setSpan(new RelativeSizeSpan(1.25f), 263 style[i+1], style[i+2]+1, 264 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 265 } else if (type == ids.smallId) { 266 buffer.setSpan(new RelativeSizeSpan(0.8f), 267 style[i+1], style[i+2]+1, 268 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 269 } else if (type == ids.subId) { 270 buffer.setSpan(new SubscriptSpan(), 271 style[i+1], style[i+2]+1, 272 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 273 } else if (type == ids.supId) { 274 buffer.setSpan(new SuperscriptSpan(), 275 style[i+1], style[i+2]+1, 276 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 277 } else if (type == ids.strikeId) { 278 buffer.setSpan(new StrikethroughSpan(), 279 style[i+1], style[i+2]+1, 280 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 281 } else if (type == ids.listItemId) { 282 addParagraphSpan(buffer, new BulletSpan(10), 283 style[i+1], style[i+2]+1); 284 } else if (type == ids.marqueeId) { 285 buffer.setSpan(TextUtils.TruncateAt.MARQUEE, 286 style[i+1], style[i+2]+1, 287 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 288 } else { 289 String tag = nativeGetString(mNative, type); 290 if (tag == null) { 291 return null; 292 } 293 294 if (tag.startsWith("font;")) { 295 String sub; 296 297 sub = subtag(tag, ";height="); 298 if (sub != null) { 299 int size = Integer.parseInt(sub); 300 addParagraphSpan(buffer, new Height(size), 301 style[i+1], style[i+2]+1); 302 } 303 304 sub = subtag(tag, ";size="); 305 if (sub != null) { 306 int size = Integer.parseInt(sub); 307 buffer.setSpan(new AbsoluteSizeSpan(size, true), 308 style[i+1], style[i+2]+1, 309 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 310 } 311 312 sub = subtag(tag, ";fgcolor="); 313 if (sub != null) { 314 buffer.setSpan(getColor(sub, true), 315 style[i+1], style[i+2]+1, 316 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 317 } 318 319 sub = subtag(tag, ";color="); 320 if (sub != null) { 321 buffer.setSpan(getColor(sub, true), 322 style[i+1], style[i+2]+1, 323 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 324 } 325 326 sub = subtag(tag, ";bgcolor="); 327 if (sub != null) { 328 buffer.setSpan(getColor(sub, false), 329 style[i+1], style[i+2]+1, 330 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 331 } 332 333 sub = subtag(tag, ";face="); 334 if (sub != null) { 335 buffer.setSpan(new TypefaceSpan(sub), 336 style[i+1], style[i+2]+1, 337 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 338 } 339 } else if (tag.startsWith("a;")) { 340 String sub; 341 342 sub = subtag(tag, ";href="); 343 if (sub != null) { 344 buffer.setSpan(new URLSpan(sub), 345 style[i+1], style[i+2]+1, 346 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 347 } 348 } else if (tag.startsWith("annotation;")) { 349 int len = tag.length(); 350 int next; 351 352 for (int t = tag.indexOf(';'); t < len; t = next) { 353 int eq = tag.indexOf('=', t); 354 if (eq < 0) { 355 break; 356 } 357 358 next = tag.indexOf(';', eq); 359 if (next < 0) { 360 next = len; 361 } 362 363 String key = tag.substring(t + 1, eq); 364 String value = tag.substring(eq + 1, next); 365 366 buffer.setSpan(new Annotation(key, value), 367 style[i+1], style[i+2]+1, 368 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 369 } 370 } 371 } 372 373 i += 3; 374 } 375 return new SpannedString(buffer); 376 } 377 378 /** 379 * Returns a span for the specified color string representation. 380 * If the specified string does not represent a color (null, empty, etc.) 381 * the color black is returned instead. 382 * 383 * @param color The color as a string. Can be a resource reference, 384 * hexadecimal, octal or a name 385 * @param foreground True if the color will be used as the foreground color, 386 * false otherwise 387 * 388 * @return A CharacterStyle 389 * 390 * @see Color#parseColor(String) 391 */ getColor(String color, boolean foreground)392 private static CharacterStyle getColor(String color, boolean foreground) { 393 int c = 0xff000000; 394 395 if (!TextUtils.isEmpty(color)) { 396 if (color.startsWith("@")) { 397 Resources res = Resources.getSystem(); 398 String name = color.substring(1); 399 int colorRes = res.getIdentifier(name, "color", "android"); 400 if (colorRes != 0) { 401 ColorStateList colors = res.getColorStateList(colorRes, null); 402 if (foreground) { 403 return new TextAppearanceSpan(null, 0, 0, colors, null); 404 } else { 405 c = colors.getDefaultColor(); 406 } 407 } 408 } else { 409 try { 410 c = Color.parseColor(color); 411 } catch (IllegalArgumentException e) { 412 c = Color.BLACK; 413 } 414 } 415 } 416 417 if (foreground) { 418 return new ForegroundColorSpan(c); 419 } else { 420 return new BackgroundColorSpan(c); 421 } 422 } 423 424 /** 425 * If a translator has messed up the edges of paragraph-level markup, 426 * fix it to actually cover the entire paragraph that it is attached to 427 * instead of just whatever range they put it on. 428 */ addParagraphSpan(Spannable buffer, Object what, int start, int end)429 private static void addParagraphSpan(Spannable buffer, Object what, 430 int start, int end) { 431 int len = buffer.length(); 432 433 if (start != 0 && start != len && buffer.charAt(start - 1) != '\n') { 434 for (start--; start > 0; start--) { 435 if (buffer.charAt(start - 1) == '\n') { 436 break; 437 } 438 } 439 } 440 441 if (end != 0 && end != len && buffer.charAt(end - 1) != '\n') { 442 for (end++; end < len; end++) { 443 if (buffer.charAt(end - 1) == '\n') { 444 break; 445 } 446 } 447 } 448 449 buffer.setSpan(what, start, end, Spannable.SPAN_PARAGRAPH); 450 } 451 subtag(String full, String attribute)452 private static String subtag(String full, String attribute) { 453 int start = full.indexOf(attribute); 454 if (start < 0) { 455 return null; 456 } 457 458 start += attribute.length(); 459 int end = full.indexOf(';', start); 460 461 if (end < 0) { 462 return full.substring(start); 463 } else { 464 return full.substring(start, end); 465 } 466 } 467 468 /** 469 * Forces the text line to be the specified height, shrinking/stretching 470 * the ascent if possible, or the descent if shrinking the ascent further 471 * will make the text unreadable. 472 */ 473 private static class Height implements LineHeightSpan.WithDensity { 474 private int mSize; 475 private static float sProportion = 0; 476 Height(int size)477 public Height(int size) { 478 mSize = size; 479 } 480 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm)481 public void chooseHeight(CharSequence text, int start, int end, 482 int spanstartv, int v, 483 Paint.FontMetricsInt fm) { 484 // Should not get called, at least not by StaticLayout. 485 chooseHeight(text, start, end, spanstartv, v, fm, null); 486 } 487 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm, TextPaint paint)488 public void chooseHeight(CharSequence text, int start, int end, 489 int spanstartv, int v, 490 Paint.FontMetricsInt fm, TextPaint paint) { 491 int size = mSize; 492 if (paint != null) { 493 size *= paint.density; 494 } 495 496 if (fm.bottom - fm.top < size) { 497 fm.top = fm.bottom - size; 498 fm.ascent = fm.ascent - size; 499 } else { 500 if (sProportion == 0) { 501 /* 502 * Calculate what fraction of the nominal ascent 503 * the height of a capital letter actually is, 504 * so that we won't reduce the ascent to less than 505 * that unless we absolutely have to. 506 */ 507 508 Paint p = new Paint(); 509 p.setTextSize(100); 510 Rect r = new Rect(); 511 p.getTextBounds("ABCDEFG", 0, 7, r); 512 513 sProportion = (r.top) / p.ascent(); 514 } 515 516 int need = (int) Math.ceil(-fm.top * sProportion); 517 518 if (size - fm.descent >= need) { 519 /* 520 * It is safe to shrink the ascent this much. 521 */ 522 523 fm.top = fm.bottom - size; 524 fm.ascent = fm.descent - size; 525 } else if (size >= need) { 526 /* 527 * We can't show all the descent, but we can at least 528 * show all the ascent. 529 */ 530 531 fm.top = fm.ascent = -need; 532 fm.bottom = fm.descent = fm.top + size; 533 } else { 534 /* 535 * Show as much of the ascent as we can, and no descent. 536 */ 537 538 fm.top = fm.ascent = -size; 539 fm.bottom = fm.descent = 0; 540 } 541 } 542 } 543 } 544 545 /** 546 * Create from an existing string block native object. This is 547 * -extremely- dangerous -- only use it if you absolutely know what you 548 * are doing! The given native object must exist for the entire lifetime 549 * of this newly creating StringBlock. 550 */ 551 @UnsupportedAppUsage StringBlock(long obj, boolean useSparse)552 public StringBlock(long obj, boolean useSparse) { 553 mNative = obj; 554 mUseSparse = useSparse; 555 mOwnsNative = false; 556 if (localLOGV) Log.v(TAG, "Created string block " + this 557 + ": " + nativeGetSize(mNative)); 558 } 559 nativeCreate(byte[] data, int offset, int size)560 private static native long nativeCreate(byte[] data, 561 int offset, 562 int size); nativeGetSize(long obj)563 private static native int nativeGetSize(long obj); nativeGetString(long obj, int idx)564 private static native String nativeGetString(long obj, int idx); nativeGetStyle(long obj, int idx)565 private static native int[] nativeGetStyle(long obj, int idx); nativeDestroy(long obj)566 private static native void nativeDestroy(long obj); 567 } 568