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