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 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
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     *              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     */
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     */
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
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
407        public Height(int size) {
408            mSize = size;
409        }
410
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
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     */
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
489    private static native long nativeCreate(byte[] data,
490                                                 int offset,
491                                                 int size);
492    private static native int nativeGetSize(long obj);
493    private static native String nativeGetString(long obj, int idx);
494    private static native int[] nativeGetStyle(long obj, int idx);
495    private static native void nativeDestroy(long obj);
496}
497