|  | /* | 
|  | * Copyright (C) 2013 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.compat.annotation.UnsupportedAppUsage; | 
|  | import android.content.Context; | 
|  | import android.text.Layout.Alignment; | 
|  | import android.text.SpannableStringBuilder; | 
|  | import android.util.ArrayMap; | 
|  | import android.util.AttributeSet; | 
|  | import android.util.Log; | 
|  | import android.view.Gravity; | 
|  | import android.view.View; | 
|  | import android.view.ViewGroup; | 
|  | import android.view.accessibility.CaptioningManager; | 
|  | import android.view.accessibility.CaptioningManager.CaptionStyle; | 
|  | import android.view.accessibility.CaptioningManager.CaptioningChangeListener; | 
|  | import android.widget.LinearLayout; | 
|  |  | 
|  | import com.android.internal.widget.SubtitleView; | 
|  |  | 
|  | import java.util.ArrayList; | 
|  | import java.util.Arrays; | 
|  | import java.util.HashMap; | 
|  | import java.util.Map; | 
|  | import java.util.Vector; | 
|  |  | 
|  | /** @hide */ | 
|  | public class WebVttRenderer extends SubtitleController.Renderer { | 
|  | private final Context mContext; | 
|  |  | 
|  | private WebVttRenderingWidget mRenderingWidget; | 
|  |  | 
|  | @UnsupportedAppUsage | 
|  | public WebVttRenderer(Context context) { | 
|  | mContext = context; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean supports(MediaFormat format) { | 
|  | if (format.containsKey(MediaFormat.KEY_MIME)) { | 
|  | return format.getString(MediaFormat.KEY_MIME).equals("text/vtt"); | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public SubtitleTrack createTrack(MediaFormat format) { | 
|  | if (mRenderingWidget == null) { | 
|  | mRenderingWidget = new WebVttRenderingWidget(mContext); | 
|  | } | 
|  |  | 
|  | return new WebVttTrack(mRenderingWidget, format); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | class TextTrackCueSpan { | 
|  | long mTimestampMs; | 
|  | boolean mEnabled; | 
|  | String mText; | 
|  | TextTrackCueSpan(String text, long timestamp) { | 
|  | mTimestampMs = timestamp; | 
|  | mText = text; | 
|  | // spans with timestamp will be enabled by Cue.onTime | 
|  | mEnabled = (mTimestampMs < 0); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean equals(Object o) { | 
|  | if (!(o instanceof TextTrackCueSpan)) { | 
|  | return false; | 
|  | } | 
|  | TextTrackCueSpan span = (TextTrackCueSpan) o; | 
|  | return mTimestampMs == span.mTimestampMs && | 
|  | mText.equals(span.mText); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @hide | 
|  | * | 
|  | * Extract all text without style, but with timestamp spans. | 
|  | */ | 
|  | class UnstyledTextExtractor implements Tokenizer.OnTokenListener { | 
|  | StringBuilder mLine = new StringBuilder(); | 
|  | Vector<TextTrackCueSpan[]> mLines = new Vector<TextTrackCueSpan[]>(); | 
|  | Vector<TextTrackCueSpan> mCurrentLine = new Vector<TextTrackCueSpan>(); | 
|  | long mLastTimestamp; | 
|  |  | 
|  | UnstyledTextExtractor() { | 
|  | init(); | 
|  | } | 
|  |  | 
|  | private void init() { | 
|  | mLine.delete(0, mLine.length()); | 
|  | mLines.clear(); | 
|  | mCurrentLine.clear(); | 
|  | mLastTimestamp = -1; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onData(String s) { | 
|  | mLine.append(s); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onStart(String tag, String[] classes, String annotation) { } | 
|  |  | 
|  | @Override | 
|  | public void onEnd(String tag) { } | 
|  |  | 
|  | @Override | 
|  | public void onTimeStamp(long timestampMs) { | 
|  | // finish any prior span | 
|  | if (mLine.length() > 0 && timestampMs != mLastTimestamp) { | 
|  | mCurrentLine.add( | 
|  | new TextTrackCueSpan(mLine.toString(), mLastTimestamp)); | 
|  | mLine.delete(0, mLine.length()); | 
|  | } | 
|  | mLastTimestamp = timestampMs; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onLineEnd() { | 
|  | // finish any pending span | 
|  | if (mLine.length() > 0) { | 
|  | mCurrentLine.add( | 
|  | new TextTrackCueSpan(mLine.toString(), mLastTimestamp)); | 
|  | mLine.delete(0, mLine.length()); | 
|  | } | 
|  |  | 
|  | TextTrackCueSpan[] spans = new TextTrackCueSpan[mCurrentLine.size()]; | 
|  | mCurrentLine.toArray(spans); | 
|  | mCurrentLine.clear(); | 
|  | mLines.add(spans); | 
|  | } | 
|  |  | 
|  | public TextTrackCueSpan[][] getText() { | 
|  | // for politeness, finish last cue-line if it ends abruptly | 
|  | if (mLine.length() > 0 || mCurrentLine.size() > 0) { | 
|  | onLineEnd(); | 
|  | } | 
|  | TextTrackCueSpan[][] lines = new TextTrackCueSpan[mLines.size()][]; | 
|  | mLines.toArray(lines); | 
|  | init(); | 
|  | return lines; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @hide | 
|  | * | 
|  | * Tokenizer tokenizes the WebVTT Cue Text into tags and data | 
|  | */ | 
|  | class Tokenizer { | 
|  | private static final String TAG = "Tokenizer"; | 
|  | private TokenizerPhase mPhase; | 
|  | private TokenizerPhase mDataTokenizer; | 
|  | private TokenizerPhase mTagTokenizer; | 
|  |  | 
|  | private OnTokenListener mListener; | 
|  | private String mLine; | 
|  | private int mHandledLen; | 
|  |  | 
|  | interface TokenizerPhase { | 
|  | TokenizerPhase start(); | 
|  | void tokenize(); | 
|  | } | 
|  |  | 
|  | class DataTokenizer implements TokenizerPhase { | 
|  | // includes both WebVTT data && escape state | 
|  | private StringBuilder mData; | 
|  |  | 
|  | public TokenizerPhase start() { | 
|  | mData = new StringBuilder(); | 
|  | return this; | 
|  | } | 
|  |  | 
|  | private boolean replaceEscape(String escape, String replacement, int pos) { | 
|  | if (mLine.startsWith(escape, pos)) { | 
|  | mData.append(mLine.substring(mHandledLen, pos)); | 
|  | mData.append(replacement); | 
|  | mHandledLen = pos + escape.length(); | 
|  | pos = mHandledLen - 1; | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void tokenize() { | 
|  | int end = mLine.length(); | 
|  | for (int pos = mHandledLen; pos < mLine.length(); pos++) { | 
|  | if (mLine.charAt(pos) == '&') { | 
|  | if (replaceEscape("&", "&", pos) || | 
|  | replaceEscape("<", "<", pos) || | 
|  | replaceEscape(">", ">", pos) || | 
|  | replaceEscape("‎", "\u200e", pos) || | 
|  | replaceEscape("‏", "\u200f", pos) || | 
|  | replaceEscape(" ", "\u00a0", pos)) { | 
|  | continue; | 
|  | } | 
|  | } else if (mLine.charAt(pos) == '<') { | 
|  | end = pos; | 
|  | mPhase = mTagTokenizer.start(); | 
|  | break; | 
|  | } | 
|  | } | 
|  | mData.append(mLine.substring(mHandledLen, end)); | 
|  | // yield mData | 
|  | mListener.onData(mData.toString()); | 
|  | mData.delete(0, mData.length()); | 
|  | mHandledLen = end; | 
|  | } | 
|  | } | 
|  |  | 
|  | class TagTokenizer implements TokenizerPhase { | 
|  | private boolean mAtAnnotation; | 
|  | private String mName, mAnnotation; | 
|  |  | 
|  | public TokenizerPhase start() { | 
|  | mName = mAnnotation = ""; | 
|  | mAtAnnotation = false; | 
|  | return this; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void tokenize() { | 
|  | if (!mAtAnnotation) | 
|  | mHandledLen++; | 
|  | if (mHandledLen < mLine.length()) { | 
|  | String[] parts; | 
|  | /** | 
|  | * Collect annotations and end-tags to closing >.  Collect tag | 
|  | * name to closing bracket or next white-space. | 
|  | */ | 
|  | if (mAtAnnotation || mLine.charAt(mHandledLen) == '/') { | 
|  | parts = mLine.substring(mHandledLen).split(">"); | 
|  | } else { | 
|  | parts = mLine.substring(mHandledLen).split("[\t\f >]"); | 
|  | } | 
|  | String part = mLine.substring( | 
|  | mHandledLen, mHandledLen + parts[0].length()); | 
|  | mHandledLen += parts[0].length(); | 
|  |  | 
|  | if (mAtAnnotation) { | 
|  | mAnnotation += " " + part; | 
|  | } else { | 
|  | mName = part; | 
|  | } | 
|  | } | 
|  |  | 
|  | mAtAnnotation = true; | 
|  |  | 
|  | if (mHandledLen < mLine.length() && mLine.charAt(mHandledLen) == '>') { | 
|  | yield_tag(); | 
|  | mPhase = mDataTokenizer.start(); | 
|  | mHandledLen++; | 
|  | } | 
|  | } | 
|  |  | 
|  | private void yield_tag() { | 
|  | if (mName.startsWith("/")) { | 
|  | mListener.onEnd(mName.substring(1)); | 
|  | } else if (mName.length() > 0 && Character.isDigit(mName.charAt(0))) { | 
|  | // timestamp | 
|  | try { | 
|  | long timestampMs = WebVttParser.parseTimestampMs(mName); | 
|  | mListener.onTimeStamp(timestampMs); | 
|  | } catch (NumberFormatException e) { | 
|  | Log.d(TAG, "invalid timestamp tag: <" + mName + ">"); | 
|  | } | 
|  | } else { | 
|  | mAnnotation = mAnnotation.replaceAll("\\s+", " "); | 
|  | if (mAnnotation.startsWith(" ")) { | 
|  | mAnnotation = mAnnotation.substring(1); | 
|  | } | 
|  | if (mAnnotation.endsWith(" ")) { | 
|  | mAnnotation = mAnnotation.substring(0, mAnnotation.length() - 1); | 
|  | } | 
|  |  | 
|  | String[] classes = null; | 
|  | int dotAt = mName.indexOf('.'); | 
|  | if (dotAt >= 0) { | 
|  | classes = mName.substring(dotAt + 1).split("\\."); | 
|  | mName = mName.substring(0, dotAt); | 
|  | } | 
|  | mListener.onStart(mName, classes, mAnnotation); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | Tokenizer(OnTokenListener listener) { | 
|  | mDataTokenizer = new DataTokenizer(); | 
|  | mTagTokenizer = new TagTokenizer(); | 
|  | reset(); | 
|  | mListener = listener; | 
|  | } | 
|  |  | 
|  | void reset() { | 
|  | mPhase = mDataTokenizer.start(); | 
|  | } | 
|  |  | 
|  | void tokenize(String s) { | 
|  | mHandledLen = 0; | 
|  | mLine = s; | 
|  | while (mHandledLen < mLine.length()) { | 
|  | mPhase.tokenize(); | 
|  | } | 
|  | /* we are finished with a line unless we are in the middle of a tag */ | 
|  | if (!(mPhase instanceof TagTokenizer)) { | 
|  | // yield END-OF-LINE | 
|  | mListener.onLineEnd(); | 
|  | } | 
|  | } | 
|  |  | 
|  | interface OnTokenListener { | 
|  | void onData(String s); | 
|  | void onStart(String tag, String[] classes, String annotation); | 
|  | void onEnd(String tag); | 
|  | void onTimeStamp(long timestampMs); | 
|  | void onLineEnd(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | class TextTrackRegion { | 
|  | final static int SCROLL_VALUE_NONE      = 300; | 
|  | final static int SCROLL_VALUE_SCROLL_UP = 301; | 
|  |  | 
|  | String mId; | 
|  | float mWidth; | 
|  | int mLines; | 
|  | float mAnchorPointX, mAnchorPointY; | 
|  | float mViewportAnchorPointX, mViewportAnchorPointY; | 
|  | int mScrollValue; | 
|  |  | 
|  | TextTrackRegion() { | 
|  | mId = ""; | 
|  | mWidth = 100; | 
|  | mLines = 3; | 
|  | mAnchorPointX = mViewportAnchorPointX = 0.f; | 
|  | mAnchorPointY = mViewportAnchorPointY = 100.f; | 
|  | mScrollValue = SCROLL_VALUE_NONE; | 
|  | } | 
|  |  | 
|  | public String toString() { | 
|  | StringBuilder res = new StringBuilder(" {id:\"").append(mId) | 
|  | .append("\", width:").append(mWidth) | 
|  | .append(", lines:").append(mLines) | 
|  | .append(", anchorPoint:(").append(mAnchorPointX) | 
|  | .append(", ").append(mAnchorPointY) | 
|  | .append("), viewportAnchorPoints:").append(mViewportAnchorPointX) | 
|  | .append(", ").append(mViewportAnchorPointY) | 
|  | .append("), scrollValue:") | 
|  | .append(mScrollValue == SCROLL_VALUE_NONE ? "none" : | 
|  | mScrollValue == SCROLL_VALUE_SCROLL_UP ? "scroll_up" : | 
|  | "INVALID") | 
|  | .append("}"); | 
|  | return res.toString(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | class TextTrackCue extends SubtitleTrack.Cue { | 
|  | final static int WRITING_DIRECTION_HORIZONTAL  = 100; | 
|  | final static int WRITING_DIRECTION_VERTICAL_RL = 101; | 
|  | final static int WRITING_DIRECTION_VERTICAL_LR = 102; | 
|  |  | 
|  | final static int ALIGNMENT_MIDDLE = 200; | 
|  | final static int ALIGNMENT_START  = 201; | 
|  | final static int ALIGNMENT_END    = 202; | 
|  | final static int ALIGNMENT_LEFT   = 203; | 
|  | final static int ALIGNMENT_RIGHT  = 204; | 
|  | private static final String TAG = "TTCue"; | 
|  |  | 
|  | String  mId; | 
|  | boolean mPauseOnExit; | 
|  | int     mWritingDirection; | 
|  | String  mRegionId; | 
|  | boolean mSnapToLines; | 
|  | Integer mLinePosition;  // null means AUTO | 
|  | boolean mAutoLinePosition; | 
|  | int     mTextPosition; | 
|  | int     mSize; | 
|  | int     mAlignment; | 
|  | // Vector<String> mText; | 
|  | String[] mStrings; | 
|  | TextTrackCueSpan[][] mLines; | 
|  | TextTrackRegion mRegion; | 
|  |  | 
|  | TextTrackCue() { | 
|  | mId = ""; | 
|  | mPauseOnExit = false; | 
|  | mWritingDirection = WRITING_DIRECTION_HORIZONTAL; | 
|  | mRegionId = ""; | 
|  | mSnapToLines = true; | 
|  | mLinePosition = null /* AUTO */; | 
|  | mTextPosition = 50; | 
|  | mSize = 100; | 
|  | mAlignment = ALIGNMENT_MIDDLE; | 
|  | mLines = null; | 
|  | mRegion = null; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean equals(Object o) { | 
|  | if (!(o instanceof TextTrackCue)) { | 
|  | return false; | 
|  | } | 
|  | if (this == o) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | try { | 
|  | TextTrackCue cue = (TextTrackCue) o; | 
|  | boolean res = mId.equals(cue.mId) && | 
|  | mPauseOnExit == cue.mPauseOnExit && | 
|  | mWritingDirection == cue.mWritingDirection && | 
|  | mRegionId.equals(cue.mRegionId) && | 
|  | mSnapToLines == cue.mSnapToLines && | 
|  | mAutoLinePosition == cue.mAutoLinePosition && | 
|  | (mAutoLinePosition || | 
|  | ((mLinePosition != null && mLinePosition.equals(cue.mLinePosition)) || | 
|  | (mLinePosition == null && cue.mLinePosition == null))) && | 
|  | mTextPosition == cue.mTextPosition && | 
|  | mSize == cue.mSize && | 
|  | mAlignment == cue.mAlignment && | 
|  | mLines.length == cue.mLines.length; | 
|  | if (res == true) { | 
|  | for (int line = 0; line < mLines.length; line++) { | 
|  | if (!Arrays.equals(mLines[line], cue.mLines[line])) { | 
|  | return false; | 
|  | } | 
|  | } | 
|  | } | 
|  | return res; | 
|  | } catch(IncompatibleClassChangeError e) { | 
|  | return false; | 
|  | } | 
|  | } | 
|  |  | 
|  | public StringBuilder appendStringsToBuilder(StringBuilder builder) { | 
|  | if (mStrings == null) { | 
|  | builder.append("null"); | 
|  | } else { | 
|  | builder.append("["); | 
|  | boolean first = true; | 
|  | for (String s: mStrings) { | 
|  | if (!first) { | 
|  | builder.append(", "); | 
|  | } | 
|  | if (s == null) { | 
|  | builder.append("null"); | 
|  | } else { | 
|  | builder.append("\""); | 
|  | builder.append(s); | 
|  | builder.append("\""); | 
|  | } | 
|  | first = false; | 
|  | } | 
|  | builder.append("]"); | 
|  | } | 
|  | return builder; | 
|  | } | 
|  |  | 
|  | public StringBuilder appendLinesToBuilder(StringBuilder builder) { | 
|  | if (mLines == null) { | 
|  | builder.append("null"); | 
|  | } else { | 
|  | builder.append("["); | 
|  | boolean first = true; | 
|  | for (TextTrackCueSpan[] spans: mLines) { | 
|  | if (!first) { | 
|  | builder.append(", "); | 
|  | } | 
|  | if (spans == null) { | 
|  | builder.append("null"); | 
|  | } else { | 
|  | builder.append("\""); | 
|  | boolean innerFirst = true; | 
|  | long lastTimestamp = -1; | 
|  | for (TextTrackCueSpan span: spans) { | 
|  | if (!innerFirst) { | 
|  | builder.append(" "); | 
|  | } | 
|  | if (span.mTimestampMs != lastTimestamp) { | 
|  | builder.append("<") | 
|  | .append(WebVttParser.timeToString( | 
|  | span.mTimestampMs)) | 
|  | .append(">"); | 
|  | lastTimestamp = span.mTimestampMs; | 
|  | } | 
|  | builder.append(span.mText); | 
|  | innerFirst = false; | 
|  | } | 
|  | builder.append("\""); | 
|  | } | 
|  | first = false; | 
|  | } | 
|  | builder.append("]"); | 
|  | } | 
|  | return builder; | 
|  | } | 
|  |  | 
|  | public String toString() { | 
|  | StringBuilder res = new StringBuilder(); | 
|  |  | 
|  | res.append(WebVttParser.timeToString(mStartTimeMs)) | 
|  | .append(" --> ").append(WebVttParser.timeToString(mEndTimeMs)) | 
|  | .append(" {id:\"").append(mId) | 
|  | .append("\", pauseOnExit:").append(mPauseOnExit) | 
|  | .append(", direction:") | 
|  | .append(mWritingDirection == WRITING_DIRECTION_HORIZONTAL ? "horizontal" : | 
|  | mWritingDirection == WRITING_DIRECTION_VERTICAL_LR ? "vertical_lr" : | 
|  | mWritingDirection == WRITING_DIRECTION_VERTICAL_RL ? "vertical_rl" : | 
|  | "INVALID") | 
|  | .append(", regionId:\"").append(mRegionId) | 
|  | .append("\", snapToLines:").append(mSnapToLines) | 
|  | .append(", linePosition:").append(mAutoLinePosition ? "auto" : | 
|  | mLinePosition) | 
|  | .append(", textPosition:").append(mTextPosition) | 
|  | .append(", size:").append(mSize) | 
|  | .append(", alignment:") | 
|  | .append(mAlignment == ALIGNMENT_END ? "end" : | 
|  | mAlignment == ALIGNMENT_LEFT ? "left" : | 
|  | mAlignment == ALIGNMENT_MIDDLE ? "middle" : | 
|  | mAlignment == ALIGNMENT_RIGHT ? "right" : | 
|  | mAlignment == ALIGNMENT_START ? "start" : "INVALID") | 
|  | .append(", text:"); | 
|  | appendStringsToBuilder(res).append("}"); | 
|  | return res.toString(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int hashCode() { | 
|  | return toString().hashCode(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onTime(long timeMs) { | 
|  | for (TextTrackCueSpan[] line: mLines) { | 
|  | for (TextTrackCueSpan span: line) { | 
|  | span.mEnabled = timeMs >= span.mTimestampMs; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | *  Supporting July 10 2013 draft version | 
|  | * | 
|  | *  @hide | 
|  | */ | 
|  | class WebVttParser { | 
|  | private static final String TAG = "WebVttParser"; | 
|  | private Phase mPhase; | 
|  | private TextTrackCue mCue; | 
|  | private Vector<String> mCueTexts; | 
|  | private WebVttCueListener mListener; | 
|  | private String mBuffer; | 
|  |  | 
|  | WebVttParser(WebVttCueListener listener) { | 
|  | mPhase = mParseStart; | 
|  | mBuffer = "";   /* mBuffer contains up to 1 incomplete line */ | 
|  | mListener = listener; | 
|  | mCueTexts = new Vector<String>(); | 
|  | } | 
|  |  | 
|  | /* parsePercentageString */ | 
|  | public static float parseFloatPercentage(String s) | 
|  | throws NumberFormatException { | 
|  | if (!s.endsWith("%")) { | 
|  | throw new NumberFormatException("does not end in %"); | 
|  | } | 
|  | s = s.substring(0, s.length() - 1); | 
|  | // parseFloat allows an exponent or a sign | 
|  | if (s.matches(".*[^0-9.].*")) { | 
|  | throw new NumberFormatException("contains an invalid character"); | 
|  | } | 
|  |  | 
|  | try { | 
|  | float value = Float.parseFloat(s); | 
|  | if (value < 0.0f || value > 100.0f) { | 
|  | throw new NumberFormatException("is out of range"); | 
|  | } | 
|  | return value; | 
|  | } catch (NumberFormatException e) { | 
|  | throw new NumberFormatException("is not a number"); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static int parseIntPercentage(String s) throws NumberFormatException { | 
|  | if (!s.endsWith("%")) { | 
|  | throw new NumberFormatException("does not end in %"); | 
|  | } | 
|  | s = s.substring(0, s.length() - 1); | 
|  | // parseInt allows "-0" that returns 0, so check for non-digits | 
|  | if (s.matches(".*[^0-9].*")) { | 
|  | throw new NumberFormatException("contains an invalid character"); | 
|  | } | 
|  |  | 
|  | try { | 
|  | int value = Integer.parseInt(s); | 
|  | if (value < 0 || value > 100) { | 
|  | throw new NumberFormatException("is out of range"); | 
|  | } | 
|  | return value; | 
|  | } catch (NumberFormatException e) { | 
|  | throw new NumberFormatException("is not a number"); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static long parseTimestampMs(String s) throws NumberFormatException { | 
|  | if (!s.matches("(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}")) { | 
|  | throw new NumberFormatException("has invalid format"); | 
|  | } | 
|  |  | 
|  | String[] parts = s.split("\\.", 2); | 
|  | long value = 0; | 
|  | for (String group: parts[0].split(":")) { | 
|  | value = value * 60 + Long.parseLong(group); | 
|  | } | 
|  | return value * 1000 + Long.parseLong(parts[1]); | 
|  | } | 
|  |  | 
|  | public static String timeToString(long timeMs) { | 
|  | return String.format("%d:%02d:%02d.%03d", | 
|  | timeMs / 3600000, (timeMs / 60000) % 60, | 
|  | (timeMs / 1000) % 60, timeMs % 1000); | 
|  | } | 
|  |  | 
|  | public void parse(String s) { | 
|  | boolean trailingCR = false; | 
|  | mBuffer = (mBuffer + s.replace("\0", "\ufffd")).replace("\r\n", "\n"); | 
|  |  | 
|  | /* keep trailing '\r' in case matching '\n' arrives in next packet */ | 
|  | if (mBuffer.endsWith("\r")) { | 
|  | trailingCR = true; | 
|  | mBuffer = mBuffer.substring(0, mBuffer.length() - 1); | 
|  | } | 
|  |  | 
|  | String[] lines = mBuffer.split("[\r\n]"); | 
|  | for (int i = 0; i < lines.length - 1; i++) { | 
|  | mPhase.parse(lines[i]); | 
|  | } | 
|  |  | 
|  | mBuffer = lines[lines.length - 1]; | 
|  | if (trailingCR) | 
|  | mBuffer += "\r"; | 
|  | } | 
|  |  | 
|  | public void eos() { | 
|  | if (mBuffer.endsWith("\r")) { | 
|  | mBuffer = mBuffer.substring(0, mBuffer.length() - 1); | 
|  | } | 
|  |  | 
|  | mPhase.parse(mBuffer); | 
|  | mBuffer = ""; | 
|  |  | 
|  | yieldCue(); | 
|  | mPhase = mParseStart; | 
|  | } | 
|  |  | 
|  | public void yieldCue() { | 
|  | if (mCue != null && mCueTexts.size() > 0) { | 
|  | mCue.mStrings = new String[mCueTexts.size()]; | 
|  | mCueTexts.toArray(mCue.mStrings); | 
|  | mCueTexts.clear(); | 
|  | mListener.onCueParsed(mCue); | 
|  | } | 
|  | mCue = null; | 
|  | } | 
|  |  | 
|  | interface Phase { | 
|  | void parse(String line); | 
|  | } | 
|  |  | 
|  | final private Phase mSkipRest = new Phase() { | 
|  | @Override | 
|  | public void parse(String line) { } | 
|  | }; | 
|  |  | 
|  | final private Phase mParseStart = new Phase() { // 5-9 | 
|  | @Override | 
|  | public void parse(String line) { | 
|  | if (line.startsWith("\ufeff")) { | 
|  | line = line.substring(1); | 
|  | } | 
|  | if (!line.equals("WEBVTT") && | 
|  | !line.startsWith("WEBVTT ") && | 
|  | !line.startsWith("WEBVTT\t")) { | 
|  | log_warning("Not a WEBVTT header", line); | 
|  | mPhase = mSkipRest; | 
|  | } else { | 
|  | mPhase = mParseHeader; | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | final private Phase mParseHeader = new Phase() { // 10-13 | 
|  | TextTrackRegion parseRegion(String s) { | 
|  | TextTrackRegion region = new TextTrackRegion(); | 
|  | for (String setting: s.split(" +")) { | 
|  | int equalAt = setting.indexOf('='); | 
|  | if (equalAt <= 0 || equalAt == setting.length() - 1) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | String name = setting.substring(0, equalAt); | 
|  | String value = setting.substring(equalAt + 1); | 
|  | if (name.equals("id")) { | 
|  | region.mId = value; | 
|  | } else if (name.equals("width")) { | 
|  | try { | 
|  | region.mWidth = parseFloatPercentage(value); | 
|  | } catch (NumberFormatException e) { | 
|  | log_warning("region setting", name, | 
|  | "has invalid value", e.getMessage(), value); | 
|  | } | 
|  | } else if (name.equals("lines")) { | 
|  | if (value.matches(".*[^0-9].*")) { | 
|  | log_warning("lines", name, "contains an invalid character", value); | 
|  | } else { | 
|  | try { | 
|  | region.mLines = Integer.parseInt(value); | 
|  | assert(region.mLines >= 0); // lines contains only digits | 
|  | } catch (NumberFormatException e) { | 
|  | log_warning("region setting", name, "is not numeric", value); | 
|  | } | 
|  | } | 
|  | } else if (name.equals("regionanchor") || | 
|  | name.equals("viewportanchor")) { | 
|  | int commaAt = value.indexOf(","); | 
|  | if (commaAt < 0) { | 
|  | log_warning("region setting", name, "contains no comma", value); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | String anchorX = value.substring(0, commaAt); | 
|  | String anchorY = value.substring(commaAt + 1); | 
|  | float x, y; | 
|  |  | 
|  | try { | 
|  | x = parseFloatPercentage(anchorX); | 
|  | } catch (NumberFormatException e) { | 
|  | log_warning("region setting", name, | 
|  | "has invalid x component", e.getMessage(), anchorX); | 
|  | continue; | 
|  | } | 
|  | try { | 
|  | y = parseFloatPercentage(anchorY); | 
|  | } catch (NumberFormatException e) { | 
|  | log_warning("region setting", name, | 
|  | "has invalid y component", e.getMessage(), anchorY); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | if (name.charAt(0) == 'r') { | 
|  | region.mAnchorPointX = x; | 
|  | region.mAnchorPointY = y; | 
|  | } else { | 
|  | region.mViewportAnchorPointX = x; | 
|  | region.mViewportAnchorPointY = y; | 
|  | } | 
|  | } else if (name.equals("scroll")) { | 
|  | if (value.equals("up")) { | 
|  | region.mScrollValue = | 
|  | TextTrackRegion.SCROLL_VALUE_SCROLL_UP; | 
|  | } else { | 
|  | log_warning("region setting", name, "has invalid value", value); | 
|  | } | 
|  | } | 
|  | } | 
|  | return region; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void parse(String line)  { | 
|  | if (line.length() == 0) { | 
|  | mPhase = mParseCueId; | 
|  | } else if (line.contains("-->")) { | 
|  | mPhase = mParseCueTime; | 
|  | mPhase.parse(line); | 
|  | } else { | 
|  | int colonAt = line.indexOf(':'); | 
|  | if (colonAt <= 0 || colonAt >= line.length() - 1) { | 
|  | log_warning("meta data header has invalid format", line); | 
|  | } | 
|  | String name = line.substring(0, colonAt); | 
|  | String value = line.substring(colonAt + 1); | 
|  |  | 
|  | if (name.equals("Region")) { | 
|  | TextTrackRegion region = parseRegion(value); | 
|  | mListener.onRegionParsed(region); | 
|  | } | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | final private Phase mParseCueId = new Phase() { | 
|  | @Override | 
|  | public void parse(String line) { | 
|  | if (line.length() == 0) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | assert(mCue == null); | 
|  |  | 
|  | if (line.equals("NOTE") || line.startsWith("NOTE ")) { | 
|  | mPhase = mParseCueText; | 
|  | } | 
|  |  | 
|  | mCue = new TextTrackCue(); | 
|  | mCueTexts.clear(); | 
|  |  | 
|  | mPhase = mParseCueTime; | 
|  | if (line.contains("-->")) { | 
|  | mPhase.parse(line); | 
|  | } else { | 
|  | mCue.mId = line; | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | final private Phase mParseCueTime = new Phase() { | 
|  | @Override | 
|  | public void parse(String line) { | 
|  | int arrowAt = line.indexOf("-->"); | 
|  | if (arrowAt < 0) { | 
|  | mCue = null; | 
|  | mPhase = mParseCueId; | 
|  | return; | 
|  | } | 
|  |  | 
|  | String start = line.substring(0, arrowAt).trim(); | 
|  | // convert only initial and first other white-space to space | 
|  | String rest = line.substring(arrowAt + 3) | 
|  | .replaceFirst("^\\s+", "").replaceFirst("\\s+", " "); | 
|  | int spaceAt = rest.indexOf(' '); | 
|  | String end = spaceAt > 0 ? rest.substring(0, spaceAt) : rest; | 
|  | rest = spaceAt > 0 ? rest.substring(spaceAt + 1) : ""; | 
|  |  | 
|  | mCue.mStartTimeMs = parseTimestampMs(start); | 
|  | mCue.mEndTimeMs = parseTimestampMs(end); | 
|  | for (String setting: rest.split(" +")) { | 
|  | int colonAt = setting.indexOf(':'); | 
|  | if (colonAt <= 0 || colonAt == setting.length() - 1) { | 
|  | continue; | 
|  | } | 
|  | String name = setting.substring(0, colonAt); | 
|  | String value = setting.substring(colonAt + 1); | 
|  |  | 
|  | if (name.equals("region")) { | 
|  | mCue.mRegionId = value; | 
|  | } else if (name.equals("vertical")) { | 
|  | if (value.equals("rl")) { | 
|  | mCue.mWritingDirection = | 
|  | TextTrackCue.WRITING_DIRECTION_VERTICAL_RL; | 
|  | } else if (value.equals("lr")) { | 
|  | mCue.mWritingDirection = | 
|  | TextTrackCue.WRITING_DIRECTION_VERTICAL_LR; | 
|  | } else { | 
|  | log_warning("cue setting", name, "has invalid value", value); | 
|  | } | 
|  | } else if (name.equals("line")) { | 
|  | try { | 
|  | /* TRICKY: we know that there are no spaces in value */ | 
|  | assert(value.indexOf(' ') < 0); | 
|  | if (value.endsWith("%")) { | 
|  | mCue.mSnapToLines = false; | 
|  | mCue.mLinePosition = parseIntPercentage(value); | 
|  | } else if (value.matches(".*[^0-9].*")) { | 
|  | log_warning("cue setting", name, | 
|  | "contains an invalid character", value); | 
|  | } else { | 
|  | mCue.mSnapToLines = true; | 
|  | mCue.mLinePosition = Integer.parseInt(value); | 
|  | } | 
|  | } catch (NumberFormatException e) { | 
|  | log_warning("cue setting", name, | 
|  | "is not numeric or percentage", value); | 
|  | } | 
|  | // TODO: add support for optional alignment value [,start|middle|end] | 
|  | } else if (name.equals("position")) { | 
|  | try { | 
|  | mCue.mTextPosition = parseIntPercentage(value); | 
|  | } catch (NumberFormatException e) { | 
|  | log_warning("cue setting", name, | 
|  | "is not numeric or percentage", value); | 
|  | } | 
|  | } else if (name.equals("size")) { | 
|  | try { | 
|  | mCue.mSize = parseIntPercentage(value); | 
|  | } catch (NumberFormatException e) { | 
|  | log_warning("cue setting", name, | 
|  | "is not numeric or percentage", value); | 
|  | } | 
|  | } else if (name.equals("align")) { | 
|  | if (value.equals("start")) { | 
|  | mCue.mAlignment = TextTrackCue.ALIGNMENT_START; | 
|  | } else if (value.equals("middle")) { | 
|  | mCue.mAlignment = TextTrackCue.ALIGNMENT_MIDDLE; | 
|  | } else if (value.equals("end")) { | 
|  | mCue.mAlignment = TextTrackCue.ALIGNMENT_END; | 
|  | } else if (value.equals("left")) { | 
|  | mCue.mAlignment = TextTrackCue.ALIGNMENT_LEFT; | 
|  | } else if (value.equals("right")) { | 
|  | mCue.mAlignment = TextTrackCue.ALIGNMENT_RIGHT; | 
|  | } else { | 
|  | log_warning("cue setting", name, "has invalid value", value); | 
|  | continue; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if (mCue.mLinePosition != null || | 
|  | mCue.mSize != 100 || | 
|  | (mCue.mWritingDirection != | 
|  | TextTrackCue.WRITING_DIRECTION_HORIZONTAL)) { | 
|  | mCue.mRegionId = ""; | 
|  | } | 
|  |  | 
|  | mPhase = mParseCueText; | 
|  | } | 
|  | }; | 
|  |  | 
|  | /* also used for notes */ | 
|  | final private Phase mParseCueText = new Phase() { | 
|  | @Override | 
|  | public void parse(String line) { | 
|  | if (line.length() == 0) { | 
|  | yieldCue(); | 
|  | mPhase = mParseCueId; | 
|  | return; | 
|  | } else if (mCue != null) { | 
|  | mCueTexts.add(line); | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | private void log_warning( | 
|  | String nameType, String name, String message, | 
|  | String subMessage, String value) { | 
|  | Log.w(this.getClass().getName(), nameType + " '" + name + "' " + | 
|  | message + " ('" + value + "' " + subMessage + ")"); | 
|  | } | 
|  |  | 
|  | private void log_warning( | 
|  | String nameType, String name, String message, String value) { | 
|  | Log.w(this.getClass().getName(), nameType + " '" + name + "' " + | 
|  | message + " ('" + value + "')"); | 
|  | } | 
|  |  | 
|  | private void log_warning(String message, String value) { | 
|  | Log.w(this.getClass().getName(), message + " ('" + value + "')"); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | interface WebVttCueListener { | 
|  | void onCueParsed(TextTrackCue cue); | 
|  | void onRegionParsed(TextTrackRegion region); | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | class WebVttTrack extends SubtitleTrack implements WebVttCueListener { | 
|  | private static final String TAG = "WebVttTrack"; | 
|  |  | 
|  | private final WebVttParser mParser = new WebVttParser(this); | 
|  | private final UnstyledTextExtractor mExtractor = | 
|  | new UnstyledTextExtractor(); | 
|  | private final Tokenizer mTokenizer = new Tokenizer(mExtractor); | 
|  | private final Vector<Long> mTimestamps = new Vector<Long>(); | 
|  | private final WebVttRenderingWidget mRenderingWidget; | 
|  |  | 
|  | private final Map<String, TextTrackRegion> mRegions = | 
|  | new HashMap<String, TextTrackRegion>(); | 
|  | private Long mCurrentRunID; | 
|  |  | 
|  | WebVttTrack(WebVttRenderingWidget renderingWidget, MediaFormat format) { | 
|  | super(format); | 
|  |  | 
|  | mRenderingWidget = renderingWidget; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public WebVttRenderingWidget getRenderingWidget() { | 
|  | return mRenderingWidget; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onData(byte[] data, boolean eos, long runID) { | 
|  | try { | 
|  | String str = new String(data, "UTF-8"); | 
|  |  | 
|  | // implement intermixing restriction for WebVTT only for now | 
|  | synchronized(mParser) { | 
|  | if (mCurrentRunID != null && runID != mCurrentRunID) { | 
|  | throw new IllegalStateException( | 
|  | "Run #" + mCurrentRunID + | 
|  | " in progress.  Cannot process run #" + runID); | 
|  | } | 
|  | mCurrentRunID = runID; | 
|  | mParser.parse(str); | 
|  | if (eos) { | 
|  | finishedRun(runID); | 
|  | mParser.eos(); | 
|  | mRegions.clear(); | 
|  | mCurrentRunID = null; | 
|  | } | 
|  | } | 
|  | } catch (java.io.UnsupportedEncodingException e) { | 
|  | Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onCueParsed(TextTrackCue cue) { | 
|  | synchronized (mParser) { | 
|  | // resolve region | 
|  | if (cue.mRegionId.length() != 0) { | 
|  | cue.mRegion = mRegions.get(cue.mRegionId); | 
|  | } | 
|  |  | 
|  | if (DEBUG) Log.v(TAG, "adding cue " + cue); | 
|  |  | 
|  | // tokenize text track string-lines into lines of spans | 
|  | mTokenizer.reset(); | 
|  | for (String s: cue.mStrings) { | 
|  | mTokenizer.tokenize(s); | 
|  | } | 
|  | cue.mLines = mExtractor.getText(); | 
|  | if (DEBUG) Log.v(TAG, cue.appendLinesToBuilder( | 
|  | cue.appendStringsToBuilder( | 
|  | new StringBuilder()).append(" simplified to: ")) | 
|  | .toString()); | 
|  |  | 
|  | // extract inner timestamps | 
|  | for (TextTrackCueSpan[] line: cue.mLines) { | 
|  | for (TextTrackCueSpan span: line) { | 
|  | if (span.mTimestampMs > cue.mStartTimeMs && | 
|  | span.mTimestampMs < cue.mEndTimeMs && | 
|  | !mTimestamps.contains(span.mTimestampMs)) { | 
|  | mTimestamps.add(span.mTimestampMs); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if (mTimestamps.size() > 0) { | 
|  | cue.mInnerTimesMs = new long[mTimestamps.size()]; | 
|  | for (int ix=0; ix < mTimestamps.size(); ++ix) { | 
|  | cue.mInnerTimesMs[ix] = mTimestamps.get(ix); | 
|  | } | 
|  | mTimestamps.clear(); | 
|  | } else { | 
|  | cue.mInnerTimesMs = null; | 
|  | } | 
|  |  | 
|  | cue.mRunID = mCurrentRunID; | 
|  | } | 
|  |  | 
|  | addCue(cue); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onRegionParsed(TextTrackRegion region) { | 
|  | synchronized(mParser) { | 
|  | mRegions.put(region.mId, region); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void updateView(Vector<SubtitleTrack.Cue> activeCues) { | 
|  | if (!mVisible) { | 
|  | // don't keep the state if we are not visible | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (DEBUG && mTimeProvider != null) { | 
|  | try { | 
|  | Log.d(TAG, "at " + | 
|  | (mTimeProvider.getCurrentTimeUs(false, true) / 1000) + | 
|  | " ms the active cues are:"); | 
|  | } catch (IllegalStateException e) { | 
|  | Log.d(TAG, "at (illegal state) the active cues are:"); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (mRenderingWidget != null) { | 
|  | mRenderingWidget.setActiveCues(activeCues); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Widget capable of rendering WebVTT captions. | 
|  | * | 
|  | * @hide | 
|  | */ | 
|  | class WebVttRenderingWidget extends ViewGroup implements SubtitleTrack.RenderingWidget { | 
|  | private static final boolean DEBUG = false; | 
|  |  | 
|  | private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT; | 
|  |  | 
|  | private static final int DEBUG_REGION_BACKGROUND = 0x800000FF; | 
|  | private static final int DEBUG_CUE_BACKGROUND = 0x80FF0000; | 
|  |  | 
|  | /** WebVtt specifies line height as 5.3% of the viewport height. */ | 
|  | private static final float LINE_HEIGHT_RATIO = 0.0533f; | 
|  |  | 
|  | /** Map of active regions, used to determine enter/exit. */ | 
|  | private final ArrayMap<TextTrackRegion, RegionLayout> mRegionBoxes = | 
|  | new ArrayMap<TextTrackRegion, RegionLayout>(); | 
|  |  | 
|  | /** Map of active cues, used to determine enter/exit. */ | 
|  | private final ArrayMap<TextTrackCue, CueLayout> mCueBoxes = | 
|  | new ArrayMap<TextTrackCue, CueLayout>(); | 
|  |  | 
|  | /** Captioning manager, used to obtain and track caption properties. */ | 
|  | private final CaptioningManager mManager; | 
|  |  | 
|  | /** Callback for rendering changes. */ | 
|  | private OnChangedListener mListener; | 
|  |  | 
|  | /** Current caption style. */ | 
|  | private CaptionStyle mCaptionStyle; | 
|  |  | 
|  | /** Current font size, computed from font scaling factor and height. */ | 
|  | private float mFontSize; | 
|  |  | 
|  | /** Whether a caption style change listener is registered. */ | 
|  | private boolean mHasChangeListener; | 
|  |  | 
|  | public WebVttRenderingWidget(Context context) { | 
|  | this(context, null); | 
|  | } | 
|  |  | 
|  | public WebVttRenderingWidget(Context context, AttributeSet attrs) { | 
|  | this(context, attrs, 0); | 
|  | } | 
|  |  | 
|  | public WebVttRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) { | 
|  | this(context, attrs, defStyleAttr, 0); | 
|  | } | 
|  |  | 
|  | public WebVttRenderingWidget( | 
|  | Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { | 
|  | super(context, attrs, defStyleAttr, defStyleRes); | 
|  |  | 
|  | // Cannot render text over video when layer type is hardware. | 
|  | setLayerType(View.LAYER_TYPE_SOFTWARE, null); | 
|  |  | 
|  | mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); | 
|  | mCaptionStyle = mManager.getUserStyle(); | 
|  | mFontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setSize(int width, int height) { | 
|  | final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); | 
|  | final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); | 
|  |  | 
|  | measure(widthSpec, heightSpec); | 
|  | layout(0, 0, width, height); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onAttachedToWindow() { | 
|  | super.onAttachedToWindow(); | 
|  |  | 
|  | manageChangeListener(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onDetachedFromWindow() { | 
|  | super.onDetachedFromWindow(); | 
|  |  | 
|  | manageChangeListener(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setOnChangedListener(OnChangedListener listener) { | 
|  | mListener = listener; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setVisible(boolean visible) { | 
|  | if (visible) { | 
|  | setVisibility(View.VISIBLE); | 
|  | } else { | 
|  | setVisibility(View.GONE); | 
|  | } | 
|  |  | 
|  | manageChangeListener(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Manages whether this renderer is listening for caption style changes. | 
|  | */ | 
|  | private void manageChangeListener() { | 
|  | final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE; | 
|  | if (mHasChangeListener != needsListener) { | 
|  | mHasChangeListener = needsListener; | 
|  |  | 
|  | if (needsListener) { | 
|  | mManager.addCaptioningChangeListener(mCaptioningListener); | 
|  |  | 
|  | final CaptionStyle captionStyle = mManager.getUserStyle(); | 
|  | final float fontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO; | 
|  | setCaptionStyle(captionStyle, fontSize); | 
|  | } else { | 
|  | mManager.removeCaptioningChangeListener(mCaptioningListener); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) { | 
|  | final Context context = getContext(); | 
|  | final CaptionStyle captionStyle = mCaptionStyle; | 
|  | final float fontSize = mFontSize; | 
|  |  | 
|  | prepForPrune(); | 
|  |  | 
|  | // Ensure we have all necessary cue and region boxes. | 
|  | final int count = activeCues.size(); | 
|  | for (int i = 0; i < count; i++) { | 
|  | final TextTrackCue cue = (TextTrackCue) activeCues.get(i); | 
|  | final TextTrackRegion region = cue.mRegion; | 
|  | if (region != null) { | 
|  | RegionLayout regionBox = mRegionBoxes.get(region); | 
|  | if (regionBox == null) { | 
|  | regionBox = new RegionLayout(context, region, captionStyle, fontSize); | 
|  | mRegionBoxes.put(region, regionBox); | 
|  | addView(regionBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); | 
|  | } | 
|  | regionBox.put(cue); | 
|  | } else { | 
|  | CueLayout cueBox = mCueBoxes.get(cue); | 
|  | if (cueBox == null) { | 
|  | cueBox = new CueLayout(context, cue, captionStyle, fontSize); | 
|  | mCueBoxes.put(cue, cueBox); | 
|  | addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); | 
|  | } | 
|  | cueBox.update(); | 
|  | cueBox.setOrder(i); | 
|  | } | 
|  | } | 
|  |  | 
|  | prune(); | 
|  |  | 
|  | // Force measurement and layout. | 
|  | final int width = getWidth(); | 
|  | final int height = getHeight(); | 
|  | setSize(width, height); | 
|  |  | 
|  | if (mListener != null) { | 
|  | mListener.onChanged(this); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void setCaptionStyle(CaptionStyle captionStyle, float fontSize) { | 
|  | captionStyle = DEFAULT_CAPTION_STYLE.applyStyle(captionStyle); | 
|  | mCaptionStyle = captionStyle; | 
|  | mFontSize = fontSize; | 
|  |  | 
|  | final int cueCount = mCueBoxes.size(); | 
|  | for (int i = 0; i < cueCount; i++) { | 
|  | final CueLayout cueBox = mCueBoxes.valueAt(i); | 
|  | cueBox.setCaptionStyle(captionStyle, fontSize); | 
|  | } | 
|  |  | 
|  | final int regionCount = mRegionBoxes.size(); | 
|  | for (int i = 0; i < regionCount; i++) { | 
|  | final RegionLayout regionBox = mRegionBoxes.valueAt(i); | 
|  | regionBox.setCaptionStyle(captionStyle, fontSize); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Remove inactive cues and regions. | 
|  | */ | 
|  | private void prune() { | 
|  | int regionCount = mRegionBoxes.size(); | 
|  | for (int i = 0; i < regionCount; i++) { | 
|  | final RegionLayout regionBox = mRegionBoxes.valueAt(i); | 
|  | if (regionBox.prune()) { | 
|  | removeView(regionBox); | 
|  | mRegionBoxes.removeAt(i); | 
|  | regionCount--; | 
|  | i--; | 
|  | } | 
|  | } | 
|  |  | 
|  | int cueCount = mCueBoxes.size(); | 
|  | for (int i = 0; i < cueCount; i++) { | 
|  | final CueLayout cueBox = mCueBoxes.valueAt(i); | 
|  | if (!cueBox.isActive()) { | 
|  | removeView(cueBox); | 
|  | mCueBoxes.removeAt(i); | 
|  | cueCount--; | 
|  | i--; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Reset active cues and regions. | 
|  | */ | 
|  | private void prepForPrune() { | 
|  | final int regionCount = mRegionBoxes.size(); | 
|  | for (int i = 0; i < regionCount; i++) { | 
|  | final RegionLayout regionBox = mRegionBoxes.valueAt(i); | 
|  | regionBox.prepForPrune(); | 
|  | } | 
|  |  | 
|  | final int cueCount = mCueBoxes.size(); | 
|  | for (int i = 0; i < cueCount; i++) { | 
|  | final CueLayout cueBox = mCueBoxes.valueAt(i); | 
|  | cueBox.prepForPrune(); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | 
|  | super.onMeasure(widthMeasureSpec, heightMeasureSpec); | 
|  |  | 
|  | final int regionCount = mRegionBoxes.size(); | 
|  | for (int i = 0; i < regionCount; i++) { | 
|  | final RegionLayout regionBox = mRegionBoxes.valueAt(i); | 
|  | regionBox.measureForParent(widthMeasureSpec, heightMeasureSpec); | 
|  | } | 
|  |  | 
|  | final int cueCount = mCueBoxes.size(); | 
|  | for (int i = 0; i < cueCount; i++) { | 
|  | final CueLayout cueBox = mCueBoxes.valueAt(i); | 
|  | cueBox.measureForParent(widthMeasureSpec, heightMeasureSpec); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void onLayout(boolean changed, int l, int t, int r, int b) { | 
|  | final int viewportWidth = r - l; | 
|  | final int viewportHeight = b - t; | 
|  |  | 
|  | setCaptionStyle(mCaptionStyle, | 
|  | mManager.getFontScale() * LINE_HEIGHT_RATIO * viewportHeight); | 
|  |  | 
|  | final int regionCount = mRegionBoxes.size(); | 
|  | for (int i = 0; i < regionCount; i++) { | 
|  | final RegionLayout regionBox = mRegionBoxes.valueAt(i); | 
|  | layoutRegion(viewportWidth, viewportHeight, regionBox); | 
|  | } | 
|  |  | 
|  | final int cueCount = mCueBoxes.size(); | 
|  | for (int i = 0; i < cueCount; i++) { | 
|  | final CueLayout cueBox = mCueBoxes.valueAt(i); | 
|  | layoutCue(viewportWidth, viewportHeight, cueBox); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Lays out a region within the viewport. The region handles layout for | 
|  | * contained cues. | 
|  | */ | 
|  | private void layoutRegion( | 
|  | int viewportWidth, int viewportHeight, | 
|  | RegionLayout regionBox) { | 
|  | final TextTrackRegion region = regionBox.getRegion(); | 
|  | final int regionHeight = regionBox.getMeasuredHeight(); | 
|  | final int regionWidth = regionBox.getMeasuredWidth(); | 
|  |  | 
|  | // TODO: Account for region anchor point. | 
|  | final float x = region.mViewportAnchorPointX; | 
|  | final float y = region.mViewportAnchorPointY; | 
|  | final int left = (int) (x * (viewportWidth - regionWidth) / 100); | 
|  | final int top = (int) (y * (viewportHeight - regionHeight) / 100); | 
|  |  | 
|  | regionBox.layout(left, top, left + regionWidth, top + regionHeight); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Lays out a cue within the viewport. | 
|  | */ | 
|  | private void layoutCue( | 
|  | int viewportWidth, int viewportHeight, CueLayout cueBox) { | 
|  | final TextTrackCue cue = cueBox.getCue(); | 
|  | final int direction = getLayoutDirection(); | 
|  | final int absAlignment = resolveCueAlignment(direction, cue.mAlignment); | 
|  | final boolean cueSnapToLines = cue.mSnapToLines; | 
|  |  | 
|  | int size = 100 * cueBox.getMeasuredWidth() / viewportWidth; | 
|  |  | 
|  | // Determine raw x-position. | 
|  | int xPosition; | 
|  | switch (absAlignment) { | 
|  | case TextTrackCue.ALIGNMENT_LEFT: | 
|  | xPosition = cue.mTextPosition; | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_RIGHT: | 
|  | xPosition = cue.mTextPosition - size; | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_MIDDLE: | 
|  | default: | 
|  | xPosition = cue.mTextPosition - size / 2; | 
|  | break; | 
|  | } | 
|  |  | 
|  | // Adjust x-position for layout. | 
|  | if (direction == LAYOUT_DIRECTION_RTL) { | 
|  | xPosition = 100 - xPosition; | 
|  | } | 
|  |  | 
|  | // If the text track cue snap-to-lines flag is set, adjust | 
|  | // x-position and size for padding. This is equivalent to placing the | 
|  | // cue within the title-safe area. | 
|  | if (cueSnapToLines) { | 
|  | final int paddingLeft = 100 * getPaddingLeft() / viewportWidth; | 
|  | final int paddingRight = 100 * getPaddingRight() / viewportWidth; | 
|  | if (xPosition < paddingLeft && xPosition + size > paddingLeft) { | 
|  | xPosition += paddingLeft; | 
|  | size -= paddingLeft; | 
|  | } | 
|  | final float rightEdge = 100 - paddingRight; | 
|  | if (xPosition < rightEdge && xPosition + size > rightEdge) { | 
|  | size -= paddingRight; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Compute absolute left position and width. | 
|  | final int left = xPosition * viewportWidth / 100; | 
|  | final int width = size * viewportWidth / 100; | 
|  |  | 
|  | // Determine initial y-position. | 
|  | final int yPosition = calculateLinePosition(cueBox); | 
|  |  | 
|  | // Compute absolute final top position and height. | 
|  | final int height = cueBox.getMeasuredHeight(); | 
|  | final int top; | 
|  | if (yPosition < 0) { | 
|  | // TODO: This needs to use the actual height of prior boxes. | 
|  | top = viewportHeight + yPosition * height; | 
|  | } else { | 
|  | top = yPosition * (viewportHeight - height) / 100; | 
|  | } | 
|  |  | 
|  | // Layout cue in final position. | 
|  | cueBox.layout(left, top, left + width, top + height); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Calculates the line position for a cue. | 
|  | * <p> | 
|  | * If the resulting position is negative, it represents a bottom-aligned | 
|  | * position relative to the number of active cues. Otherwise, it represents | 
|  | * a percentage [0-100] of the viewport height. | 
|  | */ | 
|  | private int calculateLinePosition(CueLayout cueBox) { | 
|  | final TextTrackCue cue = cueBox.getCue(); | 
|  | final Integer linePosition = cue.mLinePosition; | 
|  | final boolean snapToLines = cue.mSnapToLines; | 
|  | final boolean autoPosition = (linePosition == null); | 
|  |  | 
|  | if (!snapToLines && !autoPosition && (linePosition < 0 || linePosition > 100)) { | 
|  | // Invalid line position defaults to 100. | 
|  | return 100; | 
|  | } else if (!autoPosition) { | 
|  | // Use the valid, supplied line position. | 
|  | return linePosition; | 
|  | } else if (!snapToLines) { | 
|  | // Automatic, non-snapped line position defaults to 100. | 
|  | return 100; | 
|  | } else { | 
|  | // Automatic snapped line position uses active cue order. | 
|  | return -(cueBox.mOrder + 1); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Resolves cue alignment according to the specified layout direction. | 
|  | */ | 
|  | private static int resolveCueAlignment(int layoutDirection, int alignment) { | 
|  | switch (alignment) { | 
|  | case TextTrackCue.ALIGNMENT_START: | 
|  | return layoutDirection == View.LAYOUT_DIRECTION_LTR ? | 
|  | TextTrackCue.ALIGNMENT_LEFT : TextTrackCue.ALIGNMENT_RIGHT; | 
|  | case TextTrackCue.ALIGNMENT_END: | 
|  | return layoutDirection == View.LAYOUT_DIRECTION_LTR ? | 
|  | TextTrackCue.ALIGNMENT_RIGHT : TextTrackCue.ALIGNMENT_LEFT; | 
|  | } | 
|  | return alignment; | 
|  | } | 
|  |  | 
|  | private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() { | 
|  | @Override | 
|  | public void onFontScaleChanged(float fontScale) { | 
|  | final float fontSize = fontScale * getHeight() * LINE_HEIGHT_RATIO; | 
|  | setCaptionStyle(mCaptionStyle, fontSize); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onUserStyleChanged(CaptionStyle userStyle) { | 
|  | setCaptionStyle(userStyle, mFontSize); | 
|  | } | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * A text track region represents a portion of the video viewport and | 
|  | * provides a rendering area for text track cues. | 
|  | */ | 
|  | private static class RegionLayout extends LinearLayout { | 
|  | private final ArrayList<CueLayout> mRegionCueBoxes = new ArrayList<CueLayout>(); | 
|  | private final TextTrackRegion mRegion; | 
|  |  | 
|  | private CaptionStyle mCaptionStyle; | 
|  | private float mFontSize; | 
|  |  | 
|  | public RegionLayout(Context context, TextTrackRegion region, CaptionStyle captionStyle, | 
|  | float fontSize) { | 
|  | super(context); | 
|  |  | 
|  | mRegion = region; | 
|  | mCaptionStyle = captionStyle; | 
|  | mFontSize = fontSize; | 
|  |  | 
|  | // TODO: Add support for vertical text | 
|  | setOrientation(VERTICAL); | 
|  |  | 
|  | if (DEBUG) { | 
|  | setBackgroundColor(DEBUG_REGION_BACKGROUND); | 
|  | } else { | 
|  | setBackgroundColor(captionStyle.windowColor); | 
|  | } | 
|  | } | 
|  |  | 
|  | public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) { | 
|  | mCaptionStyle = captionStyle; | 
|  | mFontSize = fontSize; | 
|  |  | 
|  | final int cueCount = mRegionCueBoxes.size(); | 
|  | for (int i = 0; i < cueCount; i++) { | 
|  | final CueLayout cueBox = mRegionCueBoxes.get(i); | 
|  | cueBox.setCaptionStyle(captionStyle, fontSize); | 
|  | } | 
|  |  | 
|  | setBackgroundColor(captionStyle.windowColor); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Performs the parent's measurement responsibilities, then | 
|  | * automatically performs its own measurement. | 
|  | */ | 
|  | public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) { | 
|  | final TextTrackRegion region = mRegion; | 
|  | final int specWidth = MeasureSpec.getSize(widthMeasureSpec); | 
|  | final int specHeight = MeasureSpec.getSize(heightMeasureSpec); | 
|  | final int width = (int) region.mWidth; | 
|  |  | 
|  | // Determine the absolute maximum region size as the requested size. | 
|  | final int size = width * specWidth / 100; | 
|  |  | 
|  | widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); | 
|  | heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST); | 
|  | measure(widthMeasureSpec, heightMeasureSpec); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Prepares this region for pruning by setting all tracks as inactive. | 
|  | * <p> | 
|  | * Tracks that are added or updated using {@link #put(TextTrackCue)} | 
|  | * after this calling this method will be marked as active. | 
|  | */ | 
|  | public void prepForPrune() { | 
|  | final int cueCount = mRegionCueBoxes.size(); | 
|  | for (int i = 0; i < cueCount; i++) { | 
|  | final CueLayout cueBox = mRegionCueBoxes.get(i); | 
|  | cueBox.prepForPrune(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Adds a {@link TextTrackCue} to this region. If the track had already | 
|  | * been added, updates its active state. | 
|  | * | 
|  | * @param cue | 
|  | */ | 
|  | public void put(TextTrackCue cue) { | 
|  | final int cueCount = mRegionCueBoxes.size(); | 
|  | for (int i = 0; i < cueCount; i++) { | 
|  | final CueLayout cueBox = mRegionCueBoxes.get(i); | 
|  | if (cueBox.getCue() == cue) { | 
|  | cueBox.update(); | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | final CueLayout cueBox = new CueLayout(getContext(), cue, mCaptionStyle, mFontSize); | 
|  | mRegionCueBoxes.add(cueBox); | 
|  | addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); | 
|  |  | 
|  | if (getChildCount() > mRegion.mLines) { | 
|  | removeViewAt(0); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Remove all inactive tracks from this region. | 
|  | * | 
|  | * @return true if this region is empty and should be pruned | 
|  | */ | 
|  | public boolean prune() { | 
|  | int cueCount = mRegionCueBoxes.size(); | 
|  | for (int i = 0; i < cueCount; i++) { | 
|  | final CueLayout cueBox = mRegionCueBoxes.get(i); | 
|  | if (!cueBox.isActive()) { | 
|  | mRegionCueBoxes.remove(i); | 
|  | removeView(cueBox); | 
|  | cueCount--; | 
|  | i--; | 
|  | } | 
|  | } | 
|  |  | 
|  | return mRegionCueBoxes.isEmpty(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return the region data backing this layout | 
|  | */ | 
|  | public TextTrackRegion getRegion() { | 
|  | return mRegion; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * A text track cue is the unit of time-sensitive data in a text track, | 
|  | * corresponding for instance for subtitles and captions to the text that | 
|  | * appears at a particular time and disappears at another time. | 
|  | * <p> | 
|  | * A single cue may contain multiple {@link SpanLayout}s, each representing a | 
|  | * single line of text. | 
|  | */ | 
|  | private static class CueLayout extends LinearLayout { | 
|  | public final TextTrackCue mCue; | 
|  |  | 
|  | private CaptionStyle mCaptionStyle; | 
|  | private float mFontSize; | 
|  |  | 
|  | private boolean mActive; | 
|  | private int mOrder; | 
|  |  | 
|  | public CueLayout( | 
|  | Context context, TextTrackCue cue, CaptionStyle captionStyle, float fontSize) { | 
|  | super(context); | 
|  |  | 
|  | mCue = cue; | 
|  | mCaptionStyle = captionStyle; | 
|  | mFontSize = fontSize; | 
|  |  | 
|  | // TODO: Add support for vertical text. | 
|  | final boolean horizontal = cue.mWritingDirection | 
|  | == TextTrackCue.WRITING_DIRECTION_HORIZONTAL; | 
|  | setOrientation(horizontal ? VERTICAL : HORIZONTAL); | 
|  |  | 
|  | switch (cue.mAlignment) { | 
|  | case TextTrackCue.ALIGNMENT_END: | 
|  | setGravity(Gravity.END); | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_LEFT: | 
|  | setGravity(Gravity.LEFT); | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_MIDDLE: | 
|  | setGravity(horizontal | 
|  | ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL); | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_RIGHT: | 
|  | setGravity(Gravity.RIGHT); | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_START: | 
|  | setGravity(Gravity.START); | 
|  | break; | 
|  | } | 
|  |  | 
|  | if (DEBUG) { | 
|  | setBackgroundColor(DEBUG_CUE_BACKGROUND); | 
|  | } | 
|  |  | 
|  | update(); | 
|  | } | 
|  |  | 
|  | public void setCaptionStyle(CaptionStyle style, float fontSize) { | 
|  | mCaptionStyle = style; | 
|  | mFontSize = fontSize; | 
|  |  | 
|  | final int n = getChildCount(); | 
|  | for (int i = 0; i < n; i++) { | 
|  | final View child = getChildAt(i); | 
|  | if (child instanceof SpanLayout) { | 
|  | ((SpanLayout) child).setCaptionStyle(style, fontSize); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | public void prepForPrune() { | 
|  | mActive = false; | 
|  | } | 
|  |  | 
|  | public void update() { | 
|  | mActive = true; | 
|  |  | 
|  | removeAllViews(); | 
|  |  | 
|  | final int cueAlignment = resolveCueAlignment(getLayoutDirection(), mCue.mAlignment); | 
|  | final Alignment alignment; | 
|  | switch (cueAlignment) { | 
|  | case TextTrackCue.ALIGNMENT_LEFT: | 
|  | alignment = Alignment.ALIGN_LEFT; | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_RIGHT: | 
|  | alignment = Alignment.ALIGN_RIGHT; | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_MIDDLE: | 
|  | default: | 
|  | alignment = Alignment.ALIGN_CENTER; | 
|  | } | 
|  |  | 
|  | final CaptionStyle captionStyle = mCaptionStyle; | 
|  | final float fontSize = mFontSize; | 
|  | final TextTrackCueSpan[][] lines = mCue.mLines; | 
|  | final int lineCount = lines.length; | 
|  | for (int i = 0; i < lineCount; i++) { | 
|  | final SpanLayout lineBox = new SpanLayout(getContext(), lines[i]); | 
|  | lineBox.setAlignment(alignment); | 
|  | lineBox.setCaptionStyle(captionStyle, fontSize); | 
|  |  | 
|  | addView(lineBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | 
|  | super.onMeasure(widthMeasureSpec, heightMeasureSpec); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Performs the parent's measurement responsibilities, then | 
|  | * automatically performs its own measurement. | 
|  | */ | 
|  | public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) { | 
|  | final TextTrackCue cue = mCue; | 
|  | final int specWidth = MeasureSpec.getSize(widthMeasureSpec); | 
|  | final int specHeight = MeasureSpec.getSize(heightMeasureSpec); | 
|  | final int direction = getLayoutDirection(); | 
|  | final int absAlignment = resolveCueAlignment(direction, cue.mAlignment); | 
|  |  | 
|  | // Determine the maximum size of cue based on its starting position | 
|  | // and the direction in which it grows. | 
|  | final int maximumSize; | 
|  | switch (absAlignment) { | 
|  | case TextTrackCue.ALIGNMENT_LEFT: | 
|  | maximumSize = 100 - cue.mTextPosition; | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_RIGHT: | 
|  | maximumSize = cue.mTextPosition; | 
|  | break; | 
|  | case TextTrackCue.ALIGNMENT_MIDDLE: | 
|  | if (cue.mTextPosition <= 50) { | 
|  | maximumSize = cue.mTextPosition * 2; | 
|  | } else { | 
|  | maximumSize = (100 - cue.mTextPosition) * 2; | 
|  | } | 
|  | break; | 
|  | default: | 
|  | maximumSize = 0; | 
|  | } | 
|  |  | 
|  | // Determine absolute maximum cue size as the smaller of the | 
|  | // requested size and the maximum theoretical size. | 
|  | final int size = Math.min(cue.mSize, maximumSize) * specWidth / 100; | 
|  | widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); | 
|  | heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST); | 
|  | measure(widthMeasureSpec, heightMeasureSpec); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Sets the order of this cue in the list of active cues. | 
|  | * | 
|  | * @param order the order of this cue in the list of active cues | 
|  | */ | 
|  | public void setOrder(int order) { | 
|  | mOrder = order; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return whether this cue is marked as active | 
|  | */ | 
|  | public boolean isActive() { | 
|  | return mActive; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return the cue data backing this layout | 
|  | */ | 
|  | public TextTrackCue getCue() { | 
|  | return mCue; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * A text track line represents a single line of text within a cue. | 
|  | * <p> | 
|  | * A single line may contain multiple spans, each representing a section of | 
|  | * text that may be enabled or disabled at a particular time. | 
|  | */ | 
|  | private static class SpanLayout extends SubtitleView { | 
|  | private final SpannableStringBuilder mBuilder = new SpannableStringBuilder(); | 
|  | private final TextTrackCueSpan[] mSpans; | 
|  |  | 
|  | public SpanLayout(Context context, TextTrackCueSpan[] spans) { | 
|  | super(context); | 
|  |  | 
|  | mSpans = spans; | 
|  |  | 
|  | update(); | 
|  | } | 
|  |  | 
|  | public void update() { | 
|  | final SpannableStringBuilder builder = mBuilder; | 
|  | final TextTrackCueSpan[] spans = mSpans; | 
|  |  | 
|  | builder.clear(); | 
|  | builder.clearSpans(); | 
|  |  | 
|  | final int spanCount = spans.length; | 
|  | for (int i = 0; i < spanCount; i++) { | 
|  | final TextTrackCueSpan span = spans[i]; | 
|  | if (span.mEnabled) { | 
|  | builder.append(spans[i].mText); | 
|  | } | 
|  | } | 
|  |  | 
|  | setText(builder); | 
|  | } | 
|  |  | 
|  | public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) { | 
|  | setBackgroundColor(captionStyle.backgroundColor); | 
|  | setForegroundColor(captionStyle.foregroundColor); | 
|  | setEdgeColor(captionStyle.edgeColor); | 
|  | setEdgeType(captionStyle.edgeType); | 
|  | setTypeface(captionStyle.getTypeface()); | 
|  | setTextSize(fontSize); | 
|  | } | 
|  | } | 
|  | } |