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