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