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