Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2014 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.content.res.Resources;
     21 import android.graphics.Canvas;
     22 import android.graphics.Color;
     23 import android.graphics.Paint;
     24 import android.graphics.Rect;
     25 import android.graphics.Typeface;
     26 import android.os.Parcel;
     27 import android.text.ParcelableSpan;
     28 import android.text.Spannable;
     29 import android.text.SpannableStringBuilder;
     30 import android.text.TextPaint;
     31 import android.text.TextUtils;
     32 import android.text.style.CharacterStyle;
     33 import android.text.style.StyleSpan;
     34 import android.text.style.UnderlineSpan;
     35 import android.text.style.UpdateAppearance;
     36 import android.util.AttributeSet;
     37 import android.util.Log;
     38 import android.util.TypedValue;
     39 import android.view.Gravity;
     40 import android.view.View;
     41 import android.view.ViewGroup;
     42 import android.view.accessibility.CaptioningManager;
     43 import android.view.accessibility.CaptioningManager.CaptionStyle;
     44 import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
     45 import android.widget.LinearLayout;
     46 import android.widget.TextView;
     47 
     48 import java.util.ArrayList;
     49 import java.util.Arrays;
     50 import java.util.Vector;
     51 
     52 /** @hide */
     53 public class ClosedCaptionRenderer extends SubtitleController.Renderer {
     54     private final Context mContext;
     55     private ClosedCaptionWidget mRenderingWidget;
     56 
     57     public ClosedCaptionRenderer(Context context) {
     58         mContext = context;
     59     }
     60 
     61     @Override
     62     public boolean supports(MediaFormat format) {
     63         if (format.containsKey(MediaFormat.KEY_MIME)) {
     64             return format.getString(MediaFormat.KEY_MIME).equals(
     65                     MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608);
     66         }
     67         return false;
     68     }
     69 
     70     @Override
     71     public SubtitleTrack createTrack(MediaFormat format) {
     72         if (mRenderingWidget == null) {
     73             mRenderingWidget = new ClosedCaptionWidget(mContext);
     74         }
     75         return new ClosedCaptionTrack(mRenderingWidget, format);
     76     }
     77 }
     78 
     79 /** @hide */
     80 class ClosedCaptionTrack extends SubtitleTrack {
     81     private final ClosedCaptionWidget mRenderingWidget;
     82     private final CCParser mCCParser;
     83 
     84     ClosedCaptionTrack(ClosedCaptionWidget renderingWidget, MediaFormat format) {
     85         super(format);
     86 
     87         mRenderingWidget = renderingWidget;
     88         mCCParser = new CCParser(renderingWidget);
     89     }
     90 
     91     @Override
     92     public void onData(byte[] data, boolean eos, long runID) {
     93         mCCParser.parse(data);
     94     }
     95 
     96     @Override
     97     public RenderingWidget getRenderingWidget() {
     98         return mRenderingWidget;
     99     }
    100 
    101     @Override
    102     public void updateView(Vector<Cue> activeCues) {
    103         // Overriding with NO-OP, CC rendering by-passes this
    104     }
    105 }
    106 
    107 /**
    108  * @hide
    109  *
    110  * CCParser processes CEA-608 closed caption data.
    111  *
    112  * It calls back into OnDisplayChangedListener upon
    113  * display change with styled text for rendering.
    114  *
    115  */
    116 class CCParser {
    117     public static final int MAX_ROWS = 15;
    118     public static final int MAX_COLS = 32;
    119 
    120     private static final String TAG = "CCParser";
    121     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    122 
    123     private static final int INVALID = -1;
    124 
    125     // EIA-CEA-608: Table 70 - Control Codes
    126     private static final int RCL = 0x20;
    127     private static final int BS  = 0x21;
    128     private static final int AOF = 0x22;
    129     private static final int AON = 0x23;
    130     private static final int DER = 0x24;
    131     private static final int RU2 = 0x25;
    132     private static final int RU3 = 0x26;
    133     private static final int RU4 = 0x27;
    134     private static final int FON = 0x28;
    135     private static final int RDC = 0x29;
    136     private static final int TR  = 0x2a;
    137     private static final int RTD = 0x2b;
    138     private static final int EDM = 0x2c;
    139     private static final int CR  = 0x2d;
    140     private static final int ENM = 0x2e;
    141     private static final int EOC = 0x2f;
    142 
    143     // Transparent Space
    144     private static final char TS = '\u00A0';
    145 
    146     // Captioning Modes
    147     private static final int MODE_UNKNOWN = 0;
    148     private static final int MODE_PAINT_ON = 1;
    149     private static final int MODE_ROLL_UP = 2;
    150     private static final int MODE_POP_ON = 3;
    151     private static final int MODE_TEXT = 4;
    152 
    153     private final DisplayListener mListener;
    154 
    155     private int mMode = MODE_PAINT_ON;
    156     private int mRollUpSize = 4;
    157 
    158     private CCMemory mDisplay = new CCMemory();
    159     private CCMemory mNonDisplay = new CCMemory();
    160     private CCMemory mTextMem = new CCMemory();
    161 
    162     CCParser(DisplayListener listener) {
    163         mListener = listener;
    164     }
    165 
    166     void parse(byte[] data) {
    167         CCData[] ccData = CCData.fromByteArray(data);
    168 
    169         for (int i = 0; i < ccData.length; i++) {
    170             if (DEBUG) {
    171                 Log.d(TAG, ccData[i].toString());
    172             }
    173 
    174             if (handleCtrlCode(ccData[i])
    175                     || handleTabOffsets(ccData[i])
    176                     || handlePACCode(ccData[i])
    177                     || handleMidRowCode(ccData[i])) {
    178                 continue;
    179             }
    180 
    181             handleDisplayableChars(ccData[i]);
    182         }
    183     }
    184 
    185     interface DisplayListener {
    186         public void onDisplayChanged(SpannableStringBuilder[] styledTexts);
    187         public CaptionStyle getCaptionStyle();
    188     }
    189 
    190     private CCMemory getMemory() {
    191         // get the CC memory to operate on for current mode
    192         switch (mMode) {
    193         case MODE_POP_ON:
    194             return mNonDisplay;
    195         case MODE_TEXT:
    196             // TODO(chz): support only caption mode for now,
    197             // in text mode, dump everything to text mem.
    198             return mTextMem;
    199         case MODE_PAINT_ON:
    200         case MODE_ROLL_UP:
    201             return mDisplay;
    202         default:
    203             Log.w(TAG, "unrecoginized mode: " + mMode);
    204         }
    205         return mDisplay;
    206     }
    207 
    208     private boolean handleDisplayableChars(CCData ccData) {
    209         if (!ccData.isDisplayableChar()) {
    210             return false;
    211         }
    212 
    213         // Extended char includes 1 automatic backspace
    214         if (ccData.isExtendedChar()) {
    215             getMemory().bs();
    216         }
    217 
    218         getMemory().writeText(ccData.getDisplayText());
    219 
    220         if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
    221             updateDisplay();
    222         }
    223 
    224         return true;
    225     }
    226 
    227     private boolean handleMidRowCode(CCData ccData) {
    228         StyleCode m = ccData.getMidRow();
    229         if (m != null) {
    230             getMemory().writeMidRowCode(m);
    231             return true;
    232         }
    233         return false;
    234     }
    235 
    236     private boolean handlePACCode(CCData ccData) {
    237         PAC pac = ccData.getPAC();
    238 
    239         if (pac != null) {
    240             if (mMode == MODE_ROLL_UP) {
    241                 getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
    242             }
    243             getMemory().writePAC(pac);
    244             return true;
    245         }
    246 
    247         return false;
    248     }
    249 
    250     private boolean handleTabOffsets(CCData ccData) {
    251         int tabs = ccData.getTabOffset();
    252 
    253         if (tabs > 0) {
    254             getMemory().tab(tabs);
    255             return true;
    256         }
    257 
    258         return false;
    259     }
    260 
    261     private boolean handleCtrlCode(CCData ccData) {
    262         int ctrlCode = ccData.getCtrlCode();
    263         switch(ctrlCode) {
    264         case RCL:
    265             // select pop-on style
    266             mMode = MODE_POP_ON;
    267             break;
    268         case BS:
    269             getMemory().bs();
    270             break;
    271         case DER:
    272             getMemory().der();
    273             break;
    274         case RU2:
    275         case RU3:
    276         case RU4:
    277             mRollUpSize = (ctrlCode - 0x23);
    278             // erase memory if currently in other style
    279             if (mMode != MODE_ROLL_UP) {
    280                 mDisplay.erase();
    281                 mNonDisplay.erase();
    282             }
    283             // select roll-up style
    284             mMode = MODE_ROLL_UP;
    285             break;
    286         case FON:
    287             Log.i(TAG, "Flash On");
    288             break;
    289         case RDC:
    290             // select paint-on style
    291             mMode = MODE_PAINT_ON;
    292             break;
    293         case TR:
    294             mMode = MODE_TEXT;
    295             mTextMem.erase();
    296             break;
    297         case RTD:
    298             mMode = MODE_TEXT;
    299             break;
    300         case EDM:
    301             // erase display memory
    302             mDisplay.erase();
    303             updateDisplay();
    304             break;
    305         case CR:
    306             if (mMode == MODE_ROLL_UP) {
    307                 getMemory().rollUp(mRollUpSize);
    308             } else {
    309                 getMemory().cr();
    310             }
    311             if (mMode == MODE_ROLL_UP) {
    312                 updateDisplay();
    313             }
    314             break;
    315         case ENM:
    316             // erase non-display memory
    317             mNonDisplay.erase();
    318             break;
    319         case EOC:
    320             // swap display/non-display memory
    321             swapMemory();
    322             // switch to pop-on style
    323             mMode = MODE_POP_ON;
    324             updateDisplay();
    325             break;
    326         case INVALID:
    327         default:
    328             // not handled
    329             return false;
    330         }
    331 
    332         // handled
    333         return true;
    334     }
    335 
    336     private void updateDisplay() {
    337         if (mListener != null) {
    338             CaptionStyle captionStyle = mListener.getCaptionStyle();
    339             mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
    340         }
    341     }
    342 
    343     private void swapMemory() {
    344         CCMemory temp = mDisplay;
    345         mDisplay = mNonDisplay;
    346         mNonDisplay = temp;
    347     }
    348 
    349     private static class StyleCode {
    350         static final int COLOR_WHITE = 0;
    351         static final int COLOR_GREEN = 1;
    352         static final int COLOR_BLUE = 2;
    353         static final int COLOR_CYAN = 3;
    354         static final int COLOR_RED = 4;
    355         static final int COLOR_YELLOW = 5;
    356         static final int COLOR_MAGENTA = 6;
    357         static final int COLOR_INVALID = 7;
    358 
    359         static final int STYLE_ITALICS   = 0x00000001;
    360         static final int STYLE_UNDERLINE = 0x00000002;
    361 
    362         static final String[] mColorMap = {
    363             "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
    364         };
    365 
    366         final int mStyle;
    367         final int mColor;
    368 
    369         static StyleCode fromByte(byte data2) {
    370             int style = 0;
    371             int color = (data2 >> 1) & 0x7;
    372 
    373             if ((data2 & 0x1) != 0) {
    374                 style |= STYLE_UNDERLINE;
    375             }
    376 
    377             if (color == COLOR_INVALID) {
    378                 // WHITE ITALICS
    379                 color = COLOR_WHITE;
    380                 style |= STYLE_ITALICS;
    381             }
    382 
    383             return new StyleCode(style, color);
    384         }
    385 
    386         StyleCode(int style, int color) {
    387             mStyle = style;
    388             mColor = color;
    389         }
    390 
    391         boolean isItalics() {
    392             return (mStyle & STYLE_ITALICS) != 0;
    393         }
    394 
    395         boolean isUnderline() {
    396             return (mStyle & STYLE_UNDERLINE) != 0;
    397         }
    398 
    399         int getColor() {
    400             return mColor;
    401         }
    402 
    403         @Override
    404         public String toString() {
    405             StringBuilder str = new StringBuilder();
    406             str.append("{");
    407             str.append(mColorMap[mColor]);
    408             if ((mStyle & STYLE_ITALICS) != 0) {
    409                 str.append(", ITALICS");
    410             }
    411             if ((mStyle & STYLE_UNDERLINE) != 0) {
    412                 str.append(", UNDERLINE");
    413             }
    414             str.append("}");
    415 
    416             return str.toString();
    417         }
    418     }
    419 
    420     private static class PAC extends StyleCode {
    421         final int mRow;
    422         final int mCol;
    423 
    424         static PAC fromBytes(byte data1, byte data2) {
    425             int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
    426             int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
    427             int style = 0;
    428             if ((data2 & 1) != 0) {
    429                 style |= STYLE_UNDERLINE;
    430             }
    431             if ((data2 & 0x10) != 0) {
    432                 // indent code
    433                 int indent = (data2 >> 1) & 0x7;
    434                 return new PAC(row, indent * 4, style, COLOR_WHITE);
    435             } else {
    436                 // style code
    437                 int color = (data2 >> 1) & 0x7;
    438 
    439                 if (color == COLOR_INVALID) {
    440                     // WHITE ITALICS
    441                     color = COLOR_WHITE;
    442                     style |= STYLE_ITALICS;
    443                 }
    444                 return new PAC(row, -1, style, color);
    445             }
    446         }
    447 
    448         PAC(int row, int col, int style, int color) {
    449             super(style, color);
    450             mRow = row;
    451             mCol = col;
    452         }
    453 
    454         boolean isIndentPAC() {
    455             return (mCol >= 0);
    456         }
    457 
    458         int getRow() {
    459             return mRow;
    460         }
    461 
    462         int getCol() {
    463             return mCol;
    464         }
    465 
    466         @Override
    467         public String toString() {
    468             return String.format("{%d, %d}, %s",
    469                     mRow, mCol, super.toString());
    470         }
    471     }
    472 
    473     /* CCLineBuilder keeps track of displayable chars, as well as
    474      * MidRow styles and PACs, for a single line of CC memory.
    475      *
    476      * It generates styled text via getStyledText() method.
    477      */
    478     private static class CCLineBuilder {
    479         private final StringBuilder mDisplayChars;
    480         private final StyleCode[] mMidRowStyles;
    481         private final StyleCode[] mPACStyles;
    482 
    483         CCLineBuilder(String str) {
    484             mDisplayChars = new StringBuilder(str);
    485             mMidRowStyles = new StyleCode[mDisplayChars.length()];
    486             mPACStyles = new StyleCode[mDisplayChars.length()];
    487         }
    488 
    489         void setCharAt(int index, char ch) {
    490             mDisplayChars.setCharAt(index, ch);
    491             mMidRowStyles[index] = null;
    492         }
    493 
    494         void setMidRowAt(int index, StyleCode m) {
    495             mDisplayChars.setCharAt(index, ' ');
    496             mMidRowStyles[index] = m;
    497         }
    498 
    499         void setPACAt(int index, PAC pac) {
    500             mPACStyles[index] = pac;
    501         }
    502 
    503         char charAt(int index) {
    504             return mDisplayChars.charAt(index);
    505         }
    506 
    507         int length() {
    508             return mDisplayChars.length();
    509         }
    510 
    511         void applyStyleSpan(
    512                 SpannableStringBuilder styledText,
    513                 StyleCode s, int start, int end) {
    514             if (s.isItalics()) {
    515                 styledText.setSpan(
    516                         new StyleSpan(android.graphics.Typeface.ITALIC),
    517                         start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    518             }
    519             if (s.isUnderline()) {
    520                 styledText.setSpan(
    521                         new UnderlineSpan(),
    522                         start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    523             }
    524         }
    525 
    526         SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
    527             SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
    528             int start = -1, next = 0;
    529             int styleStart = -1;
    530             StyleCode curStyle = null;
    531             while (next < mDisplayChars.length()) {
    532                 StyleCode newStyle = null;
    533                 if (mMidRowStyles[next] != null) {
    534                     // apply mid-row style change
    535                     newStyle = mMidRowStyles[next];
    536                 } else if (mPACStyles[next] != null
    537                     && (styleStart < 0 || start < 0)) {
    538                     // apply PAC style change, only if:
    539                     // 1. no style set, or
    540                     // 2. style set, but prev char is none-displayable
    541                     newStyle = mPACStyles[next];
    542                 }
    543                 if (newStyle != null) {
    544                     curStyle = newStyle;
    545                     if (styleStart >= 0 && start >= 0) {
    546                         applyStyleSpan(styledText, newStyle, styleStart, next);
    547                     }
    548                     styleStart = next;
    549                 }
    550 
    551                 if (mDisplayChars.charAt(next) != TS) {
    552                     if (start < 0) {
    553                         start = next;
    554                     }
    555                 } else if (start >= 0) {
    556                     int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
    557                     int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
    558                     styledText.setSpan(
    559                             new MutableBackgroundColorSpan(captionStyle.backgroundColor),
    560                             expandedStart, expandedEnd,
    561                             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    562                     if (styleStart >= 0) {
    563                         applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
    564                     }
    565                     start = -1;
    566                 }
    567                 next++;
    568             }
    569 
    570             return styledText;
    571         }
    572     }
    573 
    574     /*
    575      * CCMemory models a console-style display.
    576      */
    577     private static class CCMemory {
    578         private final String mBlankLine;
    579         private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
    580         private int mRow;
    581         private int mCol;
    582 
    583         CCMemory() {
    584             char[] blank = new char[MAX_COLS + 2];
    585             Arrays.fill(blank, TS);
    586             mBlankLine = new String(blank);
    587         }
    588 
    589         void erase() {
    590             // erase all lines
    591             for (int i = 0; i < mLines.length; i++) {
    592                 mLines[i] = null;
    593             }
    594             mRow = MAX_ROWS;
    595             mCol = 1;
    596         }
    597 
    598         void der() {
    599             if (mLines[mRow] != null) {
    600                 for (int i = 0; i < mCol; i++) {
    601                     if (mLines[mRow].charAt(i) != TS) {
    602                         for (int j = mCol; j < mLines[mRow].length(); j++) {
    603                             mLines[j].setCharAt(j, TS);
    604                         }
    605                         return;
    606                     }
    607                 }
    608                 mLines[mRow] = null;
    609             }
    610         }
    611 
    612         void tab(int tabs) {
    613             moveCursorByCol(tabs);
    614         }
    615 
    616         void bs() {
    617             moveCursorByCol(-1);
    618             if (mLines[mRow] != null) {
    619                 mLines[mRow].setCharAt(mCol, TS);
    620                 if (mCol == MAX_COLS - 1) {
    621                     // Spec recommendation:
    622                     // if cursor was at col 32, move cursor
    623                     // back to col 31 and erase both col 31&32
    624                     mLines[mRow].setCharAt(MAX_COLS, TS);
    625                 }
    626             }
    627         }
    628 
    629         void cr() {
    630             moveCursorTo(mRow + 1, 1);
    631         }
    632 
    633         void rollUp(int windowSize) {
    634             int i;
    635             for (i = 0; i <= mRow - windowSize; i++) {
    636                 mLines[i] = null;
    637             }
    638             int startRow = mRow - windowSize + 1;
    639             if (startRow < 1) {
    640                 startRow = 1;
    641             }
    642             for (i = startRow; i < mRow; i++) {
    643                 mLines[i] = mLines[i + 1];
    644             }
    645             for (i = mRow; i < mLines.length; i++) {
    646                 // clear base row
    647                 mLines[i] = null;
    648             }
    649             // default to col 1, in case PAC is not sent
    650             mCol = 1;
    651         }
    652 
    653         void writeText(String text) {
    654             for (int i = 0; i < text.length(); i++) {
    655                 getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
    656                 moveCursorByCol(1);
    657             }
    658         }
    659 
    660         void writeMidRowCode(StyleCode m) {
    661             getLineBuffer(mRow).setMidRowAt(mCol, m);
    662             moveCursorByCol(1);
    663         }
    664 
    665         void writePAC(PAC pac) {
    666             if (pac.isIndentPAC()) {
    667                 moveCursorTo(pac.getRow(), pac.getCol());
    668             } else {
    669                 moveCursorTo(pac.getRow(), 1);
    670             }
    671             getLineBuffer(mRow).setPACAt(mCol, pac);
    672         }
    673 
    674         SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
    675             ArrayList<SpannableStringBuilder> rows =
    676                     new ArrayList<SpannableStringBuilder>(MAX_ROWS);
    677             for (int i = 1; i <= MAX_ROWS; i++) {
    678                 rows.add(mLines[i] != null ?
    679                         mLines[i].getStyledText(captionStyle) : null);
    680             }
    681             return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
    682         }
    683 
    684         private static int clamp(int x, int min, int max) {
    685             return x < min ? min : (x > max ? max : x);
    686         }
    687 
    688         private void moveCursorTo(int row, int col) {
    689             mRow = clamp(row, 1, MAX_ROWS);
    690             mCol = clamp(col, 1, MAX_COLS);
    691         }
    692 
    693         private void moveCursorToRow(int row) {
    694             mRow = clamp(row, 1, MAX_ROWS);
    695         }
    696 
    697         private void moveCursorByCol(int col) {
    698             mCol = clamp(mCol + col, 1, MAX_COLS);
    699         }
    700 
    701         private void moveBaselineTo(int baseRow, int windowSize) {
    702             if (mRow == baseRow) {
    703                 return;
    704             }
    705             int actualWindowSize = windowSize;
    706             if (baseRow < actualWindowSize) {
    707                 actualWindowSize = baseRow;
    708             }
    709             if (mRow < actualWindowSize) {
    710                 actualWindowSize = mRow;
    711             }
    712 
    713             int i;
    714             if (baseRow < mRow) {
    715                 // copy from bottom to top row
    716                 for (i = actualWindowSize - 1; i >= 0; i--) {
    717                     mLines[baseRow - i] = mLines[mRow - i];
    718                 }
    719             } else {
    720                 // copy from top to bottom row
    721                 for (i = 0; i < actualWindowSize; i++) {
    722                     mLines[baseRow - i] = mLines[mRow - i];
    723                 }
    724             }
    725             // clear rest of the rows
    726             for (i = 0; i <= baseRow - windowSize; i++) {
    727                 mLines[i] = null;
    728             }
    729             for (i = baseRow + 1; i < mLines.length; i++) {
    730                 mLines[i] = null;
    731             }
    732         }
    733 
    734         private CCLineBuilder getLineBuffer(int row) {
    735             if (mLines[row] == null) {
    736                 mLines[row] = new CCLineBuilder(mBlankLine);
    737             }
    738             return mLines[row];
    739         }
    740     }
    741 
    742     /*
    743      * CCData parses the raw CC byte pair into displayable chars,
    744      * misc control codes, Mid-Row or Preamble Address Codes.
    745      */
    746     private static class CCData {
    747         private final byte mType;
    748         private final byte mData1;
    749         private final byte mData2;
    750 
    751         private static final String[] mCtrlCodeMap = {
    752             "RCL", "BS" , "AOF", "AON",
    753             "DER", "RU2", "RU3", "RU4",
    754             "FON", "RDC", "TR" , "RTD",
    755             "EDM", "CR" , "ENM", "EOC",
    756         };
    757 
    758         private static final String[] mSpecialCharMap = {
    759             "\u00AE",
    760             "\u00B0",
    761             "\u00BD",
    762             "\u00BF",
    763             "\u2122",
    764             "\u00A2",
    765             "\u00A3",
    766             "\u266A", // Eighth note
    767             "\u00E0",
    768             "\u00A0", // Transparent space
    769             "\u00E8",
    770             "\u00E2",
    771             "\u00EA",
    772             "\u00EE",
    773             "\u00F4",
    774             "\u00FB",
    775         };
    776 
    777         private static final String[] mSpanishCharMap = {
    778             // Spanish and misc chars
    779             "\u00C1", // A
    780             "\u00C9", // E
    781             "\u00D3", // I
    782             "\u00DA", // O
    783             "\u00DC", // U
    784             "\u00FC", // u
    785             "\u2018", // opening single quote
    786             "\u00A1", // inverted exclamation mark
    787             "*",
    788             "'",
    789             "\u2014", // em dash
    790             "\u00A9", // Copyright
    791             "\u2120", // Servicemark
    792             "\u2022", // round bullet
    793             "\u201C", // opening double quote
    794             "\u201D", // closing double quote
    795             // French
    796             "\u00C0",
    797             "\u00C2",
    798             "\u00C7",
    799             "\u00C8",
    800             "\u00CA",
    801             "\u00CB",
    802             "\u00EB",
    803             "\u00CE",
    804             "\u00CF",
    805             "\u00EF",
    806             "\u00D4",
    807             "\u00D9",
    808             "\u00F9",
    809             "\u00DB",
    810             "\u00AB",
    811             "\u00BB"
    812         };
    813 
    814         private static final String[] mProtugueseCharMap = {
    815             // Portuguese
    816             "\u00C3",
    817             "\u00E3",
    818             "\u00CD",
    819             "\u00CC",
    820             "\u00EC",
    821             "\u00D2",
    822             "\u00F2",
    823             "\u00D5",
    824             "\u00F5",
    825             "{",
    826             "}",
    827             "\\",
    828             "^",
    829             "_",
    830             "|",
    831             "~",
    832             // German and misc chars
    833             "\u00C4",
    834             "\u00E4",
    835             "\u00D6",
    836             "\u00F6",
    837             "\u00DF",
    838             "\u00A5",
    839             "\u00A4",
    840             "\u2502", // vertical bar
    841             "\u00C5",
    842             "\u00E5",
    843             "\u00D8",
    844             "\u00F8",
    845             "\u250C", // top-left corner
    846             "\u2510", // top-right corner
    847             "\u2514", // lower-left corner
    848             "\u2518", // lower-right corner
    849         };
    850 
    851         static CCData[] fromByteArray(byte[] data) {
    852             CCData[] ccData = new CCData[data.length / 3];
    853 
    854             for (int i = 0; i < ccData.length; i++) {
    855                 ccData[i] = new CCData(
    856                         data[i * 3],
    857                         data[i * 3 + 1],
    858                         data[i * 3 + 2]);
    859             }
    860 
    861             return ccData;
    862         }
    863 
    864         CCData(byte type, byte data1, byte data2) {
    865             mType = type;
    866             mData1 = data1;
    867             mData2 = data2;
    868         }
    869 
    870         int getCtrlCode() {
    871             if ((mData1 == 0x14 || mData1 == 0x1c)
    872                     && mData2 >= 0x20 && mData2 <= 0x2f) {
    873                 return mData2;
    874             }
    875             return INVALID;
    876         }
    877 
    878         StyleCode getMidRow() {
    879             // only support standard Mid-row codes, ignore
    880             // optional background/foreground mid-row codes
    881             if ((mData1 == 0x11 || mData1 == 0x19)
    882                     && mData2 >= 0x20 && mData2 <= 0x2f) {
    883                 return StyleCode.fromByte(mData2);
    884             }
    885             return null;
    886         }
    887 
    888         PAC getPAC() {
    889             if ((mData1 & 0x70) == 0x10
    890                     && (mData2 & 0x40) == 0x40
    891                     && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
    892                 return PAC.fromBytes(mData1, mData2);
    893             }
    894             return null;
    895         }
    896 
    897         int getTabOffset() {
    898             if ((mData1 == 0x17 || mData1 == 0x1f)
    899                     && mData2 >= 0x21 && mData2 <= 0x23) {
    900                 return mData2 & 0x3;
    901             }
    902             return 0;
    903         }
    904 
    905         boolean isDisplayableChar() {
    906             return isBasicChar() || isSpecialChar() || isExtendedChar();
    907         }
    908 
    909         String getDisplayText() {
    910             String str = getBasicChars();
    911 
    912             if (str == null) {
    913                 str =  getSpecialChar();
    914 
    915                 if (str == null) {
    916                     str = getExtendedChar();
    917                 }
    918             }
    919 
    920             return str;
    921         }
    922 
    923         private String ctrlCodeToString(int ctrlCode) {
    924             return mCtrlCodeMap[ctrlCode - 0x20];
    925         }
    926 
    927         private boolean isBasicChar() {
    928             return mData1 >= 0x20 && mData1 <= 0x7f;
    929         }
    930 
    931         private boolean isSpecialChar() {
    932             return ((mData1 == 0x11 || mData1 == 0x19)
    933                     && mData2 >= 0x30 && mData2 <= 0x3f);
    934         }
    935 
    936         private boolean isExtendedChar() {
    937             return ((mData1 == 0x12 || mData1 == 0x1A
    938                     || mData1 == 0x13 || mData1 == 0x1B)
    939                     && mData2 >= 0x20 && mData2 <= 0x3f);
    940         }
    941 
    942         private char getBasicChar(byte data) {
    943             char c;
    944             // replace the non-ASCII ones
    945             switch (data) {
    946                 case 0x2A: c = '\u00E1'; break;
    947                 case 0x5C: c = '\u00E9'; break;
    948                 case 0x5E: c = '\u00ED'; break;
    949                 case 0x5F: c = '\u00F3'; break;
    950                 case 0x60: c = '\u00FA'; break;
    951                 case 0x7B: c = '\u00E7'; break;
    952                 case 0x7C: c = '\u00F7'; break;
    953                 case 0x7D: c = '\u00D1'; break;
    954                 case 0x7E: c = '\u00F1'; break;
    955                 case 0x7F: c = '\u2588'; break; // Full block
    956                 default: c = (char) data; break;
    957             }
    958             return c;
    959         }
    960 
    961         private String getBasicChars() {
    962             if (mData1 >= 0x20 && mData1 <= 0x7f) {
    963                 StringBuilder builder = new StringBuilder(2);
    964                 builder.append(getBasicChar(mData1));
    965                 if (mData2 >= 0x20 && mData2 <= 0x7f) {
    966                     builder.append(getBasicChar(mData2));
    967                 }
    968                 return builder.toString();
    969             }
    970 
    971             return null;
    972         }
    973 
    974         private String getSpecialChar() {
    975             if ((mData1 == 0x11 || mData1 == 0x19)
    976                     && mData2 >= 0x30 && mData2 <= 0x3f) {
    977                 return mSpecialCharMap[mData2 - 0x30];
    978             }
    979 
    980             return null;
    981         }
    982 
    983         private String getExtendedChar() {
    984             if ((mData1 == 0x12 || mData1 == 0x1A)
    985                     && mData2 >= 0x20 && mData2 <= 0x3f){
    986                 // 1 Spanish/French char
    987                 return mSpanishCharMap[mData2 - 0x20];
    988             } else if ((mData1 == 0x13 || mData1 == 0x1B)
    989                     && mData2 >= 0x20 && mData2 <= 0x3f){
    990                 // 1 Portuguese/German/Danish char
    991                 return mProtugueseCharMap[mData2 - 0x20];
    992             }
    993 
    994             return null;
    995         }
    996 
    997         @Override
    998         public String toString() {
    999             String str;
   1000 
   1001             if (mData1 < 0x10 && mData2 < 0x10) {
   1002                 // Null Pad, ignore
   1003                 return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
   1004             }
   1005 
   1006             int ctrlCode = getCtrlCode();
   1007             if (ctrlCode != INVALID) {
   1008                 return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
   1009             }
   1010 
   1011             int tabOffset = getTabOffset();
   1012             if (tabOffset > 0) {
   1013                 return String.format("[%d]Tab%d", mType, tabOffset);
   1014             }
   1015 
   1016             PAC pac = getPAC();
   1017             if (pac != null) {
   1018                 return String.format("[%d]PAC: %s", mType, pac.toString());
   1019             }
   1020 
   1021             StyleCode m = getMidRow();
   1022             if (m != null) {
   1023                 return String.format("[%d]Mid-row: %s", mType, m.toString());
   1024             }
   1025 
   1026             if (isDisplayableChar()) {
   1027                 return String.format("[%d]Displayable: %s (%02x %02x)",
   1028                         mType, getDisplayText(), mData1, mData2);
   1029             }
   1030 
   1031             return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
   1032         }
   1033     }
   1034 }
   1035 
   1036 /**
   1037  * @hide
   1038  *
   1039  * MutableBackgroundColorSpan
   1040  *
   1041  * This is a mutable version of BackgroundSpan to facilitate text
   1042  * rendering with edge styles.
   1043  *
   1044  */
   1045 class MutableBackgroundColorSpan extends CharacterStyle
   1046         implements UpdateAppearance, ParcelableSpan {
   1047     private int mColor;
   1048 
   1049     public MutableBackgroundColorSpan(int color) {
   1050         mColor = color;
   1051     }
   1052     public MutableBackgroundColorSpan(Parcel src) {
   1053         mColor = src.readInt();
   1054     }
   1055     public void setBackgroundColor(int color) {
   1056         mColor = color;
   1057     }
   1058     public int getBackgroundColor() {
   1059         return mColor;
   1060     }
   1061     @Override
   1062     public int getSpanTypeId() {
   1063         return TextUtils.BACKGROUND_COLOR_SPAN;
   1064     }
   1065     @Override
   1066     public int describeContents() {
   1067         return 0;
   1068     }
   1069     @Override
   1070     public void writeToParcel(Parcel dest, int flags) {
   1071         dest.writeInt(mColor);
   1072     }
   1073     @Override
   1074     public void updateDrawState(TextPaint ds) {
   1075         ds.bgColor = mColor;
   1076     }
   1077 }
   1078 
   1079 /**
   1080  * Widget capable of rendering CEA-608 closed captions.
   1081  *
   1082  * @hide
   1083  */
   1084 class ClosedCaptionWidget extends ViewGroup implements
   1085         SubtitleTrack.RenderingWidget,
   1086         CCParser.DisplayListener {
   1087     private static final String TAG = "ClosedCaptionWidget";
   1088 
   1089     private static final Rect mTextBounds = new Rect();
   1090     private static final String mDummyText = "1234567890123456789012345678901234";
   1091     private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT;
   1092 
   1093     /** Captioning manager, used to obtain and track caption properties. */
   1094     private final CaptioningManager mManager;
   1095 
   1096     /** Callback for rendering changes. */
   1097     private OnChangedListener mListener;
   1098 
   1099     /** Current caption style. */
   1100     private CaptionStyle mCaptionStyle;
   1101 
   1102     /* Closed caption layout. */
   1103     private CCLayout mClosedCaptionLayout;
   1104 
   1105     /** Whether a caption style change listener is registered. */
   1106     private boolean mHasChangeListener;
   1107 
   1108     public ClosedCaptionWidget(Context context) {
   1109         this(context, null);
   1110     }
   1111 
   1112     public ClosedCaptionWidget(Context context, AttributeSet attrs) {
   1113         this(context, null, 0);
   1114     }
   1115 
   1116     public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
   1117         super(context, attrs, defStyle);
   1118 
   1119         // Cannot render text over video when layer type is hardware.
   1120         setLayerType(View.LAYER_TYPE_SOFTWARE, null);
   1121 
   1122         mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
   1123         mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle());
   1124 
   1125         mClosedCaptionLayout = new CCLayout(context);
   1126         mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
   1127         addView(mClosedCaptionLayout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
   1128 
   1129         requestLayout();
   1130     }
   1131 
   1132     @Override
   1133     public void setOnChangedListener(OnChangedListener listener) {
   1134         mListener = listener;
   1135     }
   1136 
   1137     @Override
   1138     public void setSize(int width, int height) {
   1139         final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
   1140         final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
   1141 
   1142         measure(widthSpec, heightSpec);
   1143         layout(0, 0, width, height);
   1144     }
   1145 
   1146     @Override
   1147     public void setVisible(boolean visible) {
   1148         if (visible) {
   1149             setVisibility(View.VISIBLE);
   1150         } else {
   1151             setVisibility(View.GONE);
   1152         }
   1153 
   1154         manageChangeListener();
   1155     }
   1156 
   1157     @Override
   1158     public void onAttachedToWindow() {
   1159         super.onAttachedToWindow();
   1160 
   1161         manageChangeListener();
   1162     }
   1163 
   1164     @Override
   1165     public void onDetachedFromWindow() {
   1166         super.onDetachedFromWindow();
   1167 
   1168         manageChangeListener();
   1169     }
   1170 
   1171     @Override
   1172     public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
   1173         mClosedCaptionLayout.update(styledTexts);
   1174 
   1175         if (mListener != null) {
   1176             mListener.onChanged(this);
   1177         }
   1178     }
   1179 
   1180     @Override
   1181     public CaptionStyle getCaptionStyle() {
   1182         return mCaptionStyle;
   1183     }
   1184 
   1185     @Override
   1186     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1187         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   1188         mClosedCaptionLayout.measure(widthMeasureSpec, heightMeasureSpec);
   1189     }
   1190 
   1191     @Override
   1192     protected void onLayout(boolean changed, int l, int t, int r, int b) {
   1193         mClosedCaptionLayout.layout(l, t, r, b);
   1194     }
   1195 
   1196     /**
   1197      * Manages whether this renderer is listening for caption style changes.
   1198      */
   1199     private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
   1200         @Override
   1201         public void onUserStyleChanged(CaptionStyle userStyle) {
   1202             mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle);
   1203             mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
   1204         }
   1205     };
   1206 
   1207     private void manageChangeListener() {
   1208         final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
   1209         if (mHasChangeListener != needsListener) {
   1210             mHasChangeListener = needsListener;
   1211 
   1212             if (needsListener) {
   1213                 mManager.addCaptioningChangeListener(mCaptioningListener);
   1214             } else {
   1215                 mManager.removeCaptioningChangeListener(mCaptioningListener);
   1216             }
   1217         }
   1218     }
   1219 
   1220     private static class CCLineBox extends TextView {
   1221         private static final float FONT_PADDING_RATIO = 0.75f;
   1222         private static final float EDGE_OUTLINE_RATIO = 0.1f;
   1223         private static final float EDGE_SHADOW_RATIO = 0.05f;
   1224         private float mOutlineWidth;
   1225         private float mShadowRadius;
   1226         private float mShadowOffset;
   1227 
   1228         private int mTextColor = Color.WHITE;
   1229         private int mBgColor = Color.BLACK;
   1230         private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
   1231         private int mEdgeColor = Color.TRANSPARENT;
   1232 
   1233         CCLineBox(Context context) {
   1234             super(context);
   1235             setGravity(Gravity.CENTER);
   1236             setBackgroundColor(Color.TRANSPARENT);
   1237             setTextColor(Color.WHITE);
   1238             setTypeface(Typeface.MONOSPACE);
   1239             setVisibility(View.INVISIBLE);
   1240 
   1241             final Resources res = getContext().getResources();
   1242 
   1243             // get the default (will be updated later during measure)
   1244             mOutlineWidth = res.getDimensionPixelSize(
   1245                     com.android.internal.R.dimen.subtitle_outline_width);
   1246             mShadowRadius = res.getDimensionPixelSize(
   1247                     com.android.internal.R.dimen.subtitle_shadow_radius);
   1248             mShadowOffset = res.getDimensionPixelSize(
   1249                     com.android.internal.R.dimen.subtitle_shadow_offset);
   1250         }
   1251 
   1252         void setCaptionStyle(CaptionStyle captionStyle) {
   1253             mTextColor = captionStyle.foregroundColor;
   1254             mBgColor = captionStyle.backgroundColor;
   1255             mEdgeType = captionStyle.edgeType;
   1256             mEdgeColor = captionStyle.edgeColor;
   1257 
   1258             setTextColor(mTextColor);
   1259             if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
   1260                 setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor);
   1261             } else {
   1262                 setShadowLayer(0, 0, 0, 0);
   1263             }
   1264             invalidate();
   1265         }
   1266 
   1267         @Override
   1268         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1269             float fontSize = MeasureSpec.getSize(heightMeasureSpec)
   1270                     * FONT_PADDING_RATIO;
   1271             setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
   1272 
   1273             mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f;
   1274             mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;;
   1275             mShadowOffset = mShadowRadius;
   1276 
   1277             // set font scale in the X direction to match the required width
   1278             setScaleX(1.0f);
   1279             getPaint().getTextBounds(mDummyText, 0, mDummyText.length(), mTextBounds);
   1280             float actualTextWidth = mTextBounds.width();
   1281             float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec);
   1282             setScaleX(requiredTextWidth / actualTextWidth);
   1283 
   1284             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   1285         }
   1286 
   1287         @Override
   1288         protected void onDraw(Canvas c) {
   1289             if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED
   1290                     || mEdgeType == CaptionStyle.EDGE_TYPE_NONE
   1291                     || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
   1292                 // these edge styles don't require a second pass
   1293                 super.onDraw(c);
   1294                 return;
   1295             }
   1296 
   1297             if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
   1298                 drawEdgeOutline(c);
   1299             } else {
   1300                 // Raised or depressed
   1301                 drawEdgeRaisedOrDepressed(c);
   1302             }
   1303         }
   1304 
   1305         private void drawEdgeOutline(Canvas c) {
   1306             TextPaint textPaint = getPaint();
   1307 
   1308             Paint.Style previousStyle = textPaint.getStyle();
   1309             Paint.Join previousJoin = textPaint.getStrokeJoin();
   1310             float previousWidth = textPaint.getStrokeWidth();
   1311 
   1312             setTextColor(mEdgeColor);
   1313             textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
   1314             textPaint.setStrokeJoin(Paint.Join.ROUND);
   1315             textPaint.setStrokeWidth(mOutlineWidth);
   1316 
   1317             // Draw outline and background only.
   1318             super.onDraw(c);
   1319 
   1320             // Restore original settings.
   1321             setTextColor(mTextColor);
   1322             textPaint.setStyle(previousStyle);
   1323             textPaint.setStrokeJoin(previousJoin);
   1324             textPaint.setStrokeWidth(previousWidth);
   1325 
   1326             // Remove the background.
   1327             setBackgroundSpans(Color.TRANSPARENT);
   1328             // Draw foreground only.
   1329             super.onDraw(c);
   1330             // Restore the background.
   1331             setBackgroundSpans(mBgColor);
   1332         }
   1333 
   1334         private void drawEdgeRaisedOrDepressed(Canvas c) {
   1335             TextPaint textPaint = getPaint();
   1336 
   1337             Paint.Style previousStyle = textPaint.getStyle();
   1338             textPaint.setStyle(Paint.Style.FILL);
   1339 
   1340             final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED;
   1341             final int colorUp = raised ? Color.WHITE : mEdgeColor;
   1342             final int colorDown = raised ? mEdgeColor : Color.WHITE;
   1343             final float offset = mShadowRadius / 2f;
   1344 
   1345             // Draw background and text with shadow up
   1346             setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
   1347             super.onDraw(c);
   1348 
   1349             // Remove the background.
   1350             setBackgroundSpans(Color.TRANSPARENT);
   1351 
   1352             // Draw text with shadow down
   1353             setShadowLayer(mShadowRadius, +offset, +offset, colorDown);
   1354             super.onDraw(c);
   1355 
   1356             // Restore settings
   1357             textPaint.setStyle(previousStyle);
   1358 
   1359             // Restore the background.
   1360             setBackgroundSpans(mBgColor);
   1361         }
   1362 
   1363         private void setBackgroundSpans(int color) {
   1364             CharSequence text = getText();
   1365             if (text instanceof Spannable) {
   1366                 Spannable spannable = (Spannable) text;
   1367                 MutableBackgroundColorSpan[] bgSpans = spannable.getSpans(
   1368                         0, spannable.length(), MutableBackgroundColorSpan.class);
   1369                 for (int i = 0; i < bgSpans.length; i++) {
   1370                     bgSpans[i].setBackgroundColor(color);
   1371                 }
   1372             }
   1373         }
   1374     }
   1375 
   1376     private static class CCLayout extends LinearLayout {
   1377         private static final int MAX_ROWS = CCParser.MAX_ROWS;
   1378         private static final float SAFE_AREA_RATIO = 0.9f;
   1379 
   1380         private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS];
   1381 
   1382         CCLayout(Context context) {
   1383             super(context);
   1384             setGravity(Gravity.START);
   1385             setOrientation(LinearLayout.VERTICAL);
   1386             for (int i = 0; i < MAX_ROWS; i++) {
   1387                 mLineBoxes[i] = new CCLineBox(getContext());
   1388                 addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   1389             }
   1390         }
   1391 
   1392         void setCaptionStyle(CaptionStyle captionStyle) {
   1393             for (int i = 0; i < MAX_ROWS; i++) {
   1394                 mLineBoxes[i].setCaptionStyle(captionStyle);
   1395             }
   1396         }
   1397 
   1398         void update(SpannableStringBuilder[] textBuffer) {
   1399             for (int i = 0; i < MAX_ROWS; i++) {
   1400                 if (textBuffer[i] != null) {
   1401                     mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE);
   1402                     mLineBoxes[i].setVisibility(View.VISIBLE);
   1403                 } else {
   1404                     mLineBoxes[i].setVisibility(View.INVISIBLE);
   1405                 }
   1406             }
   1407         }
   1408 
   1409         @Override
   1410         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1411             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   1412 
   1413             int safeWidth = getMeasuredWidth();
   1414             int safeHeight = getMeasuredHeight();
   1415 
   1416             // CEA-608 assumes 4:3 video
   1417             if (safeWidth * 3 >= safeHeight * 4) {
   1418                 safeWidth = safeHeight * 4 / 3;
   1419             } else {
   1420                 safeHeight = safeWidth * 3 / 4;
   1421             }
   1422             safeWidth *= SAFE_AREA_RATIO;
   1423             safeHeight *= SAFE_AREA_RATIO;
   1424 
   1425             int lineHeight = safeHeight / MAX_ROWS;
   1426             int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
   1427                     lineHeight, MeasureSpec.EXACTLY);
   1428             int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
   1429                     safeWidth, MeasureSpec.EXACTLY);
   1430 
   1431             for (int i = 0; i < MAX_ROWS; i++) {
   1432                 mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec);
   1433             }
   1434         }
   1435 
   1436         @Override
   1437         protected void onLayout(boolean changed, int l, int t, int r, int b) {
   1438             // safe caption area
   1439             int viewPortWidth = r - l;
   1440             int viewPortHeight = b - t;
   1441             int safeWidth, safeHeight;
   1442             // CEA-608 assumes 4:3 video
   1443             if (viewPortWidth * 3 >= viewPortHeight * 4) {
   1444                 safeWidth = viewPortHeight * 4 / 3;
   1445                 safeHeight = viewPortHeight;
   1446             } else {
   1447                 safeWidth = viewPortWidth;
   1448                 safeHeight = viewPortWidth * 3 / 4;
   1449             }
   1450             safeWidth *= SAFE_AREA_RATIO;
   1451             safeHeight *= SAFE_AREA_RATIO;
   1452             int left = (viewPortWidth - safeWidth) / 2;
   1453             int top = (viewPortHeight - safeHeight) / 2;
   1454 
   1455             for (int i = 0; i < MAX_ROWS; i++) {
   1456                 mLineBoxes[i].layout(
   1457                         left,
   1458                         top + safeHeight * i / MAX_ROWS,
   1459                         left + safeWidth,
   1460                         top + safeHeight * (i + 1) / MAX_ROWS);
   1461             }
   1462         }
   1463     }
   1464 };
   1465