• 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.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