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