/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media; import android.graphics.Rect; import android.os.Parcel; import android.util.Log; import java.util.HashMap; import java.util.Set; import java.util.List; import java.util.ArrayList; /** * Class to hold the timed text's metadata, including: * <ul> * <li> The characters for rendering</li> * <li> The rendering position for the timed text</li> * </ul> * * <p> To render the timed text, applications need to do the following: * * <ul> * <li> Implement the {@link MediaPlayer.OnTimedTextListener} interface</li> * <li> Register the {@link MediaPlayer.OnTimedTextListener} callback on a MediaPlayer object that is used for playback</li> * <li> When a onTimedText callback is received, do the following: * <ul> * <li> call {@link #getText} to get the characters for rendering</li> * <li> call {@link #getBounds} to get the text rendering area/region</li> * </ul> * </li> * </ul> * * @see android.media.MediaPlayer */ public final class TimedText { private static final int FIRST_PUBLIC_KEY = 1; // These keys must be in sync with the keys in TextDescription.h private static final int KEY_DISPLAY_FLAGS = 1; // int private static final int KEY_STYLE_FLAGS = 2; // int private static final int KEY_BACKGROUND_COLOR_RGBA = 3; // int private static final int KEY_HIGHLIGHT_COLOR_RGBA = 4; // int private static final int KEY_SCROLL_DELAY = 5; // int private static final int KEY_WRAP_TEXT = 6; // int private static final int KEY_START_TIME = 7; // int private static final int KEY_STRUCT_BLINKING_TEXT_LIST = 8; // List<CharPos> private static final int KEY_STRUCT_FONT_LIST = 9; // List<Font> private static final int KEY_STRUCT_HIGHLIGHT_LIST = 10; // List<CharPos> private static final int KEY_STRUCT_HYPER_TEXT_LIST = 11; // List<HyperText> private static final int KEY_STRUCT_KARAOKE_LIST = 12; // List<Karaoke> private static final int KEY_STRUCT_STYLE_LIST = 13; // List<Style> private static final int KEY_STRUCT_TEXT_POS = 14; // TextPos private static final int KEY_STRUCT_JUSTIFICATION = 15; // Justification private static final int KEY_STRUCT_TEXT = 16; // Text private static final int LAST_PUBLIC_KEY = 16; private static final int FIRST_PRIVATE_KEY = 101; // The following keys are used between TimedText.java and // TextDescription.cpp in order to parce the Parcel. private static final int KEY_GLOBAL_SETTING = 101; private static final int KEY_LOCAL_SETTING = 102; private static final int KEY_START_CHAR = 103; private static final int KEY_END_CHAR = 104; private static final int KEY_FONT_ID = 105; private static final int KEY_FONT_SIZE = 106; private static final int KEY_TEXT_COLOR_RGBA = 107; private static final int LAST_PRIVATE_KEY = 107; private static final String TAG = "TimedText"; private final HashMap<Integer, Object> mKeyObjectMap = new HashMap<Integer, Object>(); private int mDisplayFlags = -1; private int mBackgroundColorRGBA = -1; private int mHighlightColorRGBA = -1; private int mScrollDelay = -1; private int mWrapText = -1; private List<CharPos> mBlinkingPosList = null; private List<CharPos> mHighlightPosList = null; private List<Karaoke> mKaraokeList = null; private List<Font> mFontList = null; private List<Style> mStyleList = null; private List<HyperText> mHyperTextList = null; private Rect mTextBounds = null; private String mTextChars = null; private Justification mJustification; /** * Helper class to hold the start char offset and end char offset * for Blinking Text or Highlight Text. endChar is the end offset * of the text (startChar + number of characters to be highlighted * or blinked). The member variables in this class are read-only. * {@hide} */ public static final class CharPos { /** * The offset of the start character */ public final int startChar; /** * The offset of the end character */ public final int endChar; /** * Constuctor * @param startChar the offset of the start character. * @param endChar the offset of the end character. */ public CharPos(int startChar, int endChar) { this.startChar = startChar; this.endChar = endChar; } } /** * Helper class to hold the justification for text display in the text box. * The member variables in this class are read-only. * {@hide} */ public static final class Justification { /** * horizontal justification 0: left, 1: centered, -1: right */ public final int horizontalJustification; /** * vertical justification 0: top, 1: centered, -1: bottom */ public final int verticalJustification; /** * Constructor * @param horizontal the horizontal justification of the text. * @param vertical the vertical justification of the text. */ public Justification(int horizontal, int vertical) { this.horizontalJustification = horizontal; this.verticalJustification = vertical; } } /** * Helper class to hold the style information to display the text. * The member variables in this class are read-only. * {@hide} */ public static final class Style { /** * The offset of the start character which applys this style */ public final int startChar; /** * The offset of the end character which applys this style */ public final int endChar; /** * ID of the font. This ID will be used to choose the font * to be used from the font list. */ public final int fontID; /** * True if the characters should be bold */ public final boolean isBold; /** * True if the characters should be italic */ public final boolean isItalic; /** * True if the characters should be underlined */ public final boolean isUnderlined; /** * The size of the font */ public final int fontSize; /** * To specify the RGBA color: 8 bits each of red, green, blue, * and an alpha(transparency) value */ public final int colorRGBA; /** * Constructor * @param startChar the offset of the start character which applys this style * @param endChar the offset of the end character which applys this style * @param fontId the ID of the font. * @param isBold whether the characters should be bold. * @param isItalic whether the characters should be italic. * @param isUnderlined whether the characters should be underlined. * @param fontSize the size of the font. * @param colorRGBA red, green, blue, and alpha value for color. */ public Style(int startChar, int endChar, int fontId, boolean isBold, boolean isItalic, boolean isUnderlined, int fontSize, int colorRGBA) { this.startChar = startChar; this.endChar = endChar; this.fontID = fontId; this.isBold = isBold; this.isItalic = isItalic; this.isUnderlined = isUnderlined; this.fontSize = fontSize; this.colorRGBA = colorRGBA; } } /** * Helper class to hold the font ID and name. * The member variables in this class are read-only. * {@hide} */ public static final class Font { /** * The font ID */ public final int ID; /** * The font name */ public final String name; /** * Constructor * @param id the font ID. * @param name the font name. */ public Font(int id, String name) { this.ID = id; this.name = name; } } /** * Helper class to hold the karaoke information. * The member variables in this class are read-only. * {@hide} */ public static final class Karaoke { /** * The start time (in milliseconds) to highlight the characters * specified by startChar and endChar. */ public final int startTimeMs; /** * The end time (in milliseconds) to highlight the characters * specified by startChar and endChar. */ public final int endTimeMs; /** * The offset of the start character to be highlighted */ public final int startChar; /** * The offset of the end character to be highlighted */ public final int endChar; /** * Constructor * @param startTimeMs the start time (in milliseconds) to highlight * the characters between startChar and endChar. * @param endTimeMs the end time (in milliseconds) to highlight * the characters between startChar and endChar. * @param startChar the offset of the start character to be highlighted. * @param endChar the offset of the end character to be highlighted. */ public Karaoke(int startTimeMs, int endTimeMs, int startChar, int endChar) { this.startTimeMs = startTimeMs; this.endTimeMs = endTimeMs; this.startChar = startChar; this.endChar = endChar; } } /** * Helper class to hold the hyper text information. * The member variables in this class are read-only. * {@hide} */ public static final class HyperText { /** * The offset of the start character */ public final int startChar; /** * The offset of the end character */ public final int endChar; /** * The linked-to URL */ public final String URL; /** * The "alt" string for user display */ public final String altString; /** * Constructor * @param startChar the offset of the start character. * @param endChar the offset of the end character. * @param url the linked-to URL. * @param alt the "alt" string for display. */ public HyperText(int startChar, int endChar, String url, String alt) { this.startChar = startChar; this.endChar = endChar; this.URL = url; this.altString = alt; } } /** * @param obj the byte array which contains the timed text. * @throws IllegalArgumentExcept if parseParcel() fails. * {@hide} */ public TimedText(Parcel parcel) { if (!parseParcel(parcel)) { mKeyObjectMap.clear(); throw new IllegalArgumentException("parseParcel() fails"); } } /** * Get the characters in the timed text. * * @return the characters as a String object in the TimedText. Applications * should stop rendering previous timed text at the current rendering region if * a null is returned, until the next non-null timed text is received. */ public String getText() { return mTextChars; } /** * Get the rectangle area or region for rendering the timed text as specified * by a Rect object. * * @return the rectangle region to render the characters in the timed text. * If no bounds information is available (a null is returned), render the * timed text at the center bottom of the display. */ public Rect getBounds() { return mTextBounds; } /* * Go over all the records, collecting metadata keys and fields in the * Parcel. These are stored in mKeyObjectMap for application to retrieve. * @return false if an error occurred during parsing. Otherwise, true. */ private boolean parseParcel(Parcel parcel) { parcel.setDataPosition(0); if (parcel.dataAvail() == 0) { return false; } int type = parcel.readInt(); if (type == KEY_LOCAL_SETTING) { type = parcel.readInt(); if (type != KEY_START_TIME) { return false; } int mStartTimeMs = parcel.readInt(); mKeyObjectMap.put(type, mStartTimeMs); type = parcel.readInt(); if (type != KEY_STRUCT_TEXT) { return false; } int textLen = parcel.readInt(); byte[] text = parcel.createByteArray(); if (text == null || text.length == 0) { mTextChars = null; } else { mTextChars = new String(text); } } else if (type != KEY_GLOBAL_SETTING) { Log.w(TAG, "Invalid timed text key found: " + type); return false; } while (parcel.dataAvail() > 0) { int key = parcel.readInt(); if (!isValidKey(key)) { Log.w(TAG, "Invalid timed text key found: " + key); return false; } Object object = null; switch (key) { case KEY_STRUCT_STYLE_LIST: { readStyle(parcel); object = mStyleList; break; } case KEY_STRUCT_FONT_LIST: { readFont(parcel); object = mFontList; break; } case KEY_STRUCT_HIGHLIGHT_LIST: { readHighlight(parcel); object = mHighlightPosList; break; } case KEY_STRUCT_KARAOKE_LIST: { readKaraoke(parcel); object = mKaraokeList; break; } case KEY_STRUCT_HYPER_TEXT_LIST: { readHyperText(parcel); object = mHyperTextList; break; } case KEY_STRUCT_BLINKING_TEXT_LIST: { readBlinkingText(parcel); object = mBlinkingPosList; break; } case KEY_WRAP_TEXT: { mWrapText = parcel.readInt(); object = mWrapText; break; } case KEY_HIGHLIGHT_COLOR_RGBA: { mHighlightColorRGBA = parcel.readInt(); object = mHighlightColorRGBA; break; } case KEY_DISPLAY_FLAGS: { mDisplayFlags = parcel.readInt(); object = mDisplayFlags; break; } case KEY_STRUCT_JUSTIFICATION: { int horizontal = parcel.readInt(); int vertical = parcel.readInt(); mJustification = new Justification(horizontal, vertical); object = mJustification; break; } case KEY_BACKGROUND_COLOR_RGBA: { mBackgroundColorRGBA = parcel.readInt(); object = mBackgroundColorRGBA; break; } case KEY_STRUCT_TEXT_POS: { int top = parcel.readInt(); int left = parcel.readInt(); int bottom = parcel.readInt(); int right = parcel.readInt(); mTextBounds = new Rect(left, top, right, bottom); break; } case KEY_SCROLL_DELAY: { mScrollDelay = parcel.readInt(); object = mScrollDelay; break; } default: { break; } } if (object != null) { if (mKeyObjectMap.containsKey(key)) { mKeyObjectMap.remove(key); } // Previous mapping will be replaced with the new object, if there was one. mKeyObjectMap.put(key, object); } } return true; } /* * To parse and store the Style list. */ private void readStyle(Parcel parcel) { boolean endOfStyle = false; int startChar = -1; int endChar = -1; int fontId = -1; boolean isBold = false; boolean isItalic = false; boolean isUnderlined = false; int fontSize = -1; int colorRGBA = -1; while (!endOfStyle && (parcel.dataAvail() > 0)) { int key = parcel.readInt(); switch (key) { case KEY_START_CHAR: { startChar = parcel.readInt(); break; } case KEY_END_CHAR: { endChar = parcel.readInt(); break; } case KEY_FONT_ID: { fontId = parcel.readInt(); break; } case KEY_STYLE_FLAGS: { int flags = parcel.readInt(); // In the absence of any bits set in flags, the text // is plain. Otherwise, 1: bold, 2: italic, 4: underline isBold = ((flags % 2) == 1); isItalic = ((flags % 4) >= 2); isUnderlined = ((flags / 4) == 1); break; } case KEY_FONT_SIZE: { fontSize = parcel.readInt(); break; } case KEY_TEXT_COLOR_RGBA: { colorRGBA = parcel.readInt(); break; } default: { // End of the Style parsing. Reset the data position back // to the position before the last parcel.readInt() call. parcel.setDataPosition(parcel.dataPosition() - 4); endOfStyle = true; break; } } } Style style = new Style(startChar, endChar, fontId, isBold, isItalic, isUnderlined, fontSize, colorRGBA); if (mStyleList == null) { mStyleList = new ArrayList<Style>(); } mStyleList.add(style); } /* * To parse and store the Font list */ private void readFont(Parcel parcel) { int entryCount = parcel.readInt(); for (int i = 0; i < entryCount; i++) { int id = parcel.readInt(); int nameLen = parcel.readInt(); byte[] text = parcel.createByteArray(); final String name = new String(text, 0, nameLen); Font font = new Font(id, name); if (mFontList == null) { mFontList = new ArrayList<Font>(); } mFontList.add(font); } } /* * To parse and store the Highlight list */ private void readHighlight(Parcel parcel) { int startChar = parcel.readInt(); int endChar = parcel.readInt(); CharPos pos = new CharPos(startChar, endChar); if (mHighlightPosList == null) { mHighlightPosList = new ArrayList<CharPos>(); } mHighlightPosList.add(pos); } /* * To parse and store the Karaoke list */ private void readKaraoke(Parcel parcel) { int entryCount = parcel.readInt(); for (int i = 0; i < entryCount; i++) { int startTimeMs = parcel.readInt(); int endTimeMs = parcel.readInt(); int startChar = parcel.readInt(); int endChar = parcel.readInt(); Karaoke kara = new Karaoke(startTimeMs, endTimeMs, startChar, endChar); if (mKaraokeList == null) { mKaraokeList = new ArrayList<Karaoke>(); } mKaraokeList.add(kara); } } /* * To parse and store HyperText list */ private void readHyperText(Parcel parcel) { int startChar = parcel.readInt(); int endChar = parcel.readInt(); int len = parcel.readInt(); byte[] url = parcel.createByteArray(); final String urlString = new String(url, 0, len); len = parcel.readInt(); byte[] alt = parcel.createByteArray(); final String altString = new String(alt, 0, len); HyperText hyperText = new HyperText(startChar, endChar, urlString, altString); if (mHyperTextList == null) { mHyperTextList = new ArrayList<HyperText>(); } mHyperTextList.add(hyperText); } /* * To parse and store blinking text list */ private void readBlinkingText(Parcel parcel) { int startChar = parcel.readInt(); int endChar = parcel.readInt(); CharPos blinkingPos = new CharPos(startChar, endChar); if (mBlinkingPosList == null) { mBlinkingPosList = new ArrayList<CharPos>(); } mBlinkingPosList.add(blinkingPos); } /* * To check whether the given key is valid. * @param key the key to be checked. * @return true if the key is a valid one. Otherwise, false. */ private boolean isValidKey(final int key) { if (!((key >= FIRST_PUBLIC_KEY) && (key <= LAST_PUBLIC_KEY)) && !((key >= FIRST_PRIVATE_KEY) && (key <= LAST_PRIVATE_KEY))) { return false; } return true; } /* * To check whether the given key is contained in this TimedText object. * @param key the key to be checked. * @return true if the key is contained in this TimedText object. * Otherwise, false. */ private boolean containsKey(final int key) { if (isValidKey(key) && mKeyObjectMap.containsKey(key)) { return true; } return false; } /* * @return a set of the keys contained in this TimedText object. */ private Set keySet() { return mKeyObjectMap.keySet(); } /* * To retrieve the object associated with the key. Caller must make sure * the key is present using the containsKey method otherwise a * RuntimeException will occur. * @param key the key used to retrieve the object. * @return an object. The object could be 1) an instance of Integer; 2) a * List of CharPos, Karaoke, Font, Style, and HyperText, or 3) an instance of * Justification. */ private Object getObject(final int key) { if (containsKey(key)) { return mKeyObjectMap.get(key); } else { throw new IllegalArgumentException("Invalid key: " + key); } } }