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 long mNative;   // final, but gets modified when closed
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                 mNative = 0;
211             }
212         }
213     }
214 
215     static final class StyleIDs {
216         private int boldId = -1;
217         private int italicId = -1;
218         private int underlineId = -1;
219         private int ttId = -1;
220         private int bigId = -1;
221         private int smallId = -1;
222         private int subId = -1;
223         private int supId = -1;
224         private int strikeId = -1;
225         private int listItemId = -1;
226         private int marqueeId = -1;
227     }
228 
229     @Nullable
applyStyles(String str, int[] style, StyleIDs ids)230     private CharSequence applyStyles(String str, int[] style, StyleIDs ids) {
231         if (style.length == 0)
232             return str;
233 
234         SpannableString buffer = new SpannableString(str);
235         int i=0;
236         while (i < style.length) {
237             int type = style[i];
238             if (localLOGV) Log.v(TAG, "Applying style span id=" + type
239                     + ", start=" + style[i+1] + ", end=" + style[i+2]);
240 
241 
242             if (type == ids.boldId) {
243                 Application application = ActivityThread.currentApplication();
244                 int fontWeightAdjustment =
245                         application.getResources().getConfiguration().fontWeightAdjustment;
246                 buffer.setSpan(new StyleSpan(Typeface.BOLD, fontWeightAdjustment),
247                                style[i+1], style[i+2]+1,
248                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
249             } else if (type == ids.italicId) {
250                 buffer.setSpan(new StyleSpan(Typeface.ITALIC),
251                                style[i+1], style[i+2]+1,
252                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
253             } else if (type == ids.underlineId) {
254                 buffer.setSpan(new UnderlineSpan(),
255                                style[i+1], style[i+2]+1,
256                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
257             } else if (type == ids.ttId) {
258                 buffer.setSpan(new TypefaceSpan("monospace"),
259                                style[i+1], style[i+2]+1,
260                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
261             } else if (type == ids.bigId) {
262                 buffer.setSpan(new RelativeSizeSpan(1.25f),
263                                style[i+1], style[i+2]+1,
264                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
265             } else if (type == ids.smallId) {
266                 buffer.setSpan(new RelativeSizeSpan(0.8f),
267                                style[i+1], style[i+2]+1,
268                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
269             } else if (type == ids.subId) {
270                 buffer.setSpan(new SubscriptSpan(),
271                                style[i+1], style[i+2]+1,
272                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
273             } else if (type == ids.supId) {
274                 buffer.setSpan(new SuperscriptSpan(),
275                                style[i+1], style[i+2]+1,
276                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
277             } else if (type == ids.strikeId) {
278                 buffer.setSpan(new StrikethroughSpan(),
279                                style[i+1], style[i+2]+1,
280                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
281             } else if (type == ids.listItemId) {
282                 addParagraphSpan(buffer, new BulletSpan(10),
283                                 style[i+1], style[i+2]+1);
284             } else if (type == ids.marqueeId) {
285                 buffer.setSpan(TextUtils.TruncateAt.MARQUEE,
286                                style[i+1], style[i+2]+1,
287                                Spannable.SPAN_INCLUSIVE_INCLUSIVE);
288             } else {
289                 String tag = nativeGetString(mNative, type);
290                 if (tag == null) {
291                     return null;
292                 }
293 
294                 if (tag.startsWith("font;")) {
295                     String sub;
296 
297                     sub = subtag(tag, ";height=");
298                     if (sub != null) {
299                         int size = Integer.parseInt(sub);
300                         addParagraphSpan(buffer, new Height(size),
301                                        style[i+1], style[i+2]+1);
302                     }
303 
304                     sub = subtag(tag, ";size=");
305                     if (sub != null) {
306                         int size = Integer.parseInt(sub);
307                         buffer.setSpan(new AbsoluteSizeSpan(size, true),
308                                        style[i+1], style[i+2]+1,
309                                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
310                     }
311 
312                     sub = subtag(tag, ";fgcolor=");
313                     if (sub != null) {
314                         buffer.setSpan(getColor(sub, true),
315                                        style[i+1], style[i+2]+1,
316                                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
317                     }
318 
319                     sub = subtag(tag, ";color=");
320                     if (sub != null) {
321                         buffer.setSpan(getColor(sub, true),
322                                 style[i+1], style[i+2]+1,
323                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
324                     }
325 
326                     sub = subtag(tag, ";bgcolor=");
327                     if (sub != null) {
328                         buffer.setSpan(getColor(sub, false),
329                                        style[i+1], style[i+2]+1,
330                                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
331                     }
332 
333                     sub = subtag(tag, ";face=");
334                     if (sub != null) {
335                         buffer.setSpan(new TypefaceSpan(sub),
336                                 style[i+1], style[i+2]+1,
337                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
338                     }
339                 } else if (tag.startsWith("a;")) {
340                     String sub;
341 
342                     sub = subtag(tag, ";href=");
343                     if (sub != null) {
344                         buffer.setSpan(new URLSpan(sub),
345                                        style[i+1], style[i+2]+1,
346                                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
347                     }
348                 } else if (tag.startsWith("annotation;")) {
349                     int len = tag.length();
350                     int next;
351 
352                     for (int t = tag.indexOf(';'); t < len; t = next) {
353                         int eq = tag.indexOf('=', t);
354                         if (eq < 0) {
355                             break;
356                         }
357 
358                         next = tag.indexOf(';', eq);
359                         if (next < 0) {
360                             next = len;
361                         }
362 
363                         String key = tag.substring(t + 1, eq);
364                         String value = tag.substring(eq + 1, next);
365 
366                         buffer.setSpan(new Annotation(key, value),
367                                        style[i+1], style[i+2]+1,
368                                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
369                     }
370                 }
371             }
372 
373             i += 3;
374         }
375         return new SpannedString(buffer);
376     }
377 
378     /**
379      * Returns a span for the specified color string representation.
380      * If the specified string does not represent a color (null, empty, etc.)
381      * the color black is returned instead.
382      *
383      * @param color The color as a string. Can be a resource reference,
384      *              hexadecimal, octal or a name
385      * @param foreground True if the color will be used as the foreground color,
386      *                   false otherwise
387      *
388      * @return A CharacterStyle
389      *
390      * @see Color#parseColor(String)
391      */
getColor(String color, boolean foreground)392     private static CharacterStyle getColor(String color, boolean foreground) {
393         int c = 0xff000000;
394 
395         if (!TextUtils.isEmpty(color)) {
396             if (color.startsWith("@")) {
397                 Resources res = Resources.getSystem();
398                 String name = color.substring(1);
399                 int colorRes = res.getIdentifier(name, "color", "android");
400                 if (colorRes != 0) {
401                     ColorStateList colors = res.getColorStateList(colorRes, null);
402                     if (foreground) {
403                         return new TextAppearanceSpan(null, 0, 0, colors, null);
404                     } else {
405                         c = colors.getDefaultColor();
406                     }
407                 }
408             } else {
409                 try {
410                     c = Color.parseColor(color);
411                 } catch (IllegalArgumentException e) {
412                     c = Color.BLACK;
413                 }
414             }
415         }
416 
417         if (foreground) {
418             return new ForegroundColorSpan(c);
419         } else {
420             return new BackgroundColorSpan(c);
421         }
422     }
423 
424     /**
425      * If a translator has messed up the edges of paragraph-level markup,
426      * fix it to actually cover the entire paragraph that it is attached to
427      * instead of just whatever range they put it on.
428      */
addParagraphSpan(Spannable buffer, Object what, int start, int end)429     private static void addParagraphSpan(Spannable buffer, Object what,
430                                          int start, int end) {
431         int len = buffer.length();
432 
433         if (start != 0 && start != len && buffer.charAt(start - 1) != '\n') {
434             for (start--; start > 0; start--) {
435                 if (buffer.charAt(start - 1) == '\n') {
436                     break;
437                 }
438             }
439         }
440 
441         if (end != 0 && end != len && buffer.charAt(end - 1) != '\n') {
442             for (end++; end < len; end++) {
443                 if (buffer.charAt(end - 1) == '\n') {
444                     break;
445                 }
446             }
447         }
448 
449         buffer.setSpan(what, start, end, Spannable.SPAN_PARAGRAPH);
450     }
451 
subtag(String full, String attribute)452     private static String subtag(String full, String attribute) {
453         int start = full.indexOf(attribute);
454         if (start < 0) {
455             return null;
456         }
457 
458         start += attribute.length();
459         int end = full.indexOf(';', start);
460 
461         if (end < 0) {
462             return full.substring(start);
463         } else {
464             return full.substring(start, end);
465         }
466     }
467 
468     /**
469      * Forces the text line to be the specified height, shrinking/stretching
470      * the ascent if possible, or the descent if shrinking the ascent further
471      * will make the text unreadable.
472      */
473     private static class Height implements LineHeightSpan.WithDensity {
474         private int mSize;
475         private static float sProportion = 0;
476 
Height(int size)477         public Height(int size) {
478             mSize = size;
479         }
480 
chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm)481         public void chooseHeight(CharSequence text, int start, int end,
482                                  int spanstartv, int v,
483                                  Paint.FontMetricsInt fm) {
484             // Should not get called, at least not by StaticLayout.
485             chooseHeight(text, start, end, spanstartv, v, fm, null);
486         }
487 
chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm, TextPaint paint)488         public void chooseHeight(CharSequence text, int start, int end,
489                                  int spanstartv, int v,
490                                  Paint.FontMetricsInt fm, TextPaint paint) {
491             int size = mSize;
492             if (paint != null) {
493                 size *= paint.density;
494             }
495 
496             if (fm.bottom - fm.top < size) {
497                 fm.top = fm.bottom - size;
498                 fm.ascent = fm.ascent - size;
499             } else {
500                 if (sProportion == 0) {
501                     /*
502                      * Calculate what fraction of the nominal ascent
503                      * the height of a capital letter actually is,
504                      * so that we won't reduce the ascent to less than
505                      * that unless we absolutely have to.
506                      */
507 
508                     Paint p = new Paint();
509                     p.setTextSize(100);
510                     Rect r = new Rect();
511                     p.getTextBounds("ABCDEFG", 0, 7, r);
512 
513                     sProportion = (r.top) / p.ascent();
514                 }
515 
516                 int need = (int) Math.ceil(-fm.top * sProportion);
517 
518                 if (size - fm.descent >= need) {
519                     /*
520                      * It is safe to shrink the ascent this much.
521                      */
522 
523                     fm.top = fm.bottom - size;
524                     fm.ascent = fm.descent - size;
525                 } else if (size >= need) {
526                     /*
527                      * We can't show all the descent, but we can at least
528                      * show all the ascent.
529                      */
530 
531                     fm.top = fm.ascent = -need;
532                     fm.bottom = fm.descent = fm.top + size;
533                 } else {
534                     /*
535                      * Show as much of the ascent as we can, and no descent.
536                      */
537 
538                     fm.top = fm.ascent = -size;
539                     fm.bottom = fm.descent = 0;
540                 }
541             }
542         }
543     }
544 
545     /**
546      * Create from an existing string block native object.  This is
547      * -extremely- dangerous -- only use it if you absolutely know what you
548      *  are doing!  The given native object must exist for the entire lifetime
549      *  of this newly creating StringBlock.
550      */
551     @UnsupportedAppUsage
StringBlock(long obj, boolean useSparse)552     public StringBlock(long obj, boolean useSparse) {
553         mNative = obj;
554         mUseSparse = useSparse;
555         mOwnsNative = false;
556         if (localLOGV) Log.v(TAG, "Created string block " + this
557                 + ": " + nativeGetSize(mNative));
558     }
559 
nativeCreate(byte[] data, int offset, int size)560     private static native long nativeCreate(byte[] data,
561                                                  int offset,
562                                                  int size);
nativeGetSize(long obj)563     private static native int nativeGetSize(long obj);
nativeGetString(long obj, int idx)564     private static native String nativeGetString(long obj, int idx);
nativeGetStyle(long obj, int idx)565     private static native int[] nativeGetStyle(long obj, int idx);
nativeDestroy(long obj)566     private static native void nativeDestroy(long obj);
567 }
568