• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.text.cea;
17 
18 import android.graphics.Color;
19 import android.graphics.Typeface;
20 import android.text.Layout.Alignment;
21 import android.text.SpannableString;
22 import android.text.SpannableStringBuilder;
23 import android.text.Spanned;
24 import android.text.style.BackgroundColorSpan;
25 import android.text.style.ForegroundColorSpan;
26 import android.text.style.StyleSpan;
27 import android.text.style.UnderlineSpan;
28 import androidx.annotation.Nullable;
29 import com.google.android.exoplayer2.C;
30 import com.google.android.exoplayer2.Format;
31 import com.google.android.exoplayer2.text.Cue;
32 import com.google.android.exoplayer2.text.Cue.AnchorType;
33 import com.google.android.exoplayer2.text.Subtitle;
34 import com.google.android.exoplayer2.text.SubtitleDecoder;
35 import com.google.android.exoplayer2.text.SubtitleInputBuffer;
36 import com.google.android.exoplayer2.util.Assertions;
37 import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
38 import com.google.android.exoplayer2.util.Log;
39 import com.google.android.exoplayer2.util.ParsableBitArray;
40 import com.google.android.exoplayer2.util.ParsableByteArray;
41 import java.nio.ByteBuffer;
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.List;
45 import org.checkerframework.checker.nullness.qual.RequiresNonNull;
46 
47 /**
48  * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708").
49  */
50 public final class Cea708Decoder extends CeaDecoder {
51 
52   private static final String TAG = "Cea708Decoder";
53 
54   private static final int NUM_WINDOWS = 8;
55 
56   private static final int DTVCC_PACKET_DATA = 0x02;
57   private static final int DTVCC_PACKET_START = 0x03;
58   private static final int CC_VALID_FLAG = 0x04;
59 
60   // Base Commands
61   private static final int GROUP_C0_END = 0x1F;  // Miscellaneous Control Codes
62   private static final int GROUP_G0_END = 0x7F;  // ASCII Printable Characters
63   private static final int GROUP_C1_END = 0x9F;  // Captioning Command Control Codes
64   private static final int GROUP_G1_END = 0xFF;  // ISO 8859-1 LATIN-1 Character Set
65 
66   // Extended Commands
67   private static final int GROUP_C2_END = 0x1F;  // Extended Control Code Set 1
68   private static final int GROUP_G2_END = 0x7F;  // Extended Miscellaneous Characters
69   private static final int GROUP_C3_END = 0x9F;  // Extended Control Code Set 2
70   private static final int GROUP_G3_END = 0xFF;  // Future Expansion
71 
72   // Group C0 Commands
73   private static final int COMMAND_NUL = 0x00;        // Nul
74   private static final int COMMAND_ETX = 0x03;        // EndOfText
75   private static final int COMMAND_BS = 0x08;         // Backspace
76   private static final int COMMAND_FF = 0x0C;         // FormFeed (Flush)
77   private static final int COMMAND_CR = 0x0D;         // CarriageReturn
78   private static final int COMMAND_HCR = 0x0E;        // ClearLine
79   private static final int COMMAND_EXT1 = 0x10;       // Extended Control Code Flag
80   private static final int COMMAND_EXT1_START = 0x11;
81   private static final int COMMAND_EXT1_END = 0x17;
82   private static final int COMMAND_P16_START = 0x18;
83   private static final int COMMAND_P16_END = 0x1F;
84 
85   // Group C1 Commands
86   private static final int COMMAND_CW0 = 0x80;  // SetCurrentWindow to 0
87   private static final int COMMAND_CW1 = 0x81;  // SetCurrentWindow to 1
88   private static final int COMMAND_CW2 = 0x82;  // SetCurrentWindow to 2
89   private static final int COMMAND_CW3 = 0x83;  // SetCurrentWindow to 3
90   private static final int COMMAND_CW4 = 0x84;  // SetCurrentWindow to 4
91   private static final int COMMAND_CW5 = 0x85;  // SetCurrentWindow to 5
92   private static final int COMMAND_CW6 = 0x86;  // SetCurrentWindow to 6
93   private static final int COMMAND_CW7 = 0x87;  // SetCurrentWindow to 7
94   private static final int COMMAND_CLW = 0x88;  // ClearWindows (+1 byte)
95   private static final int COMMAND_DSW = 0x89;  // DisplayWindows (+1 byte)
96   private static final int COMMAND_HDW = 0x8A;  // HideWindows (+1 byte)
97   private static final int COMMAND_TGW = 0x8B;  // ToggleWindows (+1 byte)
98   private static final int COMMAND_DLW = 0x8C;  // DeleteWindows (+1 byte)
99   private static final int COMMAND_DLY = 0x8D;  // Delay (+1 byte)
100   private static final int COMMAND_DLC = 0x8E;  // DelayCancel
101   private static final int COMMAND_RST = 0x8F;  // Reset
102   private static final int COMMAND_SPA = 0x90;  // SetPenAttributes (+2 bytes)
103   private static final int COMMAND_SPC = 0x91;  // SetPenColor (+3 bytes)
104   private static final int COMMAND_SPL = 0x92;  // SetPenLocation (+2 bytes)
105   private static final int COMMAND_SWA = 0x97;  // SetWindowAttributes (+4 bytes)
106   private static final int COMMAND_DF0 = 0x98;  // DefineWindow 0 (+6 bytes)
107   private static final int COMMAND_DF1 = 0x99;  // DefineWindow 1 (+6 bytes)
108   private static final int COMMAND_DF2 = 0x9A;  // DefineWindow 2 (+6 bytes)
109   private static final int COMMAND_DF3 = 0x9B;  // DefineWindow 3 (+6 bytes)
110   private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes)
111   private static final int COMMAND_DF5 = 0x9D;  // DefineWindow 5 (+6 bytes)
112   private static final int COMMAND_DF6 = 0x9E;  // DefineWindow 6 (+6 bytes)
113   private static final int COMMAND_DF7 = 0x9F;  // DefineWindow 7 (+6 bytes)
114 
115   // G0 Table Special Chars
116   private static final int CHARACTER_MN = 0x7F;  // MusicNote
117 
118   // G2 Table Special Chars
119   private static final int CHARACTER_TSP = 0x20;
120   private static final int CHARACTER_NBTSP = 0x21;
121   private static final int CHARACTER_ELLIPSIS = 0x25;
122   private static final int CHARACTER_BIG_CARONS = 0x2A;
123   private static final int CHARACTER_BIG_OE = 0x2C;
124   private static final int CHARACTER_SOLID_BLOCK = 0x30;
125   private static final int CHARACTER_OPEN_SINGLE_QUOTE = 0x31;
126   private static final int CHARACTER_CLOSE_SINGLE_QUOTE = 0x32;
127   private static final int CHARACTER_OPEN_DOUBLE_QUOTE = 0x33;
128   private static final int CHARACTER_CLOSE_DOUBLE_QUOTE = 0x34;
129   private static final int CHARACTER_BOLD_BULLET = 0x35;
130   private static final int CHARACTER_TM = 0x39;
131   private static final int CHARACTER_SMALL_CARONS = 0x3A;
132   private static final int CHARACTER_SMALL_OE = 0x3C;
133   private static final int CHARACTER_SM = 0x3D;
134   private static final int CHARACTER_DIAERESIS_Y = 0x3F;
135   private static final int CHARACTER_ONE_EIGHTH = 0x76;
136   private static final int CHARACTER_THREE_EIGHTHS = 0x77;
137   private static final int CHARACTER_FIVE_EIGHTHS = 0x78;
138   private static final int CHARACTER_SEVEN_EIGHTHS = 0x79;
139   private static final int CHARACTER_VERTICAL_BORDER = 0x7A;
140   private static final int CHARACTER_UPPER_RIGHT_BORDER = 0x7B;
141   private static final int CHARACTER_LOWER_LEFT_BORDER = 0x7C;
142   private static final int CHARACTER_HORIZONTAL_BORDER = 0x7D;
143   private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E;
144   private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F;
145 
146   private final ParsableByteArray ccData;
147   private final ParsableBitArray serviceBlockPacket;
148   // TODO: Use isWideAspectRatio in decoding.
149   @SuppressWarnings({"unused", "FieldCanBeLocal"})
150   private final boolean isWideAspectRatio;
151 
152   private final int selectedServiceNumber;
153   private final CueInfoBuilder[] cueInfoBuilders;
154 
155   private CueInfoBuilder currentCueInfoBuilder;
156   @Nullable private List<Cue> cues;
157   @Nullable private List<Cue> lastCues;
158 
159   @Nullable private DtvCcPacket currentDtvCcPacket;
160   private int currentWindow;
161 
Cea708Decoder(int accessibilityChannel, @Nullable List<byte[]> initializationData)162   public Cea708Decoder(int accessibilityChannel, @Nullable List<byte[]> initializationData) {
163     ccData = new ParsableByteArray();
164     serviceBlockPacket = new ParsableBitArray();
165     selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel;
166     isWideAspectRatio =
167         initializationData != null
168             && CodecSpecificDataUtil.parseCea708InitializationData(initializationData);
169 
170     cueInfoBuilders = new CueInfoBuilder[NUM_WINDOWS];
171     for (int i = 0; i < NUM_WINDOWS; i++) {
172       cueInfoBuilders[i] = new CueInfoBuilder();
173     }
174 
175     currentCueInfoBuilder = cueInfoBuilders[0];
176   }
177 
178   @Override
getName()179   public String getName() {
180     return "Cea708Decoder";
181   }
182 
183   @Override
flush()184   public void flush() {
185     super.flush();
186     cues = null;
187     lastCues = null;
188     currentWindow = 0;
189     currentCueInfoBuilder = cueInfoBuilders[currentWindow];
190     resetCueBuilders();
191     currentDtvCcPacket = null;
192   }
193 
194   @Override
isNewSubtitleDataAvailable()195   protected boolean isNewSubtitleDataAvailable() {
196     return cues != lastCues;
197   }
198 
199   @Override
createSubtitle()200   protected Subtitle createSubtitle() {
201     lastCues = cues;
202     return new CeaSubtitle(Assertions.checkNotNull(cues));
203   }
204 
205   @Override
decode(SubtitleInputBuffer inputBuffer)206   protected void decode(SubtitleInputBuffer inputBuffer) {
207     // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe.
208     ByteBuffer subtitleData = Assertions.checkNotNull(inputBuffer.data);
209     @SuppressWarnings("ByteBufferBackingArray")
210     byte[] inputBufferData = subtitleData.array();
211     ccData.reset(inputBufferData, subtitleData.limit());
212     while (ccData.bytesLeft() >= 3) {
213       int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07);
214 
215       int ccType = ccTypeAndValid & (DTVCC_PACKET_DATA | DTVCC_PACKET_START);
216       boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG;
217       byte ccData1 = (byte) ccData.readUnsignedByte();
218       byte ccData2 = (byte) ccData.readUnsignedByte();
219 
220       // Ignore any non-CEA-708 data
221       if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) {
222         continue;
223       }
224 
225       if (!ccValid) {
226         // This byte-pair isn't valid, ignore it and continue.
227         continue;
228       }
229 
230       if (ccType == DTVCC_PACKET_START) {
231         finalizeCurrentPacket();
232 
233         int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits
234         int packetSize = ccData1 & 0x3F; // last 6 bits
235         if (packetSize == 0) {
236           packetSize = 64;
237         }
238 
239         currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize);
240         currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
241       } else {
242         // The only remaining valid packet type is DTVCC_PACKET_DATA
243         Assertions.checkArgument(ccType == DTVCC_PACKET_DATA);
244 
245         if (currentDtvCcPacket == null) {
246           Log.e(TAG, "Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START");
247           continue;
248         }
249 
250         currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1;
251         currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
252       }
253 
254       if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) {
255         finalizeCurrentPacket();
256       }
257     }
258   }
259 
finalizeCurrentPacket()260   private void finalizeCurrentPacket() {
261     if (currentDtvCcPacket == null) {
262       // No packet to finalize;
263       return;
264     }
265 
266     processCurrentPacket();
267     currentDtvCcPacket = null;
268   }
269 
270   @RequiresNonNull("currentDtvCcPacket")
processCurrentPacket()271   private void processCurrentPacket() {
272     if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) {
273       Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1)
274           + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number "
275           + currentDtvCcPacket.sequenceNumber + "); ignoring packet");
276       return;
277     }
278 
279     serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex);
280 
281     int serviceNumber = serviceBlockPacket.readBits(3);
282     int blockSize = serviceBlockPacket.readBits(5);
283     if (serviceNumber == 7) {
284       // extended service numbers
285       serviceBlockPacket.skipBits(2);
286       serviceNumber = serviceBlockPacket.readBits(6);
287       if (serviceNumber < 7) {
288         Log.w(TAG, "Invalid extended service number: " + serviceNumber);
289       }
290     }
291 
292     // Ignore packets in which blockSize is 0
293     if (blockSize == 0) {
294       if (serviceNumber != 0) {
295         Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0");
296       }
297       return;
298     }
299 
300     if (serviceNumber != selectedServiceNumber) {
301       return;
302     }
303 
304     // The cues should be updated if we receive a C0 ETX command, any C1 command, or if after
305     // processing the service block any text has been added to the buffer. See CEA-708-B Section
306     // 8.10.4 for more details.
307     boolean cuesNeedUpdate = false;
308 
309     while (serviceBlockPacket.bitsLeft() > 0) {
310       int command = serviceBlockPacket.readBits(8);
311       if (command != COMMAND_EXT1) {
312         if (command <= GROUP_C0_END) {
313           handleC0Command(command);
314           // If the C0 command was an ETX command, the cues are updated in handleC0Command.
315         } else if (command <= GROUP_G0_END) {
316           handleG0Character(command);
317           cuesNeedUpdate = true;
318         } else if (command <= GROUP_C1_END) {
319           handleC1Command(command);
320           cuesNeedUpdate = true;
321         } else if (command <= GROUP_G1_END) {
322           handleG1Character(command);
323           cuesNeedUpdate = true;
324         } else {
325           Log.w(TAG, "Invalid base command: " + command);
326         }
327       } else {
328         // Read the extended command
329         command = serviceBlockPacket.readBits(8);
330         if (command <= GROUP_C2_END) {
331           handleC2Command(command);
332         } else if (command <= GROUP_G2_END) {
333           handleG2Character(command);
334           cuesNeedUpdate = true;
335         } else if (command <= GROUP_C3_END) {
336           handleC3Command(command);
337         } else if (command <= GROUP_G3_END) {
338           handleG3Character(command);
339           cuesNeedUpdate = true;
340         } else {
341           Log.w(TAG, "Invalid extended command: " + command);
342         }
343       }
344     }
345 
346     if (cuesNeedUpdate) {
347       cues = getDisplayCues();
348     }
349   }
350 
handleC0Command(int command)351   private void handleC0Command(int command) {
352     switch (command) {
353       case COMMAND_NUL:
354         // Do nothing.
355         break;
356       case COMMAND_ETX:
357         cues = getDisplayCues();
358         break;
359       case COMMAND_BS:
360         currentCueInfoBuilder.backspace();
361         break;
362       case COMMAND_FF:
363         resetCueBuilders();
364         break;
365       case COMMAND_CR:
366         currentCueInfoBuilder.append('\n');
367         break;
368       case COMMAND_HCR:
369         // TODO: Add support for this command.
370         break;
371       default:
372         if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) {
373           Log.w(TAG, "Currently unsupported COMMAND_EXT1 Command: " + command);
374           serviceBlockPacket.skipBits(8);
375         } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) {
376           Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command);
377           serviceBlockPacket.skipBits(16);
378         } else {
379           Log.w(TAG, "Invalid C0 command: " + command);
380         }
381     }
382   }
383 
handleC1Command(int command)384   private void handleC1Command(int command) {
385     int window;
386     switch (command) {
387       case COMMAND_CW0:
388       case COMMAND_CW1:
389       case COMMAND_CW2:
390       case COMMAND_CW3:
391       case COMMAND_CW4:
392       case COMMAND_CW5:
393       case COMMAND_CW6:
394       case COMMAND_CW7:
395         window = (command - COMMAND_CW0);
396         if (currentWindow != window) {
397           currentWindow = window;
398           currentCueInfoBuilder = cueInfoBuilders[window];
399         }
400         break;
401       case COMMAND_CLW:
402         for (int i = 1; i <= NUM_WINDOWS; i++) {
403           if (serviceBlockPacket.readBit()) {
404             cueInfoBuilders[NUM_WINDOWS - i].clear();
405           }
406         }
407         break;
408       case COMMAND_DSW:
409         for (int i = 1; i <= NUM_WINDOWS; i++) {
410           if (serviceBlockPacket.readBit()) {
411             cueInfoBuilders[NUM_WINDOWS - i].setVisibility(true);
412           }
413         }
414         break;
415       case COMMAND_HDW:
416         for (int i = 1; i <= NUM_WINDOWS; i++) {
417           if (serviceBlockPacket.readBit()) {
418             cueInfoBuilders[NUM_WINDOWS - i].setVisibility(false);
419           }
420         }
421         break;
422       case COMMAND_TGW:
423         for (int i = 1; i <= NUM_WINDOWS; i++) {
424           if (serviceBlockPacket.readBit()) {
425             CueInfoBuilder cueInfoBuilder = cueInfoBuilders[NUM_WINDOWS - i];
426             cueInfoBuilder.setVisibility(!cueInfoBuilder.isVisible());
427           }
428         }
429         break;
430       case COMMAND_DLW:
431         for (int i = 1; i <= NUM_WINDOWS; i++) {
432           if (serviceBlockPacket.readBit()) {
433             cueInfoBuilders[NUM_WINDOWS - i].reset();
434           }
435         }
436         break;
437       case COMMAND_DLY:
438         // TODO: Add support for delay commands.
439         serviceBlockPacket.skipBits(8);
440         break;
441       case COMMAND_DLC:
442         // TODO: Add support for delay commands.
443         break;
444       case COMMAND_RST:
445         resetCueBuilders();
446         break;
447       case COMMAND_SPA:
448         if (!currentCueInfoBuilder.isDefined()) {
449           // ignore this command if the current window/cue isn't defined
450           serviceBlockPacket.skipBits(16);
451         } else {
452           handleSetPenAttributes();
453         }
454         break;
455       case COMMAND_SPC:
456         if (!currentCueInfoBuilder.isDefined()) {
457           // ignore this command if the current window/cue isn't defined
458           serviceBlockPacket.skipBits(24);
459         } else {
460           handleSetPenColor();
461         }
462         break;
463       case COMMAND_SPL:
464         if (!currentCueInfoBuilder.isDefined()) {
465           // ignore this command if the current window/cue isn't defined
466           serviceBlockPacket.skipBits(16);
467         } else {
468           handleSetPenLocation();
469         }
470         break;
471       case COMMAND_SWA:
472         if (!currentCueInfoBuilder.isDefined()) {
473           // ignore this command if the current window/cue isn't defined
474           serviceBlockPacket.skipBits(32);
475         } else {
476           handleSetWindowAttributes();
477         }
478         break;
479       case COMMAND_DF0:
480       case COMMAND_DF1:
481       case COMMAND_DF2:
482       case COMMAND_DF3:
483       case COMMAND_DF4:
484       case COMMAND_DF5:
485       case COMMAND_DF6:
486       case COMMAND_DF7:
487         window = (command - COMMAND_DF0);
488         handleDefineWindow(window);
489         // We also set the current window to the newly defined window.
490         if (currentWindow != window) {
491           currentWindow = window;
492           currentCueInfoBuilder = cueInfoBuilders[window];
493         }
494         break;
495       default:
496         Log.w(TAG, "Invalid C1 command: " + command);
497     }
498   }
499 
handleC2Command(int command)500   private void handleC2Command(int command) {
501     // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
502     if (command <= 0x07) {
503       // Do nothing.
504     } else if (command <= 0x0F) {
505       serviceBlockPacket.skipBits(8);
506     } else if (command <= 0x17) {
507       serviceBlockPacket.skipBits(16);
508     } else if (command <= 0x1F) {
509       serviceBlockPacket.skipBits(24);
510     }
511   }
512 
handleC3Command(int command)513   private void handleC3Command(int command) {
514     // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
515     if (command <= 0x87) {
516       serviceBlockPacket.skipBits(32);
517     } else if (command <= 0x8F) {
518       serviceBlockPacket.skipBits(40);
519     } else if (command <= 0x9F) {
520       // 90-9F are variable length codes; the first byte defines the header with the first
521       // 2 bits specifying the type and the last 6 bits specifying the remaining length of the
522       // command in bytes
523       serviceBlockPacket.skipBits(2);
524       int length = serviceBlockPacket.readBits(6);
525       serviceBlockPacket.skipBits(8 * length);
526     }
527   }
528 
handleG0Character(int characterCode)529   private void handleG0Character(int characterCode) {
530     if (characterCode == CHARACTER_MN) {
531       currentCueInfoBuilder.append('\u266B');
532     } else {
533       currentCueInfoBuilder.append((char) (characterCode & 0xFF));
534     }
535   }
536 
handleG1Character(int characterCode)537   private void handleG1Character(int characterCode) {
538     currentCueInfoBuilder.append((char) (characterCode & 0xFF));
539   }
540 
handleG2Character(int characterCode)541   private void handleG2Character(int characterCode) {
542     switch (characterCode) {
543       case CHARACTER_TSP:
544         currentCueInfoBuilder.append('\u0020');
545         break;
546       case CHARACTER_NBTSP:
547         currentCueInfoBuilder.append('\u00A0');
548         break;
549       case CHARACTER_ELLIPSIS:
550         currentCueInfoBuilder.append('\u2026');
551         break;
552       case CHARACTER_BIG_CARONS:
553         currentCueInfoBuilder.append('\u0160');
554         break;
555       case CHARACTER_BIG_OE:
556         currentCueInfoBuilder.append('\u0152');
557         break;
558       case CHARACTER_SOLID_BLOCK:
559         currentCueInfoBuilder.append('\u2588');
560         break;
561       case CHARACTER_OPEN_SINGLE_QUOTE:
562         currentCueInfoBuilder.append('\u2018');
563         break;
564       case CHARACTER_CLOSE_SINGLE_QUOTE:
565         currentCueInfoBuilder.append('\u2019');
566         break;
567       case CHARACTER_OPEN_DOUBLE_QUOTE:
568         currentCueInfoBuilder.append('\u201C');
569         break;
570       case CHARACTER_CLOSE_DOUBLE_QUOTE:
571         currentCueInfoBuilder.append('\u201D');
572         break;
573       case CHARACTER_BOLD_BULLET:
574         currentCueInfoBuilder.append('\u2022');
575         break;
576       case CHARACTER_TM:
577         currentCueInfoBuilder.append('\u2122');
578         break;
579       case CHARACTER_SMALL_CARONS:
580         currentCueInfoBuilder.append('\u0161');
581         break;
582       case CHARACTER_SMALL_OE:
583         currentCueInfoBuilder.append('\u0153');
584         break;
585       case CHARACTER_SM:
586         currentCueInfoBuilder.append('\u2120');
587         break;
588       case CHARACTER_DIAERESIS_Y:
589         currentCueInfoBuilder.append('\u0178');
590         break;
591       case CHARACTER_ONE_EIGHTH:
592         currentCueInfoBuilder.append('\u215B');
593         break;
594       case CHARACTER_THREE_EIGHTHS:
595         currentCueInfoBuilder.append('\u215C');
596         break;
597       case CHARACTER_FIVE_EIGHTHS:
598         currentCueInfoBuilder.append('\u215D');
599         break;
600       case CHARACTER_SEVEN_EIGHTHS:
601         currentCueInfoBuilder.append('\u215E');
602         break;
603       case CHARACTER_VERTICAL_BORDER:
604         currentCueInfoBuilder.append('\u2502');
605         break;
606       case CHARACTER_UPPER_RIGHT_BORDER:
607         currentCueInfoBuilder.append('\u2510');
608         break;
609       case CHARACTER_LOWER_LEFT_BORDER:
610         currentCueInfoBuilder.append('\u2514');
611         break;
612       case CHARACTER_HORIZONTAL_BORDER:
613         currentCueInfoBuilder.append('\u2500');
614         break;
615       case CHARACTER_LOWER_RIGHT_BORDER:
616         currentCueInfoBuilder.append('\u2518');
617         break;
618       case CHARACTER_UPPER_LEFT_BORDER:
619         currentCueInfoBuilder.append('\u250C');
620         break;
621       default:
622         Log.w(TAG, "Invalid G2 character: " + characterCode);
623         // The CEA-708 specification doesn't specify what to do in the case of an unexpected
624         // value in the G2 character range, so we ignore it.
625     }
626   }
627 
handleG3Character(int characterCode)628   private void handleG3Character(int characterCode) {
629     if (characterCode == 0xA0) {
630       currentCueInfoBuilder.append('\u33C4');
631     } else {
632       Log.w(TAG, "Invalid G3 character: " + characterCode);
633       // Substitute any unsupported G3 character with an underscore as per CEA-708 specification.
634       currentCueInfoBuilder.append('_');
635     }
636   }
637 
handleSetPenAttributes()638   private void handleSetPenAttributes() {
639     // the SetPenAttributes command contains 2 bytes of data
640     // first byte
641     int textTag = serviceBlockPacket.readBits(4);
642     int offset = serviceBlockPacket.readBits(2);
643     int penSize = serviceBlockPacket.readBits(2);
644     // second byte
645     boolean italicsToggle = serviceBlockPacket.readBit();
646     boolean underlineToggle = serviceBlockPacket.readBit();
647     int edgeType = serviceBlockPacket.readBits(3);
648     int fontStyle = serviceBlockPacket.readBits(3);
649 
650     currentCueInfoBuilder.setPenAttributes(
651         textTag, offset, penSize, italicsToggle, underlineToggle, edgeType, fontStyle);
652   }
653 
handleSetPenColor()654   private void handleSetPenColor() {
655     // the SetPenColor command contains 3 bytes of data
656     // first byte
657     int foregroundO = serviceBlockPacket.readBits(2);
658     int foregroundR = serviceBlockPacket.readBits(2);
659     int foregroundG = serviceBlockPacket.readBits(2);
660     int foregroundB = serviceBlockPacket.readBits(2);
661     int foregroundColor =
662         CueInfoBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, foregroundO);
663     // second byte
664     int backgroundO = serviceBlockPacket.readBits(2);
665     int backgroundR = serviceBlockPacket.readBits(2);
666     int backgroundG = serviceBlockPacket.readBits(2);
667     int backgroundB = serviceBlockPacket.readBits(2);
668     int backgroundColor =
669         CueInfoBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, backgroundO);
670     // third byte
671     serviceBlockPacket.skipBits(2); // null padding
672     int edgeR = serviceBlockPacket.readBits(2);
673     int edgeG = serviceBlockPacket.readBits(2);
674     int edgeB = serviceBlockPacket.readBits(2);
675     int edgeColor = CueInfoBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB);
676 
677     currentCueInfoBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor);
678   }
679 
handleSetPenLocation()680   private void handleSetPenLocation() {
681     // the SetPenLocation command contains 2 bytes of data
682     // first byte
683     serviceBlockPacket.skipBits(4);
684     int row = serviceBlockPacket.readBits(4);
685     // second byte
686     serviceBlockPacket.skipBits(2);
687     int column = serviceBlockPacket.readBits(6);
688 
689     currentCueInfoBuilder.setPenLocation(row, column);
690   }
691 
handleSetWindowAttributes()692   private void handleSetWindowAttributes() {
693     // the SetWindowAttributes command contains 4 bytes of data
694     // first byte
695     int fillO = serviceBlockPacket.readBits(2);
696     int fillR = serviceBlockPacket.readBits(2);
697     int fillG = serviceBlockPacket.readBits(2);
698     int fillB = serviceBlockPacket.readBits(2);
699     int fillColor = CueInfoBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO);
700     // second byte
701     int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType
702     int borderR = serviceBlockPacket.readBits(2);
703     int borderG = serviceBlockPacket.readBits(2);
704     int borderB = serviceBlockPacket.readBits(2);
705     int borderColor = CueInfoBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB);
706     // third byte
707     if (serviceBlockPacket.readBit()) {
708       borderType |= 0x04; // set the top bit of the 3-bit borderType
709     }
710     boolean wordWrapToggle = serviceBlockPacket.readBit();
711     int printDirection = serviceBlockPacket.readBits(2);
712     int scrollDirection = serviceBlockPacket.readBits(2);
713     int justification = serviceBlockPacket.readBits(2);
714     // fourth byte
715     // Note that we don't intend to support display effects
716     serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2)
717 
718     currentCueInfoBuilder.setWindowAttributes(
719         fillColor,
720         borderColor,
721         wordWrapToggle,
722         borderType,
723         printDirection,
724         scrollDirection,
725         justification);
726   }
727 
handleDefineWindow(int window)728   private void handleDefineWindow(int window) {
729     CueInfoBuilder cueInfoBuilder = cueInfoBuilders[window];
730 
731     // the DefineWindow command contains 6 bytes of data
732     // first byte
733     serviceBlockPacket.skipBits(2); // null padding
734     boolean visible = serviceBlockPacket.readBit();
735     boolean rowLock = serviceBlockPacket.readBit();
736     boolean columnLock = serviceBlockPacket.readBit();
737     int priority = serviceBlockPacket.readBits(3);
738     // second byte
739     boolean relativePositioning = serviceBlockPacket.readBit();
740     int verticalAnchor = serviceBlockPacket.readBits(7);
741     // third byte
742     int horizontalAnchor = serviceBlockPacket.readBits(8);
743     // fourth byte
744     int anchorId = serviceBlockPacket.readBits(4);
745     int rowCount = serviceBlockPacket.readBits(4);
746     // fifth byte
747     serviceBlockPacket.skipBits(2); // null padding
748     int columnCount = serviceBlockPacket.readBits(6);
749     // sixth byte
750     serviceBlockPacket.skipBits(2); // null padding
751     int windowStyle = serviceBlockPacket.readBits(3);
752     int penStyle = serviceBlockPacket.readBits(3);
753 
754     cueInfoBuilder.defineWindow(
755         visible,
756         rowLock,
757         columnLock,
758         priority,
759         relativePositioning,
760         verticalAnchor,
761         horizontalAnchor,
762         rowCount,
763         columnCount,
764         anchorId,
765         windowStyle,
766         penStyle);
767   }
768 
getDisplayCues()769   private List<Cue> getDisplayCues() {
770     List<Cea708CueInfo> displayCueInfos = new ArrayList<>();
771     for (int i = 0; i < NUM_WINDOWS; i++) {
772       if (!cueInfoBuilders[i].isEmpty() && cueInfoBuilders[i].isVisible()) {
773         @Nullable Cea708CueInfo cueInfo = cueInfoBuilders[i].build();
774         if (cueInfo != null) {
775           displayCueInfos.add(cueInfo);
776         }
777       }
778     }
779     Collections.sort(
780         displayCueInfos,
781         (thisInfo, thatInfo) -> Integer.compare(thisInfo.priority, thatInfo.priority));
782     List<Cue> displayCues = new ArrayList<>(displayCueInfos.size());
783     for (int i = 0; i < displayCueInfos.size(); i++) {
784       displayCues.add(displayCueInfos.get(i).cue);
785     }
786     return Collections.unmodifiableList(displayCues);
787   }
788 
resetCueBuilders()789   private void resetCueBuilders() {
790     for (int i = 0; i < NUM_WINDOWS; i++) {
791       cueInfoBuilders[i].reset();
792     }
793   }
794 
795   private static final class DtvCcPacket {
796 
797     public final int sequenceNumber;
798     public final int packetSize;
799     public final byte[] packetData;
800 
801     int currentIndex;
802 
DtvCcPacket(int sequenceNumber, int packetSize)803     public DtvCcPacket(int sequenceNumber, int packetSize) {
804       this.sequenceNumber = sequenceNumber;
805       this.packetSize = packetSize;
806       packetData = new byte[2 * packetSize - 1];
807       currentIndex = 0;
808     }
809 
810   }
811 
812   // TODO: There is a lot of overlap between Cea708Decoder.CueInfoBuilder and
813   // Cea608Decoder.CueBuilder which could be refactored into a separate class.
814   private static final class CueInfoBuilder {
815 
816     private static final int RELATIVE_CUE_SIZE = 99;
817     private static final int VERTICAL_SIZE = 74;
818     private static final int HORIZONTAL_SIZE = 209;
819 
820     private static final int DEFAULT_PRIORITY = 4;
821 
822     private static final int MAXIMUM_ROW_COUNT = 15;
823 
824     private static final int JUSTIFICATION_LEFT = 0;
825     private static final int JUSTIFICATION_RIGHT = 1;
826     private static final int JUSTIFICATION_CENTER = 2;
827     private static final int JUSTIFICATION_FULL = 3;
828 
829     private static final int DIRECTION_LEFT_TO_RIGHT = 0;
830     private static final int DIRECTION_RIGHT_TO_LEFT = 1;
831     private static final int DIRECTION_TOP_TO_BOTTOM = 2;
832     private static final int DIRECTION_BOTTOM_TO_TOP = 3;
833 
834     // TODO: Add other border/edge types when utilized.
835     private static final int BORDER_AND_EDGE_TYPE_NONE = 0;
836     private static final int BORDER_AND_EDGE_TYPE_UNIFORM = 3;
837 
838     public static final int COLOR_SOLID_WHITE = getArgbColorFromCeaColor(2, 2, 2, 0);
839     public static final int COLOR_SOLID_BLACK = getArgbColorFromCeaColor(0, 0, 0, 0);
840     public static final int COLOR_TRANSPARENT = getArgbColorFromCeaColor(0, 0, 0, 3);
841 
842     // TODO: Add other sizes when utilized.
843     private static final int PEN_SIZE_STANDARD = 1;
844 
845     // TODO: Add other pen font styles when utilized.
846     private static final int PEN_FONT_STYLE_DEFAULT = 0;
847     private static final int PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS = 1;
848     private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS = 2;
849     private static final int PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS = 3;
850     private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS = 4;
851 
852     // TODO: Add other pen offsets when utilized.
853     private static final int PEN_OFFSET_NORMAL = 1;
854 
855     // The window style properties are specified in the CEA-708 specification.
856     private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[] {
857         JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT,
858         JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER,
859         JUSTIFICATION_LEFT
860     };
861     private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[] {
862         DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
863         DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
864         DIRECTION_TOP_TO_BOTTOM
865     };
866     private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[] {
867         DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
868         DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
869         DIRECTION_RIGHT_TO_LEFT
870     };
871     private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[] {
872         false, false, false, true, true, true, false
873     };
874     private static final int[] WINDOW_STYLE_FILL = new int[] {
875         COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
876         COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK
877     };
878 
879     // The pen style properties are specified in the CEA-708 specification.
880     private static final int[] PEN_STYLE_FONT_STYLE = new int[] {
881         PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS,
882         PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
883         PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS,
884         PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
885         PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS
886     };
887     private static final int[] PEN_STYLE_EDGE_TYPE = new int[] {
888         BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE,
889         BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM,
890         BORDER_AND_EDGE_TYPE_UNIFORM
891     };
892     private static final int[] PEN_STYLE_BACKGROUND = new int[] {
893         COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
894         COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT};
895 
896     private final List<SpannableString> rolledUpCaptions;
897     private final SpannableStringBuilder captionStringBuilder;
898 
899     // Window/Cue properties
900     private boolean defined;
901     private boolean visible;
902     private int priority;
903     private boolean relativePositioning;
904     private int verticalAnchor;
905     private int horizontalAnchor;
906     private int anchorId;
907     private int rowCount;
908     private boolean rowLock;
909     private int justification;
910     private int windowStyleId;
911     private int penStyleId;
912     private int windowFillColor;
913 
914     // Pen/Text properties
915     private int italicsStartPosition;
916     private int underlineStartPosition;
917     private int foregroundColorStartPosition;
918     private int foregroundColor;
919     private int backgroundColorStartPosition;
920     private int backgroundColor;
921     private int row;
922 
CueInfoBuilder()923     public CueInfoBuilder() {
924       rolledUpCaptions = new ArrayList<>();
925       captionStringBuilder = new SpannableStringBuilder();
926       reset();
927     }
928 
isEmpty()929     public boolean isEmpty() {
930       return !isDefined() || (rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0);
931     }
932 
reset()933     public void reset() {
934       clear();
935 
936       defined = false;
937       visible = false;
938       priority = DEFAULT_PRIORITY;
939       relativePositioning = false;
940       verticalAnchor = 0;
941       horizontalAnchor = 0;
942       anchorId = 0;
943       rowCount = MAXIMUM_ROW_COUNT;
944       rowLock = true;
945       justification = JUSTIFICATION_LEFT;
946       windowStyleId = 0;
947       penStyleId = 0;
948       windowFillColor = COLOR_SOLID_BLACK;
949 
950       foregroundColor = COLOR_SOLID_WHITE;
951       backgroundColor = COLOR_SOLID_BLACK;
952     }
953 
clear()954     public void clear() {
955       rolledUpCaptions.clear();
956       captionStringBuilder.clear();
957       italicsStartPosition = C.POSITION_UNSET;
958       underlineStartPosition = C.POSITION_UNSET;
959       foregroundColorStartPosition = C.POSITION_UNSET;
960       backgroundColorStartPosition = C.POSITION_UNSET;
961       row = 0;
962     }
963 
isDefined()964     public boolean isDefined() {
965       return defined;
966     }
967 
setVisibility(boolean visible)968     public void setVisibility(boolean visible) {
969       this.visible = visible;
970     }
971 
isVisible()972     public boolean isVisible() {
973       return visible;
974     }
975 
defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority, boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount, int columnCount, int anchorId, int windowStyleId, int penStyleId)976     public void defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority,
977         boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount,
978         int columnCount, int anchorId, int windowStyleId, int penStyleId) {
979       this.defined = true;
980       this.visible = visible;
981       this.rowLock = rowLock;
982       this.priority = priority;
983       this.relativePositioning = relativePositioning;
984       this.verticalAnchor = verticalAnchor;
985       this.horizontalAnchor = horizontalAnchor;
986       this.anchorId = anchorId;
987 
988       // Decoders must add one to rowCount to get the desired number of rows.
989       if (this.rowCount != rowCount + 1) {
990         this.rowCount = rowCount + 1;
991 
992         // Trim any rolled up captions that are no longer valid, if applicable.
993         while ((rowLock && (rolledUpCaptions.size() >= this.rowCount))
994             || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
995           rolledUpCaptions.remove(0);
996         }
997       }
998 
999       // TODO: Add support for column lock and count.
1000 
1001       if (windowStyleId != 0 && this.windowStyleId != windowStyleId) {
1002         this.windowStyleId = windowStyleId;
1003         // windowStyleId is 1-based.
1004         int windowStyleIdIndex = windowStyleId - 1;
1005         // Note that Border type and border color are the same for all window styles.
1006         setWindowAttributes(WINDOW_STYLE_FILL[windowStyleIdIndex], COLOR_TRANSPARENT,
1007             WINDOW_STYLE_WORD_WRAP[windowStyleIdIndex], BORDER_AND_EDGE_TYPE_NONE,
1008             WINDOW_STYLE_PRINT_DIRECTION[windowStyleIdIndex],
1009             WINDOW_STYLE_SCROLL_DIRECTION[windowStyleIdIndex],
1010             WINDOW_STYLE_JUSTIFICATION[windowStyleIdIndex]);
1011       }
1012 
1013       if (penStyleId != 0 && this.penStyleId != penStyleId) {
1014         this.penStyleId = penStyleId;
1015         // penStyleId is 1-based.
1016         int penStyleIdIndex = penStyleId - 1;
1017         // Note that pen size, offset, italics, underline, foreground color, and foreground
1018         // opacity are the same for all pen styles.
1019         setPenAttributes(0, PEN_OFFSET_NORMAL, PEN_SIZE_STANDARD, false, false,
1020             PEN_STYLE_EDGE_TYPE[penStyleIdIndex], PEN_STYLE_FONT_STYLE[penStyleIdIndex]);
1021         setPenColor(COLOR_SOLID_WHITE, PEN_STYLE_BACKGROUND[penStyleIdIndex], COLOR_SOLID_BLACK);
1022       }
1023     }
1024 
1025 
setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle, int borderType, int printDirection, int scrollDirection, int justification)1026     public void setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle,
1027         int borderType, int printDirection, int scrollDirection, int justification) {
1028       this.windowFillColor = fillColor;
1029       // TODO: Add support for border color and types.
1030       // TODO: Add support for word wrap.
1031       // TODO: Add support for other scroll directions.
1032       // TODO: Add support for other print directions.
1033       this.justification = justification;
1034 
1035     }
1036 
setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle, boolean underlineToggle, int edgeType, int fontStyle)1037     public void setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle,
1038         boolean underlineToggle, int edgeType, int fontStyle) {
1039       // TODO: Add support for text tags.
1040       // TODO: Add support for other offsets.
1041       // TODO: Add support for other pen sizes.
1042 
1043       if (italicsStartPosition != C.POSITION_UNSET) {
1044         if (!italicsToggle) {
1045           captionStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
1046               captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1047           italicsStartPosition = C.POSITION_UNSET;
1048         }
1049       } else if (italicsToggle) {
1050         italicsStartPosition = captionStringBuilder.length();
1051       }
1052 
1053       if (underlineStartPosition != C.POSITION_UNSET) {
1054         if (!underlineToggle) {
1055           captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
1056               captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1057           underlineStartPosition = C.POSITION_UNSET;
1058         }
1059       } else if (underlineToggle) {
1060         underlineStartPosition = captionStringBuilder.length();
1061       }
1062 
1063       // TODO: Add support for edge types.
1064       // TODO: Add support for other font styles.
1065     }
1066 
setPenColor(int foregroundColor, int backgroundColor, int edgeColor)1067     public void setPenColor(int foregroundColor, int backgroundColor, int edgeColor) {
1068       if (foregroundColorStartPosition != C.POSITION_UNSET) {
1069         if (this.foregroundColor != foregroundColor) {
1070           captionStringBuilder.setSpan(new ForegroundColorSpan(this.foregroundColor),
1071               foregroundColorStartPosition, captionStringBuilder.length(),
1072               Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1073         }
1074       }
1075       if (foregroundColor != COLOR_SOLID_WHITE) {
1076         foregroundColorStartPosition = captionStringBuilder.length();
1077         this.foregroundColor = foregroundColor;
1078       }
1079 
1080       if (backgroundColorStartPosition != C.POSITION_UNSET) {
1081         if (this.backgroundColor != backgroundColor) {
1082           captionStringBuilder.setSpan(new BackgroundColorSpan(this.backgroundColor),
1083               backgroundColorStartPosition, captionStringBuilder.length(),
1084               Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1085         }
1086       }
1087       if (backgroundColor != COLOR_SOLID_BLACK) {
1088         backgroundColorStartPosition = captionStringBuilder.length();
1089         this.backgroundColor = backgroundColor;
1090       }
1091 
1092       // TODO: Add support for edge color.
1093     }
1094 
setPenLocation(int row, int column)1095     public void setPenLocation(int row, int column) {
1096       // TODO: Support moving the pen location with a window properly.
1097 
1098       // Until we support proper pen locations, if we encounter a row that's different from the
1099       // previous one, we should append a new line. Otherwise, we'll see strings that should be
1100       // on new lines concatenated with the previous, resulting in 2 words being combined, as
1101       // well as potentially drawing beyond the width of the window/screen.
1102       if (this.row != row) {
1103         append('\n');
1104       }
1105       this.row = row;
1106     }
1107 
backspace()1108     public void backspace() {
1109       int length = captionStringBuilder.length();
1110       if (length > 0) {
1111         captionStringBuilder.delete(length - 1, length);
1112       }
1113     }
1114 
append(char text)1115     public void append(char text) {
1116       if (text == '\n') {
1117         rolledUpCaptions.add(buildSpannableString());
1118         captionStringBuilder.clear();
1119 
1120         if (italicsStartPosition != C.POSITION_UNSET) {
1121           italicsStartPosition = 0;
1122         }
1123         if (underlineStartPosition != C.POSITION_UNSET) {
1124           underlineStartPosition = 0;
1125         }
1126         if (foregroundColorStartPosition != C.POSITION_UNSET) {
1127           foregroundColorStartPosition = 0;
1128         }
1129         if (backgroundColorStartPosition != C.POSITION_UNSET) {
1130           backgroundColorStartPosition = 0;
1131         }
1132 
1133         while ((rowLock && (rolledUpCaptions.size() >= rowCount))
1134             || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
1135           rolledUpCaptions.remove(0);
1136         }
1137       } else {
1138         captionStringBuilder.append(text);
1139       }
1140     }
1141 
buildSpannableString()1142     public SpannableString buildSpannableString() {
1143       SpannableStringBuilder spannableStringBuilder =
1144           new SpannableStringBuilder(captionStringBuilder);
1145       int length = spannableStringBuilder.length();
1146 
1147       if (length > 0) {
1148         if (italicsStartPosition != C.POSITION_UNSET) {
1149           spannableStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
1150               length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1151         }
1152 
1153         if (underlineStartPosition != C.POSITION_UNSET) {
1154           spannableStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
1155               length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1156         }
1157 
1158         if (foregroundColorStartPosition != C.POSITION_UNSET) {
1159           spannableStringBuilder.setSpan(new ForegroundColorSpan(foregroundColor),
1160               foregroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1161         }
1162 
1163         if (backgroundColorStartPosition != C.POSITION_UNSET) {
1164           spannableStringBuilder.setSpan(new BackgroundColorSpan(backgroundColor),
1165               backgroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1166         }
1167       }
1168 
1169       return new SpannableString(spannableStringBuilder);
1170     }
1171 
1172     @Nullable
build()1173     public Cea708CueInfo build() {
1174       if (isEmpty()) {
1175         // The cue is empty.
1176         return null;
1177       }
1178 
1179       SpannableStringBuilder cueString = new SpannableStringBuilder();
1180 
1181       // Add any rolled up captions, separated by new lines.
1182       for (int i = 0; i < rolledUpCaptions.size(); i++) {
1183         cueString.append(rolledUpCaptions.get(i));
1184         cueString.append('\n');
1185       }
1186       // Add the current line.
1187       cueString.append(buildSpannableString());
1188 
1189       // TODO: Add support for right-to-left languages (i.e. where right would correspond to normal
1190       // alignment).
1191       Alignment alignment;
1192       switch (justification) {
1193         case JUSTIFICATION_FULL:
1194           // TODO: Add support for full justification.
1195         case JUSTIFICATION_LEFT:
1196           alignment = Alignment.ALIGN_NORMAL;
1197           break;
1198         case JUSTIFICATION_RIGHT:
1199           alignment = Alignment.ALIGN_OPPOSITE;
1200           break;
1201         case JUSTIFICATION_CENTER:
1202           alignment = Alignment.ALIGN_CENTER;
1203           break;
1204         default:
1205           throw new IllegalArgumentException("Unexpected justification value: " + justification);
1206       }
1207 
1208       float position;
1209       float line;
1210       if (relativePositioning) {
1211         position = (float) horizontalAnchor / RELATIVE_CUE_SIZE;
1212         line = (float) verticalAnchor / RELATIVE_CUE_SIZE;
1213       } else {
1214         position = (float) horizontalAnchor / HORIZONTAL_SIZE;
1215         line = (float) verticalAnchor / VERTICAL_SIZE;
1216       }
1217       // Apply screen-edge padding to the line and position.
1218       position = (position * 0.9f) + 0.05f;
1219       line = (line * 0.9f) + 0.05f;
1220 
1221       // anchorId specifies where the anchor should be placed on the caption cue/window. The 9
1222       // possible configurations are as follows:
1223       //   0-----1-----2
1224       //   |           |
1225       //   3     4     5
1226       //   |           |
1227       //   6-----7-----8
1228       @AnchorType int verticalAnchorType;
1229       if (anchorId % 3 == 0) {
1230         verticalAnchorType = Cue.ANCHOR_TYPE_START;
1231       } else if (anchorId % 3 == 1) {
1232         verticalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
1233       } else {
1234         verticalAnchorType = Cue.ANCHOR_TYPE_END;
1235       }
1236       // TODO: Add support for right-to-left languages (i.e. where start is on the right).
1237       @AnchorType int horizontalAnchorType;
1238       if (anchorId / 3 == 0) {
1239         horizontalAnchorType = Cue.ANCHOR_TYPE_START;
1240       } else if (anchorId / 3 == 1) {
1241         horizontalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
1242       } else {
1243         horizontalAnchorType = Cue.ANCHOR_TYPE_END;
1244       }
1245 
1246       boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK);
1247 
1248       return new Cea708CueInfo(
1249           cueString,
1250           alignment,
1251           line,
1252           Cue.LINE_TYPE_FRACTION,
1253           verticalAnchorType,
1254           position,
1255           horizontalAnchorType,
1256           Cue.DIMEN_UNSET,
1257           windowColorSet,
1258           windowFillColor,
1259           priority);
1260     }
1261 
getArgbColorFromCeaColor(int red, int green, int blue)1262     public static int getArgbColorFromCeaColor(int red, int green, int blue) {
1263       return getArgbColorFromCeaColor(red, green, blue, 0);
1264     }
1265 
getArgbColorFromCeaColor(int red, int green, int blue, int opacity)1266     public static int getArgbColorFromCeaColor(int red, int green, int blue, int opacity) {
1267       Assertions.checkIndex(red, 0, 4);
1268       Assertions.checkIndex(green, 0, 4);
1269       Assertions.checkIndex(blue, 0, 4);
1270       Assertions.checkIndex(opacity, 0, 4);
1271 
1272       int alpha;
1273       switch (opacity) {
1274         case 0:
1275         case 1:
1276           // Note the value of '1' is actually FLASH, but we don't support that.
1277           alpha = 255;
1278           break;
1279         case 2:
1280           alpha = 127;
1281           break;
1282         case 3:
1283           alpha = 0;
1284           break;
1285         default:
1286           alpha = 255;
1287       }
1288 
1289       // TODO: Add support for the Alternative Minimum Color List or the full 64 RGB combinations.
1290 
1291       // Return values based on the Minimum Color List
1292       return Color.argb(alpha,
1293           (red > 1 ? 255 : 0),
1294           (green > 1 ? 255 : 0),
1295           (blue > 1 ? 255 : 0));
1296     }
1297   }
1298 
1299   /** A {@link Cue} for CEA-708. */
1300   private static final class Cea708CueInfo {
1301 
1302     public final Cue cue;
1303 
1304     /** The priority of the cue box. */
1305     public final int priority;
1306 
1307     /**
1308      * @param text See {@link Cue#text}.
1309      * @param textAlignment See {@link Cue#textAlignment}.
1310      * @param line See {@link Cue#line}.
1311      * @param lineType See {@link Cue#lineType}.
1312      * @param lineAnchor See {@link Cue#lineAnchor}.
1313      * @param position See {@link Cue#position}.
1314      * @param positionAnchor See {@link Cue#positionAnchor}.
1315      * @param size See {@link Cue#size}.
1316      * @param windowColorSet See {@link Cue#windowColorSet}.
1317      * @param windowColor See {@link Cue#windowColor}.
1318      * @param priority See (@link #priority}.
1319      */
Cea708CueInfo( CharSequence text, Alignment textAlignment, float line, @Cue.LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor, int priority)1320     public Cea708CueInfo(
1321         CharSequence text,
1322         Alignment textAlignment,
1323         float line,
1324         @Cue.LineType int lineType,
1325         @AnchorType int lineAnchor,
1326         float position,
1327         @AnchorType int positionAnchor,
1328         float size,
1329         boolean windowColorSet,
1330         int windowColor,
1331         int priority) {
1332       this.cue =
1333           new Cue(
1334               text,
1335               textAlignment,
1336               line,
1337               lineType,
1338               lineAnchor,
1339               position,
1340               positionAnchor,
1341               size,
1342               windowColorSet,
1343               windowColor);
1344       this.priority = priority;
1345     }
1346   }
1347 }
1348