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 
17 package android.media;
18 
19 import android.graphics.Rect;
20 import android.os.Parcel;
21 import android.util.Log;
22 import java.util.HashMap;
23 import java.util.Set;
24 import java.util.List;
25 import 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  */
49 public 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          */
CharPos(int startChar, int endChar)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          */
Justification(int horizontal, int vertical)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          */
Style(int startChar, int endChar, int fontId, boolean isBold, boolean isItalic, boolean isUnderlined, int fontSize, int colorRGBA)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          */
Font(int id, String name)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          */
Karaoke(int startTimeMs, int endTimeMs, int startChar, int endChar)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          */
HyperText(int startChar, int endChar, String url, String alt)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      */
TimedText(Parcel parcel)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      */
getText()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      */
getBounds()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      */
parseParcel(Parcel parcel)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      */
readStyle(Parcel parcel)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      */
readFont(Parcel parcel)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      */
readHighlight(Parcel parcel)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      */
readKaraoke(Parcel parcel)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      */
readHyperText(Parcel parcel)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      */
readBlinkingText(Parcel parcel)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      */
isValidKey(final int key)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      */
containsKey(final int key)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      */
keySet()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      */
getObject(final int key)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