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