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 || mLinePosition == cue.mLinePosition) &&
    437                     mTextPosition == cue.mTextPosition &&
    438                     mSize == cue.mSize &&
    439                     mAlignment == cue.mAlignment &&
    440                     mLines.length == cue.mLines.length;
    441             if (res == true) {
    442                 for (int line = 0; line < mLines.length; line++) {
    443                     if (!Arrays.equals(mLines[line], cue.mLines[line])) {
    444                         return false;
    445                     }
    446                 }
    447             }
    448             return res;
    449         } catch(IncompatibleClassChangeError e) {
    450             return false;
    451         }
    452     }
    453 
    454     public StringBuilder appendStringsToBuilder(StringBuilder builder) {
    455         if (mStrings == null) {
    456             builder.append("null");
    457         } else {
    458             builder.append("[");
    459             boolean first = true;
    460             for (String s: mStrings) {
    461                 if (!first) {
    462                     builder.append(", ");
    463                 }
    464                 if (s == null) {
    465                     builder.append("null");
    466                 } else {
    467                     builder.append("\"");
    468                     builder.append(s);
    469                     builder.append("\"");
    470                 }
    471                 first = false;
    472             }
    473             builder.append("]");
    474         }
    475         return builder;
    476     }
    477 
    478     public StringBuilder appendLinesToBuilder(StringBuilder builder) {
    479         if (mLines == null) {
    480             builder.append("null");
    481         } else {
    482             builder.append("[");
    483             boolean first = true;
    484             for (TextTrackCueSpan[] spans: mLines) {
    485                 if (!first) {
    486                     builder.append(", ");
    487                 }
    488                 if (spans == null) {
    489                     builder.append("null");
    490                 } else {
    491                     builder.append("\"");
    492                     boolean innerFirst = true;
    493                     long lastTimestamp = -1;
    494                     for (TextTrackCueSpan span: spans) {
    495                         if (!innerFirst) {
    496                             builder.append(" ");
    497                         }
    498                         if (span.mTimestampMs != lastTimestamp) {
    499                             builder.append("<")
    500                                     .append(WebVttParser.timeToString(
    501                                             span.mTimestampMs))
    502                                     .append(">");
    503                             lastTimestamp = span.mTimestampMs;
    504                         }
    505                         builder.append(span.mText);
    506                         innerFirst = false;
    507                     }
    508                     builder.append("\"");
    509                 }
    510                 first = false;
    511             }
    512             builder.append("]");
    513         }
    514         return builder;
    515     }
    516 
    517     public String toString() {
    518         StringBuilder res = new StringBuilder();
    519 
    520         res.append(WebVttParser.timeToString(mStartTimeMs))
    521                 .append(" --> ").append(WebVttParser.timeToString(mEndTimeMs))
    522                 .append(" {id:\"").append(mId)
    523                 .append("\", pauseOnExit:").append(mPauseOnExit)
    524                 .append(", direction:")
    525                 .append(mWritingDirection == WRITING_DIRECTION_HORIZONTAL ? "horizontal" :
    526                         mWritingDirection == WRITING_DIRECTION_VERTICAL_LR ? "vertical_lr" :
    527                         mWritingDirection == WRITING_DIRECTION_VERTICAL_RL ? "vertical_rl" :
    528                         "INVALID")
    529                 .append(", regionId:\"").append(mRegionId)
    530                 .append("\", snapToLines:").append(mSnapToLines)
    531                 .append(", linePosition:").append(mAutoLinePosition ? "auto" :
    532                                                   mLinePosition)
    533                 .append(", textPosition:").append(mTextPosition)
    534                 .append(", size:").append(mSize)
    535                 .append(", alignment:")
    536                 .append(mAlignment == ALIGNMENT_END ? "end" :
    537                         mAlignment == ALIGNMENT_LEFT ? "left" :
    538                         mAlignment == ALIGNMENT_MIDDLE ? "middle" :
    539                         mAlignment == ALIGNMENT_RIGHT ? "right" :
    540                         mAlignment == ALIGNMENT_START ? "start" : "INVALID")
    541                 .append(", text:");
    542         appendStringsToBuilder(res).append("}");
    543         return res.toString();
    544     }
    545 
    546     @Override
    547     public int hashCode() {
    548         return toString().hashCode();
    549     }
    550 
    551     @Override
    552     public void onTime(long timeMs) {
    553         for (TextTrackCueSpan[] line: mLines) {
    554             for (TextTrackCueSpan span: line) {
    555                 span.mEnabled = timeMs >= span.mTimestampMs;
    556             }
    557         }
    558     }
    559 }
    560 
    561 /** @hide */
    562 class WebVttParser {
    563     private static final String TAG = "WebVttParser";
    564     private Phase mPhase;
    565     private TextTrackCue mCue;
    566     private Vector<String> mCueTexts;
    567     private WebVttCueListener mListener;
    568     private String mBuffer;
    569 
    570     WebVttParser(WebVttCueListener listener) {
    571         mPhase = mParseStart;
    572         mBuffer = "";   /* mBuffer contains up to 1 incomplete line */
    573         mListener = listener;
    574         mCueTexts = new Vector<String>();
    575     }
    576 
    577     /* parsePercentageString */
    578     public static float parseFloatPercentage(String s)
    579             throws NumberFormatException {
    580         if (!s.endsWith("%")) {
    581             throw new NumberFormatException("does not end in %");
    582         }
    583         s = s.substring(0, s.length() - 1);
    584         // parseFloat allows an exponent or a sign
    585         if (s.matches(".*[^0-9.].*")) {
    586             throw new NumberFormatException("contains an invalid character");
    587         }
    588 
    589         try {
    590             float value = Float.parseFloat(s);
    591             if (value < 0.0f || value > 100.0f) {
    592                 throw new NumberFormatException("is out of range");
    593             }
    594             return value;
    595         } catch (NumberFormatException e) {
    596             throw new NumberFormatException("is not a number");
    597         }
    598     }
    599 
    600     public static int parseIntPercentage(String s) throws NumberFormatException {
    601         if (!s.endsWith("%")) {
    602             throw new NumberFormatException("does not end in %");
    603         }
    604         s = s.substring(0, s.length() - 1);
    605         // parseInt allows "-0" that returns 0, so check for non-digits
    606         if (s.matches(".*[^0-9].*")) {
    607             throw new NumberFormatException("contains an invalid character");
    608         }
    609 
    610         try {
    611             int value = Integer.parseInt(s);
    612             if (value < 0 || value > 100) {
    613                 throw new NumberFormatException("is out of range");
    614             }
    615             return value;
    616         } catch (NumberFormatException e) {
    617             throw new NumberFormatException("is not a number");
    618         }
    619     }
    620 
    621     public static long parseTimestampMs(String s) throws NumberFormatException {
    622         if (!s.matches("(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}")) {
    623             throw new NumberFormatException("has invalid format");
    624         }
    625 
    626         String[] parts = s.split("\\.", 2);
    627         long value = 0;
    628         for (String group: parts[0].split(":")) {
    629             value = value * 60 + Long.parseLong(group);
    630         }
    631         return value * 1000 + Long.parseLong(parts[1]);
    632     }
    633 
    634     public static String timeToString(long timeMs) {
    635         return String.format("%d:%02d:%02d.%03d",
    636                 timeMs / 3600000, (timeMs / 60000) % 60,
    637                 (timeMs / 1000) % 60, timeMs % 1000);
    638     }
    639 
    640     public void parse(String s) {
    641         boolean trailingCR = false;
    642         mBuffer = (mBuffer + s.replace("\0", "\ufffd")).replace("\r\n", "\n");
    643 
    644         /* keep trailing '\r' in case matching '\n' arrives in next packet */
    645         if (mBuffer.endsWith("\r")) {
    646             trailingCR = true;
    647             mBuffer = mBuffer.substring(0, mBuffer.length() - 1);
    648         }
    649 
    650         String[] lines = mBuffer.split("[\r\n]");
    651         for (int i = 0; i < lines.length - 1; i++) {
    652             mPhase.parse(lines[i]);
    653         }
    654 
    655         mBuffer = lines[lines.length - 1];
    656         if (trailingCR)
    657             mBuffer += "\r";
    658     }
    659 
    660     public void eos() {
    661         if (mBuffer.endsWith("\r")) {
    662             mBuffer = mBuffer.substring(0, mBuffer.length() - 1);
    663         }
    664 
    665         mPhase.parse(mBuffer);
    666         mBuffer = "";
    667 
    668         yieldCue();
    669         mPhase = mParseStart;
    670     }
    671 
    672     public void yieldCue() {
    673         if (mCue != null && mCueTexts.size() > 0) {
    674             mCue.mStrings = new String[mCueTexts.size()];
    675             mCueTexts.toArray(mCue.mStrings);
    676             mCueTexts.clear();
    677             mListener.onCueParsed(mCue);
    678         }
    679         mCue = null;
    680     }
    681 
    682     interface Phase {
    683         void parse(String line);
    684     }
    685 
    686     final private Phase mSkipRest = new Phase() {
    687         @Override
    688         public void parse(String line) { }
    689     };
    690 
    691     final private Phase mParseStart = new Phase() { // 5-9
    692         @Override
    693         public void parse(String line) {
    694             if (line.startsWith("\ufeff")) {
    695                 line = line.substring(1);
    696             }
    697             if (!line.equals("WEBVTT") &&
    698                     !line.startsWith("WEBVTT ") &&
    699                     !line.startsWith("WEBVTT\t")) {
    700                 log_warning("Not a WEBVTT header", line);
    701                 mPhase = mSkipRest;
    702             } else {
    703                 mPhase = mParseHeader;
    704             }
    705         }
    706     };
    707 
    708     final private Phase mParseHeader = new Phase() { // 10-13
    709         TextTrackRegion parseRegion(String s) {
    710             TextTrackRegion region = new TextTrackRegion();
    711             for (String setting: s.split(" +")) {
    712                 int equalAt = setting.indexOf('=');
    713                 if (equalAt <= 0 || equalAt == setting.length() - 1) {
    714                     continue;
    715                 }
    716 
    717                 String name = setting.substring(0, equalAt);
    718                 String value = setting.substring(equalAt + 1);
    719                 if (name.equals("id")) {
    720                     region.mId = value;
    721                 } else if (name.equals("width")) {
    722                     try {
    723                         region.mWidth = parseFloatPercentage(value);
    724                     } catch (NumberFormatException e) {
    725                         log_warning("region setting", name,
    726                                 "has invalid value", e.getMessage(), value);
    727                     }
    728                 } else if (name.equals("lines")) {
    729                     try {
    730                         int lines = Integer.parseInt(value);
    731                         if (lines >= 0) {
    732                             region.mLines = lines;
    733                         } else {
    734                             log_warning("region setting", name, "is negative", value);
    735                         }
    736                     } catch (NumberFormatException e) {
    737                         log_warning("region setting", name, "is not numeric", value);
    738                     }
    739                 } else if (name.equals("regionanchor") ||
    740                            name.equals("viewportanchor")) {
    741                     int commaAt = value.indexOf(",");
    742                     if (commaAt < 0) {
    743                         log_warning("region setting", name, "contains no comma", value);
    744                         continue;
    745                     }
    746 
    747                     String anchorX = value.substring(0, commaAt);
    748                     String anchorY = value.substring(commaAt + 1);
    749                     float x, y;
    750 
    751                     try {
    752                         x = parseFloatPercentage(anchorX);
    753                     } catch (NumberFormatException e) {
    754                         log_warning("region setting", name,
    755                                 "has invalid x component", e.getMessage(), anchorX);
    756                         continue;
    757                     }
    758                     try {
    759                         y = parseFloatPercentage(anchorY);
    760                     } catch (NumberFormatException e) {
    761                         log_warning("region setting", name,
    762                                 "has invalid y component", e.getMessage(), anchorY);
    763                         continue;
    764                     }
    765 
    766                     if (name.charAt(0) == 'r') {
    767                         region.mAnchorPointX = x;
    768                         region.mAnchorPointY = y;
    769                     } else {
    770                         region.mViewportAnchorPointX = x;
    771                         region.mViewportAnchorPointY = y;
    772                     }
    773                 } else if (name.equals("scroll")) {
    774                     if (value.equals("up")) {
    775                         region.mScrollValue =
    776                             TextTrackRegion.SCROLL_VALUE_SCROLL_UP;
    777                     } else {
    778                         log_warning("region setting", name, "has invalid value", value);
    779                     }
    780                 }
    781             }
    782             return region;
    783         }
    784 
    785         @Override
    786         public void parse(String line)  {
    787             if (line.length() == 0) {
    788                 mPhase = mParseCueId;
    789             } else if (line.contains("-->")) {
    790                 mPhase = mParseCueTime;
    791                 mPhase.parse(line);
    792             } else {
    793                 int colonAt = line.indexOf(':');
    794                 if (colonAt <= 0 || colonAt >= line.length() - 1) {
    795                     log_warning("meta data header has invalid format", line);
    796                 }
    797                 String name = line.substring(0, colonAt);
    798                 String value = line.substring(colonAt + 1);
    799 
    800                 if (name.equals("Region")) {
    801                     TextTrackRegion region = parseRegion(value);
    802                     mListener.onRegionParsed(region);
    803                 }
    804             }
    805         }
    806     };
    807 
    808     final private Phase mParseCueId = new Phase() {
    809         @Override
    810         public void parse(String line) {
    811             if (line.length() == 0) {
    812                 return;
    813             }
    814 
    815             assert(mCue == null);
    816 
    817             if (line.equals("NOTE") || line.startsWith("NOTE ")) {
    818                 mPhase = mParseCueText;
    819             }
    820 
    821             mCue = new TextTrackCue();
    822             mCueTexts.clear();
    823 
    824             mPhase = mParseCueTime;
    825             if (line.contains("-->")) {
    826                 mPhase.parse(line);
    827             } else {
    828                 mCue.mId = line;
    829             }
    830         }
    831     };
    832 
    833     final private Phase mParseCueTime = new Phase() {
    834         @Override
    835         public void parse(String line) {
    836             int arrowAt = line.indexOf("-->");
    837             if (arrowAt < 0) {
    838                 mCue = null;
    839                 mPhase = mParseCueId;
    840                 return;
    841             }
    842 
    843             String start = line.substring(0, arrowAt).trim();
    844             // convert only initial and first other white-space to space
    845             String rest = line.substring(arrowAt + 3)
    846                     .replaceFirst("^\\s+", "").replaceFirst("\\s+", " ");
    847             int spaceAt = rest.indexOf(' ');
    848             String end = spaceAt > 0 ? rest.substring(0, spaceAt) : rest;
    849             rest = spaceAt > 0 ? rest.substring(spaceAt + 1) : "";
    850 
    851             mCue.mStartTimeMs = parseTimestampMs(start);
    852             mCue.mEndTimeMs = parseTimestampMs(end);
    853             for (String setting: rest.split(" +")) {
    854                 int colonAt = setting.indexOf(':');
    855                 if (colonAt <= 0 || colonAt == setting.length() - 1) {
    856                     continue;
    857                 }
    858                 String name = setting.substring(0, colonAt);
    859                 String value = setting.substring(colonAt + 1);
    860 
    861                 if (name.equals("region")) {
    862                     mCue.mRegionId = value;
    863                 } else if (name.equals("vertical")) {
    864                     if (value.equals("rl")) {
    865                         mCue.mWritingDirection =
    866                             TextTrackCue.WRITING_DIRECTION_VERTICAL_RL;
    867                     } else if (value.equals("lr")) {
    868                         mCue.mWritingDirection =
    869                             TextTrackCue.WRITING_DIRECTION_VERTICAL_LR;
    870                     } else {
    871                         log_warning("cue setting", name, "has invalid value", value);
    872                     }
    873                 } else if (name.equals("line")) {
    874                     try {
    875                         int linePosition;
    876                         /* TRICKY: we know that there are no spaces in value */
    877                         assert(value.indexOf(' ') < 0);
    878                         if (value.endsWith("%")) {
    879                             linePosition = Integer.parseInt(
    880                                     value.substring(0, value.length() - 1));
    881                             if (linePosition < 0 || linePosition > 100) {
    882                                 log_warning("cue setting", name, "is out of range", value);
    883                                 continue;
    884                             }
    885                             mCue.mSnapToLines = false;
    886                             mCue.mLinePosition = linePosition;
    887                         } else {
    888                             mCue.mSnapToLines = true;
    889                             mCue.mLinePosition = Integer.parseInt(value);
    890                         }
    891                     } catch (NumberFormatException e) {
    892                         log_warning("cue setting", name,
    893                                "is not numeric or percentage", value);
    894                     }
    895                 } else if (name.equals("position")) {
    896                     try {
    897                         mCue.mTextPosition = parseIntPercentage(value);
    898                     } catch (NumberFormatException e) {
    899                         log_warning("cue setting", name,
    900                                "is not numeric or percentage", value);
    901                     }
    902                 } else if (name.equals("size")) {
    903                     try {
    904                         mCue.mSize = parseIntPercentage(value);
    905                     } catch (NumberFormatException e) {
    906                         log_warning("cue setting", name,
    907                                "is not numeric or percentage", value);
    908                     }
    909                 } else if (name.equals("align")) {
    910                     if (value.equals("start")) {
    911                         mCue.mAlignment = TextTrackCue.ALIGNMENT_START;
    912                     } else if (value.equals("middle")) {
    913                         mCue.mAlignment = TextTrackCue.ALIGNMENT_MIDDLE;
    914                     } else if (value.equals("end")) {
    915                         mCue.mAlignment = TextTrackCue.ALIGNMENT_END;
    916                     } else if (value.equals("left")) {
    917                         mCue.mAlignment = TextTrackCue.ALIGNMENT_LEFT;
    918                     } else if (value.equals("right")) {
    919                         mCue.mAlignment = TextTrackCue.ALIGNMENT_RIGHT;
    920                     } else {
    921                         log_warning("cue setting", name, "has invalid value", value);
    922                         continue;
    923                     }
    924                 }
    925             }
    926 
    927             if (mCue.mLinePosition != null ||
    928                     mCue.mSize != 100 ||
    929                     (mCue.mWritingDirection !=
    930                         TextTrackCue.WRITING_DIRECTION_HORIZONTAL)) {
    931                 mCue.mRegionId = "";
    932             }
    933 
    934             mPhase = mParseCueText;
    935         }
    936     };
    937 
    938     /* also used for notes */
    939     final private Phase mParseCueText = new Phase() {
    940         @Override
    941         public void parse(String line) {
    942             if (line.length() == 0) {
    943                 yieldCue();
    944                 mPhase = mParseCueId;
    945                 return;
    946             } else if (mCue != null) {
    947                 mCueTexts.add(line);
    948             }
    949         }
    950     };
    951 
    952     private void log_warning(
    953             String nameType, String name, String message,
    954             String subMessage, String value) {
    955         Log.w(this.getClass().getName(), nameType + " '" + name + "' " +
    956                 message + " ('" + value + "' " + subMessage + ")");
    957     }
    958 
    959     private void log_warning(
    960             String nameType, String name, String message, String value) {
    961         Log.w(this.getClass().getName(), nameType + " '" + name + "' " +
    962                 message + " ('" + value + "')");
    963     }
    964 
    965     private void log_warning(String message, String value) {
    966         Log.w(this.getClass().getName(), message + " ('" + value + "')");
    967     }
    968 }
    969 
    970 /** @hide */
    971 interface WebVttCueListener {
    972     void onCueParsed(TextTrackCue cue);
    973     void onRegionParsed(TextTrackRegion region);
    974 }
    975 
    976 /** @hide */
    977 class WebVttTrack extends SubtitleTrack implements WebVttCueListener {
    978     private static final String TAG = "WebVttTrack";
    979 
    980     private final WebVttParser mParser = new WebVttParser(this);
    981     private final UnstyledTextExtractor mExtractor =
    982         new UnstyledTextExtractor();
    983     private final Tokenizer mTokenizer = new Tokenizer(mExtractor);
    984     private final Vector<Long> mTimestamps = new Vector<Long>();
    985     private final WebVttRenderingWidget mRenderingWidget;
    986 
    987     private final Map<String, TextTrackRegion> mRegions =
    988         new HashMap<String, TextTrackRegion>();
    989     private Long mCurrentRunID;
    990 
    991     WebVttTrack(WebVttRenderingWidget renderingWidget, MediaFormat format) {
    992         super(format);
    993 
    994         mRenderingWidget = renderingWidget;
    995     }
    996 
    997     @Override
    998     public WebVttRenderingWidget getRenderingWidget() {
    999         return mRenderingWidget;
   1000     }
   1001 
   1002     @Override
   1003     public void onData(String data, boolean eos, long runID) {
   1004         // implement intermixing restriction for WebVTT only for now
   1005         synchronized(mParser) {
   1006             if (mCurrentRunID != null && runID != mCurrentRunID) {
   1007                 throw new IllegalStateException(
   1008                         "Run #" + mCurrentRunID +
   1009                         " in progress.  Cannot process run #" + runID);
   1010             }
   1011             mCurrentRunID = runID;
   1012             mParser.parse(data);
   1013             if (eos) {
   1014                 finishedRun(runID);
   1015                 mParser.eos();
   1016                 mRegions.clear();
   1017                 mCurrentRunID = null;
   1018             }
   1019         }
   1020     }
   1021 
   1022     @Override
   1023     public void onCueParsed(TextTrackCue cue) {
   1024         synchronized (mParser) {
   1025             // resolve region
   1026             if (cue.mRegionId.length() != 0) {
   1027                 cue.mRegion = mRegions.get(cue.mRegionId);
   1028             }
   1029 
   1030             if (DEBUG) Log.v(TAG, "adding cue " + cue);
   1031 
   1032             // tokenize text track string-lines into lines of spans
   1033             mTokenizer.reset();
   1034             for (String s: cue.mStrings) {
   1035                 mTokenizer.tokenize(s);
   1036             }
   1037             cue.mLines = mExtractor.getText();
   1038             if (DEBUG) Log.v(TAG, cue.appendLinesToBuilder(
   1039                     cue.appendStringsToBuilder(
   1040                         new StringBuilder()).append(" simplified to: "))
   1041                             .toString());
   1042 
   1043             // extract inner timestamps
   1044             for (TextTrackCueSpan[] line: cue.mLines) {
   1045                 for (TextTrackCueSpan span: line) {
   1046                     if (span.mTimestampMs > cue.mStartTimeMs &&
   1047                             span.mTimestampMs < cue.mEndTimeMs &&
   1048                             !mTimestamps.contains(span.mTimestampMs)) {
   1049                         mTimestamps.add(span.mTimestampMs);
   1050                     }
   1051                 }
   1052             }
   1053 
   1054             if (mTimestamps.size() > 0) {
   1055                 cue.mInnerTimesMs = new long[mTimestamps.size()];
   1056                 for (int ix=0; ix < mTimestamps.size(); ++ix) {
   1057                     cue.mInnerTimesMs[ix] = mTimestamps.get(ix);
   1058                 }
   1059                 mTimestamps.clear();
   1060             } else {
   1061                 cue.mInnerTimesMs = null;
   1062             }
   1063 
   1064             cue.mRunID = mCurrentRunID;
   1065         }
   1066 
   1067         addCue(cue);
   1068     }
   1069 
   1070     @Override
   1071     public void onRegionParsed(TextTrackRegion region) {
   1072         synchronized(mParser) {
   1073             mRegions.put(region.mId, region);
   1074         }
   1075     }
   1076 
   1077     @Override
   1078     public void updateView(Vector<SubtitleTrack.Cue> activeCues) {
   1079         if (!mVisible) {
   1080             // don't keep the state if we are not visible
   1081             return;
   1082         }
   1083 
   1084         if (DEBUG && mTimeProvider != null) {
   1085             try {
   1086                 Log.d(TAG, "at " +
   1087                         (mTimeProvider.getCurrentTimeUs(false, true) / 1000) +
   1088                         " ms the active cues are:");
   1089             } catch (IllegalStateException e) {
   1090                 Log.d(TAG, "at (illegal state) the active cues are:");
   1091             }
   1092         }
   1093 
   1094         mRenderingWidget.setActiveCues(activeCues);
   1095     }
   1096 }
   1097 
   1098 /**
   1099  * Widget capable of rendering WebVTT captions.
   1100  *
   1101  * @hide
   1102  */
   1103 class WebVttRenderingWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
   1104     private static final boolean DEBUG = false;
   1105     private static final int DEBUG_REGION_BACKGROUND = 0x800000FF;
   1106     private static final int DEBUG_CUE_BACKGROUND = 0x80FF0000;
   1107 
   1108     /** WebVtt specifies line height as 5.3% of the viewport height. */
   1109     private static final float LINE_HEIGHT_RATIO = 0.0533f;
   1110 
   1111     /** Map of active regions, used to determine enter/exit. */
   1112     private final ArrayMap<TextTrackRegion, RegionLayout> mRegionBoxes =
   1113             new ArrayMap<TextTrackRegion, RegionLayout>();
   1114 
   1115     /** Map of active cues, used to determine enter/exit. */
   1116     private final ArrayMap<TextTrackCue, CueLayout> mCueBoxes =
   1117             new ArrayMap<TextTrackCue, CueLayout>();
   1118 
   1119     /** Captioning manager, used to obtain and track caption properties. */
   1120     private final CaptioningManager mManager;
   1121 
   1122     /** Callback for rendering changes. */
   1123     private OnChangedListener mListener;
   1124 
   1125     /** Current caption style. */
   1126     private CaptionStyle mCaptionStyle;
   1127 
   1128     /** Current font size, computed from font scaling factor and height. */
   1129     private float mFontSize;
   1130 
   1131     /** Whether a caption style change listener is registered. */
   1132     private boolean mHasChangeListener;
   1133 
   1134     public WebVttRenderingWidget(Context context) {
   1135         this(context, null);
   1136     }
   1137 
   1138     public WebVttRenderingWidget(Context context, AttributeSet attrs) {
   1139         this(context, null, 0);
   1140     }
   1141 
   1142     public WebVttRenderingWidget(Context context, AttributeSet attrs, int defStyle) {
   1143         super(context, attrs, defStyle);
   1144 
   1145         // Cannot render text over video when layer type is hardware.
   1146         setLayerType(View.LAYER_TYPE_SOFTWARE, null);
   1147 
   1148         mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
   1149         mCaptionStyle = mManager.getUserStyle();
   1150         mFontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
   1151     }
   1152 
   1153     @Override
   1154     public void setSize(int width, int height) {
   1155         final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
   1156         final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
   1157 
   1158         measure(widthSpec, heightSpec);
   1159         layout(0, 0, width, height);
   1160     }
   1161 
   1162     @Override
   1163     public void onAttachedToWindow() {
   1164         super.onAttachedToWindow();
   1165 
   1166         manageChangeListener();
   1167     }
   1168 
   1169     @Override
   1170     public void onDetachedFromWindow() {
   1171         super.onDetachedFromWindow();
   1172 
   1173         manageChangeListener();
   1174     }
   1175 
   1176     @Override
   1177     public void setOnChangedListener(OnChangedListener listener) {
   1178         mListener = listener;
   1179     }
   1180 
   1181     @Override
   1182     public void setVisible(boolean visible) {
   1183         if (visible) {
   1184             setVisibility(View.VISIBLE);
   1185         } else {
   1186             setVisibility(View.GONE);
   1187         }
   1188 
   1189         manageChangeListener();
   1190     }
   1191 
   1192     /**
   1193      * Manages whether this renderer is listening for caption style changes.
   1194      */
   1195     private void manageChangeListener() {
   1196         final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
   1197         if (mHasChangeListener != needsListener) {
   1198             mHasChangeListener = needsListener;
   1199 
   1200             if (needsListener) {
   1201                 mManager.addCaptioningChangeListener(mCaptioningListener);
   1202 
   1203                 final CaptionStyle captionStyle = mManager.getUserStyle();
   1204                 final float fontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
   1205                 setCaptionStyle(captionStyle, fontSize);
   1206             } else {
   1207                 mManager.removeCaptioningChangeListener(mCaptioningListener);
   1208             }
   1209         }
   1210     }
   1211 
   1212     public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) {
   1213         final Context context = getContext();
   1214         final CaptionStyle captionStyle = mCaptionStyle;
   1215         final float fontSize = mFontSize;
   1216 
   1217         prepForPrune();
   1218 
   1219         // Ensure we have all necessary cue and region boxes.
   1220         final int count = activeCues.size();
   1221         for (int i = 0; i < count; i++) {
   1222             final TextTrackCue cue = (TextTrackCue) activeCues.get(i);
   1223             final TextTrackRegion region = cue.mRegion;
   1224             if (region != null) {
   1225                 RegionLayout regionBox = mRegionBoxes.get(region);
   1226                 if (regionBox == null) {
   1227                     regionBox = new RegionLayout(context, region, captionStyle, fontSize);
   1228                     mRegionBoxes.put(region, regionBox);
   1229                     addView(regionBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   1230                 }
   1231                 regionBox.put(cue);
   1232             } else {
   1233                 CueLayout cueBox = mCueBoxes.get(cue);
   1234                 if (cueBox == null) {
   1235                     cueBox = new CueLayout(context, cue, captionStyle, fontSize);
   1236                     mCueBoxes.put(cue, cueBox);
   1237                     addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   1238                 }
   1239                 cueBox.update();
   1240                 cueBox.setOrder(i);
   1241             }
   1242         }
   1243 
   1244         prune();
   1245 
   1246         // Force measurement and layout.
   1247         final int width = getWidth();
   1248         final int height = getHeight();
   1249         setSize(width, height);
   1250 
   1251         if (mListener != null) {
   1252             mListener.onChanged(this);
   1253         }
   1254     }
   1255 
   1256     private void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
   1257         mCaptionStyle = captionStyle;
   1258         mFontSize = fontSize;
   1259 
   1260         final int cueCount = mCueBoxes.size();
   1261         for (int i = 0; i < cueCount; i++) {
   1262             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1263             cueBox.setCaptionStyle(captionStyle, fontSize);
   1264         }
   1265 
   1266         final int regionCount = mRegionBoxes.size();
   1267         for (int i = 0; i < regionCount; i++) {
   1268             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1269             regionBox.setCaptionStyle(captionStyle, fontSize);
   1270         }
   1271     }
   1272 
   1273     /**
   1274      * Remove inactive cues and regions.
   1275      */
   1276     private void prune() {
   1277         int regionCount = mRegionBoxes.size();
   1278         for (int i = 0; i < regionCount; i++) {
   1279             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1280             if (regionBox.prune()) {
   1281                 removeView(regionBox);
   1282                 mRegionBoxes.removeAt(i);
   1283                 regionCount--;
   1284                 i--;
   1285             }
   1286         }
   1287 
   1288         int cueCount = mCueBoxes.size();
   1289         for (int i = 0; i < cueCount; i++) {
   1290             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1291             if (!cueBox.isActive()) {
   1292                 removeView(cueBox);
   1293                 mCueBoxes.removeAt(i);
   1294                 cueCount--;
   1295                 i--;
   1296             }
   1297         }
   1298     }
   1299 
   1300     /**
   1301      * Reset active cues and regions.
   1302      */
   1303     private void prepForPrune() {
   1304         final int regionCount = mRegionBoxes.size();
   1305         for (int i = 0; i < regionCount; i++) {
   1306             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1307             regionBox.prepForPrune();
   1308         }
   1309 
   1310         final int cueCount = mCueBoxes.size();
   1311         for (int i = 0; i < cueCount; i++) {
   1312             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1313             cueBox.prepForPrune();
   1314         }
   1315     }
   1316 
   1317     @Override
   1318     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1319         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   1320 
   1321         final int regionCount = mRegionBoxes.size();
   1322         for (int i = 0; i < regionCount; i++) {
   1323             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1324             regionBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
   1325         }
   1326 
   1327         final int cueCount = mCueBoxes.size();
   1328         for (int i = 0; i < cueCount; i++) {
   1329             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1330             cueBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
   1331         }
   1332     }
   1333 
   1334     @Override
   1335     protected void onLayout(boolean changed, int l, int t, int r, int b) {
   1336         final int viewportWidth = r - l;
   1337         final int viewportHeight = b - t;
   1338 
   1339         setCaptionStyle(mCaptionStyle,
   1340                 mManager.getFontScale() * LINE_HEIGHT_RATIO * viewportHeight);
   1341 
   1342         final int regionCount = mRegionBoxes.size();
   1343         for (int i = 0; i < regionCount; i++) {
   1344             final RegionLayout regionBox = mRegionBoxes.valueAt(i);
   1345             layoutRegion(viewportWidth, viewportHeight, regionBox);
   1346         }
   1347 
   1348         final int cueCount = mCueBoxes.size();
   1349         for (int i = 0; i < cueCount; i++) {
   1350             final CueLayout cueBox = mCueBoxes.valueAt(i);
   1351             layoutCue(viewportWidth, viewportHeight, cueBox);
   1352         }
   1353     }
   1354 
   1355     /**
   1356      * Lays out a region within the viewport. The region handles layout for
   1357      * contained cues.
   1358      */
   1359     private void layoutRegion(
   1360             int viewportWidth, int viewportHeight,
   1361             RegionLayout regionBox) {
   1362         final TextTrackRegion region = regionBox.getRegion();
   1363         final int regionHeight = regionBox.getMeasuredHeight();
   1364         final int regionWidth = regionBox.getMeasuredWidth();
   1365 
   1366         // TODO: Account for region anchor point.
   1367         final float x = region.mViewportAnchorPointX;
   1368         final float y = region.mViewportAnchorPointY;
   1369         final int left = (int) (x * (viewportWidth - regionWidth) / 100);
   1370         final int top = (int) (y * (viewportHeight - regionHeight) / 100);
   1371 
   1372         regionBox.layout(left, top, left + regionWidth, top + regionHeight);
   1373     }
   1374 
   1375     /**
   1376      * Lays out a cue within the viewport.
   1377      */
   1378     private void layoutCue(
   1379             int viewportWidth, int viewportHeight, CueLayout cueBox) {
   1380         final TextTrackCue cue = cueBox.getCue();
   1381         final int direction = getLayoutDirection();
   1382         final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);
   1383         final boolean cueSnapToLines = cue.mSnapToLines;
   1384 
   1385         int size = 100 * cueBox.getMeasuredWidth() / viewportWidth;
   1386 
   1387         // Determine raw x-position.
   1388         int xPosition;
   1389         switch (absAlignment) {
   1390             case TextTrackCue.ALIGNMENT_LEFT:
   1391                 xPosition = cue.mTextPosition;
   1392                 break;
   1393             case TextTrackCue.ALIGNMENT_RIGHT:
   1394                 xPosition = cue.mTextPosition - size;
   1395                 break;
   1396             case TextTrackCue.ALIGNMENT_MIDDLE:
   1397             default:
   1398                 xPosition = cue.mTextPosition - size / 2;
   1399                 break;
   1400         }
   1401 
   1402         // Adjust x-position for layout.
   1403         if (direction == LAYOUT_DIRECTION_RTL) {
   1404             xPosition = 100 - xPosition;
   1405         }
   1406 
   1407         // If the text track cue snap-to-lines flag is set, adjust
   1408         // x-position and size for padding. This is equivalent to placing the
   1409         // cue within the title-safe area.
   1410         if (cueSnapToLines) {
   1411             final int paddingLeft = 100 * getPaddingLeft() / viewportWidth;
   1412             final int paddingRight = 100 * getPaddingRight() / viewportWidth;
   1413             if (xPosition < paddingLeft && xPosition + size > paddingLeft) {
   1414                 xPosition += paddingLeft;
   1415                 size -= paddingLeft;
   1416             }
   1417             final float rightEdge = 100 - paddingRight;
   1418             if (xPosition < rightEdge && xPosition + size > rightEdge) {
   1419                 size -= paddingRight;
   1420             }
   1421         }
   1422 
   1423         // Compute absolute left position and width.
   1424         final int left = xPosition * viewportWidth / 100;
   1425         final int width = size * viewportWidth / 100;
   1426 
   1427         // Determine initial y-position.
   1428         final int yPosition = calculateLinePosition(cueBox);
   1429 
   1430         // Compute absolute final top position and height.
   1431         final int height = cueBox.getMeasuredHeight();
   1432         final int top;
   1433         if (yPosition < 0) {
   1434             // TODO: This needs to use the actual height of prior boxes.
   1435             top = viewportHeight + yPosition * height;
   1436         } else {
   1437             top = yPosition * (viewportHeight - height) / 100;
   1438         }
   1439 
   1440         // Layout cue in final position.
   1441         cueBox.layout(left, top, left + width, top + height);
   1442     }
   1443 
   1444     /**
   1445      * Calculates the line position for a cue.
   1446      * <p>
   1447      * If the resulting position is negative, it represents a bottom-aligned
   1448      * position relative to the number of active cues. Otherwise, it represents
   1449      * a percentage [0-100] of the viewport height.
   1450      */
   1451     private int calculateLinePosition(CueLayout cueBox) {
   1452         final TextTrackCue cue = cueBox.getCue();
   1453         final Integer linePosition = cue.mLinePosition;
   1454         final boolean snapToLines = cue.mSnapToLines;
   1455         final boolean autoPosition = (linePosition == null);
   1456 
   1457         if (!snapToLines && !autoPosition && (linePosition < 0 || linePosition > 100)) {
   1458             // Invalid line position defaults to 100.
   1459             return 100;
   1460         } else if (!autoPosition) {
   1461             // Use the valid, supplied line position.
   1462             return linePosition;
   1463         } else if (!snapToLines) {
   1464             // Automatic, non-snapped line position defaults to 100.
   1465             return 100;
   1466         } else {
   1467             // Automatic snapped line position uses active cue order.
   1468             return -(cueBox.mOrder + 1);
   1469         }
   1470     }
   1471 
   1472     /**
   1473      * Resolves cue alignment according to the specified layout direction.
   1474      */
   1475     private static int resolveCueAlignment(int layoutDirection, int alignment) {
   1476         switch (alignment) {
   1477             case TextTrackCue.ALIGNMENT_START:
   1478                 return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
   1479                         TextTrackCue.ALIGNMENT_LEFT : TextTrackCue.ALIGNMENT_RIGHT;
   1480             case TextTrackCue.ALIGNMENT_END:
   1481                 return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
   1482                         TextTrackCue.ALIGNMENT_RIGHT : TextTrackCue.ALIGNMENT_LEFT;
   1483         }
   1484         return alignment;
   1485     }
   1486 
   1487     private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
   1488         @Override
   1489         public void onFontScaleChanged(float fontScale) {
   1490             final float fontSize = fontScale * getHeight() * LINE_HEIGHT_RATIO;
   1491             setCaptionStyle(mCaptionStyle, fontSize);
   1492         }
   1493 
   1494         @Override
   1495         public void onUserStyleChanged(CaptionStyle userStyle) {
   1496             setCaptionStyle(userStyle, mFontSize);
   1497         }
   1498     };
   1499 
   1500     /**
   1501      * A text track region represents a portion of the video viewport and
   1502      * provides a rendering area for text track cues.
   1503      */
   1504     private static class RegionLayout extends LinearLayout {
   1505         private final ArrayList<CueLayout> mRegionCueBoxes = new ArrayList<CueLayout>();
   1506         private final TextTrackRegion mRegion;
   1507 
   1508         private CaptionStyle mCaptionStyle;
   1509         private float mFontSize;
   1510 
   1511         public RegionLayout(Context context, TextTrackRegion region, CaptionStyle captionStyle,
   1512                 float fontSize) {
   1513             super(context);
   1514 
   1515             mRegion = region;
   1516             mCaptionStyle = captionStyle;
   1517             mFontSize = fontSize;
   1518 
   1519             // TODO: Add support for vertical text
   1520             setOrientation(VERTICAL);
   1521 
   1522             if (DEBUG) {
   1523                 setBackgroundColor(DEBUG_REGION_BACKGROUND);
   1524             }
   1525         }
   1526 
   1527         public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
   1528             mCaptionStyle = captionStyle;
   1529             mFontSize = fontSize;
   1530 
   1531             final int cueCount = mRegionCueBoxes.size();
   1532             for (int i = 0; i < cueCount; i++) {
   1533                 final CueLayout cueBox = mRegionCueBoxes.get(i);
   1534                 cueBox.setCaptionStyle(captionStyle, fontSize);
   1535             }
   1536         }
   1537 
   1538         /**
   1539          * Performs the parent's measurement responsibilities, then
   1540          * automatically performs its own measurement.
   1541          */
   1542         public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
   1543             final TextTrackRegion region = mRegion;
   1544             final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
   1545             final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
   1546             final int width = (int) region.mWidth;
   1547 
   1548             // Determine the absolute maximum region size as the requested size.
   1549             final int size = width * specWidth / 100;
   1550 
   1551             widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
   1552             heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
   1553             measure(widthMeasureSpec, heightMeasureSpec);
   1554         }
   1555 
   1556         /**
   1557          * Prepares this region for pruning by setting all tracks as inactive.
   1558          * <p>
   1559          * Tracks that are added or updated using {@link #put(TextTrackCue)}
   1560          * after this calling this method will be marked as active.
   1561          */
   1562         public void prepForPrune() {
   1563             final int cueCount = mRegionCueBoxes.size();
   1564             for (int i = 0; i < cueCount; i++) {
   1565                 final CueLayout cueBox = mRegionCueBoxes.get(i);
   1566                 cueBox.prepForPrune();
   1567             }
   1568         }
   1569 
   1570         /**
   1571          * Adds a {@link TextTrackCue} to this region. If the track had already
   1572          * been added, updates its active state.
   1573          *
   1574          * @param cue
   1575          */
   1576         public void put(TextTrackCue cue) {
   1577             final int cueCount = mRegionCueBoxes.size();
   1578             for (int i = 0; i < cueCount; i++) {
   1579                 final CueLayout cueBox = mRegionCueBoxes.get(i);
   1580                 if (cueBox.getCue() == cue) {
   1581                     cueBox.update();
   1582                     return;
   1583                 }
   1584             }
   1585 
   1586             final CueLayout cueBox = new CueLayout(getContext(), cue, mCaptionStyle, mFontSize);
   1587             mRegionCueBoxes.add(cueBox);
   1588             addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   1589 
   1590             if (getChildCount() > mRegion.mLines) {
   1591                 removeViewAt(0);
   1592             }
   1593         }
   1594 
   1595         /**
   1596          * Remove all inactive tracks from this region.
   1597          *
   1598          * @return true if this region is empty and should be pruned
   1599          */
   1600         public boolean prune() {
   1601             int cueCount = mRegionCueBoxes.size();
   1602             for (int i = 0; i < cueCount; i++) {
   1603                 final CueLayout cueBox = mRegionCueBoxes.get(i);
   1604                 if (!cueBox.isActive()) {
   1605                     mRegionCueBoxes.remove(i);
   1606                     removeView(cueBox);
   1607                     cueCount--;
   1608                     i--;
   1609                 }
   1610             }
   1611 
   1612             return mRegionCueBoxes.isEmpty();
   1613         }
   1614 
   1615         /**
   1616          * @return the region data backing this layout
   1617          */
   1618         public TextTrackRegion getRegion() {
   1619             return mRegion;
   1620         }
   1621     }
   1622 
   1623     /**
   1624      * A text track cue is the unit of time-sensitive data in a text track,
   1625      * corresponding for instance for subtitles and captions to the text that
   1626      * appears at a particular time and disappears at another time.
   1627      * <p>
   1628      * A single cue may contain multiple {@link SpanLayout}s, each representing a
   1629      * single line of text.
   1630      */
   1631     private static class CueLayout extends LinearLayout {
   1632         public final TextTrackCue mCue;
   1633 
   1634         private CaptionStyle mCaptionStyle;
   1635         private float mFontSize;
   1636 
   1637         private boolean mActive;
   1638         private int mOrder;
   1639 
   1640         public CueLayout(
   1641                 Context context, TextTrackCue cue, CaptionStyle captionStyle, float fontSize) {
   1642             super(context);
   1643 
   1644             mCue = cue;
   1645             mCaptionStyle = captionStyle;
   1646             mFontSize = fontSize;
   1647 
   1648             // TODO: Add support for vertical text.
   1649             final boolean horizontal = cue.mWritingDirection
   1650                     == TextTrackCue.WRITING_DIRECTION_HORIZONTAL;
   1651             setOrientation(horizontal ? VERTICAL : HORIZONTAL);
   1652 
   1653             switch (cue.mAlignment) {
   1654                 case TextTrackCue.ALIGNMENT_END:
   1655                     setGravity(Gravity.END);
   1656                     break;
   1657                 case TextTrackCue.ALIGNMENT_LEFT:
   1658                     setGravity(Gravity.LEFT);
   1659                     break;
   1660                 case TextTrackCue.ALIGNMENT_MIDDLE:
   1661                     setGravity(horizontal
   1662                             ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL);
   1663                     break;
   1664                 case TextTrackCue.ALIGNMENT_RIGHT:
   1665                     setGravity(Gravity.RIGHT);
   1666                     break;
   1667                 case TextTrackCue.ALIGNMENT_START:
   1668                     setGravity(Gravity.START);
   1669                     break;
   1670             }
   1671 
   1672             if (DEBUG) {
   1673                 setBackgroundColor(DEBUG_CUE_BACKGROUND);
   1674             }
   1675 
   1676             update();
   1677         }
   1678 
   1679         public void setCaptionStyle(CaptionStyle style, float fontSize) {
   1680             mCaptionStyle = style;
   1681             mFontSize = fontSize;
   1682 
   1683             final int n = getChildCount();
   1684             for (int i = 0; i < n; i++) {
   1685                 final View child = getChildAt(i);
   1686                 if (child instanceof SpanLayout) {
   1687                     ((SpanLayout) child).setCaptionStyle(style, fontSize);
   1688                 }
   1689             }
   1690         }
   1691 
   1692         public void prepForPrune() {
   1693             mActive = false;
   1694         }
   1695 
   1696         public void update() {
   1697             mActive = true;
   1698 
   1699             removeAllViews();
   1700 
   1701             final int cueAlignment = resolveCueAlignment(getLayoutDirection(), mCue.mAlignment);
   1702             final Alignment alignment;
   1703             switch (cueAlignment) {
   1704                 case TextTrackCue.ALIGNMENT_LEFT:
   1705                     alignment = Alignment.ALIGN_LEFT;
   1706                     break;
   1707                 case TextTrackCue.ALIGNMENT_RIGHT:
   1708                     alignment = Alignment.ALIGN_RIGHT;
   1709                     break;
   1710                 case TextTrackCue.ALIGNMENT_MIDDLE:
   1711                 default:
   1712                     alignment = Alignment.ALIGN_CENTER;
   1713             }
   1714 
   1715             final CaptionStyle captionStyle = mCaptionStyle;
   1716             final float fontSize = mFontSize;
   1717             final TextTrackCueSpan[][] lines = mCue.mLines;
   1718             final int lineCount = lines.length;
   1719             for (int i = 0; i < lineCount; i++) {
   1720                 final SpanLayout lineBox = new SpanLayout(getContext(), lines[i]);
   1721                 lineBox.setAlignment(alignment);
   1722                 lineBox.setCaptionStyle(captionStyle, fontSize);
   1723 
   1724                 addView(lineBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   1725             }
   1726         }
   1727 
   1728         @Override
   1729         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1730             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   1731         }
   1732 
   1733         /**
   1734          * Performs the parent's measurement responsibilities, then
   1735          * automatically performs its own measurement.
   1736          */
   1737         public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
   1738             final TextTrackCue cue = mCue;
   1739             final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
   1740             final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
   1741             final int direction = getLayoutDirection();
   1742             final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);
   1743 
   1744             // Determine the maximum size of cue based on its starting position
   1745             // and the direction in which it grows.
   1746             final int maximumSize;
   1747             switch (absAlignment) {
   1748                 case TextTrackCue.ALIGNMENT_LEFT:
   1749                     maximumSize = 100 - cue.mTextPosition;
   1750                     break;
   1751                 case TextTrackCue.ALIGNMENT_RIGHT:
   1752                     maximumSize = cue.mTextPosition;
   1753                     break;
   1754                 case TextTrackCue.ALIGNMENT_MIDDLE:
   1755                     if (cue.mTextPosition <= 50) {
   1756                         maximumSize = cue.mTextPosition * 2;
   1757                     } else {
   1758                         maximumSize = (100 - cue.mTextPosition) * 2;
   1759                     }
   1760                     break;
   1761                 default:
   1762                     maximumSize = 0;
   1763             }
   1764 
   1765             // Determine absolute maximum cue size as the smaller of the
   1766             // requested size and the maximum theoretical size.
   1767             final int size = Math.min(cue.mSize, maximumSize) * specWidth / 100;
   1768             widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
   1769             heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
   1770             measure(widthMeasureSpec, heightMeasureSpec);
   1771         }
   1772 
   1773         /**
   1774          * Sets the order of this cue in the list of active cues.
   1775          *
   1776          * @param order the order of this cue in the list of active cues
   1777          */
   1778         public void setOrder(int order) {
   1779             mOrder = order;
   1780         }
   1781 
   1782         /**
   1783          * @return whether this cue is marked as active
   1784          */
   1785         public boolean isActive() {
   1786             return mActive;
   1787         }
   1788 
   1789         /**
   1790          * @return the cue data backing this layout
   1791          */
   1792         public TextTrackCue getCue() {
   1793             return mCue;
   1794         }
   1795     }
   1796 
   1797     /**
   1798      * A text track line represents a single line of text within a cue.
   1799      * <p>
   1800      * A single line may contain multiple spans, each representing a section of
   1801      * text that may be enabled or disabled at a particular time.
   1802      */
   1803     private static class SpanLayout extends SubtitleView {
   1804         private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
   1805         private final TextTrackCueSpan[] mSpans;
   1806 
   1807         public SpanLayout(Context context, TextTrackCueSpan[] spans) {
   1808             super(context);
   1809 
   1810             mSpans = spans;
   1811 
   1812             update();
   1813         }
   1814 
   1815         public void update() {
   1816             final SpannableStringBuilder builder = mBuilder;
   1817             final TextTrackCueSpan[] spans = mSpans;
   1818 
   1819             builder.clear();
   1820             builder.clearSpans();
   1821 
   1822             final int spanCount = spans.length;
   1823             for (int i = 0; i < spanCount; i++) {
   1824                 final TextTrackCueSpan span = spans[i];
   1825                 if (span.mEnabled) {
   1826                     builder.append(spans[i].mText);
   1827                 }
   1828             }
   1829 
   1830             setText(builder);
   1831         }
   1832 
   1833         public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
   1834             setBackgroundColor(captionStyle.backgroundColor);
   1835             setForegroundColor(captionStyle.foregroundColor);
   1836             setEdgeColor(captionStyle.edgeColor);
   1837             setEdgeType(captionStyle.edgeType);
   1838             setTypeface(captionStyle.getTypeface());
   1839             setTextSize(fontSize);
   1840         }
   1841     }
   1842 }
   1843