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