1/*
2 * Copyright (C) 2011 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.media;
18
19import android.graphics.Rect;
20import android.os.Parcel;
21import android.util.Log;
22import java.util.HashMap;
23import java.util.Set;
24import java.util.List;
25import java.util.ArrayList;
26
27/**
28 * Class to hold the timed text's metadata, including:
29 * <ul>
30 * <li> The characters for rendering</li>
31 * <li> The rendering position for the timed text</li>
32 * </ul>
33 *
34 * <p> To render the timed text, applications need to do the following:
35 *
36 * <ul>
37 * <li> Implement the {@link MediaPlayer.OnTimedTextListener} interface</li>
38 * <li> Register the {@link MediaPlayer.OnTimedTextListener} callback on a MediaPlayer object that is used for playback</li>
39 * <li> When a onTimedText callback is received, do the following:
40 * <ul>
41 * <li> call {@link #getText} to get the characters for rendering</li>
42 * <li> call {@link #getBounds} to get the text rendering area/region</li>
43 * </ul>
44 * </li>
45 * </ul>
46 *
47 * @see android.media.MediaPlayer
48 */
49public final class TimedText
50{
51    private static final int FIRST_PUBLIC_KEY                 = 1;
52
53    // These keys must be in sync with the keys in TextDescription.h
54    private static final int KEY_DISPLAY_FLAGS                 = 1; // int
55    private static final int KEY_STYLE_FLAGS                   = 2; // int
56    private static final int KEY_BACKGROUND_COLOR_RGBA         = 3; // int
57    private static final int KEY_HIGHLIGHT_COLOR_RGBA          = 4; // int
58    private static final int KEY_SCROLL_DELAY                  = 5; // int
59    private static final int KEY_WRAP_TEXT                     = 6; // int
60    private static final int KEY_START_TIME                    = 7; // int
61    private static final int KEY_STRUCT_BLINKING_TEXT_LIST     = 8; // List<CharPos>
62    private static final int KEY_STRUCT_FONT_LIST              = 9; // List<Font>
63    private static final int KEY_STRUCT_HIGHLIGHT_LIST         = 10; // List<CharPos>
64    private static final int KEY_STRUCT_HYPER_TEXT_LIST        = 11; // List<HyperText>
65    private static final int KEY_STRUCT_KARAOKE_LIST           = 12; // List<Karaoke>
66    private static final int KEY_STRUCT_STYLE_LIST             = 13; // List<Style>
67    private static final int KEY_STRUCT_TEXT_POS               = 14; // TextPos
68    private static final int KEY_STRUCT_JUSTIFICATION          = 15; // Justification
69    private static final int KEY_STRUCT_TEXT                   = 16; // Text
70
71    private static final int LAST_PUBLIC_KEY                  = 16;
72
73    private static final int FIRST_PRIVATE_KEY                = 101;
74
75    // The following keys are used between TimedText.java and
76    // TextDescription.cpp in order to parce the Parcel.
77    private static final int KEY_GLOBAL_SETTING               = 101;
78    private static final int KEY_LOCAL_SETTING                = 102;
79    private static final int KEY_START_CHAR                   = 103;
80    private static final int KEY_END_CHAR                     = 104;
81    private static final int KEY_FONT_ID                      = 105;
82    private static final int KEY_FONT_SIZE                    = 106;
83    private static final int KEY_TEXT_COLOR_RGBA              = 107;
84
85    private static final int LAST_PRIVATE_KEY                 = 107;
86
87    private static final String TAG = "TimedText";
88
89    private final HashMap<Integer, Object> mKeyObjectMap =
90            new HashMap<Integer, Object>();
91
92    private int mDisplayFlags = -1;
93    private int mBackgroundColorRGBA = -1;
94    private int mHighlightColorRGBA = -1;
95    private int mScrollDelay = -1;
96    private int mWrapText = -1;
97
98    private List<CharPos> mBlinkingPosList = null;
99    private List<CharPos> mHighlightPosList = null;
100    private List<Karaoke> mKaraokeList = null;
101    private List<Font> mFontList = null;
102    private List<Style> mStyleList = null;
103    private List<HyperText> mHyperTextList = null;
104
105    private Rect mTextBounds = null;
106    private String mTextChars = null;
107
108    private Justification mJustification;
109
110    /**
111     * Helper class to hold the start char offset and end char offset
112     * for Blinking Text or Highlight Text. endChar is the end offset
113     * of the text (startChar + number of characters to be highlighted
114     * or blinked). The member variables in this class are read-only.
115     * {@hide}
116     */
117    public static final class CharPos {
118        /**
119         * The offset of the start character
120         */
121        public final int startChar;
122
123        /**
124         * The offset of the end character
125         */
126        public final int endChar;
127
128        /**
129         * Constuctor
130         * @param startChar the offset of the start character.
131         * @param endChar the offset of the end character.
132         */
133        public CharPos(int startChar, int endChar) {
134            this.startChar = startChar;
135            this.endChar = endChar;
136        }
137    }
138
139    /**
140     * Helper class to hold the justification for text display in the text box.
141     * The member variables in this class are read-only.
142     * {@hide}
143     */
144    public static final class Justification {
145        /**
146         * horizontal justification  0: left, 1: centered, -1: right
147         */
148        public final int horizontalJustification;
149
150        /**
151         * vertical justification  0: top, 1: centered, -1: bottom
152         */
153        public final int verticalJustification;
154
155        /**
156         * Constructor
157         * @param horizontal the horizontal justification of the text.
158         * @param vertical the vertical justification of the text.
159         */
160        public Justification(int horizontal, int vertical) {
161            this.horizontalJustification = horizontal;
162            this.verticalJustification = vertical;
163        }
164    }
165
166    /**
167     * Helper class to hold the style information to display the text.
168     * The member variables in this class are read-only.
169     * {@hide}
170     */
171    public static final class Style {
172        /**
173         * The offset of the start character which applys this style
174         */
175        public final int startChar;
176
177        /**
178         * The offset of the end character which applys this style
179         */
180        public final int endChar;
181
182        /**
183         * ID of the font. This ID will be used to choose the font
184         * to be used from the font list.
185         */
186        public final int fontID;
187
188        /**
189         * True if the characters should be bold
190         */
191        public final boolean isBold;
192
193        /**
194         * True if the characters should be italic
195         */
196        public final boolean isItalic;
197
198        /**
199         * True if the characters should be underlined
200         */
201        public final boolean isUnderlined;
202
203        /**
204         * The size of the font
205         */
206        public final int fontSize;
207
208        /**
209         * To specify the RGBA color: 8 bits each of red, green, blue,
210         * and an alpha(transparency) value
211         */
212        public final int colorRGBA;
213
214        /**
215         * Constructor
216         * @param startChar the offset of the start character which applys this style
217         * @param endChar the offset of the end character which applys this style
218         * @param fontId the ID of the font.
219         * @param isBold whether the characters should be bold.
220         * @param isItalic whether the characters should be italic.
221         * @param isUnderlined whether the characters should be underlined.
222         * @param fontSize the size of the font.
223         * @param colorRGBA red, green, blue, and alpha value for color.
224         */
225        public Style(int startChar, int endChar, int fontId,
226                     boolean isBold, boolean isItalic, boolean isUnderlined,
227                     int fontSize, int colorRGBA) {
228            this.startChar = startChar;
229            this.endChar = endChar;
230            this.fontID = fontId;
231            this.isBold = isBold;
232            this.isItalic = isItalic;
233            this.isUnderlined = isUnderlined;
234            this.fontSize = fontSize;
235            this.colorRGBA = colorRGBA;
236        }
237    }
238
239    /**
240     * Helper class to hold the font ID and name.
241     * The member variables in this class are read-only.
242     * {@hide}
243     */
244    public static final class Font {
245        /**
246         * The font ID
247         */
248        public final int ID;
249
250        /**
251         * The font name
252         */
253        public final String name;
254
255        /**
256         * Constructor
257         * @param id the font ID.
258         * @param name the font name.
259         */
260        public Font(int id, String name) {
261            this.ID = id;
262            this.name = name;
263        }
264    }
265
266    /**
267     * Helper class to hold the karaoke information.
268     * The member variables in this class are read-only.
269     * {@hide}
270     */
271    public static final class Karaoke {
272        /**
273         * The start time (in milliseconds) to highlight the characters
274         * specified by startChar and endChar.
275         */
276        public final int startTimeMs;
277
278        /**
279         * The end time (in milliseconds) to highlight the characters
280         * specified by startChar and endChar.
281         */
282        public final int endTimeMs;
283
284        /**
285         * The offset of the start character to be highlighted
286         */
287        public final int startChar;
288
289        /**
290         * The offset of the end character to be highlighted
291         */
292        public final int endChar;
293
294        /**
295         * Constructor
296         * @param startTimeMs the start time (in milliseconds) to highlight
297         * the characters between startChar and endChar.
298         * @param endTimeMs the end time (in milliseconds) to highlight
299         * the characters between startChar and endChar.
300         * @param startChar the offset of the start character to be highlighted.
301         * @param endChar the offset of the end character to be highlighted.
302         */
303        public Karaoke(int startTimeMs, int endTimeMs, int startChar, int endChar) {
304            this.startTimeMs = startTimeMs;
305            this.endTimeMs = endTimeMs;
306            this.startChar = startChar;
307            this.endChar = endChar;
308        }
309    }
310
311    /**
312     * Helper class to hold the hyper text information.
313     * The member variables in this class are read-only.
314     * {@hide}
315     */
316    public static final class HyperText {
317        /**
318         * The offset of the start character
319         */
320        public final int startChar;
321
322        /**
323         * The offset of the end character
324         */
325        public final int endChar;
326
327        /**
328         * The linked-to URL
329         */
330        public final String URL;
331
332        /**
333         * The "alt" string for user display
334         */
335        public final String altString;
336
337
338        /**
339         * Constructor
340         * @param startChar the offset of the start character.
341         * @param endChar the offset of the end character.
342         * @param url the linked-to URL.
343         * @param alt the "alt" string for display.
344         */
345        public HyperText(int startChar, int endChar, String url, String alt) {
346            this.startChar = startChar;
347            this.endChar = endChar;
348            this.URL = url;
349            this.altString = alt;
350        }
351    }
352
353    /**
354     * @param obj the byte array which contains the timed text.
355     * @throws IllegalArgumentExcept if parseParcel() fails.
356     * {@hide}
357     */
358    public TimedText(Parcel parcel) {
359        if (!parseParcel(parcel)) {
360            mKeyObjectMap.clear();
361            throw new IllegalArgumentException("parseParcel() fails");
362        }
363    }
364
365    /**
366     * Get the characters in the timed text.
367     *
368     * @return the characters as a String object in the TimedText. Applications
369     * should stop rendering previous timed text at the current rendering region if
370     * a null is returned, until the next non-null timed text is received.
371     */
372    public String getText() {
373        return mTextChars;
374    }
375
376    /**
377     * Get the rectangle area or region for rendering the timed text as specified
378     * by a Rect object.
379     *
380     * @return the rectangle region to render the characters in the timed text.
381     * If no bounds information is available (a null is returned), render the
382     * timed text at the center bottom of the display.
383     */
384    public Rect getBounds() {
385        return mTextBounds;
386    }
387
388    /*
389     * Go over all the records, collecting metadata keys and fields in the
390     * Parcel. These are stored in mKeyObjectMap for application to retrieve.
391     * @return false if an error occurred during parsing. Otherwise, true.
392     */
393    private boolean parseParcel(Parcel parcel) {
394        parcel.setDataPosition(0);
395        if (parcel.dataAvail() == 0) {
396            return false;
397        }
398
399        int type = parcel.readInt();
400        if (type == KEY_LOCAL_SETTING) {
401            type = parcel.readInt();
402            if (type != KEY_START_TIME) {
403                return false;
404            }
405            int mStartTimeMs = parcel.readInt();
406            mKeyObjectMap.put(type, mStartTimeMs);
407
408            type = parcel.readInt();
409            if (type != KEY_STRUCT_TEXT) {
410                return false;
411            }
412
413            int textLen = parcel.readInt();
414            byte[] text = parcel.createByteArray();
415            if (text == null || text.length == 0) {
416                mTextChars = null;
417            } else {
418                mTextChars = new String(text);
419            }
420
421        } else if (type != KEY_GLOBAL_SETTING) {
422            Log.w(TAG, "Invalid timed text key found: " + type);
423            return false;
424        }
425
426        while (parcel.dataAvail() > 0) {
427            int key = parcel.readInt();
428            if (!isValidKey(key)) {
429                Log.w(TAG, "Invalid timed text key found: " + key);
430                return false;
431            }
432
433            Object object = null;
434
435            switch (key) {
436                case KEY_STRUCT_STYLE_LIST: {
437                    readStyle(parcel);
438                    object = mStyleList;
439                    break;
440                }
441                case KEY_STRUCT_FONT_LIST: {
442                    readFont(parcel);
443                    object = mFontList;
444                    break;
445                }
446                case KEY_STRUCT_HIGHLIGHT_LIST: {
447                    readHighlight(parcel);
448                    object = mHighlightPosList;
449                    break;
450                }
451                case KEY_STRUCT_KARAOKE_LIST: {
452                    readKaraoke(parcel);
453                    object = mKaraokeList;
454                    break;
455                }
456                case KEY_STRUCT_HYPER_TEXT_LIST: {
457                    readHyperText(parcel);
458                    object = mHyperTextList;
459
460                    break;
461                }
462                case KEY_STRUCT_BLINKING_TEXT_LIST: {
463                    readBlinkingText(parcel);
464                    object = mBlinkingPosList;
465
466                    break;
467                }
468                case KEY_WRAP_TEXT: {
469                    mWrapText = parcel.readInt();
470                    object = mWrapText;
471                    break;
472                }
473                case KEY_HIGHLIGHT_COLOR_RGBA: {
474                    mHighlightColorRGBA = parcel.readInt();
475                    object = mHighlightColorRGBA;
476                    break;
477                }
478                case KEY_DISPLAY_FLAGS: {
479                    mDisplayFlags = parcel.readInt();
480                    object = mDisplayFlags;
481                    break;
482                }
483                case KEY_STRUCT_JUSTIFICATION: {
484
485                    int horizontal = parcel.readInt();
486                    int vertical = parcel.readInt();
487                    mJustification = new Justification(horizontal, vertical);
488
489                    object = mJustification;
490                    break;
491                }
492                case KEY_BACKGROUND_COLOR_RGBA: {
493                    mBackgroundColorRGBA = parcel.readInt();
494                    object = mBackgroundColorRGBA;
495                    break;
496                }
497                case KEY_STRUCT_TEXT_POS: {
498                    int top = parcel.readInt();
499                    int left = parcel.readInt();
500                    int bottom = parcel.readInt();
501                    int right = parcel.readInt();
502                    mTextBounds = new Rect(left, top, right, bottom);
503
504                    break;
505                }
506                case KEY_SCROLL_DELAY: {
507                    mScrollDelay = parcel.readInt();
508                    object = mScrollDelay;
509                    break;
510                }
511                default: {
512                    break;
513                }
514            }
515
516            if (object != null) {
517                if (mKeyObjectMap.containsKey(key)) {
518                    mKeyObjectMap.remove(key);
519                }
520                // Previous mapping will be replaced with the new object, if there was one.
521                mKeyObjectMap.put(key, object);
522            }
523        }
524
525        return true;
526    }
527
528    /*
529     * To parse and store the Style list.
530     */
531    private void readStyle(Parcel parcel) {
532        boolean endOfStyle = false;
533        int startChar = -1;
534        int endChar = -1;
535        int fontId = -1;
536        boolean isBold = false;
537        boolean isItalic = false;
538        boolean isUnderlined = false;
539        int fontSize = -1;
540        int colorRGBA = -1;
541        while (!endOfStyle && (parcel.dataAvail() > 0)) {
542            int key = parcel.readInt();
543            switch (key) {
544                case KEY_START_CHAR: {
545                    startChar = parcel.readInt();
546                    break;
547                }
548                case KEY_END_CHAR: {
549                    endChar = parcel.readInt();
550                    break;
551                }
552                case KEY_FONT_ID: {
553                    fontId = parcel.readInt();
554                    break;
555                }
556                case KEY_STYLE_FLAGS: {
557                    int flags = parcel.readInt();
558                    // In the absence of any bits set in flags, the text
559                    // is plain. Otherwise, 1: bold, 2: italic, 4: underline
560                    isBold = ((flags % 2) == 1);
561                    isItalic = ((flags % 4) >= 2);
562                    isUnderlined = ((flags / 4) == 1);
563                    break;
564                }
565                case KEY_FONT_SIZE: {
566                    fontSize = parcel.readInt();
567                    break;
568                }
569                case KEY_TEXT_COLOR_RGBA: {
570                    colorRGBA = parcel.readInt();
571                    break;
572                }
573                default: {
574                    // End of the Style parsing. Reset the data position back
575                    // to the position before the last parcel.readInt() call.
576                    parcel.setDataPosition(parcel.dataPosition() - 4);
577                    endOfStyle = true;
578                    break;
579                }
580            }
581        }
582
583        Style style = new Style(startChar, endChar, fontId, isBold,
584                                isItalic, isUnderlined, fontSize, colorRGBA);
585        if (mStyleList == null) {
586            mStyleList = new ArrayList<Style>();
587        }
588        mStyleList.add(style);
589    }
590
591    /*
592     * To parse and store the Font list
593     */
594    private void readFont(Parcel parcel) {
595        int entryCount = parcel.readInt();
596
597        for (int i = 0; i < entryCount; i++) {
598            int id = parcel.readInt();
599            int nameLen = parcel.readInt();
600
601            byte[] text = parcel.createByteArray();
602            final String name = new String(text, 0, nameLen);
603
604            Font font = new Font(id, name);
605
606            if (mFontList == null) {
607                mFontList = new ArrayList<Font>();
608            }
609            mFontList.add(font);
610        }
611    }
612
613    /*
614     * To parse and store the Highlight list
615     */
616    private void readHighlight(Parcel parcel) {
617        int startChar = parcel.readInt();
618        int endChar = parcel.readInt();
619        CharPos pos = new CharPos(startChar, endChar);
620
621        if (mHighlightPosList == null) {
622            mHighlightPosList = new ArrayList<CharPos>();
623        }
624        mHighlightPosList.add(pos);
625    }
626
627    /*
628     * To parse and store the Karaoke list
629     */
630    private void readKaraoke(Parcel parcel) {
631        int entryCount = parcel.readInt();
632
633        for (int i = 0; i < entryCount; i++) {
634            int startTimeMs = parcel.readInt();
635            int endTimeMs = parcel.readInt();
636            int startChar = parcel.readInt();
637            int endChar = parcel.readInt();
638            Karaoke kara = new Karaoke(startTimeMs, endTimeMs,
639                                       startChar, endChar);
640
641            if (mKaraokeList == null) {
642                mKaraokeList = new ArrayList<Karaoke>();
643            }
644            mKaraokeList.add(kara);
645        }
646    }
647
648    /*
649     * To parse and store HyperText list
650     */
651    private void readHyperText(Parcel parcel) {
652        int startChar = parcel.readInt();
653        int endChar = parcel.readInt();
654
655        int len = parcel.readInt();
656        byte[] url = parcel.createByteArray();
657        final String urlString = new String(url, 0, len);
658
659        len = parcel.readInt();
660        byte[] alt = parcel.createByteArray();
661        final String altString = new String(alt, 0, len);
662        HyperText hyperText = new HyperText(startChar, endChar, urlString, altString);
663
664
665        if (mHyperTextList == null) {
666            mHyperTextList = new ArrayList<HyperText>();
667        }
668        mHyperTextList.add(hyperText);
669    }
670
671    /*
672     * To parse and store blinking text list
673     */
674    private void readBlinkingText(Parcel parcel) {
675        int startChar = parcel.readInt();
676        int endChar = parcel.readInt();
677        CharPos blinkingPos = new CharPos(startChar, endChar);
678
679        if (mBlinkingPosList == null) {
680            mBlinkingPosList = new ArrayList<CharPos>();
681        }
682        mBlinkingPosList.add(blinkingPos);
683    }
684
685    /*
686     * To check whether the given key is valid.
687     * @param key the key to be checked.
688     * @return true if the key is a valid one. Otherwise, false.
689     */
690    private boolean isValidKey(final int key) {
691        if (!((key >= FIRST_PUBLIC_KEY) && (key <= LAST_PUBLIC_KEY))
692                && !((key >= FIRST_PRIVATE_KEY) && (key <= LAST_PRIVATE_KEY))) {
693            return false;
694        }
695        return true;
696    }
697
698    /*
699     * To check whether the given key is contained in this TimedText object.
700     * @param key the key to be checked.
701     * @return true if the key is contained in this TimedText object.
702     *         Otherwise, false.
703     */
704    private boolean containsKey(final int key) {
705        if (isValidKey(key) && mKeyObjectMap.containsKey(key)) {
706            return true;
707        }
708        return false;
709    }
710
711    /*
712     * @return a set of the keys contained in this TimedText object.
713     */
714    private Set keySet() {
715        return mKeyObjectMap.keySet();
716    }
717
718    /*
719     * To retrieve the object associated with the key. Caller must make sure
720     * the key is present using the containsKey method otherwise a
721     * RuntimeException will occur.
722     * @param key the key used to retrieve the object.
723     * @return an object. The object could be 1) an instance of Integer; 2) a
724     * List of CharPos, Karaoke, Font, Style, and HyperText, or 3) an instance of
725     * Justification.
726     */
727    private Object getObject(final int key) {
728        if (containsKey(key)) {
729            return mKeyObjectMap.get(key);
730        } else {
731            throw new IllegalArgumentException("Invalid key: " + key);
732        }
733    }
734}
735