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