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