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