content: /*
   * Copyright (C) 2016 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 com.google.android.exoplayer2.text.cea;
  
  import static java.lang.Math.min;
  
  import android.graphics.Color;
  import android.graphics.Typeface;
  import android.text.Layout.Alignment;
  import android.text.SpannableString;
  import android.text.SpannableStringBuilder;
  import android.text.Spanned;
  import android.text.style.ForegroundColorSpan;
  import android.text.style.StyleSpan;
  import android.text.style.UnderlineSpan;
  import androidx.annotation.Nullable;
  import com.google.android.exoplayer2.C;
  import com.google.android.exoplayer2.Format;
  import com.google.android.exoplayer2.text.Cue;
  import com.google.android.exoplayer2.text.Subtitle;
  import com.google.android.exoplayer2.text.SubtitleDecoder;
  import com.google.android.exoplayer2.text.SubtitleDecoderException;
  import com.google.android.exoplayer2.text.SubtitleInputBuffer;
  import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
  import com.google.android.exoplayer2.util.Assertions;
  import com.google.android.exoplayer2.util.Log;
  import com.google.android.exoplayer2.util.MimeTypes;
  import com.google.android.exoplayer2.util.ParsableByteArray;
  import com.google.android.exoplayer2.util.Util;
  import java.nio.ByteBuffer;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import org.checkerframework.checker.nullness.compatqual.NullableType;
  
  /** A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608"). */
  public final class Cea608Decoder extends CeaDecoder {
  
    /**
     * The minimum value for the {@code validDataChannelTimeoutMs} constructor parameter permitted by
     * ANSI/CTA-608-E R-2014 Annex C.9.
     */
    public static final long MIN_DATA_CHANNEL_TIMEOUT_MS = 16_000;
  
    private static final String TAG = "Cea608Decoder";
  
    private static final int CC_VALID_FLAG = 0x04;
    private static final int CC_TYPE_FLAG = 0x02;
    private static final int CC_FIELD_FLAG = 0x01;
  
    private static final int NTSC_CC_FIELD_1 = 0x00;
    private static final int NTSC_CC_FIELD_2 = 0x01;
    private static final int NTSC_CC_CHANNEL_1 = 0x00;
    private static final int NTSC_CC_CHANNEL_2 = 0x01;
  
    private static final int CC_MODE_UNKNOWN = 0;
    private static final int CC_MODE_ROLL_UP = 1;
    private static final int CC_MODE_POP_ON = 2;
    private static final int CC_MODE_PAINT_ON = 3;
  
    private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9};
    private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28};
  
    private static final int[] STYLE_COLORS =
        new int[] {
          Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA
        };
    private static final int STYLE_ITALICS = 0x07;
    private static final int STYLE_UNCHANGED = 0x08;
  
    // The default number of rows to display in roll-up captions mode.
    private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;
  
    // An implied first byte for packets that are only 2 bytes long, consisting of marker bits
    // (0b11111) + valid bit (0b1) + NTSC field 1 type bits (0b00).
    private static final byte CC_IMPLICIT_DATA_HEADER = (byte) 0xFC;
  
    /**
     * Command initiating pop-on style captioning. Subsequent data should be loaded into a
     * non-displayed memory and held there until the {@link #CTRL_END_OF_CAPTION} command is received,
     * at which point the non-displayed memory becomes the displayed memory (and vice versa).
     */
    private static final byte CTRL_RESUME_CAPTION_LOADING = 0x20;
  
    private static final byte CTRL_BACKSPACE = 0x21;
  
    private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;
  
    /**
     * Command initiating roll-up style captioning, with the maximum of 2 rows displayed
     * simultaneously.
     */
    private static final byte CTRL_ROLL_UP_CAPTIONS_2_ROWS = 0x25;
    /**
     * Command initiating roll-up style captioning, with the maximum of 3 rows displayed
     * simultaneously.
     */
    private static final byte CTRL_ROLL_UP_CAPTIONS_3_ROWS = 0x26;
    /**
     * Command initiating roll-up style captioning, with the maximum of 4 rows displayed
     * simultaneously.
     */
    private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27;
  
    /**
     * Command initiating paint-on style captioning. Subsequent data should be addressed immediately
     * to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command.
     */
    private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29;
    /**
     * TEXT commands are switching to TEXT service. All consecutive incoming data must be filtered out
     * until a command is received that switches back to the CAPTION service.
     */
    private static final byte CTRL_TEXT_RESTART = 0x2A;
  
    private static final byte CTRL_RESUME_TEXT_DISPLAY = 0x2B;
  
    private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C;
    private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
    private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;
  
    /**
     * Command indicating the end of a pop-on style caption. At this point the caption loaded in
     * non-displayed memory should be swapped with the one in displayed memory. If no {@link
     * #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the receiver into
     * pop-on style.
     */
    private static final byte CTRL_END_OF_CAPTION = 0x2F;
  
    // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).
    private static final int[] BASIC_CHARACTER_SET =
        new int[] {
          0x20,
          0x21,
          0x22,
          0x23,
          0x24,
          0x25,
          0x26,
          0x27, //   ! " # $ % & '
          0x28,
          0x29, // ( )
          0xE1, // 2A: 225 'á' "Latin small letter A with acute"
          0x2B,
          0x2C,
          0x2D,
          0x2E,
          0x2F, //       + , - . /
          0x30,
          0x31,
          0x32,
          0x33,
          0x34,
          0x35,
          0x36,
          0x37, // 0 1 2 3 4 5 6 7
          0x38,
          0x39,
          0x3A,
          0x3B,
          0x3C,
          0x3D,
          0x3E,
          0x3F, // 8 9 : ; < = > ?
          0x40,
          0x41,
          0x42,
          0x43,
          0x44,
          0x45,
          0x46,
          0x47, // @ A B C D E F G
          0x48,
          0x49,
          0x4A,
          0x4B,
          0x4C,
          0x4D,
          0x4E,
          0x4F, // H I J K L M N O
          0x50,
          0x51,
          0x52,
          0x53,
          0x54,
          0x55,
          0x56,
          0x57, // P Q R S T U V W
          0x58,
          0x59,
          0x5A,
          0x5B, // X Y Z [
          0xE9, // 5C: 233 'é' "Latin small letter E with acute"
          0x5D, //           ]
          0xED, // 5E: 237 'í' "Latin small letter I with acute"
          0xF3, // 5F: 243 'ó' "Latin small letter O with acute"
          0xFA, // 60: 250 'ú' "Latin small letter U with acute"
          0x61,
          0x62,
          0x63,
          0x64,
          0x65,
          0x66,
          0x67, //   a b c d e f g
          0x68,
          0x69,
          0x6A,
          0x6B,
          0x6C,
          0x6D,
          0x6E,
          0x6F, // h i j k l m n o
          0x70,
          0x71,
          0x72,
          0x73,
          0x74,
          0x75,
          0x76,
          0x77, // p q r s t u v w
          0x78,
          0x79,
          0x7A, // x y z
          0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla"
          0xF7, // 7C: 247 '÷' "Division sign"
          0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde"
          0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde"
          0x25A0 // 7F:         "Black Square" (NB: 2588 = Full Block)
        };
  
    // Special North American 608 CC char set.
    private static final int[] SPECIAL_CHARACTER_SET =
        new int[] {
          0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol
          0xB0, // 31: 176 '°' "Degree Sign"
          0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol)
          0xBF, // 33: 191 '¿' "Inverted Question Mark"
          0x2122, // 34:         "Trade Mark Sign" (tm superscript)
          0xA2, // 35: 162 '¢' "Cent Sign"
          0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling
          0x266A, // 37:         "Eighth Note" - music note
          0xE0, // 38: 224 'à' "Latin small letter A with grave"
          0x20, // 39:         TRANSPARENT SPACE - for now use ordinary space
          0xE8, // 3A: 232 'è' "Latin small letter E with grave"
          0xE2, // 3B: 226 'â' "Latin small letter A with circumflex"
          0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex"
          0xEE, // 3D: 238 'î' "Latin small letter I with circumflex"
          0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex"
          0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
        };
  
    // Extended Spanish/Miscellaneous and French char set.
    private static final int[] SPECIAL_ES_FR_CHARACTER_SET =
        new int[] {
          // Spanish and misc.
          0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,
          0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,
          // French.
          0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,
          0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB
        };
  
    // Extended Portuguese and German/Danish char set.
    private static final int[] SPECIAL_PT_DE_CHARACTER_SET =
        new int[] {
          // Portuguese.
          0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,
          0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,
          // German/Danish.
          0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,
          0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518
        };
  
    private static final boolean[] ODD_PARITY_BYTE_TABLE = {
      false, true, true, false, true, false, false, true, // 0
      true, false, false, true, false, true, true, false, // 8
      true, false, false, true, false, true, true, false, // 16
      false, true, true, false, true, false, false, true, // 24
      true, false, false, true, false, true, true, false, // 32
      false, true, true, false, true, false, false, true, // 40
      false, true, true, false, true, false, false, true, // 48
      true, false, false, true, false, true, true, false, // 56
      true, false, false, true, false, true, true, false, // 64
      false, true, true, false, true, false, false, true, // 72
      false, true, true, false, true, false, false, true, // 80
      true, false, false, true, false, true, true, false, // 88
      false, true, true, false, true, false, false, true, // 96
      true, false, false, true, false, true, true, false, // 104
      true, false, false, true, false, true, true, false, // 112
      false, true, true, false, true, false, false, true, // 120
      true, false, false, true, false, true, true, false, // 128
      false, true, true, false, true, false, false, true, // 136
      false, true, true, false, true, false, false, true, // 144
      true, false, false, true, false, true, true, false, // 152
      false, true, true, false, true, false, false, true, // 160
      true, false, false, true, false, true, true, false, // 168
      true, false, false, true, false, true, true, false, // 176
      false, true, true, false, true, false, false, true, // 184
      false, true, true, false, true, false, false, true, // 192
      true, false, false, true, false, true, true, false, // 200
      true, false, false, true, false, true, true, false, // 208
      false, true, true, false, true, false, false, true, // 216
      true, false, false, true, false, true, true, false, // 224
      false, true, true, false, true, false, false, true, // 232
      false, true, true, false, true, false, false, true, // 240
      true, false, false, true, false, true, true, false, // 248
    };
  
    private final ParsableByteArray ccData;
    private final int packetLength;
    private final int selectedField;
    private final int selectedChannel;
    private final long validDataChannelTimeoutUs;
    private final ArrayList<CueBuilder> cueBuilders;
  
    private CueBuilder currentCueBuilder;
    @Nullable private List<Cue> cues;
    @Nullable private List<Cue> lastCues;
  
    private int captionMode;
    private int captionRowCount;
  
    private boolean isCaptionValid;
    private boolean repeatableControlSet;
    private byte repeatableControlCc1;
    private byte repeatableControlCc2;
    private int currentChannel;
  
    // The incoming characters may belong to 3 different services based on the last received control
    // codes. The 3 services are Captioning, Text and XDS. The decoder only processes Captioning
    // service bytes and drops the rest.
    private boolean isInCaptionService;
  
    private long lastCueUpdateUs;
  
    /**
     * Constructs an instance.
     *
     * @param mimeType The MIME type of the CEA-608 data.
     * @param accessibilityChannel The Accessibility channel, or {@link Format#NO_VALUE} if unknown.
     * @param validDataChannelTimeoutMs The timeout (in milliseconds) permitted by ANSI/CTA-608-E
     *     R-2014 Annex C.9 to clear "stuck" captions where no removal control code is received. The
     *     timeout should be at least {@link #MIN_DATA_CHANNEL_TIMEOUT_MS} or {@link C#TIME_UNSET} for
     *     no timeout.
     */
    public Cea608Decoder(String mimeType, int accessibilityChannel, long validDataChannelTimeoutMs) {
      ccData = new ParsableByteArray();
      cueBuilders = new ArrayList<>();
      currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
      currentChannel = NTSC_CC_CHANNEL_1;
      this.validDataChannelTimeoutUs =
          validDataChannelTimeoutMs > 0 ? validDataChannelTimeoutMs * 1000 : C.TIME_UNSET;
      packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
      switch (accessibilityChannel) {
        case 1:
          selectedChannel = NTSC_CC_CHANNEL_1;
          selectedField = NTSC_CC_FIELD_1;
          break;
        case 2:
          selectedChannel = NTSC_CC_CHANNEL_2;
          selectedField = NTSC_CC_FIELD_1;
          break;
        case 3:
          selectedChannel = NTSC_CC_CHANNEL_1;
          selectedField = NTSC_CC_FIELD_2;
          break;
        case 4:
          selectedChannel = NTSC_CC_CHANNEL_2;
          selectedField = NTSC_CC_FIELD_2;
          break;
        default:
          Log.w(TAG, "Invalid channel. Defaulting to CC1.");
          selectedChannel = NTSC_CC_CHANNEL_1;
          selectedField = NTSC_CC_FIELD_1;
      }
  
      setCaptionMode(CC_MODE_UNKNOWN);
      resetCueBuilders();
      isInCaptionService = true;
      lastCueUpdateUs = C.TIME_UNSET;
    }
  
    @Override
    public String getName() {
      return "Cea608Decoder";
    }
  
    @Override
    public void flush() {
      super.flush();
      cues = null;
      lastCues = null;
      setCaptionMode(CC_MODE_UNKNOWN);
      setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT);
      resetCueBuilders();
      isCaptionValid = false;
      repeatableControlSet = false;
      repeatableControlCc1 = 0;
      repeatableControlCc2 = 0;
      currentChannel = NTSC_CC_CHANNEL_1;
      isInCaptionService = true;
      lastCueUpdateUs = C.TIME_UNSET;
    }
  
    @Override
    public void release() {
      // Do nothing
    }
  
    @Nullable
    @Override
    public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException {
      SubtitleOutputBuffer outputBuffer = super.dequeueOutputBuffer();
      if (outputBuffer != null) {
        return outputBuffer;
      }
      if (shouldClearStuckCaptions()) {
        outputBuffer = getAvailableOutputBuffer();
        if (outputBuffer != null) {
          cues = Collections.emptyList();
          lastCueUpdateUs = C.TIME_UNSET;
          Subtitle subtitle = createSubtitle();
          outputBuffer.setContent(getPositionUs(), subtitle, Format.OFFSET_SAMPLE_RELATIVE);
          return outputBuffer;
        }
      }
      return null;
    }
  
    @Override
    protected boolean isNewSubtitleDataAvailable() {
      return cues != lastCues;
    }
  
    @Override
    protected Subtitle createSubtitle() {
      lastCues = cues;
      return new CeaSubtitle(Assertions.checkNotNull(cues));
    }
  
    @SuppressWarnings("ByteBufferBackingArray")
    @Override
    protected void decode(SubtitleInputBuffer inputBuffer) {
      ByteBuffer subtitleData = Assertions.checkNotNull(inputBuffer.data);
      ccData.reset(subtitleData.array(), subtitleData.limit());
      boolean captionDataProcessed = false;
      while (ccData.bytesLeft() >= packetLength) {
        int ccHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER : ccData.readUnsignedByte();
  
        int ccByte1 = ccData.readUnsignedByte();
        int ccByte2 = ccData.readUnsignedByte();
  
        // TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according
        // to the CEA-608 specification. We need to determine if the data should be handled
        // differently when that is not the case.
  
        if ((ccHeader & CC_TYPE_FLAG) != 0) {
          // Do not process anything that is not part of the 608 byte stream.
          continue;
        }
  
        if ((ccHeader & CC_FIELD_FLAG) != selectedField) {
          // Do not process packets not within the selected field.
          continue;
        }
  
        // Strip the parity bit from each byte to get CC data.
        byte ccData1 = (byte) (ccByte1 & 0x7F);
        byte ccData2 = (byte) (ccByte2 & 0x7F);
  
        if (ccData1 == 0 && ccData2 == 0) {
          // Ignore empty captions.
          continue;
        }
  
        boolean previousIsCaptionValid = isCaptionValid;
        isCaptionValid =
            (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG
                && ODD_PARITY_BYTE_TABLE[ccByte1]
                && ODD_PARITY_BYTE_TABLE[ccByte2];
  
        if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) {
          // Ignore repeated valid commands.
          continue;
        }
  
        if (!isCaptionValid) {
          if (previousIsCaptionValid) {
            // The encoder has flipped the validity bit to indicate captions are being turned off.
            resetCueBuilders();
            captionDataProcessed = true;
          }
          continue;
        }
  
        maybeUpdateIsInCaptionService(ccData1, ccData2);
        if (!isInCaptionService) {
          // Only the Captioning service is supported. Drop all other bytes.
          continue;
        }
  
        if (!updateAndVerifyCurrentChannel(ccData1)) {
          // Wrong channel.
          continue;
        }
  
        if (isCtrlCode(ccData1)) {
          if (isSpecialNorthAmericanChar(ccData1, ccData2)) {
            currentCueBuilder.append(getSpecialNorthAmericanChar(ccData2));
          } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) {
            // Remove standard equivalent of the special extended char before appending new one.
            currentCueBuilder.backspace();
            currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2));
          } else if (isMidrowCtrlCode(ccData1, ccData2)) {
            handleMidrowCtrl(ccData2);
          } else if (isPreambleAddressCode(ccData1, ccData2)) {
            handlePreambleAddressCode(ccData1, ccData2);
          } else if (isTabCtrlCode(ccData1, ccData2)) {
            currentCueBuilder.tabOffset = ccData2 - 0x20;
          } else if (isMiscCode(ccData1, ccData2)) {
            handleMiscCode(ccData2);
          }
        } else {
          // Basic North American character set.
          currentCueBuilder.append(getBasicChar(ccData1));
          if ((ccData2 & 0xE0) != 0x00) {
            currentCueBuilder.append(getBasicChar(ccData2));
          }
        }
        captionDataProcessed = true;
      }
  
      if (captionDataProcessed) {
        if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
          cues = getDisplayCues();
          lastCueUpdateUs = getPositionUs();
        }
      }
    }
  
    private boolean updateAndVerifyCurrentChannel(byte cc1) {
      if (isCtrlCode(cc1)) {
        currentChannel = getChannel(cc1);
      }
      return currentChannel == selectedChannel;
    }
  
    private boolean isRepeatedCommand(boolean captionValid, byte cc1, byte cc2) {
      // Most control commands are sent twice in succession to ensure they are received properly. We
      // don't want to process duplicate commands, so if we see the same repeatable command twice in a
      // row then we ignore the second one.
      if (captionValid && isRepeatable(cc1)) {
        if (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) {
          // This is a repeated command, so we ignore it.
          repeatableControlSet = false;
          return true;
        } else {
          // This is the first occurrence of a repeatable command. Set the repeatable control
          // variables so that we can recognize and ignore a duplicate (if there is one), and then
          // continue to process the command below.
          repeatableControlSet = true;
          repeatableControlCc1 = cc1;
          repeatableControlCc2 = cc2;
        }
      } else {
        // This command is not repeatable.
        repeatableControlSet = false;
      }
      return false;
    }
  
    private void handleMidrowCtrl(byte cc2) {
      // TODO: support the extended styles (i.e. backgrounds and transparencies)
  
      // A midrow control code advances the cursor.
      currentCueBuilder.append(' ');
  
      // cc2 - 0|0|1|0|STYLE|U
      boolean underline = (cc2 & 0x01) == 0x01;
      int style = (cc2 >> 1) & 0x07;
      currentCueBuilder.setStyle(style, underline);
    }
  
    private void handlePreambleAddressCode(byte cc1, byte cc2) {
      // cc1 - 0|0|0|1|C|E|ROW
      // C is the channel toggle, E is the extended flag, and ROW is the encoded row
      int row = ROW_INDICES[cc1 & 0x07];
      // TODO: support the extended address and style
  
      // cc2 - 0|1|N|ATTRBTE|U
      // N is the next row down toggle, ATTRBTE is the 4-byte encoded attribute, and U is the
      // underline toggle.
      boolean nextRowDown = (cc2 & 0x20) != 0;
      if (nextRowDown) {
        row++;
      }
  
      if (row != currentCueBuilder.row) {
        if (captionMode != CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
          currentCueBuilder = new CueBuilder(captionMode, captionRowCount);
          cueBuilders.add(currentCueBuilder);
        }
        currentCueBuilder.row = row;
      }
  
      // cc2 - 0|1|N|0|STYLE|U
      // cc2 - 0|1|N|1|CURSR|U
      boolean isCursor = (cc2 & 0x10) == 0x10;
      boolean underline = (cc2 & 0x01) == 0x01;
      int cursorOrStyle = (cc2 >> 1) & 0x07;
  
      // We need to call setStyle even for the isCursor case, to update the underline bit.
      // STYLE_UNCHANGED is used for this case.
      currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline);
  
      if (isCursor) {
        currentCueBuilder.indent = COLUMN_INDICES[cursorOrStyle];
      }
    }
  
    private void handleMiscCode(byte cc2) {
      switch (cc2) {
        case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
          setCaptionMode(CC_MODE_ROLL_UP);
          setCaptionRowCount(2);
          return;
        case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
          setCaptionMode(CC_MODE_ROLL_UP);
          setCaptionRowCount(3);
          return;
        case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
          setCaptionMode(CC_MODE_ROLL_UP);
          setCaptionRowCount(4);
          return;
        case CTRL_RESUME_CAPTION_LOADING:
          setCaptionMode(CC_MODE_POP_ON);
          return;
        case CTRL_RESUME_DIRECT_CAPTIONING:
          setCaptionMode(CC_MODE_PAINT_ON);
          return;
        default:
          // Fall through.
          break;
      }
  
      if (captionMode == CC_MODE_UNKNOWN) {
        return;
      }
  
      switch (cc2) {
        case CTRL_ERASE_DISPLAYED_MEMORY:
          cues = Collections.emptyList();
          if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
            resetCueBuilders();
          }
          break;
        case CTRL_ERASE_NON_DISPLAYED_MEMORY:
          resetCueBuilders();
          break;
        case CTRL_END_OF_CAPTION:
          cues = getDisplayCues();
          resetCueBuilders();
          break;
        case CTRL_CARRIAGE_RETURN:
          // carriage returns only apply to rollup captions; don't bother if we don't have anything
          // to add a carriage return to
          if (captionMode == CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
            currentCueBuilder.rollUp();
          }
          break;
        case CTRL_BACKSPACE:
          currentCueBuilder.backspace();
          break;
        case CTRL_DELETE_TO_END_OF_ROW:
          // TODO: implement
          break;
        default:
          // Fall through.
          break;
      }
    }
  
    private List<Cue> getDisplayCues() {
      // CEA-608 does not define middle and end alignment, however content providers artificially
      // introduce them using whitespace. When each cue is built, we try and infer the alignment based
      // on the amount of whitespace either side of the text. To avoid consecutive cues being aligned
      // differently, we force all cues to have the same alignment, with start alignment given
      // preference, then middle alignment, then end alignment.
      @Cue.AnchorType int positionAnchor = Cue.ANCHOR_TYPE_END;
      int cueBuilderCount = cueBuilders.size();
      List<@NullableType Cue> cueBuilderCues = new ArrayList<>(cueBuilderCount);
      for (int i = 0; i < cueBuilderCount; i++) {
        @Nullable Cue cue = cueBuilders.get(i).build(/* forcedPositionAnchor= */ Cue.TYPE_UNSET);
        cueBuilderCues.add(cue);
        if (cue != null) {
          positionAnchor = min(positionAnchor, cue.positionAnchor);
        }
      }
  
      // Skip null cues and rebuild any that don't have the preferred alignment.
      List<Cue> displayCues = new ArrayList<>(cueBuilderCount);
      for (int i = 0; i < cueBuilderCount; i++) {
        @Nullable Cue cue = cueBuilderCues.get(i);
        if (cue != null) {
          if (cue.positionAnchor != positionAnchor) {
            // The last time we built this cue it was non-null, it will be non-null this time too.
            cue = Assertions.checkNotNull(cueBuilders.get(i).build(positionAnchor));
          }
          displayCues.add(cue);
        }
      }
  
      return displayCues;
    }
  
    private void setCaptionMode(int captionMode) {
      if (this.captionMode == captionMode) {
        return;
      }
  
      int oldCaptionMode = this.captionMode;
      this.captionMode = captionMode;
  
      if (captionMode == CC_MODE_PAINT_ON) {
        // Switching to paint-on mode should have no effect except to select the mode.
        for (int i = 0; i < cueBuilders.size(); i++) {
          cueBuilders.get(i).setCaptionMode(captionMode);
        }
        return;
      }
  
      // Clear the working memory.
      resetCueBuilders();
      if (oldCaptionMode == CC_MODE_PAINT_ON
          || captionMode == CC_MODE_ROLL_UP
          || captionMode == CC_MODE_UNKNOWN) {
        // When switching from paint-on or to roll-up or unknown, we also need to clear the caption.
        cues = Collections.emptyList();
      }
    }
  
    private void setCaptionRowCount(int captionRowCount) {
      this.captionRowCount = captionRowCount;
      currentCueBuilder.setCaptionRowCount(captionRowCount);
    }
  
    private void resetCueBuilders() {
      currentCueBuilder.reset(captionMode);
      cueBuilders.clear();
      cueBuilders.add(currentCueBuilder);
    }
  
    private void maybeUpdateIsInCaptionService(byte cc1, byte cc2) {
      if (isXdsControlCode(cc1)) {
        isInCaptionService = false;
      } else if (isServiceSwitchCommand(cc1)) {
        switch (cc2) {
          case CTRL_TEXT_RESTART:
          case CTRL_RESUME_TEXT_DISPLAY:
            isInCaptionService = false;
            break;
          case CTRL_END_OF_CAPTION:
          case CTRL_RESUME_CAPTION_LOADING:
          case CTRL_RESUME_DIRECT_CAPTIONING:
          case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
          case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
          case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
            isInCaptionService = true;
            break;
          default:
            // No update.
        }
      }
    }
  
    private static char getBasicChar(byte ccData) {
      int index = (ccData & 0x7F) - 0x20;
      return (char) BASIC_CHARACTER_SET[index];
    }
  
    private static boolean isSpecialNorthAmericanChar(byte cc1, byte cc2) {
      // cc1 - 0|0|0|1|C|0|0|1
      // cc2 - 0|0|1|1|X|X|X|X
      return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30);
    }
  
    private static char getSpecialNorthAmericanChar(byte ccData) {
      int index = ccData & 0x0F;
      return (char) SPECIAL_CHARACTER_SET[index];
    }
  
    private static boolean isExtendedWestEuropeanChar(byte cc1, byte cc2) {
      // cc1 - 0|0|0|1|C|0|1|S
      // cc2 - 0|0|1|X|X|X|X|X
      return ((cc1 & 0xF6) == 0x12) && ((cc2 & 0xE0) == 0x20);
    }
  
    private static char getExtendedWestEuropeanChar(byte cc1, byte cc2) {
      if ((cc1 & 0x01) == 0x00) {
        // Extended Spanish/Miscellaneous and French character set (S = 0).
        return getExtendedEsFrChar(cc2);
      } else {
        // Extended Portuguese and German/Danish character set (S = 1).
        return getExtendedPtDeChar(cc2);
      }
    }
  
    private static char getExtendedEsFrChar(byte ccData) {
      int index = ccData & 0x1F;
      return (char) SPECIAL_ES_FR_CHARACTER_SET[index];
    }
  
    private static char getExtendedPtDeChar(byte ccData) {
      int index = ccData & 0x1F;
      return (char) SPECIAL_PT_DE_CHARACTER_SET[index];
    }
  
    private static boolean isCtrlCode(byte cc1) {
      // cc1 - 0|0|0|X|X|X|X|X
      return (cc1 & 0xE0) == 0x00;
    }
  
    private static int getChannel(byte cc1) {
      // cc1 - X|X|X|X|C|X|X|X
      return (cc1 >> 3) & 0x1;
    }
  
    private static boolean isMidrowCtrlCode(byte cc1, byte cc2) {
      // cc1 - 0|0|0|1|C|0|0|1
      // cc2 - 0|0|1|0|X|X|X|X
      return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x20);
    }
  
    private static boolean isPreambleAddressCode(byte cc1, byte cc2) {
      // cc1 - 0|0|0|1|C|X|X|X
      // cc2 - 0|1|X|X|X|X|X|X
      return ((cc1 & 0xF0) == 0x10) && ((cc2 & 0xC0) == 0x40);
    }
  
    private static boolean isTabCtrlCode(byte cc1, byte cc2) {
      // cc1 - 0|0|0|1|C|1|1|1
      // cc2 - 0|0|1|0|0|0|0|1 to 0|0|1|0|0|0|1|1
      return ((cc1 & 0xF7) == 0x17) && (cc2 >= 0x21 && cc2 <= 0x23);
    }
  
    private static boolean isMiscCode(byte cc1, byte cc2) {
      // cc1 - 0|0|0|1|C|1|0|F
      // cc2 - 0|0|1|0|X|X|X|X
      return ((cc1 & 0xF6) == 0x14) && ((cc2 & 0xF0) == 0x20);
    }
  
    private static boolean isRepeatable(byte cc1) {
      // cc1 - 0|0|0|1|X|X|X|X
      return (cc1 & 0xF0) == 0x10;
    }
  
    private static boolean isXdsControlCode(byte cc1) {
      return 0x01 <= cc1 && cc1 <= 0x0F;
    }
  
    private static boolean isServiceSwitchCommand(byte cc1) {
      // cc1 - 0|0|0|1|C|1|0|F
      return (cc1 & 0xF6) == 0x14;
    }
  
    private static final class CueBuilder {
  
      // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608
      // positions to normalized screen position.
      private static final int SCREEN_CHARWIDTH = 32;
      private static final int BASE_ROW = 15;
  
      private final List<CueStyle> cueStyles;
      private final List<SpannableString> rolledUpCaptions;
      private final StringBuilder captionStringBuilder;
  
      private int row;
      private int indent;
      private int tabOffset;
      private int captionMode;
      private int captionRowCount;
  
      public CueBuilder(int captionMode, int captionRowCount) {
        cueStyles = new ArrayList<>();
        rolledUpCaptions = new ArrayList<>();
        captionStringBuilder = new StringBuilder();
        reset(captionMode);
        this.captionRowCount = captionRowCount;
      }
  
      public void reset(int captionMode) {
        this.captionMode = captionMode;
        cueStyles.clear();
        rolledUpCaptions.clear();
        captionStringBuilder.setLength(0);
        row = BASE_ROW;
        indent = 0;
        tabOffset = 0;
      }
  
      public boolean isEmpty() {
        return cueStyles.isEmpty()
            && rolledUpCaptions.isEmpty()
            && captionStringBuilder.length() == 0;
      }
  
      public void setCaptionMode(int captionMode) {
        this.captionMode = captionMode;
      }
  
      public void setCaptionRowCount(int captionRowCount) {
        this.captionRowCount = captionRowCount;
      }
  
      public void setStyle(int style, boolean underline) {
        cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length()));
      }
  
      public void backspace() {
        int length = captionStringBuilder.length();
        if (length > 0) {
          captionStringBuilder.delete(length - 1, length);
          // Decrement style start positions if necessary.
          for (int i = cueStyles.size() - 1; i >= 0; i--) {
            CueStyle style = cueStyles.get(i);
            if (style.start == length) {
              style.start--;
            } else {
              // All earlier cues must have style.start < length.
              break;
            }
          }
        }
      }
  
      public void append(char text) {
        // Don't accept more than 32 chars. We'll trim further, considering indent & tabOffset, in
        // build().
        if (captionStringBuilder.length() < SCREEN_CHARWIDTH) {
          captionStringBuilder.append(text);
        }
      }
  
      public void rollUp() {
        rolledUpCaptions.add(buildCurrentLine());
        captionStringBuilder.setLength(0);
        cueStyles.clear();
        int numRows = min(captionRowCount, row);
        while (rolledUpCaptions.size() >= numRows) {
          rolledUpCaptions.remove(0);
        }
      }
  
      @Nullable
      public Cue build(@Cue.AnchorType int forcedPositionAnchor) {
        // The number of empty columns before the start of the text, in the range [0-31].
        int startPadding = indent + tabOffset;
        int maxTextLength = SCREEN_CHARWIDTH - startPadding;
        SpannableStringBuilder cueString = new SpannableStringBuilder();
        // Add any rolled up captions, separated by new lines.
        for (int i = 0; i < rolledUpCaptions.size(); i++) {
          cueString.append(Util.truncateAscii(rolledUpCaptions.get(i), maxTextLength));
          cueString.append('\n');
        }
        // Add the current line.
        cueString.append(Util.truncateAscii(buildCurrentLine(), maxTextLength));
  
        if (cueString.length() == 0) {
          // The cue is empty.
          return null;
        }
  
        int positionAnchor;
        // The number of empty columns after the end of the text, in the same range.
        int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length();
        int startEndPaddingDelta = startPadding - endPadding;
        if (forcedPositionAnchor != Cue.TYPE_UNSET) {
          positionAnchor = forcedPositionAnchor;
        } else if (captionMode == CC_MODE_POP_ON
            && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) {
          // Treat approximately centered pop-on captions as middle aligned. We also treat captions
          // that are wider than they should be in this way. See
          // https://github.com/google/ExoPlayer/issues/3534.
          positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
        } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) {
          // Treat pop-on captions with less padding at the end than the start as end aligned.
          positionAnchor = Cue.ANCHOR_TYPE_END;
        } else {
          // For all other cases assume start aligned.
          positionAnchor = Cue.ANCHOR_TYPE_START;
        }
  
        float position;
        switch (positionAnchor) {
          case Cue.ANCHOR_TYPE_MIDDLE:
            position = 0.5f;
            break;
          case Cue.ANCHOR_TYPE_END:
            position = (float) (SCREEN_CHARWIDTH - endPadding) / SCREEN_CHARWIDTH;
            // Adjust the position to fit within the safe area.
            position = position * 0.8f + 0.1f;
            break;
          case Cue.ANCHOR_TYPE_START:
          default:
            position = (float) startPadding / SCREEN_CHARWIDTH;
            // Adjust the position to fit within the safe area.
            position = position * 0.8f + 0.1f;
            break;
        }
  
        int line;
        // Note: Row indices are in the range [1-15], Cue.line counts from 0 (top) and -1 (bottom).
        if (row > (BASE_ROW / 2)) {
          line = row - BASE_ROW;
          // Two line adjustments. The first is because line indices from the bottom of the window
          // start from -1 rather than 0. The second is a blank row to act as the safe area.
          line -= 2;
        } else {
          // The `row` of roll-up cues positions the bottom line (even for cues shown in the top
          // half of the screen), so we need to consider the number of rows in this cue. In
          // non-roll-up, we don't need any further adjustments because we leave the first line
          // (cue.line=0) blank to act as the safe area, so positioning row=1 at Cue.line=1 is
          // correct.
          line = captionMode == CC_MODE_ROLL_UP ? row - (captionRowCount - 1) : row;
        }
  
        return new Cue.Builder()
            .setText(cueString)
            .setTextAlignment(Alignment.ALIGN_NORMAL)
            .setLine(line, Cue.LINE_TYPE_NUMBER)
            .setPosition(position)
            .setPositionAnchor(positionAnchor)
            .build();
      }
  
      private SpannableString buildCurrentLine() {
        SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder);
        int length = builder.length();
  
        int underlineStartPosition = C.INDEX_UNSET;
        int italicStartPosition = C.INDEX_UNSET;
        int colorStartPosition = 0;
        int color = Color.WHITE;
  
        boolean nextItalic = false;
        int nextColor = Color.WHITE;
  
        for (int i = 0; i < cueStyles.size(); i++) {
          CueStyle cueStyle = cueStyles.get(i);
          boolean underline = cueStyle.underline;
          int style = cueStyle.style;
          if (style != STYLE_UNCHANGED) {
            // If the style is a color then italic is cleared.
            nextItalic = style == STYLE_ITALICS;
            // If the style is italic then the color is left unchanged.
            nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style];
          }
  
          int position = cueStyle.start;
          int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length;
          if (position == nextPosition) {
            // There are more cueStyles to process at the current position.
            continue;
          }
  
          // Process changes to underline up to the current position.
          if (underlineStartPosition != C.INDEX_UNSET && !underline) {
            setUnderlineSpan(builder, underlineStartPosition, position);
            underlineStartPosition = C.INDEX_UNSET;
          } else if (underlineStartPosition == C.INDEX_UNSET && underline) {
            underlineStartPosition = position;
          }
          // Process changes to italic up to the current position.
          if (italicStartPosition != C.INDEX_UNSET && !nextItalic) {
            setItalicSpan(builder, italicStartPosition, position);
            italicStartPosition = C.INDEX_UNSET;
          } else if (italicStartPosition == C.INDEX_UNSET && nextItalic) {
            italicStartPosition = position;
          }
          // Process changes to color up to the current position.
          if (nextColor != color) {
            setColorSpan(builder, colorStartPosition, position, color);
            color = nextColor;
            colorStartPosition = position;
          }
        }
  
        // Add any final spans.
        if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) {
          setUnderlineSpan(builder, underlineStartPosition, length);
        }
        if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) {
          setItalicSpan(builder, italicStartPosition, length);
        }
        if (colorStartPosition != length) {
          setColorSpan(builder, colorStartPosition, length, color);
        }
  
        return new SpannableString(builder);
      }
  
      private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) {
        builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
  
      private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) {
        builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
  
      private static void setColorSpan(
          SpannableStringBuilder builder, int start, int end, int color) {
        if (color == Color.WHITE) {
          // White is treated as the default color (i.e. no span is attached).
          return;
        }
        builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
  
      private static class CueStyle {
  
        public final int style;
        public final boolean underline;
  
        public int start;
  
        public CueStyle(int style, boolean underline, int start) {
          this.style = style;
          this.underline = underline;
          this.start = start;
        }
      }
    }
  
    /** See ANSI/CTA-608-E R-2014 Annex C.9 for Caption Erase Logic. */
    private boolean shouldClearStuckCaptions() {
      if (validDataChannelTimeoutUs == C.TIME_UNSET || lastCueUpdateUs == C.TIME_UNSET) {
        return false;
      }
      long elapsedUs = getPositionUs() - lastCueUpdateUs;
      return elapsedUs >= validDataChannelTimeoutUs;
    }
  }
  