content: /*
   * Copyright (C) 2014 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.media;
  
  import android.content.Context;
  import android.content.res.Resources;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Paint;
  import android.graphics.Rect;
  import android.graphics.Typeface;
  import android.text.Spannable;
  import android.text.SpannableStringBuilder;
  import android.text.TextPaint;
  import android.text.style.CharacterStyle;
  import android.text.style.StyleSpan;
  import android.text.style.UnderlineSpan;
  import android.text.style.UpdateAppearance;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.TypedValue;
  import android.view.Gravity;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.accessibility.CaptioningManager;
  import android.view.accessibility.CaptioningManager.CaptionStyle;
  import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
  import android.widget.LinearLayout;
  import android.widget.TextView;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Vector;
  
  /** @hide */
  public class ClosedCaptionRenderer extends SubtitleController.Renderer {
      private final Context mContext;
      private Cea608CCWidget mCCWidget;
  
      public ClosedCaptionRenderer(Context context) {
          mContext = context;
      }
  
      @Override
      public boolean supports(MediaFormat format) {
          if (format.containsKey(MediaFormat.KEY_MIME)) {
              String mimeType = format.getString(MediaFormat.KEY_MIME);
              return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType);
          }
          return false;
      }
  
      @Override
      public SubtitleTrack createTrack(MediaFormat format) {
          String mimeType = format.getString(MediaFormat.KEY_MIME);
          if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
              if (mCCWidget == null) {
                  mCCWidget = new Cea608CCWidget(mContext);
              }
              return new Cea608CaptionTrack(mCCWidget, format);
          }
          throw new RuntimeException("No matching format: " + format.toString());
      }
  }
  
  /** @hide */
  class Cea608CaptionTrack extends SubtitleTrack {
      private final Cea608CCParser mCCParser;
      private final Cea608CCWidget mRenderingWidget;
  
      Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) {
          super(format);
  
          mRenderingWidget = renderingWidget;
          mCCParser = new Cea608CCParser(mRenderingWidget);
      }
  
      @Override
      public void onData(byte[] data, boolean eos, long runID) {
          mCCParser.parse(data);
      }
  
      @Override
      public RenderingWidget getRenderingWidget() {
          return mRenderingWidget;
      }
  
      @Override
      public void updateView(Vector<Cue> activeCues) {
          // Overriding with NO-OP, CC rendering by-passes this
      }
  }
  
  /**
   * Abstract widget class to render a closed caption track.
   *
   * @hide
   */
  abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
  
      /** @hide */
      interface ClosedCaptionLayout {
          void setCaptionStyle(CaptionStyle captionStyle);
          void setFontScale(float scale);
      }
  
      private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT;
  
      /** Captioning manager, used to obtain and track caption properties. */
      private final CaptioningManager mManager;
  
      /** Current caption style. */
      protected CaptionStyle mCaptionStyle;
  
      /** Callback for rendering changes. */
      protected OnChangedListener mListener;
  
      /** Concrete layout of CC. */
      protected ClosedCaptionLayout mClosedCaptionLayout;
  
      /** Whether a caption style change listener is registered. */
      private boolean mHasChangeListener;
  
      public ClosedCaptionWidget(Context context) {
          this(context, null);
      }
  
      public ClosedCaptionWidget(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
          this(context, attrs, defStyle, 0);
      }
  
      public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyleAttr,
              int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
  
          // Cannot render text over video when layer type is hardware.
          setLayerType(View.LAYER_TYPE_SOFTWARE, null);
  
          mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
          mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle());
  
          mClosedCaptionLayout = createCaptionLayout(context);
          mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
          mClosedCaptionLayout.setFontScale(mManager.getFontScale());
          addView((ViewGroup) mClosedCaptionLayout, LayoutParams.MATCH_PARENT,
                  LayoutParams.MATCH_PARENT);
  
          requestLayout();
      }
  
      public abstract ClosedCaptionLayout createCaptionLayout(Context context);
  
      @Override
      public void setOnChangedListener(OnChangedListener listener) {
          mListener = listener;
      }
  
      @Override
      public void setSize(int width, int height) {
          final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
          final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
  
          measure(widthSpec, heightSpec);
          layout(0, 0, width, height);
      }
  
      @Override
      public void setVisible(boolean visible) {
          if (visible) {
              setVisibility(View.VISIBLE);
          } else {
              setVisibility(View.GONE);
          }
  
          manageChangeListener();
      }
  
      @Override
      public void onAttachedToWindow() {
          super.onAttachedToWindow();
  
          manageChangeListener();
      }
  
      @Override
      public void onDetachedFromWindow() {
          super.onDetachedFromWindow();
  
          manageChangeListener();
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          ((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec);
      }
  
      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
          ((ViewGroup) mClosedCaptionLayout).layout(l, t, r, b);
      }
  
      /**
       * Manages whether this renderer is listening for caption style changes.
       */
      private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
          @Override
          public void onUserStyleChanged(CaptionStyle userStyle) {
              mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle);
              mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
          }
  
          @Override
          public void onFontScaleChanged(float fontScale) {
              mClosedCaptionLayout.setFontScale(fontScale);
          }
      };
  
      private void manageChangeListener() {
          final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
          if (mHasChangeListener != needsListener) {
              mHasChangeListener = needsListener;
  
              if (needsListener) {
                  mManager.addCaptioningChangeListener(mCaptioningListener);
              } else {
                  mManager.removeCaptioningChangeListener(mCaptioningListener);
              }
          }
      }
  }
  
  /**
   * @hide
   *
   * CCParser processes CEA-608 closed caption data.
   *
   * It calls back into OnDisplayChangedListener upon
   * display change with styled text for rendering.
   *
   */
  class Cea608CCParser {
      public static final int MAX_ROWS = 15;
      public static final int MAX_COLS = 32;
  
      private static final String TAG = "Cea608CCParser";
      private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
  
      private static final int INVALID = -1;
  
      // EIA-CEA-608: Table 70 - Control Codes
      private static final int RCL = 0x20;
      private static final int BS  = 0x21;
      private static final int AOF = 0x22;
      private static final int AON = 0x23;
      private static final int DER = 0x24;
      private static final int RU2 = 0x25;
      private static final int RU3 = 0x26;
      private static final int RU4 = 0x27;
      private static final int FON = 0x28;
      private static final int RDC = 0x29;
      private static final int TR  = 0x2a;
      private static final int RTD = 0x2b;
      private static final int EDM = 0x2c;
      private static final int CR  = 0x2d;
      private static final int ENM = 0x2e;
      private static final int EOC = 0x2f;
  
      // Transparent Space
      private static final char TS = '\u00A0';
  
      // Captioning Modes
      private static final int MODE_UNKNOWN = 0;
      private static final int MODE_PAINT_ON = 1;
      private static final int MODE_ROLL_UP = 2;
      private static final int MODE_POP_ON = 3;
      private static final int MODE_TEXT = 4;
  
      private final DisplayListener mListener;
  
      private int mMode = MODE_PAINT_ON;
      private int mRollUpSize = 4;
      private int mPrevCtrlCode = INVALID;
  
      private CCMemory mDisplay = new CCMemory();
      private CCMemory mNonDisplay = new CCMemory();
      private CCMemory mTextMem = new CCMemory();
  
      Cea608CCParser(DisplayListener listener) {
          mListener = listener;
      }
  
      public void parse(byte[] data) {
          CCData[] ccData = CCData.fromByteArray(data);
  
          for (int i = 0; i < ccData.length; i++) {
              if (DEBUG) {
                  Log.d(TAG, ccData[i].toString());
              }
  
              if (handleCtrlCode(ccData[i])
                      || handleTabOffsets(ccData[i])
                      || handlePACCode(ccData[i])
                      || handleMidRowCode(ccData[i])) {
                  continue;
              }
  
              handleDisplayableChars(ccData[i]);
          }
      }
  
      interface DisplayListener {
          void onDisplayChanged(SpannableStringBuilder[] styledTexts);
          CaptionStyle getCaptionStyle();
      }
  
      private CCMemory getMemory() {
          // get the CC memory to operate on for current mode
          switch (mMode) {
          case MODE_POP_ON:
              return mNonDisplay;
          case MODE_TEXT:
              // TODO(chz): support only caption mode for now,
              // in text mode, dump everything to text mem.
              return mTextMem;
          case MODE_PAINT_ON:
          case MODE_ROLL_UP:
              return mDisplay;
          default:
              Log.w(TAG, "unrecoginized mode: " + mMode);
          }
          return mDisplay;
      }
  
      private boolean handleDisplayableChars(CCData ccData) {
          if (!ccData.isDisplayableChar()) {
              return false;
          }
  
          // Extended char includes 1 automatic backspace
          if (ccData.isExtendedChar()) {
              getMemory().bs();
          }
  
          getMemory().writeText(ccData.getDisplayText());
  
          if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
              updateDisplay();
          }
  
          return true;
      }
  
      private boolean handleMidRowCode(CCData ccData) {
          StyleCode m = ccData.getMidRow();
          if (m != null) {
              getMemory().writeMidRowCode(m);
              return true;
          }
          return false;
      }
  
      private boolean handlePACCode(CCData ccData) {
          PAC pac = ccData.getPAC();
  
          if (pac != null) {
              if (mMode == MODE_ROLL_UP) {
                  getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
              }
              getMemory().writePAC(pac);
              return true;
          }
  
          return false;
      }
  
      private boolean handleTabOffsets(CCData ccData) {
          int tabs = ccData.getTabOffset();
  
          if (tabs > 0) {
              getMemory().tab(tabs);
              return true;
          }
  
          return false;
      }
  
      private boolean handleCtrlCode(CCData ccData) {
          int ctrlCode = ccData.getCtrlCode();
  
          if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) {
              // discard double ctrl codes (but if there's a 3rd one, we still take that)
              mPrevCtrlCode = INVALID;
              return true;
          }
  
          switch(ctrlCode) {
          case RCL:
              // select pop-on style
              mMode = MODE_POP_ON;
              break;
          case BS:
              getMemory().bs();
              break;
          case DER:
              getMemory().der();
              break;
          case RU2:
          case RU3:
          case RU4:
              mRollUpSize = (ctrlCode - 0x23);
              // erase memory if currently in other style
              if (mMode != MODE_ROLL_UP) {
                  mDisplay.erase();
                  mNonDisplay.erase();
              }
              // select roll-up style
              mMode = MODE_ROLL_UP;
              break;
          case FON:
              Log.i(TAG, "Flash On");
              break;
          case RDC:
              // select paint-on style
              mMode = MODE_PAINT_ON;
              break;
          case TR:
              mMode = MODE_TEXT;
              mTextMem.erase();
              break;
          case RTD:
              mMode = MODE_TEXT;
              break;
          case EDM:
              // erase display memory
              mDisplay.erase();
              updateDisplay();
              break;
          case CR:
              if (mMode == MODE_ROLL_UP) {
                  getMemory().rollUp(mRollUpSize);
              } else {
                  getMemory().cr();
              }
              if (mMode == MODE_ROLL_UP) {
                  updateDisplay();
              }
              break;
          case ENM:
              // erase non-display memory
              mNonDisplay.erase();
              break;
          case EOC:
              // swap display/non-display memory
              swapMemory();
              // switch to pop-on style
              mMode = MODE_POP_ON;
              updateDisplay();
              break;
          case INVALID:
          default:
              mPrevCtrlCode = INVALID;
              return false;
          }
  
          mPrevCtrlCode = ctrlCode;
  
          // handled
          return true;
      }
  
      private void updateDisplay() {
          if (mListener != null) {
              CaptionStyle captionStyle = mListener.getCaptionStyle();
              mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
          }
      }
  
      private void swapMemory() {
          CCMemory temp = mDisplay;
          mDisplay = mNonDisplay;
          mNonDisplay = temp;
      }
  
      private static class StyleCode {
          static final int COLOR_WHITE = 0;
          static final int COLOR_GREEN = 1;
          static final int COLOR_BLUE = 2;
          static final int COLOR_CYAN = 3;
          static final int COLOR_RED = 4;
          static final int COLOR_YELLOW = 5;
          static final int COLOR_MAGENTA = 6;
          static final int COLOR_INVALID = 7;
  
          static final int STYLE_ITALICS   = 0x00000001;
          static final int STYLE_UNDERLINE = 0x00000002;
  
          static final String[] mColorMap = {
              "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
          };
  
          final int mStyle;
          final int mColor;
  
          static StyleCode fromByte(byte data2) {
              int style = 0;
              int color = (data2 >> 1) & 0x7;
  
              if ((data2 & 0x1) != 0) {
                  style |= STYLE_UNDERLINE;
              }
  
              if (color == COLOR_INVALID) {
                  // WHITE ITALICS
                  color = COLOR_WHITE;
                  style |= STYLE_ITALICS;
              }
  
              return new StyleCode(style, color);
          }
  
          StyleCode(int style, int color) {
              mStyle = style;
              mColor = color;
          }
  
          boolean isItalics() {
              return (mStyle & STYLE_ITALICS) != 0;
          }
  
          boolean isUnderline() {
              return (mStyle & STYLE_UNDERLINE) != 0;
          }
  
          int getColor() {
              return mColor;
          }
  
          @Override
          public String toString() {
              StringBuilder str = new StringBuilder();
              str.append("{");
              str.append(mColorMap[mColor]);
              if ((mStyle & STYLE_ITALICS) != 0) {
                  str.append(", ITALICS");
              }
              if ((mStyle & STYLE_UNDERLINE) != 0) {
                  str.append(", UNDERLINE");
              }
              str.append("}");
  
              return str.toString();
          }
      }
  
      private static class PAC extends StyleCode {
          final int mRow;
          final int mCol;
  
          static PAC fromBytes(byte data1, byte data2) {
              int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
              int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
              int style = 0;
              if ((data2 & 1) != 0) {
                  style |= STYLE_UNDERLINE;
              }
              if ((data2 & 0x10) != 0) {
                  // indent code
                  int indent = (data2 >> 1) & 0x7;
                  return new PAC(row, indent * 4, style, COLOR_WHITE);
              } else {
                  // style code
                  int color = (data2 >> 1) & 0x7;
  
                  if (color == COLOR_INVALID) {
                      // WHITE ITALICS
                      color = COLOR_WHITE;
                      style |= STYLE_ITALICS;
                  }
                  return new PAC(row, -1, style, color);
              }
          }
  
          PAC(int row, int col, int style, int color) {
              super(style, color);
              mRow = row;
              mCol = col;
          }
  
          boolean isIndentPAC() {
              return (mCol >= 0);
          }
  
          int getRow() {
              return mRow;
          }
  
          int getCol() {
              return mCol;
          }
  
          @Override
          public String toString() {
              return String.format("{%d, %d}, %s",
                      mRow, mCol, super.toString());
          }
      }
  
      /**
       * Mutable version of BackgroundSpan to facilitate text rendering with edge styles.
       *
       * @hide
       */
      public static class MutableBackgroundColorSpan extends CharacterStyle
              implements UpdateAppearance {
          private int mColor;
  
          public MutableBackgroundColorSpan(int color) {
              mColor = color;
          }
  
          public void setBackgroundColor(int color) {
              mColor = color;
          }
  
          public int getBackgroundColor() {
              return mColor;
          }
  
          @Override
          public void updateDrawState(TextPaint ds) {
              ds.bgColor = mColor;
          }
      }
  
      /* CCLineBuilder keeps track of displayable chars, as well as
       * MidRow styles and PACs, for a single line of CC memory.
       *
       * It generates styled text via getStyledText() method.
       */
      private static class CCLineBuilder {
          private final StringBuilder mDisplayChars;
          private final StyleCode[] mMidRowStyles;
          private final StyleCode[] mPACStyles;
  
          CCLineBuilder(String str) {
              mDisplayChars = new StringBuilder(str);
              mMidRowStyles = new StyleCode[mDisplayChars.length()];
              mPACStyles = new StyleCode[mDisplayChars.length()];
          }
  
          void setCharAt(int index, char ch) {
              mDisplayChars.setCharAt(index, ch);
              mMidRowStyles[index] = null;
          }
  
          void setMidRowAt(int index, StyleCode m) {
              mDisplayChars.setCharAt(index, ' ');
              mMidRowStyles[index] = m;
          }
  
          void setPACAt(int index, PAC pac) {
              mPACStyles[index] = pac;
          }
  
          char charAt(int index) {
              return mDisplayChars.charAt(index);
          }
  
          int length() {
              return mDisplayChars.length();
          }
  
          void applyStyleSpan(
                  SpannableStringBuilder styledText,
                  StyleCode s, int start, int end) {
              if (s.isItalics()) {
                  styledText.setSpan(
                          new StyleSpan(android.graphics.Typeface.ITALIC),
                          start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
              }
              if (s.isUnderline()) {
                  styledText.setSpan(
                          new UnderlineSpan(),
                          start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
              }
          }
  
          SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
              SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
              int start = -1, next = 0;
              int styleStart = -1;
              StyleCode curStyle = null;
              while (next < mDisplayChars.length()) {
                  StyleCode newStyle = null;
                  if (mMidRowStyles[next] != null) {
                      // apply mid-row style change
                      newStyle = mMidRowStyles[next];
                  } else if (mPACStyles[next] != null
                      && (styleStart < 0 || start < 0)) {
                      // apply PAC style change, only if:
                      // 1. no style set, or
                      // 2. style set, but prev char is none-displayable
                      newStyle = mPACStyles[next];
                  }
                  if (newStyle != null) {
                      curStyle = newStyle;
                      if (styleStart >= 0 && start >= 0) {
                          applyStyleSpan(styledText, newStyle, styleStart, next);
                      }
                      styleStart = next;
                  }
  
                  if (mDisplayChars.charAt(next) != TS) {
                      if (start < 0) {
                          start = next;
                      }
                  } else if (start >= 0) {
                      int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
                      int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
                      styledText.setSpan(
                              new MutableBackgroundColorSpan(captionStyle.backgroundColor),
                              expandedStart, expandedEnd,
                              Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                      if (styleStart >= 0) {
                          applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
                      }
                      start = -1;
                  }
                  next++;
              }
  
              return styledText;
          }
      }
  
      /*
       * CCMemory models a console-style display.
       */
      private static class CCMemory {
          private final String mBlankLine;
          private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
          private int mRow;
          private int mCol;
  
          CCMemory() {
              char[] blank = new char[MAX_COLS + 2];
              Arrays.fill(blank, TS);
              mBlankLine = new String(blank);
          }
  
          void erase() {
              // erase all lines
              for (int i = 0; i < mLines.length; i++) {
                  mLines[i] = null;
              }
              mRow = MAX_ROWS;
              mCol = 1;
          }
  
          void der() {
              if (mLines[mRow] != null) {
                  for (int i = 0; i < mCol; i++) {
                      if (mLines[mRow].charAt(i) != TS) {
                          for (int j = mCol; j < mLines[mRow].length(); j++) {
                              mLines[j].setCharAt(j, TS);
                          }
                          return;
                      }
                  }
                  mLines[mRow] = null;
              }
          }
  
          void tab(int tabs) {
              moveCursorByCol(tabs);
          }
  
          void bs() {
              moveCursorByCol(-1);
              if (mLines[mRow] != null) {
                  mLines[mRow].setCharAt(mCol, TS);
                  if (mCol == MAX_COLS - 1) {
                      // Spec recommendation:
                      // if cursor was at col 32, move cursor
                      // back to col 31 and erase both col 31&32
                      mLines[mRow].setCharAt(MAX_COLS, TS);
                  }
              }
          }
  
          void cr() {
              moveCursorTo(mRow + 1, 1);
          }
  
          void rollUp(int windowSize) {
              int i;
              for (i = 0; i <= mRow - windowSize; i++) {
                  mLines[i] = null;
              }
              int startRow = mRow - windowSize + 1;
              if (startRow < 1) {
                  startRow = 1;
              }
              for (i = startRow; i < mRow; i++) {
                  mLines[i] = mLines[i + 1];
              }
              for (i = mRow; i < mLines.length; i++) {
                  // clear base row
                  mLines[i] = null;
              }
              // default to col 1, in case PAC is not sent
              mCol = 1;
          }
  
          void writeText(String text) {
              for (int i = 0; i < text.length(); i++) {
                  getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
                  moveCursorByCol(1);
              }
          }
  
          void writeMidRowCode(StyleCode m) {
              getLineBuffer(mRow).setMidRowAt(mCol, m);
              moveCursorByCol(1);
          }
  
          void writePAC(PAC pac) {
              if (pac.isIndentPAC()) {
                  moveCursorTo(pac.getRow(), pac.getCol());
              } else {
                  moveCursorTo(pac.getRow(), 1);
              }
              getLineBuffer(mRow).setPACAt(mCol, pac);
          }
  
          SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
              ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS);
              for (int i = 1; i <= MAX_ROWS; i++) {
                  rows.add(mLines[i] != null ?
                          mLines[i].getStyledText(captionStyle) : null);
              }
              return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
          }
  
          private static int clamp(int x, int min, int max) {
              return x < min ? min : (x > max ? max : x);
          }
  
          private void moveCursorTo(int row, int col) {
              mRow = clamp(row, 1, MAX_ROWS);
              mCol = clamp(col, 1, MAX_COLS);
          }
  
          private void moveCursorToRow(int row) {
              mRow = clamp(row, 1, MAX_ROWS);
          }
  
          private void moveCursorByCol(int col) {
              mCol = clamp(mCol + col, 1, MAX_COLS);
          }
  
          private void moveBaselineTo(int baseRow, int windowSize) {
              if (mRow == baseRow) {
                  return;
              }
              int actualWindowSize = windowSize;
              if (baseRow < actualWindowSize) {
                  actualWindowSize = baseRow;
              }
              if (mRow < actualWindowSize) {
                  actualWindowSize = mRow;
              }
  
              int i;
              if (baseRow < mRow) {
                  // copy from bottom to top row
                  for (i = actualWindowSize - 1; i >= 0; i--) {
                      mLines[baseRow - i] = mLines[mRow - i];
                  }
              } else {
                  // copy from top to bottom row
                  for (i = 0; i < actualWindowSize; i++) {
                      mLines[baseRow - i] = mLines[mRow - i];
                  }
              }
              // clear rest of the rows
              for (i = 0; i <= baseRow - windowSize; i++) {
                  mLines[i] = null;
              }
              for (i = baseRow + 1; i < mLines.length; i++) {
                  mLines[i] = null;
              }
          }
  
          private CCLineBuilder getLineBuffer(int row) {
              if (mLines[row] == null) {
                  mLines[row] = new CCLineBuilder(mBlankLine);
              }
              return mLines[row];
          }
      }
  
      /*
       * CCData parses the raw CC byte pair into displayable chars,
       * misc control codes, Mid-Row or Preamble Address Codes.
       */
      private static class CCData {
          private final byte mType;
          private final byte mData1;
          private final byte mData2;
  
          private static final String[] mCtrlCodeMap = {
              "RCL", "BS" , "AOF", "AON",
              "DER", "RU2", "RU3", "RU4",
              "FON", "RDC", "TR" , "RTD",
              "EDM", "CR" , "ENM", "EOC",
          };
  
          private static final String[] mSpecialCharMap = {
              "\u00AE",
              "\u00B0",
              "\u00BD",
              "\u00BF",
              "\u2122",
              "\u00A2",
              "\u00A3",
              "\u266A", // Eighth note
              "\u00E0",
              "\u00A0", // Transparent space
              "\u00E8",
              "\u00E2",
              "\u00EA",
              "\u00EE",
              "\u00F4",
              "\u00FB",
          };
  
          private static final String[] mSpanishCharMap = {
              // Spanish and misc chars
              "\u00C1", // A
              "\u00C9", // E
              "\u00D3", // I
              "\u00DA", // O
              "\u00DC", // U
              "\u00FC", // u
              "\u2018", // opening single quote
              "\u00A1", // inverted exclamation mark
              "*",
              "'",
              "\u2014", // em dash
              "\u00A9", // Copyright
              "\u2120", // Servicemark
              "\u2022", // round bullet
              "\u201C", // opening double quote
              "\u201D", // closing double quote
              // French
              "\u00C0",
              "\u00C2",
              "\u00C7",
              "\u00C8",
              "\u00CA",
              "\u00CB",
              "\u00EB",
              "\u00CE",
              "\u00CF",
              "\u00EF",
              "\u00D4",
              "\u00D9",
              "\u00F9",
              "\u00DB",
              "\u00AB",
              "\u00BB"
          };
  
          private static final String[] mProtugueseCharMap = {
              // Portuguese
              "\u00C3",
              "\u00E3",
              "\u00CD",
              "\u00CC",
              "\u00EC",
              "\u00D2",
              "\u00F2",
              "\u00D5",
              "\u00F5",
              "{",
              "}",
              "\\",
              "^",
              "_",
              "|",
              "~",
              // German and misc chars
              "\u00C4",
              "\u00E4",
              "\u00D6",
              "\u00F6",
              "\u00DF",
              "\u00A5",
              "\u00A4",
              "\u2502", // vertical bar
              "\u00C5",
              "\u00E5",
              "\u00D8",
              "\u00F8",
              "\u250C", // top-left corner
              "\u2510", // top-right corner
              "\u2514", // lower-left corner
              "\u2518", // lower-right corner
          };
  
          static CCData[] fromByteArray(byte[] data) {
              CCData[] ccData = new CCData[data.length / 3];
  
              for (int i = 0; i < ccData.length; i++) {
                  ccData[i] = new CCData(
                          data[i * 3],
                          data[i * 3 + 1],
                          data[i * 3 + 2]);
              }
  
              return ccData;
          }
  
          CCData(byte type, byte data1, byte data2) {
              mType = type;
              mData1 = data1;
              mData2 = data2;
          }
  
          int getCtrlCode() {
              if ((mData1 == 0x14 || mData1 == 0x1c)
                      && mData2 >= 0x20 && mData2 <= 0x2f) {
                  return mData2;
              }
              return INVALID;
          }
  
          StyleCode getMidRow() {
              // only support standard Mid-row codes, ignore
              // optional background/foreground mid-row codes
              if ((mData1 == 0x11 || mData1 == 0x19)
                      && mData2 >= 0x20 && mData2 <= 0x2f) {
                  return StyleCode.fromByte(mData2);
              }
              return null;
          }
  
          PAC getPAC() {
              if ((mData1 & 0x70) == 0x10
                      && (mData2 & 0x40) == 0x40
                      && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
                  return PAC.fromBytes(mData1, mData2);
              }
              return null;
          }
  
          int getTabOffset() {
              if ((mData1 == 0x17 || mData1 == 0x1f)
                      && mData2 >= 0x21 && mData2 <= 0x23) {
                  return mData2 & 0x3;
              }
              return 0;
          }
  
          boolean isDisplayableChar() {
              return isBasicChar() || isSpecialChar() || isExtendedChar();
          }
  
          String getDisplayText() {
              String str = getBasicChars();
  
              if (str == null) {
                  str =  getSpecialChar();
  
                  if (str == null) {
                      str = getExtendedChar();
                  }
              }
  
              return str;
          }
  
          private String ctrlCodeToString(int ctrlCode) {
              return mCtrlCodeMap[ctrlCode - 0x20];
          }
  
          private boolean isBasicChar() {
              return mData1 >= 0x20 && mData1 <= 0x7f;
          }
  
          private boolean isSpecialChar() {
              return ((mData1 == 0x11 || mData1 == 0x19)
                      && mData2 >= 0x30 && mData2 <= 0x3f);
          }
  
          private boolean isExtendedChar() {
              return ((mData1 == 0x12 || mData1 == 0x1A
                      || mData1 == 0x13 || mData1 == 0x1B)
                      && mData2 >= 0x20 && mData2 <= 0x3f);
          }
  
          private char getBasicChar(byte data) {
              char c;
              // replace the non-ASCII ones
              switch (data) {
                  case 0x2A: c = '\u00E1'; break;
                  case 0x5C: c = '\u00E9'; break;
                  case 0x5E: c = '\u00ED'; break;
                  case 0x5F: c = '\u00F3'; break;
                  case 0x60: c = '\u00FA'; break;
                  case 0x7B: c = '\u00E7'; break;
                  case 0x7C: c = '\u00F7'; break;
                  case 0x7D: c = '\u00D1'; break;
                  case 0x7E: c = '\u00F1'; break;
                  case 0x7F: c = '\u2588'; break; // Full block
                  default: c = (char) data; break;
              }
              return c;
          }
  
          private String getBasicChars() {
              if (mData1 >= 0x20 && mData1 <= 0x7f) {
                  StringBuilder builder = new StringBuilder(2);
                  builder.append(getBasicChar(mData1));
                  if (mData2 >= 0x20 && mData2 <= 0x7f) {
                      builder.append(getBasicChar(mData2));
                  }
                  return builder.toString();
              }
  
              return null;
          }
  
          private String getSpecialChar() {
              if ((mData1 == 0x11 || mData1 == 0x19)
                      && mData2 >= 0x30 && mData2 <= 0x3f) {
                  return mSpecialCharMap[mData2 - 0x30];
              }
  
              return null;
          }
  
          private String getExtendedChar() {
              if ((mData1 == 0x12 || mData1 == 0x1A)
                      && mData2 >= 0x20 && mData2 <= 0x3f){
                  // 1 Spanish/French char
                  return mSpanishCharMap[mData2 - 0x20];
              } else if ((mData1 == 0x13 || mData1 == 0x1B)
                      && mData2 >= 0x20 && mData2 <= 0x3f){
                  // 1 Portuguese/German/Danish char
                  return mProtugueseCharMap[mData2 - 0x20];
              }
  
              return null;
          }
  
          @Override
          public String toString() {
              String str;
  
              if (mData1 < 0x10 && mData2 < 0x10) {
                  // Null Pad, ignore
                  return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
              }
  
              int ctrlCode = getCtrlCode();
              if (ctrlCode != INVALID) {
                  return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
              }
  
              int tabOffset = getTabOffset();
              if (tabOffset > 0) {
                  return String.format("[%d]Tab%d", mType, tabOffset);
              }
  
              PAC pac = getPAC();
              if (pac != null) {
                  return String.format("[%d]PAC: %s", mType, pac.toString());
              }
  
              StyleCode m = getMidRow();
              if (m != null) {
                  return String.format("[%d]Mid-row: %s", mType, m.toString());
              }
  
              if (isDisplayableChar()) {
                  return String.format("[%d]Displayable: %s (%02x %02x)",
                          mType, getDisplayText(), mData1, mData2);
              }
  
              return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
          }
      }
  }
  
  /**
   * Widget capable of rendering CEA-608 closed captions.
   *
   * @hide
   */
  class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener {
      private static final Rect mTextBounds = new Rect();
      private static final String mDummyText = "1234567890123456789012345678901234";
  
      public Cea608CCWidget(Context context) {
          this(context, null);
      }
  
      public Cea608CCWidget(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      public Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) {
          this(context, attrs, defStyle, 0);
      }
  
      public Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr,
              int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
      }
  
      @Override
      public ClosedCaptionLayout createCaptionLayout(Context context) {
          return new CCLayout(context);
      }
  
      @Override
      public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
          ((CCLayout) mClosedCaptionLayout).update(styledTexts);
  
          if (mListener != null) {
              mListener.onChanged(this);
          }
      }
  
      @Override
      public CaptionStyle getCaptionStyle() {
          return mCaptionStyle;
      }
  
      private static class CCLineBox extends TextView {
          private static final float FONT_PADDING_RATIO = 0.75f;
          private static final float EDGE_OUTLINE_RATIO = 0.1f;
          private static final float EDGE_SHADOW_RATIO = 0.05f;
          private float mOutlineWidth;
          private float mShadowRadius;
          private float mShadowOffset;
  
          private int mTextColor = Color.WHITE;
          private int mBgColor = Color.BLACK;
          private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
          private int mEdgeColor = Color.TRANSPARENT;
  
          CCLineBox(Context context) {
              super(context);
              setGravity(Gravity.CENTER);
              setBackgroundColor(Color.TRANSPARENT);
              setTextColor(Color.WHITE);
              setTypeface(Typeface.MONOSPACE);
              setVisibility(View.INVISIBLE);
  
              final Resources res = getContext().getResources();
  
              // get the default (will be updated later during measure)
              mOutlineWidth = res.getDimensionPixelSize(
                      com.android.internal.R.dimen.subtitle_outline_width);
              mShadowRadius = res.getDimensionPixelSize(
                      com.android.internal.R.dimen.subtitle_shadow_radius);
              mShadowOffset = res.getDimensionPixelSize(
                      com.android.internal.R.dimen.subtitle_shadow_offset);
          }
  
          void setCaptionStyle(CaptionStyle captionStyle) {
              mTextColor = captionStyle.foregroundColor;
              mBgColor = captionStyle.backgroundColor;
              mEdgeType = captionStyle.edgeType;
              mEdgeColor = captionStyle.edgeColor;
  
              setTextColor(mTextColor);
              if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
                  setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor);
              } else {
                  setShadowLayer(0, 0, 0, 0);
              }
              invalidate();
          }
  
          @Override
          protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
              float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO;
              setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
  
              mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f;
              mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;;
              mShadowOffset = mShadowRadius;
  
              // set font scale in the X direction to match the required width
              setScaleX(1.0f);
              getPaint().getTextBounds(mDummyText, 0, mDummyText.length(), mTextBounds);
              float actualTextWidth = mTextBounds.width();
              float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec);
              setScaleX(requiredTextWidth / actualTextWidth);
  
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          }
  
          @Override
          protected void onDraw(Canvas c) {
              if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED
                      || mEdgeType == CaptionStyle.EDGE_TYPE_NONE
                      || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
                  // these edge styles don't require a second pass
                  super.onDraw(c);
                  return;
              }
  
              if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
                  drawEdgeOutline(c);
              } else {
                  // Raised or depressed
                  drawEdgeRaisedOrDepressed(c);
              }
          }
  
          private void drawEdgeOutline(Canvas c) {
              TextPaint textPaint = getPaint();
  
              Paint.Style previousStyle = textPaint.getStyle();
              Paint.Join previousJoin = textPaint.getStrokeJoin();
              float previousWidth = textPaint.getStrokeWidth();
  
              setTextColor(mEdgeColor);
              textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
              textPaint.setStrokeJoin(Paint.Join.ROUND);
              textPaint.setStrokeWidth(mOutlineWidth);
  
              // Draw outline and background only.
              super.onDraw(c);
  
              // Restore original settings.
              setTextColor(mTextColor);
              textPaint.setStyle(previousStyle);
              textPaint.setStrokeJoin(previousJoin);
              textPaint.setStrokeWidth(previousWidth);
  
              // Remove the background.
              setBackgroundSpans(Color.TRANSPARENT);
              // Draw foreground only.
              super.onDraw(c);
              // Restore the background.
              setBackgroundSpans(mBgColor);
          }
  
          private void drawEdgeRaisedOrDepressed(Canvas c) {
              TextPaint textPaint = getPaint();
  
              Paint.Style previousStyle = textPaint.getStyle();
              textPaint.setStyle(Paint.Style.FILL);
  
              final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED;
              final int colorUp = raised ? Color.WHITE : mEdgeColor;
              final int colorDown = raised ? mEdgeColor : Color.WHITE;
              final float offset = mShadowRadius / 2f;
  
              // Draw background and text with shadow up
              setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
              super.onDraw(c);
  
              // Remove the background.
              setBackgroundSpans(Color.TRANSPARENT);
  
              // Draw text with shadow down
              setShadowLayer(mShadowRadius, +offset, +offset, colorDown);
              super.onDraw(c);
  
              // Restore settings
              textPaint.setStyle(previousStyle);
  
              // Restore the background.
              setBackgroundSpans(mBgColor);
          }
  
          private void setBackgroundSpans(int color) {
              CharSequence text = getText();
              if (text instanceof Spannable) {
                  Spannable spannable = (Spannable) text;
                  Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans(
                          0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class);
                  for (int i = 0; i < bgSpans.length; i++) {
                      bgSpans[i].setBackgroundColor(color);
                  }
              }
          }
      }
  
      private static class CCLayout extends LinearLayout implements ClosedCaptionLayout {
          private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS;
          private static final float SAFE_AREA_RATIO = 0.9f;
  
          private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS];
  
          CCLayout(Context context) {
              super(context);
              setGravity(Gravity.START);
              setOrientation(LinearLayout.VERTICAL);
              for (int i = 0; i < MAX_ROWS; i++) {
                  mLineBoxes[i] = new CCLineBox(getContext());
                  addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
              }
          }
  
          @Override
          public void setCaptionStyle(CaptionStyle captionStyle) {
              for (int i = 0; i < MAX_ROWS; i++) {
                  mLineBoxes[i].setCaptionStyle(captionStyle);
              }
          }
  
          @Override
          public void setFontScale(float fontScale) {
              // Ignores the font scale changes of the system wide CC preference.
          }
  
          void update(SpannableStringBuilder[] textBuffer) {
              for (int i = 0; i < MAX_ROWS; i++) {
                  if (textBuffer[i] != null) {
                      mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE);
                      mLineBoxes[i].setVisibility(View.VISIBLE);
                  } else {
                      mLineBoxes[i].setVisibility(View.INVISIBLE);
                  }
              }
          }
  
          @Override
          protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  
              int safeWidth = getMeasuredWidth();
              int safeHeight = getMeasuredHeight();
  
              // CEA-608 assumes 4:3 video
              if (safeWidth * 3 >= safeHeight * 4) {
                  safeWidth = safeHeight * 4 / 3;
              } else {
                  safeHeight = safeWidth * 3 / 4;
              }
              safeWidth *= SAFE_AREA_RATIO;
              safeHeight *= SAFE_AREA_RATIO;
  
              int lineHeight = safeHeight / MAX_ROWS;
              int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                      lineHeight, MeasureSpec.EXACTLY);
              int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                      safeWidth, MeasureSpec.EXACTLY);
  
              for (int i = 0; i < MAX_ROWS; i++) {
                  mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec);
              }
          }
  
          @Override
          protected void onLayout(boolean changed, int l, int t, int r, int b) {
              // safe caption area
              int viewPortWidth = r - l;
              int viewPortHeight = b - t;
              int safeWidth, safeHeight;
              // CEA-608 assumes 4:3 video
              if (viewPortWidth * 3 >= viewPortHeight * 4) {
                  safeWidth = viewPortHeight * 4 / 3;
                  safeHeight = viewPortHeight;
              } else {
                  safeWidth = viewPortWidth;
                  safeHeight = viewPortWidth * 3 / 4;
              }
              safeWidth *= SAFE_AREA_RATIO;
              safeHeight *= SAFE_AREA_RATIO;
              int left = (viewPortWidth - safeWidth) / 2;
              int top = (viewPortHeight - safeHeight) / 2;
  
              for (int i = 0; i < MAX_ROWS; i++) {
                  mLineBoxes[i].layout(
                          left,
                          top + safeHeight * i / MAX_ROWS,
                          left + safeWidth,
                          top + safeHeight * (i + 1) / MAX_ROWS);
              }
          }
      }
  }
  