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