Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2013 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.content.Context;
     20 import android.text.Layout.Alignment;
     21 import android.text.SpannableStringBuilder;
     22 import android.util.ArrayMap;
     23 import android.util.AttributeSet;
     24 import android.util.Log;
     25 import android.view.Gravity;
     26 import android.view.View;
     27 import android.view.ViewGroup;
     28 import android.view.accessibility.CaptioningManager;
     29 import android.view.accessibility.CaptioningManager.CaptionStyle;
     30 import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
     31 import android.widget.LinearLayout;
     32 
     33 import com.android.internal.widget.SubtitleView;
     34 
     35 import java.util.ArrayList;
     36 import java.util.Arrays;
     37 import java.util.HashMap;
     38 import java.util.Map;
     39 import java.util.Vector;
     40 
     41 /** @hide */
     42 public class WebVttRenderer extends SubtitleController.Renderer {
     43     private final Context mContext;
     44 
     45     private WebVttRenderingWidget mRenderingWidget;
     46 
     47     public WebVttRenderer(Context context) {
     48         mContext = context;
     49     }
     50 
     51     @Override
     52     public boolean supports(MediaFormat format) {
     53         if (format.containsKey(MediaFormat.KEY_MIME)) {
     54             return format.getString(MediaFormat.KEY_MIME).equals("text/vtt");
     55         }
     56         return false;
     57     }
     58 
     59     @Override
     60     public SubtitleTrack createTrack(MediaFormat format) {
     61         if (mRenderingWidget == null) {
     62             mRenderingWidget = new WebVttRenderingWidget(mContext);
     63         }
     64 
     65         return new WebVttTrack(mRenderingWidget, format);
     66     }
     67 }
     68 
     69 /** @hide */
     70 class TextTrackCueSpan {
     71     long mTimestampMs;
     72     boolean mEnabled;
     73     String mText;
     74     TextTrackCueSpan(String text, long timestamp) {
     75         mTimestampMs = timestamp;
     76         mText = text;
     77         // spans with timestamp will be enabled by Cue.onTime
     78         mEnabled = (mTimestampMs < 0);
     79     }
     80 
     81     @Override
     82     public boolean equals(Object o) {
     83         if (!(o instanceof TextTrackCueSpan)) {
     84             return false;
     85         }
     86         TextTrackCueSpan span = (TextTrackCueSpan) o;
     87         return mTimestampMs == span.mTimestampMs &&
     88                 mText.equals(span.mText);
     89     }
     90 }
     91 
     92 /**
     93  * @hide
     94  *
     95  * Extract all text without style, but with timestamp spans.
     96  */
     97 class UnstyledTextExtractor implements Tokenizer.OnTokenListener {
     98     StringBuilder mLine = new StringBuilder();
     99     Vector<TextTrackCueSpan[]> mLines = new Vector<TextTrackCueSpan[]>();
    100     Vector<TextTrackCueSpan> mCurrentLine = new Vector<TextTrackCueSpan>();
    101     long mLastTimestamp;
    102 
    103     UnstyledTextExtractor() {
    104         init();
    105     }
    106 
    107     private void init() {
    108         mLine.delete(0, mLine.length());
    109         mLines.clear();
    110         mCurrentLine.clear();
    111         mLastTimestamp = -1;
    112     }
    113 
    114     @Override
    115     public void onData(String s) {
    116         mLine.append(s);
    117     }
    118 
    119     @Override
    120     public void onStart(String tag, String[] classes, String annotation) { }
    121 
    122     @Override
    123     public void onEnd(String tag) { }
    124 
    125     @Override
    126     public void onTimeStamp(long timestampMs) {
    127         // finish any prior span
    128         if (mLine.length() > 0 && timestampMs != mLastTimestamp) {
    129             mCurrentLine.add(
    130                     new TextTrackCueSpan(mLine.toString(), mLastTimestamp));
    131             mLine.delete(0, mLine.length());
    132         }
    133         mLastTimestamp = timestampMs;
    134     }
    135 
    136     @Override
    137     public void onLineEnd() {
    138         // finish any pending span
    139         if (mLine.length() > 0) {
    140             mCurrentLine.add(
    141                     new TextTrackCueSpan(mLine.toString(), mLastTimestamp));
    142             mLine.delete(0, mLine.length());
    143         }
    144 
    145         TextTrackCueSpan[] spans = new TextTrackCueSpan[mCurrentLine.size()];
    146         mCurrentLine.toArray(spans);
    147         mCurrentLine.clear();
    148         mLines.add(spans);
    149     }
    150 
    151     public TextTrackCueSpan[][] getText() {
    152         // for politeness, finish last cue-line if it ends abruptly
    153         if (mLine.length() > 0 || mCurrentLine.size() > 0) {
    154             onLineEnd();
    155         }
    156         TextTrackCueSpan[][] lines = new TextTrackCueSpan[mLines.size()][];
    157         mLines.toArray(lines);
    158         init();
    159         return lines;
    160     }
    161 }
    162 
    163 /**
    164  * @hide
    165  *
    166  * Tokenizer tokenizes the WebVTT Cue Text into tags and data
    167  */
    168 class Tokenizer {
    169     private static final String TAG = "Tokenizer";
    170     private TokenizerPhase mPhase;
    171     private TokenizerPhase mDataTokenizer;
    172     private TokenizerPhase mTagTokenizer;
    173 
    174     private OnTokenListener mListener;
    175     private String mLine;
    176     private int mHandledLen;
    177 
    178     interface TokenizerPhase {
    179         TokenizerPhase start();
    180         void tokenize();
    181     }
    182 
    183     class DataTokenizer implements TokenizerPhase {
    184         // includes both WebVTT data && escape state
    185         private StringBuilder mData;
    186 
    187         public TokenizerPhase start() {
    188             mData = new StringBuilder();
    189             return this;
    190         }
    191 
    192         private boolean replaceEscape(String escape, String replacement, int pos) {
    193             if (mLine.startsWith(escape, pos)) {
    194                 mData.append(mLine.substring(mHandledLen, pos));
    195                 mData.append(replacement);
    196                 mHandledLen = pos + escape.length();
    197                 pos = mHandledLen - 1;
    198                 return true;
    199             }
    200             return false;
    201         }
    202 
    203         @Override
    204         public void tokenize() {
    205             int end = mLine.length();
    206             for (int pos = mHandledLen; pos < mLine.length(); pos++) {
    207                 if (mLine.charAt(pos) == '&') {
    208                     if (replaceEscape("&amp;", "&", pos) ||
    209                             replaceEscape("&lt;", "<", pos) ||
    210                             replaceEscape("&gt;", ">", pos) ||
    211                             replaceEscape("&lrm;", "\u200e", pos) ||
    212                             replaceEscape("&rlm;", "\u200f", pos) ||
    213                             replaceEscape("&nbsp;", "\u00a0", pos)) {
    214                         continue;
    215                     }
    216                 } else if (mLine.charAt(pos) == '<') {
    217                     end = pos;
    218                     mPhase = mTagTokenizer.start();
    219                     break;
    220                 }
    221             }
    222             mData.append(mLine.substring(mHandledLen, end));
    223             // yield mData
    224             mListener.onData(mData.toString());
    225             mData.delete(0, mData.length());
    226             mHandledLen = end;
    227         }
    228     }
    229 
    230     class TagTokenizer implements TokenizerPhase {
    231         private boolean mAtAnnotation;
    232         private String mName, mAnnotation;
    233 
    234         public TokenizerPhase start() {
    235             mName = mAnnotation = "";
    236             mAtAnnotation = false;
    237             return this;
    238         }
    239 
    240         @Override
    241         public void tokenize() {
    242             if (!mAtAnnotation)
    243                 mHandledLen++;
    244             if (mHandledLen < mLine.length()) {
    245                 String[] parts;
    246                 /**
    247                  * Collect annotations and end-tags to closing >.  Collect tag
    248                  * name to closing bracket or next white-space.
    249                  */
    250                 if (mAtAnnotation || mLine.charAt(mHandledLen) == '/') {
    251                     parts = mLine.substring(mHandledLen).split(">");
    252                 } else {
    253                     parts = mLine.substring(mHandledLen).split("[\t\f >]");
    254                 }
    255                 String part = mLine.substring(
    256                             mHandledLen, mHandledLen + parts[0].length());
    257                 mHandledLen += parts[0].length();
    258 
    259                 if (mAtAnnotation) {
    260                     mAnnotation += " " + part;
    261                 } else {
    262                     mName = part;
    263                 }
    264             }
    265 
    266             mAtAnnotation = true;
    267 
    268             if (mHandledLen < mLine.length() && mLine.charAt(mHandledLen) == '>') {
    269                 yield_tag();
    270                 mPhase = mDataTokenizer.start();
    271                 mHandledLen++;
    272             }
    273         }
    274 
    275         private void yield_tag() {
    276             if (mName.startsWith("/")) {
    277                 mListener.onEnd(mName.substring(1));
    278             } else if (mName.length() > 0 && Character.isDigit(mName.charAt(0))) {
    279                 // timestamp
    280                 try {
    281                     long timestampMs = WebVttParser.parseTimestampMs(mName);
    282                     mListener.onTimeStamp(timestampMs);
    283                 } catch (NumberFormatException e) {
    284                     Log.d(TAG, "invalid timestamp tag: <" + mName + ">");
    285                 }
    286             } else {
    287                 mAnnotation = mAnnotation.replaceAll("\\s+", " ");
    288                 if (mAnnotation.startsWith(" ")) {
    289                     mAnnotation = mAnnotation.substring(1);
    290                 }
    291                 if (mAnnotation.endsWith(" ")) {
    292                     mAnnotation = mAnnotation.substring(0, mAnnotation.length() - 1);
    293                 }
    294 
    295                 String[] classes = null;
    296                 int dotAt = mName.indexOf('.');
    297                 if (dotAt >= 0) {
    298                     classes = mName.substring(dotAt + 1).split("\\.");
    299                     mName = mName.substring(0, dotAt);
    300                 }
    301                 mListener.onStart(mName, classes, mAnnotation);
    302             }
    303         }
    304     }
    305 
    306     Tokenizer(OnTokenListener listener) {
    307         mDataTokenizer = new DataTokenizer();
    308         mTagTokenizer = new TagTokenizer();
    309         reset();
    310         mListener = listener;
    311     }
    312 
    313     void reset() {
    314         mPhase = mDataTokenizer.start();
    315     }
    316 
    317     void tokenize(String s) {
    318         mHandledLen = 0;
    319         mLine = s;
    320         while (mHandledLen < mLine.length()) {
    321             mPhase.tokenize();
    322         }
    323         /* we are finished with a line unless we are in the middle of a tag */
    324         if (!(mPhase instanceof TagTokenizer)) {
    325             // yield END-OF-LINE
    326             mListener.onLineEnd();
    327         }
    328     }
    329 
    330     interface OnTokenListener {
    331         void onData(String s);
    332         void onStart(String tag, String[] classes, String annotation);
    333         void onEnd(String tag);
    334         void onTimeStamp(long timestampMs);
    335         void onLineEnd();
    336     }
    337 }
    338 
    339 /** @hide */
    340 class TextTrackRegion {
    341     final static int SCROLL_VALUE_NONE      = 300;
    342     final static int SCROLL_VALUE_SCROLL_UP = 301;
    343 
    344     String mId;
    345     float mWidth;
    346     int mLines;
    347     float mAnchorPointX, mAnchorPointY;
    348     float mViewportAnchorPointX, mViewportAnchorPointY;
    349     int mScrollValue;
    350 
    351     TextTrackRegion() {
    352         mId = "";
    353         mWidth = 100;
    354         mLines = 3;
    355         mAnchorPointX = mViewportAnchorPointX = 0.f;
    356         mAnchorPointY = mViewportAnchorPointY = 100.f;
    357         mScrollValue = SCROLL_VALUE_NONE;
    358     }
    359 
    360     public String toString() {
    361         StringBuilder res = new StringBuilder(" {id:\"").append(mId)
    362             .append("\", width:").append(mWidth)
    363             .append(", lines:").append(mLines)
    364             .append(", anchorPoint:(").append(mAnchorPointX)
    365             .append(", ").append(mAnchorPointY)
    366             .append("), viewportAnchorPoints:").append(mViewportAnchorPointX)
    367             .append(", ").append(mViewportAnchorPointY)
    368             .append("), scrollValue:")
    369             .append(mScrollValue == SCROLL_VALUE_NONE ? "none" :
    370                     mScrollValue == SCROLL_VALUE_SCROLL_UP ? "scroll_up" :
    371                     "INVALID")
    372             .append("}");
    373         return res.toString();
    374     }
    375 }
    376 
    377 /** @hide */
    378 class TextTrackCue extends SubtitleTrack.Cue {
    379     final static int WRITING_DIRECTION_HORIZONTAL  = 100;
    380     final static int WRITING_DIRECTION_VERTICAL_RL = 101;
    381     final static int WRITING_DIRECTION_VERTICAL_LR = 102;
    382 
    383     final static int ALIGNMENT_MIDDLE = 200;
    384     final static int ALIGNMENT_START  = 201;
    385     final static int ALIGNMENT_END    = 202;
    386     final static int ALIGNMENT_LEFT   = 203;
    387     final static int ALIGNMENT_RIGHT  = 204;
    388     private static final String TAG = "TTCue";
    389 
    390     String  mId;
    391     boolean mPauseOnExit;
    392     int     mWritingDirection;
    393     String  mRegionId;
    394     boolean mSnapToLines;
    395     Integer mLinePosition;  // null means AUTO
    396     boolean mAutoLinePosition;
    397     int     mTextPosition;
    398     int     mSize;
    399     int     mAlignment;
    400     // Vector<String> mText;
    401     String[] mStrings;
    402     TextTrackCueSpan[][] mLines;
    403     TextTrackRegion mRegion;
    404 
    405     TextTrackCue() {
    406         mId = "";
    407         mPauseOnExit = false;
    408         mWritingDirection = WRITING_DIRECTION_HORIZONTAL;
    409         mRegionId = "";
    410         mSnapToLines = true;
    411         mLinePosition = null /* AUTO */;
    412         mTextPosition = 50;
    413         mSize = 100;
    414         mAlignment = ALIGNMENT_MIDDLE;
    415         mLines = null;
    416         mRegion = null;
    417     }
    418 
    419     @Override
    420     public boolean equals(Object o) {
    421         if (!(o instanceof TextTrackCue)) {
    422             return false;
    423         }
    424         if (this == o) {
    425             return true;
    426         }
    427 
    428         try {
    429             TextTrackCue cue = (TextTrackCue) o;
    430             boolean res = mId.equals(cue.mId) &&
    431                     mPauseOnExit == cue.mPauseOnExit &&
    432                     mWritingDirection == cue.mWritingDirection &&
    433                     mRegionId.equals(cue.mRegionId) &&
    434                     mSnapToLines == cue.mSnapToLines &&
    435                     mAutoLinePosition == cue.mAutoLinePosition &&
    436                     (mAutoLinePosition ||
    437                             ((mLinePosition != null && mLinePosition.equals(cue.mLinePosition)) ||
    438                              (mLinePosition == null && cue.mLinePosition == null))) &&
    439                     mTextPosition == cue.mTextPosition &&
    440                     mSize == cue.mSize &&
    441                     mAlignment == cue.mAlignment &&
    442                     mLines.length == cue.mLines.length;
    443             if (res == true) {
    444                 for (int line = 0; line < mLines.length; line++) {
    445                     if (!Arrays.equals(mLines[line], cue.mLines[line])) {
    446                         return false;
    447                     }
    448                 }
    449             }
    450             return res;
    451         } catch(IncompatibleClassChangeError e) {
    452             return false;
    453         }
    454     }
    455 
    456     public StringBuilder appendStringsToBuilder(StringBuilder builder) {
    457         if (mStrings == null) {
    458             builder.append("null");
    459         } else {
    460             builder.append("[");
    461             boolean first = true;
    462             for (String s: mStrings) {
    463                 if (!first) {
    464                     builder.append(", ");
    465                 }
    466                 if (s == null) {
    467                     builder.append("null");
    468                 } else {
    469                     builder.append("\"");
    470                     builder.append(s);
    471                     builder.append("\"");
    472                 }
    473                 first = false;
    474             }
    475             builder.append("]");
    476         }
    477         return builder;
    478     }
    479 
    480     public StringBuilder appendLinesToBuilder(StringBuilder builder) {
    481         if (mLines == null) {
    482             builder.append("null");
    483         } else {
    484             builder.append("[");
    485             boolean first = true;
    486             for (TextTrackCueSpan[] spans: mLines) {
    487                 if (!first) {
    488                     builder.append(", ");
    489                 }
    490                 if (spans == null) {
    491                     builder.append("null");
    492                 } else {
    493                     builder.append("\"");
    494                     boolean innerFirst = true;
    495                     long lastTimestamp = -1;
    496                     for (TextTrackCueSpan span: spans) {
    497                         if (!innerFirst) {
    498                             builder.append(" ");
    499                         }
    500                         if (span.mTimestampMs != lastTimestamp) {
    501                             builder.append("<")
    502                                     .append(WebVttParser.timeToString(
    503                                             span.mTimestampMs))
    504                                     .append(">");
    505                             lastTimestamp = span.mTimestampMs;
    506                         }
    507                         builder.append(span.mText);
    508                         innerFirst = false;
    509                     }
    510                     builder.append("\"");
    511                 }
    512                 first = false;
    513             }
    514             builder.append("]");
    515         }
    516         return builder;
    517     }
    518 
    519     public String toString() {
    520         StringBuilder res = new StringBuilder();
    521 
    522         res.append(WebVttParser.timeToString(mStartTimeMs))
    523                 .append(" --> ").append(WebVttParser.timeToString(mEndTimeMs))
    524                 .append(" {id:\"").append(mId)
    525                 .append("\", pauseOnExit:").append(mPauseOnExit)
    526                 .append(", direction:")
    527                 .append(mWritingDirection == WRITING_DIRECTION_HORIZONTAL ? "horizontal" :
    528                         mWritingDirection == WRITING_DIRECTION_VERTICAL_LR ? "vertical_lr" :
    529                         mWritingDirection == WRITING_DIRECTION_VERTICAL_RL ? "vertical_rl" :
    530                         "INVALID")
    531                 .append(", regionId:\"").append(mRegionId)
    532                 .append("\", snapToLines:").append(mSnapToLines)
    533                 .append(", linePosition:").append(mAutoLinePosition ? "auto" :
    534                                                   mLinePosition)
    535                 .append(", textPosition:").append(mTextPosition)
    536                 .append(", size:").append(mSize)
    537                 .append(", alignment:")
    538                 .append(mAlignment == ALIGNMENT_END ? "end" :
    539                         mAlignment == ALIGNMENT_LEFT ? "left" :
    540                         mAlignment == ALIGNMENT_MIDDLE ? "middle" :
    541                         mAlignment == ALIGNMENT_RIGHT ? "right" :
    542                         mAlignment == ALIGNMENT_START ? "start" : "INVALID")
    543                 .append(", text:");
    544         appendStringsToBuilder(res).append("}");
    545         return res.toString();
    546     }
    547 
    548     @Override
    549     public int hashCode() {
    550         return toString().hashCode();
    551     }
    552 
    553     @Override
    554     public void onTime(long timeMs) {
    555         for (TextTrackCueSpan[] line: mLines) {
    556             for (TextTrackCueSpan span: line) {
    557                 span.mEnabled = timeMs >= span.mTimestampMs;
    558             }
    559         }
    560     }
    561 }
    562 
    563 /**
    564  *  Supporting July 10 2013 draft version
    565  *
    566  *  @hide
    567  */
    568 class WebVttParser {
    569     private static final String TAG = "WebVttParser";
    570     private Phase mPhase;
    571     private TextTrackCue mCue;
    572     private Vector<String> mCueTexts;
    573     private WebVttCueListener mListener;
    574     private String mBuffer;
    575 
    576     WebVttParser(WebVttCueListener listener) {
    577         mPhase = mParseStart;
    578         mBuffer = "";   /* mBuffer contains up to 1 incomplete line */
    579         mListener = listener;
    580         mCueTexts = new Vector<String>();
    581     }
    582 
    583     /* parsePercentageString */
    584     public static float parseFloatPercentage(String s)
    585             throws NumberFormatException {
    586         if (!s.endsWith("%")) {
    587             throw new NumberFormatException("does not end in %");
    588         }
    589         s = s.substring(0, s.length() - 1);
    590         // parseFloat allows an exponent or a sign
    591         if (s.matches(".*[^0-9.].*")) {
    592             throw new NumberFormatException("contains an invalid character");
    593         }
    594 
    595         try {
    596             float value = Float.parseFloat(s);
    597             if (value < 0.0f || value > 100.0f) {
    598                 throw new NumberFormatException("is out of range");
    599             }
    600             return value;
    601         } catch (NumberFormatException e) {
    602             throw new NumberFormatException("is not a number");
    603         }
    604     }
    605 
    606     public static int parseIntPercentage(String s) throws NumberFormatException {
    607         if (!s.endsWith("%")) {
    608             throw new NumberFormatException("does not end in %");
    609         }
    610         s = s.substring(0, s.length() - 1);
    611         // parseInt allows "-0" that returns 0, so check for non-digits
    612         if (s.matches(".*[^0-9].*")) {
    613             throw new NumberFormatException("contains an invalid character");
    614         }
    615 
    616         try {
    617             int value = Integer.parseInt(s);
    618             if (value < 0 || value > 100) {
    619                 throw new NumberFormatException("is out of range");
    620             }
    621             return value;
    622         } catch (NumberFormatException e) {
    623             throw new NumberFormatException("is not a number");
    624         }
    625     }
    626 
    627     public static long parseTimestampMs(String s) throws NumberFormatException {
    628         if (!s.matches("(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}")) {
    629             throw new NumberFormatException("has invalid format");
    630         }
    631 
    632         String[] parts = s.split("\\.", 2);
    633         long value = 0;
    634         for (String group: parts[0].split(":")) {
    635             value = value * 60 + Long.parseLong(group);
    636         }
    637         return value * 1000 + Long.parseLong(parts[1]);
    638     }
    639 
    640     public static String timeToString(long timeMs) {
    641         return String.format("%d:%02d:%02d.%03d",
    642                 timeMs / 3600000, (timeMs / 60000) % 60,
    643                 (timeMs / 1000) % 60, timeMs % 1000);
    644     }
    645 
    646     public void parse(String s) {
    647         boolean trailingCR = false;
    648         mBuffer = (mBuffer + s.replace("\0", "\ufffd")).replace("\r\n", "\n");
    649 
    650         /* keep trailing '\r' in case matching '\n' arrives in next packet */
    651         if (mBuffer.endsWith("\r")) {
    652             trailingCR = true;
    653             mBuffer = mBuffer.substring(0, mBuffer.length() - 1);
    654         }
    655 
    656         String[] lines = mBuffer.split("[\r\n]");
    657         for (int i = 0; i < lines.length - 1; i++) {
    658             mPhase.parse(lines[i]);
    659         }
    660 
    661         mBuffer = lines[lines.length - 1];
    662         if (trailingCR)
    663             mBuffer += "\r";
    664     }
    665 
    666     public void eos() {
    667         if (mBuffer.endsWith("\r")) {
    668             mBuffer = mBuffer.substring(0, mBuffer.length() - 1);
    669         }
    670 
    671         mPhase.parse(mBuffer);
    672         mBuffer = "";
    673 
    674         yieldCue();
    675         mPhase = mParseStart;
    676     }
    677 
    678     public void yieldCue() {
    679         if (mCue != null && mCueTexts.size() > 0) {
    680             mCue.mStrings = new String[mCueTexts.size()];
    681             mCueTexts.toArray(mCue.mStrings);
    682             mCueTexts.clear();
    683             mListener.onCueParsed(mCue);
    684         }
    685         mCue = null;
    686     }
    687 
    688     interface Phase {
    689         void parse(String line);
    690     }
    691 
    692     final private Phase mSkipRest = new Phase() {
    693         @Override
    694         public void parse(String line) { }
    695     };
    696 
    697     final private Phase mParseStart = new Phase() { // 5-9
    698         @Override
    699         public void parse(String line) {
    700             if (line.startsWith("\ufeff")) {
    701                 line = line.substring(1);
    702             }
    703             if (!line.equals("WEBVTT") &&
    704                     !line.startsWith("WEBVTT ") &&
    705                     !line.startsWith("WEBVTT\t")) {
    706                 log_warning("Not a WEBVTT header", line);
    707                 mPhase = mSkipRest;
    708             } else {
    709                 mPhase = mParseHeader;
    710             }
    711         }
    712     };
    713 
    714     final private Phase mParseHeader = new Phase() { // 10-13
    715         TextTrackRegion parseRegion(String s) {
    716             TextTrackRegion region = new TextTrackRegion();
    717             for (String setting: s.split(" +")) {
    718                 int equalAt = setting.indexOf('=');
    719                 if (equalAt <= 0 || equalAt == setting.length() - 1) {
    720                     continue;
    721                 }
    722 
    723                 String name = setting.substring(0, equalAt);
    724                 String value = setting.substring(equalAt + 1);
    725                 if (name.equals("id")) {
    726                     region.mId = value;
    727                 } else if (name.equals("width")) {
    728                     try {
    729                         region.mWidth = parseFloatPercentage(value);
    730                     } catch (NumberFormatException e) {
    731                         log_warning("region setting", name,
    732                                 "has invalid value", e.getMessage(), value);
    733                     }
    734                 } else if (name.equals("lines")) {
    735                     if (value.matches(".*[^0-9].*")) {
    736                         log_warning("lines", name, "contains an invalid character", value);
    737                     } else {
    738                         try {
    739                             region.mLines = Integer.parseInt(value);
    740                             assert(region.mLines >= 0); // lines contains only digits
    741                         } catch (NumberFormatException e) {
    742                             log_warning("region setting", name, "is not numeric", value);
    743                         }
    744                     }
    745                 } else if (name.equals("regionanchor") ||
    746                            name.equals("viewportanchor")) {
    747                     int commaAt = value.indexOf(",");
    748                     if (commaAt < 0) {
    749                         log_warning("region setting", name, "contains no comma", value);
    750                         continue;
    751                     }
    752 
    753                     String anchorX = value.substring(0, commaAt);
    754                     String anchorY = value.substring(commaAt + 1);
    755                     float x, y;
    756 
    757                     try {
    758                         x = parseFloatPercentage(anchorX);
    759                     } catch (NumberFormatException e) {
    760                         log_warning("region setting", name,
    761                                 "has invalid x component", e.getMessage(), anchorX);
    762                         continue;
    763                     }
    764                     try {
    765                         y = parseFloatPercentage(anchorY);
    766                     } catch (NumberFormatException e) {
    767                         log_warning("region setting", name,
    768                                 "has invalid y component", e.getMessage(), anchorY);
    769                         continue;
    770                     }
    771 
    772                     if (name.charAt(0) == 'r') {
    773                         region.mAnchorPointX = x;
    774                         region.mAnchorPointY = y;
    775                     } else {
    776                         region.mViewportAnchorPointX = x;
    777                         region.mViewportAnchorPointY = y;
    778                     }
    779                 } else if (name.equals("scroll")) {
    780                     if (value.equals("up")) {
    781                         region.mScrollValue =
    782                             TextTrackRegion.SCROLL_VALUE_SCROLL_UP;
    783                     } else {
    784                         log_warning("region setting", name, "has invalid value", value);
    785                     }
    786                 }
    787             }
    788             return region;
    789         }
    790 
    791         @Override
    792         public void parse(String line)  {
    793             if (line.length() == 0) {
    794                 mPhase = mParseCueId;
    795             } else if (line.contains("-->")) {
    796                 mPhase = mParseCueTime;
    797                 mPhase.parse(line);
    798             } else {
    799                 int colonAt = line.indexOf(':');
    800                 if (colonAt <= 0 || colonAt >= line.length() - 1) {
    801                     log_warning("meta data header has invalid format", line);
    802                 }
    803                 String name = line.substring(0, colonAt);
    804                 String value = line.substring(colonAt + 1);
    805 
    806                 if (name.equals("Region")) {
    807                     TextTrackRegion region = parseRegion(value);
    808                     mListener.onRegionParsed(region);
    809                 }
    810             }
    811         }
    812     };
    813 
    814     final private Phase mParseCueId = new Phase() {
    815         @Override
    816         public void parse(String line) {
    817             if (line.length() == 0) {
    818                 return;
    819             }
    820 
    821             assert(mCue == null);
    822 
    823             if (line.equals("NOTE") || line.startsWith("NOTE ")) {
    824                 mPhase = mParseCueText;
    825             }
    826 
    827             mCue = new TextTrackCue();
    828             mCueTexts.clear();
    829 
    830             mPhase = mParseCueTime;
    831             if (line.contains("-->")) {
    832                 mPhase.parse(line);
    833             } else {
    834                 mCue.mId = line;
    835             }
    836         }
    837     };
    838 
    839     final private Phase mParseCueTime = new Phase() {
    840         @Override
    841         public void parse(String line) {
    842             int arrowAt = line.indexOf("-->");
    843             if (arrowAt < 0) {
    844                 mCue = null;
    845                 mPhase = mParseCueId;
    846                 return;
    847             }
    848 
    849             String start = line.substring(0, arrowAt).trim();
    850             // convert only initial and first other white-space to space
    851             String rest = line.substring(arrowAt + 3)
    852                     .replaceFirst("^\\s+", "").replaceFirst("\\s+", " ");
    853             int spaceAt = rest.indexOf(' ');
    854             String end = spaceAt > 0 ? rest.substring(0, spaceAt) : rest;
    855             rest = spaceAt > 0 ? rest.substring(spaceAt + 1) : "";
    856 
    857             mCue.mStartTimeMs = parseTimestampMs(start);
    858             mCue.mEndTimeMs = parseTimestampMs(end);
    859             for (String setting: rest.split(" +")) {
    860                 int colonAt = setting.indexOf(':');
    861                 if (colonAt <= 0 || colonAt == setting.length() - 1) {
    862                     continue;
    863                 }
    864                 String name = setting.substring(0, colonAt);
    865                 String value = setting.substring(colonAt + 1);
    866 
    867                 if (name.equals("region")) {
    868                     mCue.mRegionId = value;
    869                 } else if (name.equals("vertical")) {
    870                     if (value.equals("rl")) {
    871                         mCue.mWritingDirection =
    872                             TextTrackCue.WRITING_DIRECTION_VERTICAL_RL;
    873                     } else if (value.equals("lr")) {
    874                         mCue.mWritingDirection =
    875                             TextTrackCue.WRITING_DIRECTION_VERTICAL_LR;
    876                     } else {
    877                         log_warning("cue setting", name, "has invalid value", value);
    878                     }
    879                 } else if (name.equals("line")) {
    880                     try {
    881                         /* TRICKY: we know that there are no spaces in value */
    882                         assert(value.indexOf(' ') < 0);
    883                         if (value.endsWith("%")) {
    884                             mCue.mSnapToLines = false;
    885                             mCue.mLinePosition = parseIntPercentage(value);
    886                         } else if (value.matches(".*[^0-9].*")) {
    887                             log_warning("cue setting", name,
    888                                     "contains an invalid character", value);
    889                         } else {
    890                             mCue.mSnapToLines = true;
    891                             mCue.mLinePosition = Integer.parseInt(value);
    892                         }
    893                     } catch (NumberFormatException e) {
    894                         log_warning("cue setting", name,
    895                                 "is not numeric or percentage", value);
    896                     }
    897                     // TODO: add support for optional alignment value [,start|middle|end]
    898                 } else if (name.equals("position")) {
    899                     try {
    900                         mCue.mTextPosition = parseIntPercentage(value);
    901                     } catch (NumberFormatException e) {
    902                         log_warning("cue setting", name,
    903                                "is not numeric or percentage", value);
    904                     }
    905                 } else if (name.equals("size")) {
    906                     try {
    907                         mCue.mSize = parseIntPercentage(value);
    908                     } catch (NumberFormatException e) {
    909                         log_warning("cue setting", name,
    910                                "is not numeric or percentage", value);
    911                     }
    912                 } else if (name.equals("align")) {
    913                     if (value.equals("start")) {
    914                         mCue.mAlignment = TextTrackCue.ALIGNMENT_START;
    915                     } else if (value.equals("middle")) {
    916                         mCue.mAlignment = TextTrackCue.ALIGNMENT_MIDDLE;
    917                     } else if (value.equals("end")) {
    918                         mCue.mAlignment = TextTrackCue.ALIGNMENT_END;
    919                     } else if (value.equals("left")) {
    920                         mCue.mAlignment = TextTrackCue.ALIGNMENT_LEFT;
    921                     } else if (value.equals("right")) {
    922                         mCue.mAlignment = TextTrackCue.ALIGNMENT_RIGHT;
    923                     } else {
    924                         log_warning("cue setting", name, "has invalid value", value);
    925                         continue;
    926                     }
    927                 }
    928             }
    929 
    930             if (mCue.mLinePosition != null ||
    931                     mCue.mSize != 100 ||
    932                     (mCue.mWritingDirection !=
    933                         TextTrackCue.WRITING_DIRECTION_HORIZONTAL)) {
    934                 mCue.mRegionId = "";
    935             }
    936 
    937             mPhase = mParseCueText;
    938         }
    939     };
    940 
    941     /* also used for notes */
    942     final private Phase mParseCueText = new Phase() {
    943         @Override
    944         public void parse(String line) {
    945             if (line.length() == 0) {
    946                 yieldCue();
    947                 mPhase = mParseCueId;
    948                 return;
    949             } else if (mCue != null) {
    950                 mCueTexts.add(line);
    951             }
    952         }
    953     };
    954 
    955     private void log_warning(
    956             String nameType, String name, String message,
    957             String subMessage, String value) {
    958         Log.w(this.getClass().getName(), nameType + " '" + name + "' " +
    959                 message + " ('" + value + "' " + subMessage + ")");
    960     }
    961 
    962     private void log_warning(
    963             String nameType, String name, String message, String value) {
    964         Log.w(this.getClass().getName(), nameType + " '" + name + "' " +
    965                 message + " ('" + value + "')");
    966     }
    967 
    968     private void log_warning(String message, String value) {
    969         Log.w(this.getClass().getName(), message + " ('" + value + "')");
    970     }
    971 }
    972 
    973 /** @hide */
    974 interface WebVttCueListener {
    975     void onCueParsed(TextTrackCue cue);
    976     void onRegionParsed(TextTrackRegion region);
    977 }
    978 
    979 /** @hide */
    980 class WebVttTrack extends SubtitleTrack implements WebVttCueListener {
    981     private static final String TAG = "WebVttTrack";
    982 
    983     private final WebVttParser mParser = new WebVttParser(this);
    984     private final UnstyledTextExtractor mExtractor =
    985         new UnstyledTextExtractor();
    986     private final Tokenizer mTokenizer = new Tokenizer(mExtractor);
    987     private final Vector<Long> mTimestamps = new Vector<Long>();
    988     private final WebVttRenderingWidget mRenderingWidget;
    989 
    990     private final Map<String, TextTrackRegion> mRegions =
    991         new HashMap<String, TextTrackRegion>();
    992     private Long mCurrentRunID;
    993 
    994     WebVttTrack(WebVttRenderingWidget renderingWidget, MediaFormat format) {
    995         super(format);
    996 
    997         mRenderingWidget = renderingWidget;
    998     }
    999 
   1000     @Override
   1001     public WebVttRenderingWidget getRenderingWidget() {
   1002         return mRenderingWidget;
   1003     }
   1004 
   1005     @Override
   1006     public void onData(byte[] data, boolean eos, long runID) {
   1007         try {
   1008             String str = new String(data, "UTF-8");
   1009 
   1010             // implement intermixing restriction for WebVTT only for now
   1011             synchronized(mParser) {
   1012                 if (mCurrentRunID != null && runID != mCurrentRunID) {
   1013                     throw new IllegalStateException(
   1014                             "Run #" + mCurrentRunID +
   1015                             " in progress.  Cannot process run #" + runID);
   1016                 }
   1017                 mCurrentRunID = runID;
   1018                 mParser.parse(str);
   1019                 if (eos) {
   1020                     finishedRun(runID);
   1021                     mParser.eos();
   1022                     mRegions.clear();
   1023                     mCurrentRunID = null;
   1024                 }
   1025             }
   1026         } catch (java.io.UnsupportedEncodingException e) {
   1027             Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e);
   1028         }
   1029     }
   1030 
   1031     @Override
   1032     public void onCueParsed(TextTrackCue cue) {
   1033         synchronized (mParser) {
   1034             // resolve region
   1035             if (cue.mRegionId.length() != 0) {
   1036                 cue.mRegion = mRegions.get(cue.mRegionId);
   1037             }
   1038 
   1039             if (DEBUG) Log.v(TAG, "adding cue " + cue);
   1040 
   1041             // tokenize text track string-lines into lines of spans
   1042             mTokenizer.reset();
   1043             for (String s: cue.mStrings) {
   1044                 mTokenizer.tokenize(s);
   1045             }
   1046             cue.mLines = mExtractor.getText();
   1047             if (DEBUG) Log.v(TAG, cue.appendLinesToBuilder(
   1048                     cue.appendStringsToBuilder(
   1049                         new StringBuilder()).append(" simplified to: "))
   1050                             .toString());
   1051 
   1052             // extract inner timestamps
   1053             for (TextTrackCueSpan[] line: cue.mLines) {
   1054                 for (TextTrackCueSpan span: line) {
   1055                     if (span.mTimestampMs > cue.mStartTimeMs &&
   1056                             span.mTimestampMs < cue.mEndTimeMs &&
   1057                             !mTimestamps.contains(span.mTimestampMs)) {
   1058                         mTimestamps.add(span.mTimestampMs);
   1059                     }
   1060                 }
   1061             }
   1062 
   1063             if (mTimestamps.size() > 0) {
   1064                 cue.mInnerTimesMs = new long[mTimestamps.size()];
   1065                 for (int ix=0; ix < mTimestamps.size(); ++ix) {
   1066                     cue.mInnerTimesMs[ix] = mTimestamps.get(ix);
   1067                 }
   1068                 mTimestamps.clear();
   1069             } else {
   1070                 cue.mInnerTimesMs = null;
   1071             }
   1072 
   1073             cue.mRunID = mCurrentRunID;
   1074         }
   1075 
   1076         addCue(cue);
   1077     }
   1078 
   1079     @Override
   1080     public void onRegionParsed(TextTrackRegion region) {
   1081         synchronized(mParser) {
   1082             mRegions.put(region.mId, region);
   1083         }
   1084     }
   1085 
   1086     @Override
   1087     public void updateView(Vector<SubtitleTrack.Cue> activeCues) {
   1088         if (!mVisible) {
   1089             // don't keep the state if we are not visible
   1090             return;
   1091         }
   1092 
   1093         if (DEBUG && mTimeProvider != null) {
   1094             try {
   1095                 Log.d(TAG, "at " +
   1096                         (mTimeProvider.getCurrentTimeUs(false, true) / 1000) +
   1097                         " ms the active cues are:");
   1098             } catch (IllegalStateException e) {
   1099                 Log.d(TAG, "at (illegal state) the active cues are:");
   1100             }
   1101         }
   1102 
   1103         if (mRenderingWidget != null) {
   1104             mRenderingWidget.setActiveCues(activeCues);
   1105         }
   1106     }
   1107 }
   1108 
   1109 /**
   1110  * Widget capable of rendering WebVTT captions.
   1111  *
   1112  * @hide
   1113  */
   1114 class WebVttRenderingWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
   1115     private static final boolean DEBUG = false;
   1116 
   1117     private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT;
   1118 
   1119     private static final int DEBUG_REGION_BACKGROUND = 0x800000FF;
   1120     private static final int DEBUG_CUE_BACKGROUND = 0x80FF0000;
   1121 
   1122     /** WebVtt specifies line height as 5.3% of the viewport height. */
   1123     private static final float LINE_HEIGHT_RATIO = 0.0533f;
   1124 
   1125     /** Map of active regions, used to determine enter/exit. */
   1126     private final ArrayMap<TextTrackRegion, RegionLayout> mRegionBoxes =
   1127             new ArrayMap<TextTrackRegion, RegionLayout>();
   1128 
   1129     /** Map of active cues, used to determine enter/exit. */
   1130     private final ArrayMap<TextTrackCue, CueLayout> mCueBoxes =
   1131             new ArrayMap<TextTrackCue, CueLayout>();
   1132 
   1133     /** Captioning manager, used to obtain and track caption properties. */
   1134     private final CaptioningManager mManager;
   1135 
   1136     /** Callback for rendering changes. */
   1137     private OnChangedListener mListener;
   1138 
   1139     /** Current caption style. */
   1140     private CaptionStyle mCaptionStyle;
   1141 
   1142     /** Current font size, computed from font scaling factor and height. */
   1143     private float mFontSize;
   1144 
   1145     /** Whether a caption style change listener is registered. */
   1146     private boolean mHasChangeListener;
   1147 
   1148     public WebVttRenderingWidget(Context context) {
   1149         this(context, null);
   1150     }
   1151 
   1152     public WebVttRenderingWidget(Context context, AttributeSet attrs) {
   1153         this(context, attrs, 0);
   1154     }
   1155 
   1156     public WebVttRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) {
   1157         this(context, attrs, defStyleAttr, 0);
   1158     }
   1159 
   1160     public WebVttRenderingWidget(
   1161             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
   1162         super(context, attrs, defStyleAttr, defStyleRes);
   1163 
   1164         // Cannot render text over video when layer type is hardware.
   1165         setLayerType(View.LAYER_TYPE_SOFTWARE, null);
   1166 
   1167         mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
   1168         mCaptionStyle = mManager.getUserStyle();
   1169         mFontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
   1170     }
   1171 
   1172     @Override
   1173     public void setSize(int width, int height) {
   1174         final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
   1175         final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
   1176 
   1177         measure(widthSpec, heightSpec);
   1178         layout(0, 0, width, height);
   1179     }
   1180 
   1181     @Override
   1182     public void onAttachedToWindow() {
   1183         super.onAttachedToWindow();
   1184 
   1185         manageChangeListener();
   1186     }
   1187 
   1188     @Override
   1189     public void onDetachedFromWindow() {
   1190         super.onDetachedFromWindow();
   1191 
   1192         manageChangeListener();
   1193     }
   1194 
   1195     @Override
   1196     public void setOnChangedListener(OnChangedListener listener) {
   1197         mListener = listener;
   1198     }
   1199 
   1200     @Override
   1201     public void setVisible(boolean visible) {
   1202         if (visible) {
   1203             setVisibility(View.VISIBLE);
   1204         } else {
   1205             setVisibility(View.GONE);
   1206         }
   1207 
   1208         manageChangeListener();
   1209     }
   1210 
   1211     /**
   1212      * Manages whether this renderer is listening for caption style changes.
   1213      */
   1214     private void manageChangeListener() {
   1215         final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
   1216         if (mHasChangeListener != needsListener) {
   1217             mHasChangeListener = needsListener;
   1218 
   1219             if (needsListener) {
   1220                 mManager.addCaptioningChangeListener(mCaptioningListener);
   1221 
   1222                 final CaptionStyle captionStyle = mManager.getUserStyle();
   1223                 final float fontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
   1224                 setCaptionStyle(captionStyle, fontSize);
   1225             } else {
   1226                 mManager.removeCaptioningChangeListener(mCaptioningListener);
   1227             }
   1228         }
   1229     }
   1230 
   1231     public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) {
   1232         final Context context = getContext();
   1233         final CaptionStyle captionStyle = mCaptionStyle;
   1234         final float fontSize = mFontSize;
   1235 
   1236         prepForPrune();
   1237 
   1238         // Ensure we have all necessary cue and region boxes.
   1239         final int count = activeCues.size();
   1240         for (int i = 0; i < count; i++) {
   1241             final TextTrackCue cue = (TextTrackCue) activeCues.get(i);
   1242             final TextTrackRegion region = cue.mRegion;
   1243             if (region != null) {
   1244                 RegionLayout regionBox = mRegionBoxes.get(region);
   1245                 if (regionBox == null) {
   1246                     regionBox = new RegionLayout(context, region, captionStyle, fontSize);
   1247                     mRegionBoxes.put(region, regionBox);
   1248                     addView(regionBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   1249                 }
   1250                 regionBox.put(cue);
   1251             } else {
   1252                 CueLayout cueBox = mCueBoxes.get(cue);
   1253                 if (cueBox == null) {
   1254                     cueBox = new CueLayout(context, cue, captionStyle, fontSize);
   1255                     mCueBoxes.put(cue, cueBox);
   1256                     addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   1257                 }
   1258                 cueBox.update();
   1259                 cueBox.setOrder(i);
   1260             }
   1261         }
   1262 
   1263         prune();
   1264 
   1265         // Force measurement and layout.
   1266         final int width = getWidth();
   1267         final int height = getHeight();
   1268         setSize(width, height);
   1269 
   1270         if (mListener != null) {
   1271             mListener.onChanged(this);
   1272         }
   1273     }
   1274 
   1275     private void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
   1276         captionStyle = DEFAULT_CAPTION_STYLE.applyStyle(captionStyle);
   1277         mCaptionStyle = captionStyle;
   1278         mFontSize = fontSize;
   1279 
   1280         final int cueCount = mCueBoxes.size();
   1281         for (int i = 0; i < cueCount; i++) {
   1282             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1283             cueBox.setCaptionStyle(captionStyle, fontSize);
   1284         }
   1285 
   1286         final int regionCount = mRegionBoxes.size();
   1287         for (int i = 0; i < regionCount; i++) {
   1288             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1289             regionBox.setCaptionStyle(captionStyle, fontSize);
   1290         }
   1291     }
   1292 
   1293     /**
   1294      * Remove inactive cues and regions.
   1295      */
   1296     private void prune() {
   1297         int regionCount = mRegionBoxes.size();
   1298         for (int i = 0; i < regionCount; i++) {
   1299             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1300             if (regionBox.prune()) {
   1301                 removeView(regionBox);
   1302                 mRegionBoxes.removeAt(i);
   1303                 regionCount--;
   1304                 i--;
   1305             }
   1306         }
   1307 
   1308         int cueCount = mCueBoxes.size();
   1309         for (int i = 0; i < cueCount; i++) {
   1310             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1311             if (!cueBox.isActive()) {
   1312                 removeView(cueBox);
   1313                 mCueBoxes.removeAt(i);
   1314                 cueCount--;
   1315                 i--;
   1316             }
   1317         }
   1318     }
   1319 
   1320     /**
   1321      * Reset active cues and regions.
   1322      */
   1323     private void prepForPrune() {
   1324         final int regionCount = mRegionBoxes.size();
   1325         for (int i = 0; i < regionCount; i++) {
   1326             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1327             regionBox.prepForPrune();
   1328         }
   1329 
   1330         final int cueCount = mCueBoxes.size();
   1331         for (int i = 0; i < cueCount; i++) {
   1332             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1333             cueBox.prepForPrune();
   1334         }
   1335     }
   1336 
   1337     @Override
   1338     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1339         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   1340 
   1341         final int regionCount = mRegionBoxes.size();
   1342         for (int i = 0; i < regionCount; i++) {
   1343             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1344             regionBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
   1345         }
   1346 
   1347         final int cueCount = mCueBoxes.size();
   1348         for (int i = 0; i < cueCount; i++) {
   1349             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1350             cueBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
   1351         }
   1352     }
   1353 
   1354     @Override
   1355     protected void onLayout(boolean changed, int l, int t, int r, int b) {
   1356         final int viewportWidth = r - l;
   1357         final int viewportHeight = b - t;
   1358 
   1359         setCaptionStyle(mCaptionStyle,
   1360                 mManager.getFontScale() * LINE_HEIGHT_RATIO * viewportHeight);
   1361 
   1362         final int regionCount = mRegionBoxes.size();
   1363         for (int i = 0; i < regionCount; i++) {
   1364             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1365             layoutRegion(viewportWidth, viewportHeight, regionBox);
   1366         }
   1367 
   1368         final int cueCount = mCueBoxes.size();
   1369         for (int i = 0; i < cueCount; i++) {
   1370             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1371             layoutCue(viewportWidth, viewportHeight, cueBox);
   1372         }
   1373     }
   1374 
   1375     /**
   1376      * Lays out a region within the viewport. The region handles layout for
   1377      * contained cues.
   1378      */
   1379     private void layoutRegion(
   1380             int viewportWidth, int viewportHeight,
   1381             RegionLayout regionBox) {
   1382         final TextTrackRegion region = regionBox.getRegion();
   1383         final int regionHeight = regionBox.getMeasuredHeight();
   1384         final int regionWidth = regionBox.getMeasuredWidth();
   1385 
   1386         // TODO: Account for region anchor point.
   1387         final float x = region.mViewportAnchorPointX;
   1388         final float y = region.mViewportAnchorPointY;
   1389         final int left = (int) (x * (viewportWidth - regionWidth) / 100);
   1390         final int top = (int) (y * (viewportHeight - regionHeight) / 100);
   1391 
   1392         regionBox.layout(left, top, left + regionWidth, top + regionHeight);
   1393     }
   1394 
   1395     /**
   1396      * Lays out a cue within the viewport.
   1397      */
   1398     private void layoutCue(
   1399             int viewportWidth, int viewportHeight, CueLayout cueBox) {
   1400         final TextTrackCue cue = cueBox.getCue();
   1401         final int direction = getLayoutDirection();
   1402         final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);
   1403         final boolean cueSnapToLines = cue.mSnapToLines;
   1404 
   1405         int size = 100 * cueBox.getMeasuredWidth() / viewportWidth;
   1406 
   1407         // Determine raw x-position.
   1408         int xPosition;
   1409         switch (absAlignment) {
   1410             case TextTrackCue.ALIGNMENT_LEFT:
   1411                 xPosition = cue.mTextPosition;
   1412                 break;
   1413             case TextTrackCue.ALIGNMENT_RIGHT:
   1414                 xPosition = cue.mTextPosition - size;
   1415                 break;
   1416             case TextTrackCue.ALIGNMENT_MIDDLE:
   1417             default:
   1418                 xPosition = cue.mTextPosition - size / 2;
   1419                 break;
   1420         }
   1421 
   1422         // Adjust x-position for layout.
   1423         if (direction == LAYOUT_DIRECTION_RTL) {
   1424             xPosition = 100 - xPosition;
   1425         }
   1426 
   1427         // If the text track cue snap-to-lines flag is set, adjust
   1428         // x-position and size for padding. This is equivalent to placing the
   1429         // cue within the title-safe area.
   1430         if (cueSnapToLines) {
   1431             final int paddingLeft = 100 * getPaddingLeft() / viewportWidth;
   1432             final int paddingRight = 100 * getPaddingRight() / viewportWidth;
   1433             if (xPosition < paddingLeft && xPosition + size > paddingLeft) {
   1434                 xPosition += paddingLeft;
   1435                 size -= paddingLeft;
   1436             }
   1437             final float rightEdge = 100 - paddingRight;
   1438             if (xPosition < rightEdge && xPosition + size > rightEdge) {
   1439                 size -= paddingRight;
   1440             }
   1441         }
   1442 
   1443         // Compute absolute left position and width.
   1444         final int left = xPosition * viewportWidth / 100;
   1445         final int width = size * viewportWidth / 100;
   1446 
   1447         // Determine initial y-position.
   1448         final int yPosition = calculateLinePosition(cueBox);
   1449 
   1450         // Compute absolute final top position and height.
   1451         final int height = cueBox.getMeasuredHeight();
   1452         final int top;
   1453         if (yPosition < 0) {
   1454             // TODO: This needs to use the actual height of prior boxes.
   1455             top = viewportHeight + yPosition * height;
   1456         } else {
   1457             top = yPosition * (viewportHeight - height) / 100;
   1458         }
   1459 
   1460         // Layout cue in final position.
   1461         cueBox.layout(left, top, left + width, top + height);
   1462     }
   1463 
   1464     /**
   1465      * Calculates the line position for a cue.
   1466      * <p>
   1467      * If the resulting position is negative, it represents a bottom-aligned
   1468      * position relative to the number of active cues. Otherwise, it represents
   1469      * a percentage [0-100] of the viewport height.
   1470      */
   1471     private int calculateLinePosition(CueLayout cueBox) {
   1472         final TextTrackCue cue = cueBox.getCue();
   1473         final Integer linePosition = cue.mLinePosition;
   1474         final boolean snapToLines = cue.mSnapToLines;
   1475         final boolean autoPosition = (linePosition == null);
   1476 
   1477         if (!snapToLines && !autoPosition && (linePosition < 0 || linePosition > 100)) {
   1478             // Invalid line position defaults to 100.
   1479             return 100;
   1480         } else if (!autoPosition) {
   1481             // Use the valid, supplied line position.
   1482             return linePosition;
   1483         } else if (!snapToLines) {
   1484             // Automatic, non-snapped line position defaults to 100.
   1485             return 100;
   1486         } else {
   1487             // Automatic snapped line position uses active cue order.
   1488             return -(cueBox.mOrder + 1);
   1489         }
   1490     }
   1491 
   1492     /**
   1493      * Resolves cue alignment according to the specified layout direction.
   1494      */
   1495     private static int resolveCueAlignment(int layoutDirection, int alignment) {
   1496         switch (alignment) {
   1497             case TextTrackCue.ALIGNMENT_START:
   1498                 return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
   1499                         TextTrackCue.ALIGNMENT_LEFT : TextTrackCue.ALIGNMENT_RIGHT;
   1500             case TextTrackCue.ALIGNMENT_END:
   1501                 return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
   1502                         TextTrackCue.ALIGNMENT_RIGHT : TextTrackCue.ALIGNMENT_LEFT;
   1503         }
   1504         return alignment;
   1505     }
   1506 
   1507     private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
   1508         @Override
   1509         public void onFontScaleChanged(float fontScale) {
   1510             final float fontSize = fontScale * getHeight() * LINE_HEIGHT_RATIO;
   1511             setCaptionStyle(mCaptionStyle, fontSize);
   1512         }
   1513 
   1514         @Override
   1515         public void onUserStyleChanged(CaptionStyle userStyle) {
   1516             setCaptionStyle(userStyle, mFontSize);
   1517         }
   1518     };
   1519 
   1520     /**
   1521      * A text track region represents a portion of the video viewport and
   1522      * provides a rendering area for text track cues.
   1523      */
   1524     private static class RegionLayout extends LinearLayout {
   1525         private final ArrayList<CueLayout> mRegionCueBoxes = new ArrayList<CueLayout>();
   1526         private final TextTrackRegion mRegion;
   1527 
   1528         private CaptionStyle mCaptionStyle;
   1529         private float mFontSize;
   1530 
   1531         public RegionLayout(Context context, TextTrackRegion region, CaptionStyle captionStyle,
   1532                 float fontSize) {
   1533             super(context);
   1534 
   1535             mRegion = region;
   1536             mCaptionStyle = captionStyle;
   1537             mFontSize = fontSize;
   1538 
   1539             // TODO: Add support for vertical text
   1540             setOrientation(VERTICAL);
   1541 
   1542             if (DEBUG) {
   1543                 setBackgroundColor(DEBUG_REGION_BACKGROUND);
   1544             } else {
   1545                 setBackgroundColor(captionStyle.windowColor);
   1546             }
   1547         }
   1548 
   1549         public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
   1550             mCaptionStyle = captionStyle;
   1551             mFontSize = fontSize;
   1552 
   1553             final int cueCount = mRegionCueBoxes.size();
   1554             for (int i = 0; i < cueCount; i++) {
   1555                 final CueLayout cueBox = mRegionCueBoxes.get(i);
   1556                 cueBox.setCaptionStyle(captionStyle, fontSize);
   1557             }
   1558 
   1559             setBackgroundColor(captionStyle.windowColor);
   1560         }
   1561 
   1562         /**
   1563          * Performs the parent's measurement responsibilities, then
   1564          * automatically performs its own measurement.
   1565          */
   1566         public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
   1567             final TextTrackRegion region = mRegion;
   1568             final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
   1569             final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
   1570             final int width = (int) region.mWidth;
   1571 
   1572             // Determine the absolute maximum region size as the requested size.
   1573             final int size = width * specWidth / 100;
   1574 
   1575             widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
   1576             heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
   1577             measure(widthMeasureSpec, heightMeasureSpec);
   1578         }
   1579 
   1580         /**
   1581          * Prepares this region for pruning by setting all tracks as inactive.
   1582          * <p>
   1583          * Tracks that are added or updated using {@link #put(TextTrackCue)}
   1584          * after this calling this method will be marked as active.
   1585          */
   1586         public void prepForPrune() {
   1587             final int cueCount = mRegionCueBoxes.size();
   1588             for (int i = 0; i < cueCount; i++) {
   1589                 final CueLayout cueBox = mRegionCueBoxes.get(i);
   1590                 cueBox.prepForPrune();
   1591             }
   1592         }
   1593 
   1594         /**
   1595          * Adds a {@link TextTrackCue} to this region. If the track had already
   1596          * been added, updates its active state.
   1597          *
   1598          * @param cue
   1599          */
   1600         public void put(TextTrackCue cue) {
   1601             final int cueCount = mRegionCueBoxes.size();
   1602             for (int i = 0; i < cueCount; i++) {
   1603                 final CueLayout cueBox = mRegionCueBoxes.get(i);
   1604                 if (cueBox.getCue() == cue) {
   1605                     cueBox.update();
   1606                     return;
   1607                 }
   1608             }
   1609 
   1610             final CueLayout cueBox = new CueLayout(getContext(), cue, mCaptionStyle, mFontSize);
   1611             mRegionCueBoxes.add(cueBox);
   1612             addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   1613 
   1614             if (getChildCount() > mRegion.mLines) {
   1615                 removeViewAt(0);
   1616             }
   1617         }
   1618 
   1619         /**
   1620          * Remove all inactive tracks from this region.
   1621          *
   1622          * @return true if this region is empty and should be pruned
   1623          */
   1624         public boolean prune() {
   1625             int cueCount = mRegionCueBoxes.size();
   1626             for (int i = 0; i < cueCount; i++) {
   1627                 final CueLayout cueBox = mRegionCueBoxes.get(i);
   1628                 if (!cueBox.isActive()) {
   1629                     mRegionCueBoxes.remove(i);
   1630                     removeView(cueBox);
   1631                     cueCount--;
   1632                     i--;
   1633                 }
   1634             }
   1635 
   1636             return mRegionCueBoxes.isEmpty();
   1637         }
   1638 
   1639         /**
   1640          * @return the region data backing this layout
   1641          */
   1642         public TextTrackRegion getRegion() {
   1643             return mRegion;
   1644         }
   1645     }
   1646 
   1647     /**
   1648      * A text track cue is the unit of time-sensitive data in a text track,
   1649      * corresponding for instance for subtitles and captions to the text that
   1650      * appears at a particular time and disappears at another time.
   1651      * <p>
   1652      * A single cue may contain multiple {@link SpanLayout}s, each representing a
   1653      * single line of text.
   1654      */
   1655     private static class CueLayout extends LinearLayout {
   1656         public final TextTrackCue mCue;
   1657 
   1658         private CaptionStyle mCaptionStyle;
   1659         private float mFontSize;
   1660 
   1661         private boolean mActive;
   1662         private int mOrder;
   1663 
   1664         public CueLayout(
   1665                 Context context, TextTrackCue cue, CaptionStyle captionStyle, float fontSize) {
   1666             super(context);
   1667 
   1668             mCue = cue;
   1669             mCaptionStyle = captionStyle;
   1670             mFontSize = fontSize;
   1671 
   1672             // TODO: Add support for vertical text.
   1673             final boolean horizontal = cue.mWritingDirection
   1674                     == TextTrackCue.WRITING_DIRECTION_HORIZONTAL;
   1675             setOrientation(horizontal ? VERTICAL : HORIZONTAL);
   1676 
   1677             switch (cue.mAlignment) {
   1678                 case TextTrackCue.ALIGNMENT_END:
   1679                     setGravity(Gravity.END);
   1680                     break;
   1681                 case TextTrackCue.ALIGNMENT_LEFT:
   1682                     setGravity(Gravity.LEFT);
   1683                     break;
   1684                 case TextTrackCue.ALIGNMENT_MIDDLE:
   1685                     setGravity(horizontal
   1686                             ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL);
   1687                     break;
   1688                 case TextTrackCue.ALIGNMENT_RIGHT:
   1689                     setGravity(Gravity.RIGHT);
   1690                     break;
   1691                 case TextTrackCue.ALIGNMENT_START:
   1692                     setGravity(Gravity.START);
   1693                     break;
   1694             }
   1695 
   1696             if (DEBUG) {
   1697                 setBackgroundColor(DEBUG_CUE_BACKGROUND);
   1698             }
   1699 
   1700             update();
   1701         }
   1702 
   1703         public void setCaptionStyle(CaptionStyle style, float fontSize) {
   1704             mCaptionStyle = style;
   1705             mFontSize = fontSize;
   1706 
   1707             final int n = getChildCount();
   1708             for (int i = 0; i < n; i++) {
   1709                 final View child = getChildAt(i);
   1710                 if (child instanceof SpanLayout) {
   1711                     ((SpanLayout) child).setCaptionStyle(style, fontSize);
   1712                 }
   1713             }
   1714         }
   1715 
   1716         public void prepForPrune() {
   1717             mActive = false;
   1718         }
   1719 
   1720         public void update() {
   1721             mActive = true;
   1722 
   1723             removeAllViews();
   1724 
   1725             final int cueAlignment = resolveCueAlignment(getLayoutDirection(), mCue.mAlignment);
   1726             final Alignment alignment;
   1727             switch (cueAlignment) {
   1728                 case TextTrackCue.ALIGNMENT_LEFT:
   1729                     alignment = Alignment.ALIGN_LEFT;
   1730                     break;
   1731                 case TextTrackCue.ALIGNMENT_RIGHT:
   1732                     alignment = Alignment.ALIGN_RIGHT;
   1733                     break;
   1734                 case TextTrackCue.ALIGNMENT_MIDDLE:
   1735                 default:
   1736                     alignment = Alignment.ALIGN_CENTER;
   1737             }
   1738 
   1739             final CaptionStyle captionStyle = mCaptionStyle;
   1740             final float fontSize = mFontSize;
   1741             final TextTrackCueSpan[][] lines = mCue.mLines;
   1742             final int lineCount = lines.length;
   1743             for (int i = 0; i < lineCount; i++) {
   1744                 final SpanLayout lineBox = new SpanLayout(getContext(), lines[i]);
   1745                 lineBox.setAlignment(alignment);
   1746                 lineBox.setCaptionStyle(captionStyle, fontSize);
   1747 
   1748                 addView(lineBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   1749             }
   1750         }
   1751 
   1752         @Override
   1753         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1754             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   1755         }
   1756 
   1757         /**
   1758          * Performs the parent's measurement responsibilities, then
   1759          * automatically performs its own measurement.
   1760          */
   1761         public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
   1762             final TextTrackCue cue = mCue;
   1763             final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
   1764             final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
   1765             final int direction = getLayoutDirection();
   1766             final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);
   1767 
   1768             // Determine the maximum size of cue based on its starting position
   1769             // and the direction in which it grows.
   1770             final int maximumSize;
   1771             switch (absAlignment) {
   1772                 case TextTrackCue.ALIGNMENT_LEFT:
   1773                     maximumSize = 100 - cue.mTextPosition;
   1774                     break;
   1775                 case TextTrackCue.ALIGNMENT_RIGHT:
   1776                     maximumSize = cue.mTextPosition;
   1777                     break;
   1778                 case TextTrackCue.ALIGNMENT_MIDDLE:
   1779                     if (cue.mTextPosition <= 50) {
   1780                         maximumSize = cue.mTextPosition * 2;
   1781                     } else {
   1782                         maximumSize = (100 - cue.mTextPosition) * 2;
   1783                     }
   1784                     break;
   1785                 default:
   1786                     maximumSize = 0;
   1787             }
   1788 
   1789             // Determine absolute maximum cue size as the smaller of the
   1790             // requested size and the maximum theoretical size.
   1791             final int size = Math.min(cue.mSize, maximumSize) * specWidth / 100;
   1792             widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
   1793             heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
   1794             measure(widthMeasureSpec, heightMeasureSpec);
   1795         }
   1796 
   1797         /**
   1798          * Sets the order of this cue in the list of active cues.
   1799          *
   1800          * @param order the order of this cue in the list of active cues
   1801          */
   1802         public void setOrder(int order) {
   1803             mOrder = order;
   1804         }
   1805 
   1806         /**
   1807          * @return whether this cue is marked as active
   1808          */
   1809         public boolean isActive() {
   1810             return mActive;
   1811         }
   1812 
   1813         /**
   1814          * @return the cue data backing this layout
   1815          */
   1816         public TextTrackCue getCue() {
   1817             return mCue;
   1818         }
   1819     }
   1820 
   1821     /**
   1822      * A text track line represents a single line of text within a cue.
   1823      * <p>
   1824      * A single line may contain multiple spans, each representing a section of
   1825      * text that may be enabled or disabled at a particular time.
   1826      */
   1827     private static class SpanLayout extends SubtitleView {
   1828         private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
   1829         private final TextTrackCueSpan[] mSpans;
   1830 
   1831         public SpanLayout(Context context, TextTrackCueSpan[] spans) {
   1832             super(context);
   1833 
   1834             mSpans = spans;
   1835 
   1836             update();
   1837         }
   1838 
   1839         public void update() {
   1840             final SpannableStringBuilder builder = mBuilder;
   1841             final TextTrackCueSpan[] spans = mSpans;
   1842 
   1843             builder.clear();
   1844             builder.clearSpans();
   1845 
   1846             final int spanCount = spans.length;
   1847             for (int i = 0; i < spanCount; i++) {
   1848                 final TextTrackCueSpan span = spans[i];
   1849                 if (span.mEnabled) {
   1850                     builder.append(spans[i].mText);
   1851                 }
   1852             }
   1853 
   1854             setText(builder);
   1855         }
   1856 
   1857         public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
   1858             setBackgroundColor(captionStyle.backgroundColor);
   1859             setForegroundColor(captionStyle.foregroundColor);
   1860             setEdgeColor(captionStyle.edgeColor);
   1861             setEdgeType(captionStyle.edgeType);
   1862             setTypeface(captionStyle.getTypeface());
   1863             setTextSize(fontSize);
   1864         }
   1865     }
   1866 }
   1867