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
17package android.content.res;
18
19import android.graphics.Color;
20import android.text.*;
21import android.text.style.*;
22import android.util.Log;
23import android.util.SparseArray;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.Typeface;
27
28import java.util.Arrays;
29
30/**
31 * Conveniences for retrieving data out of a compiled string resource.
32 *
33 * {@hide}
34 */
35final class StringBlock {
36    private static final String TAG = "AssetManager";
37    private static final boolean localLOGV = false;
38
39    private final int 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
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
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
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
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
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     *              HTML 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#getHtmlColor(String)
321     */
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);
332                    if (foreground) {
333                        return new TextAppearanceSpan(null, 0, 0, colors, null);
334                    } else {
335                        c = colors.getDefaultColor();
336                    }
337                }
338            } else {
339                c = Color.getHtmlColor(color);
340            }
341        }
342
343        if (foreground) {
344            return new ForegroundColorSpan(c);
345        } else {
346            return new BackgroundColorSpan(c);
347        }
348    }
349
350    /**
351     * If a translator has messed up the edges of paragraph-level markup,
352     * fix it to actually cover the entire paragraph that it is attached to
353     * instead of just whatever range they put it on.
354     */
355    private static void addParagraphSpan(Spannable buffer, Object what,
356                                         int start, int end) {
357        int len = buffer.length();
358
359        if (start != 0 && start != len && buffer.charAt(start - 1) != '\n') {
360            for (start--; start > 0; start--) {
361                if (buffer.charAt(start - 1) == '\n') {
362                    break;
363                }
364            }
365        }
366
367        if (end != 0 && end != len && buffer.charAt(end - 1) != '\n') {
368            for (end++; end < len; end++) {
369                if (buffer.charAt(end - 1) == '\n') {
370                    break;
371                }
372            }
373        }
374
375        buffer.setSpan(what, start, end, Spannable.SPAN_PARAGRAPH);
376    }
377
378    private static String subtag(String full, String attribute) {
379        int start = full.indexOf(attribute);
380        if (start < 0) {
381            return null;
382        }
383
384        start += attribute.length();
385        int end = full.indexOf(';', start);
386
387        if (end < 0) {
388            return full.substring(start);
389        } else {
390            return full.substring(start, end);
391        }
392    }
393
394    /**
395     * Forces the text line to be the specified height, shrinking/stretching
396     * the ascent if possible, or the descent if shrinking the ascent further
397     * will make the text unreadable.
398     */
399    private static class Height implements LineHeightSpan.WithDensity {
400        private int mSize;
401        private static float sProportion = 0;
402
403        public Height(int size) {
404            mSize = size;
405        }
406
407        public void chooseHeight(CharSequence text, int start, int end,
408                                 int spanstartv, int v,
409                                 Paint.FontMetricsInt fm) {
410            // Should not get called, at least not by StaticLayout.
411            chooseHeight(text, start, end, spanstartv, v, fm, null);
412        }
413
414        public void chooseHeight(CharSequence text, int start, int end,
415                                 int spanstartv, int v,
416                                 Paint.FontMetricsInt fm, TextPaint paint) {
417            int size = mSize;
418            if (paint != null) {
419                size *= paint.density;
420            }
421
422            if (fm.bottom - fm.top < size) {
423                fm.top = fm.bottom - size;
424                fm.ascent = fm.ascent - size;
425            } else {
426                if (sProportion == 0) {
427                    /*
428                     * Calculate what fraction of the nominal ascent
429                     * the height of a capital letter actually is,
430                     * so that we won't reduce the ascent to less than
431                     * that unless we absolutely have to.
432                     */
433
434                    Paint p = new Paint();
435                    p.setTextSize(100);
436                    Rect r = new Rect();
437                    p.getTextBounds("ABCDEFG", 0, 7, r);
438
439                    sProportion = (r.top) / p.ascent();
440                }
441
442                int need = (int) Math.ceil(-fm.top * sProportion);
443
444                if (size - fm.descent >= need) {
445                    /*
446                     * It is safe to shrink the ascent this much.
447                     */
448
449                    fm.top = fm.bottom - size;
450                    fm.ascent = fm.descent - size;
451                } else if (size >= need) {
452                    /*
453                     * We can't show all the descent, but we can at least
454                     * show all the ascent.
455                     */
456
457                    fm.top = fm.ascent = -need;
458                    fm.bottom = fm.descent = fm.top + size;
459                } else {
460                    /*
461                     * Show as much of the ascent as we can, and no descent.
462                     */
463
464                    fm.top = fm.ascent = -size;
465                    fm.bottom = fm.descent = 0;
466                }
467            }
468        }
469    }
470
471    /**
472     * Create from an existing string block native object.  This is
473     * -extremely- dangerous -- only use it if you absolutely know what you
474     *  are doing!  The given native object must exist for the entire lifetime
475     *  of this newly creating StringBlock.
476     */
477    StringBlock(int obj, boolean useSparse) {
478        mNative = obj;
479        mUseSparse = useSparse;
480        mOwnsNative = false;
481        if (localLOGV) Log.v(TAG, "Created string block " + this
482                + ": " + nativeGetSize(mNative));
483    }
484
485    private static native int nativeCreate(byte[] data,
486                                                 int offset,
487                                                 int size);
488    private static native int nativeGetSize(int obj);
489    private static native String nativeGetString(int obj, int idx);
490    private static native int[] nativeGetStyle(int obj, int idx);
491    private static native void nativeDestroy(int obj);
492}
493