1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import android.content.Context;
20 import android.graphics.Canvas;
21 import android.graphics.Color;
22 import android.graphics.Paint;
23 import android.graphics.Rect;
24 import android.graphics.Typeface;
25 import android.os.Handler;
26 import android.os.Message;
27 import android.text.SpannableStringBuilder;
28 import android.text.Spanned;
29 import android.text.style.CharacterStyle;
30 import android.text.style.RelativeSizeSpan;
31 import android.text.style.StyleSpan;
32 import android.text.style.SubscriptSpan;
33 import android.text.style.SuperscriptSpan;
34 import android.text.style.UnderlineSpan;
35 import android.util.AttributeSet;
36 import android.text.Layout.Alignment;
37 import android.util.Log;
38 import android.text.TextUtils;
39 import android.view.Gravity;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.accessibility.CaptioningManager;
43 import android.view.accessibility.CaptioningManager.CaptionStyle;
44 import android.widget.RelativeLayout;
45 import android.widget.TextView;
46 
47 import java.io.UnsupportedEncodingException;
48 import java.nio.charset.Charset;
49 import java.nio.charset.StandardCharsets;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Comparator;
53 import java.util.List;
54 import java.util.Vector;
55 
56 import com.android.internal.widget.SubtitleView;
57 
58 /** @hide */
59 public class Cea708CaptionRenderer extends SubtitleController.Renderer {
60     private final Context mContext;
61     private Cea708CCWidget mCCWidget;
62 
Cea708CaptionRenderer(Context context)63     public Cea708CaptionRenderer(Context context) {
64         mContext = context;
65     }
66 
67     @Override
supports(MediaFormat format)68     public boolean supports(MediaFormat format) {
69         if (format.containsKey(MediaFormat.KEY_MIME)) {
70             String mimeType = format.getString(MediaFormat.KEY_MIME);
71             return MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_708.equals(mimeType);
72         }
73         return false;
74     }
75 
76     @Override
createTrack(MediaFormat format)77     public SubtitleTrack createTrack(MediaFormat format) {
78         String mimeType = format.getString(MediaFormat.KEY_MIME);
79         if (MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_708.equals(mimeType)) {
80             if (mCCWidget == null) {
81                 mCCWidget = new Cea708CCWidget(mContext);
82             }
83             return new Cea708CaptionTrack(mCCWidget, format);
84         }
85         throw new RuntimeException("No matching format: " + format.toString());
86     }
87 }
88 
89 /** @hide */
90 class Cea708CaptionTrack extends SubtitleTrack {
91     private final Cea708CCParser mCCParser;
92     private final Cea708CCWidget mRenderingWidget;
93 
Cea708CaptionTrack(Cea708CCWidget renderingWidget, MediaFormat format)94     Cea708CaptionTrack(Cea708CCWidget renderingWidget, MediaFormat format) {
95         super(format);
96 
97         mRenderingWidget = renderingWidget;
98         mCCParser = new Cea708CCParser(mRenderingWidget);
99     }
100 
101     @Override
onData(byte[] data, boolean eos, long runID)102     public void onData(byte[] data, boolean eos, long runID) {
103         mCCParser.parse(data);
104     }
105 
106     @Override
getRenderingWidget()107     public RenderingWidget getRenderingWidget() {
108         return mRenderingWidget;
109     }
110 
111     @Override
updateView(Vector<Cue> activeCues)112     public void updateView(Vector<Cue> activeCues) {
113         // Overriding with NO-OP, CC rendering by-passes this
114     }
115 }
116 
117 /**
118  * @hide
119  *
120  * A class for parsing CEA-708, which is the standard for closed captioning for ATSC DTV.
121  *
122  * <p>ATSC DTV closed caption data are carried on picture user data of video streams.
123  * This class starts to parse from picture user data payload, so extraction process of user_data
124  * from video streams is up to outside of this code.
125  *
126  * <p>There are 4 steps to decode user_data to provide closed caption services. Step 1 and 2 are
127  * done in NuPlayer and libstagefright.
128  *
129  * <h3>Step 1. user_data -&gt; CcPacket</h3>
130  *
131  * <p>First, user_data consists of cc_data packets, which are 3-byte segments. Here, CcPacket is a
132  * collection of cc_data packets in a frame along with same presentation timestamp. Because cc_data
133  * packets must be reassembled in the frame display order, CcPackets are reordered.
134  *
135  * <h3>Step 2. CcPacket -&gt; DTVCC packet</h3>
136  *
137  * <p>Each cc_data packet has a one byte for declaring a type of itself and data validity, and the
138  * subsequent two bytes for input data of a DTVCC packet. There are 4 types for cc_data packet.
139  * We're interested in DTVCC_PACKET_START(type 3) and DTVCC_PACKET_DATA(type 2). Each DTVCC packet
140  * begins with DTVCC_PACKET_START(type 3) and the following cc_data packets which has
141  * DTVCC_PACKET_DATA(type 2) are appended into the DTVCC packet being assembled.
142  *
143  * <h3>Step 3. DTVCC packet -&gt; Service Blocks</h3>
144  *
145  * <p>A DTVCC packet consists of multiple service blocks. Each service block represents a caption
146  * track and has a service number, which ranges from 1 to 63, that denotes caption track identity.
147  * In here, we listen at most one chosen caption track by service number. Otherwise, just skip the
148  * other service blocks.
149  *
150  * <h3>Step 4. Interpreting Service Block Data ({@link #parseServiceBlockData}, {@code parseXX},
151  * and {@link #parseExt1} methods)</h3>
152  *
153  * <p>Service block data is actual caption stream. it looks similar to telnet. It uses most parts of
154  * ASCII table and consists of specially defined commands and some ASCII control codes which work
155  * in a behavior slightly different from their original purpose. ASCII control codes and caption
156  * commands are explicit instructions that control the state of a closed caption service and the
157  * other ASCII and text codes are implicit instructions that send their characters to buffer.
158  *
159  * <p>There are 4 main code groups and 4 extended code groups. Both the range of code groups are the
160  * same as the range of a byte.
161  *
162  * <p>4 main code groups: C0, C1, G0, G1
163  * <br>4 extended code groups: C2, C3, G2, G3
164  *
165  * <p>Each code group has its own handle method. For example, {@link #parseC0} handles C0 code group
166  * and so on. And {@link #parseServiceBlockData} method maps a stream on the main code groups while
167  * {@link #parseExt1} method maps on the extended code groups.
168  *
169  * <p>The main code groups:
170  * <ul>
171  * <li>C0 - contains modified ASCII control codes. It is not intended by CEA-708 but Korea TTA
172  *      standard for ATSC CC uses P16 character heavily, which is unclear entity in CEA-708 doc,
173  *      even for the alphanumeric characters instead of ASCII characters.</li>
174  * <li>C1 - contains the caption commands. There are 3 categories of a caption command.</li>
175  * <ul>
176  * <li>Window commands: The window commands control a caption window which is addressable area being
177  *                  with in the Safe title area. (CWX, CLW, DSW, HDW, TGW, DLW, SWA, DFX)</li>
178  * <li>Pen commands: Th pen commands control text style and location. (SPA, SPC, SPL)</li>
179  * <li>Job commands: The job commands make a delay and recover from the delay. (DLY, DLC, RST)</li>
180  * </ul>
181  * <li>G0 - same as printable ASCII character set except music note character.</li>
182  * <li>G1 - same as ISO 8859-1 Latin 1 character set.</li>
183  * </ul>
184  * <p>Most of the extended code groups are being skipped.
185  *
186  */
187 class Cea708CCParser {
188     private static final String TAG = "Cea708CCParser";
189     private static final boolean DEBUG = false;
190 
191     private static final String MUSIC_NOTE_CHAR = new String(
192             "\u266B".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
193 
194     private final StringBuffer mBuffer = new StringBuffer();
195     private int mCommand = 0;
196 
197     // Assign a dummy listener in order to avoid null checks.
198     private DisplayListener mListener = new DisplayListener() {
199         @Override
200         public void emitEvent(CaptionEvent event) {
201             // do nothing
202         }
203     };
204 
205     /**
206      * {@link Cea708Parser} emits caption event of three different types.
207      * {@link DisplayListener#emitEvent} is invoked with the parameter
208      * {@link CaptionEvent} to pass all the results to an observer of the decoding process .
209      *
210      * <p>{@link CaptionEvent#type} determines the type of the result and
211      * {@link CaptionEvent#obj} contains the output value of a caption event.
212      * The observer must do the casting to the corresponding type.
213      *
214      * <ul><li>{@code CAPTION_EMIT_TYPE_BUFFER}: Passes a caption text buffer to a observer.
215      * {@code obj} must be of {@link String}.</li>
216      *
217      * <li>{@code CAPTION_EMIT_TYPE_CONTROL}: Passes a caption character control code to a observer.
218      * {@code obj} must be of {@link Character}.</li>
219      *
220      * <li>{@code CAPTION_EMIT_TYPE_CLEAR_COMMAND}: Passes a clear command to a observer.
221      * {@code obj} must be {@code NULL}.</li></ul>
222      */
223     public static final int CAPTION_EMIT_TYPE_BUFFER = 1;
224     public static final int CAPTION_EMIT_TYPE_CONTROL = 2;
225     public static final int CAPTION_EMIT_TYPE_COMMAND_CWX = 3;
226     public static final int CAPTION_EMIT_TYPE_COMMAND_CLW = 4;
227     public static final int CAPTION_EMIT_TYPE_COMMAND_DSW = 5;
228     public static final int CAPTION_EMIT_TYPE_COMMAND_HDW = 6;
229     public static final int CAPTION_EMIT_TYPE_COMMAND_TGW = 7;
230     public static final int CAPTION_EMIT_TYPE_COMMAND_DLW = 8;
231     public static final int CAPTION_EMIT_TYPE_COMMAND_DLY = 9;
232     public static final int CAPTION_EMIT_TYPE_COMMAND_DLC = 10;
233     public static final int CAPTION_EMIT_TYPE_COMMAND_RST = 11;
234     public static final int CAPTION_EMIT_TYPE_COMMAND_SPA = 12;
235     public static final int CAPTION_EMIT_TYPE_COMMAND_SPC = 13;
236     public static final int CAPTION_EMIT_TYPE_COMMAND_SPL = 14;
237     public static final int CAPTION_EMIT_TYPE_COMMAND_SWA = 15;
238     public static final int CAPTION_EMIT_TYPE_COMMAND_DFX = 16;
239 
Cea708CCParser(DisplayListener listener)240     Cea708CCParser(DisplayListener listener) {
241         if (listener != null) {
242             mListener = listener;
243         }
244     }
245 
246     interface DisplayListener {
emitEvent(CaptionEvent event)247         void emitEvent(CaptionEvent event);
248     }
249 
emitCaptionEvent(CaptionEvent captionEvent)250     private void emitCaptionEvent(CaptionEvent captionEvent) {
251         // Emit the existing string buffer before a new event is arrived.
252         emitCaptionBuffer();
253         mListener.emitEvent(captionEvent);
254     }
255 
emitCaptionBuffer()256     private void emitCaptionBuffer() {
257         if (mBuffer.length() > 0) {
258             mListener.emitEvent(new CaptionEvent(CAPTION_EMIT_TYPE_BUFFER, mBuffer.toString()));
259             mBuffer.setLength(0);
260         }
261     }
262 
263     // Step 3. DTVCC packet -> Service Blocks (parseDtvCcPacket method)
parse(byte[] data)264     public void parse(byte[] data) {
265         // From this point, starts to read DTVCC coding layer.
266         // First, identify code groups, which is defined in CEA-708B Section 7.1.
267         int pos = 0;
268         while (pos < data.length) {
269             pos = parseServiceBlockData(data, pos);
270         }
271 
272         // Emit the buffer after reading codes.
273         emitCaptionBuffer();
274     }
275 
276     // Step 4. Main code groups
parseServiceBlockData(byte[] data, int pos)277     private int parseServiceBlockData(byte[] data, int pos) {
278         // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6.
279         mCommand = data[pos] & 0xff;
280         ++pos;
281         if (mCommand == Const.CODE_C0_EXT1) {
282             if (DEBUG) {
283                 Log.d(TAG, String.format("parseServiceBlockData EXT1 %x", mCommand));
284             }
285             pos = parseExt1(data, pos);
286         } else if (mCommand >= Const.CODE_C0_RANGE_START
287                 && mCommand <= Const.CODE_C0_RANGE_END) {
288             if (DEBUG) {
289                 Log.d(TAG, String.format("parseServiceBlockData C0 %x", mCommand));
290             }
291             pos = parseC0(data, pos);
292         } else if (mCommand >= Const.CODE_C1_RANGE_START
293                 && mCommand <= Const.CODE_C1_RANGE_END) {
294             if (DEBUG) {
295                 Log.d(TAG, String.format("parseServiceBlockData C1 %x", mCommand));
296             }
297             pos = parseC1(data, pos);
298         } else if (mCommand >= Const.CODE_G0_RANGE_START
299                 && mCommand <= Const.CODE_G0_RANGE_END) {
300             if (DEBUG) {
301                 Log.d(TAG, String.format("parseServiceBlockData G0 %x", mCommand));
302             }
303             pos = parseG0(data, pos);
304         } else if (mCommand >= Const.CODE_G1_RANGE_START
305                 && mCommand <= Const.CODE_G1_RANGE_END) {
306             if (DEBUG) {
307                 Log.d(TAG, String.format("parseServiceBlockData G1 %x", mCommand));
308             }
309             pos = parseG1(data, pos);
310         }
311         return pos;
312     }
313 
parseC0(byte[] data, int pos)314     private int parseC0(byte[] data, int pos) {
315         // For the details of C0 code group, see CEA-708B Section 7.4.1.
316         // CL Group: C0 Subset of ASCII Control codes
317         if (mCommand >= Const.CODE_C0_SKIP2_RANGE_START
318                 && mCommand <= Const.CODE_C0_SKIP2_RANGE_END) {
319             if (mCommand == Const.CODE_C0_P16) {
320                 // P16 escapes next two bytes for the large character maps.(no standard rule)
321                 // For Korea broadcasting, express whole letters by using this.
322                 try {
323                     if (data[pos] == 0) {
324                         mBuffer.append((char) data[pos + 1]);
325                     } else {
326                         String value = new String(Arrays.copyOfRange(data, pos, pos + 2), "EUC-KR");
327                         mBuffer.append(value);
328                     }
329                 } catch (UnsupportedEncodingException e) {
330                     Log.e(TAG, "P16 Code - Could not find supported encoding", e);
331                 }
332             }
333             pos += 2;
334         } else if (mCommand >= Const.CODE_C0_SKIP1_RANGE_START
335                 && mCommand <= Const.CODE_C0_SKIP1_RANGE_END) {
336             ++pos;
337         } else {
338             // NUL, BS, FF, CR interpreted as they are in ASCII control codes.
339             // HCR moves the pen location to th beginning of the current line and deletes contents.
340             // FF clears the screen and moves the pen location to (0,0).
341             // ETX is the NULL command which is used to flush text to the current window when no
342             // other command is pending.
343             switch (mCommand) {
344                 case Const.CODE_C0_NUL:
345                     break;
346                 case Const.CODE_C0_ETX:
347                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
348                     break;
349                 case Const.CODE_C0_BS:
350                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
351                     break;
352                 case Const.CODE_C0_FF:
353                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
354                     break;
355                 case Const.CODE_C0_CR:
356                     mBuffer.append('\n');
357                     break;
358                 case Const.CODE_C0_HCR:
359                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
360                     break;
361                 default:
362                     break;
363             }
364         }
365         return pos;
366     }
367 
parseC1(byte[] data, int pos)368     private int parseC1(byte[] data, int pos) {
369         // For the details of C1 code group, see CEA-708B Section 8.10.
370         // CR Group: C1 Caption Control Codes
371         switch (mCommand) {
372             case Const.CODE_C1_CW0:
373             case Const.CODE_C1_CW1:
374             case Const.CODE_C1_CW2:
375             case Const.CODE_C1_CW3:
376             case Const.CODE_C1_CW4:
377             case Const.CODE_C1_CW5:
378             case Const.CODE_C1_CW6:
379             case Const.CODE_C1_CW7: {
380                 // SetCurrentWindow0-7
381                 int windowId = mCommand - Const.CODE_C1_CW0;
382                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CWX, windowId));
383                 if (DEBUG) {
384                     Log.d(TAG, String.format("CaptionCommand CWX windowId: %d", windowId));
385                 }
386                 break;
387             }
388 
389             case Const.CODE_C1_CLW: {
390                 // ClearWindows
391                 int windowBitmap = data[pos] & 0xff;
392                 ++pos;
393                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CLW, windowBitmap));
394                 if (DEBUG) {
395                     Log.d(TAG, String.format("CaptionCommand CLW windowBitmap: %d", windowBitmap));
396                 }
397                 break;
398             }
399 
400             case Const.CODE_C1_DSW: {
401                 // DisplayWindows
402                 int windowBitmap = data[pos] & 0xff;
403                 ++pos;
404                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DSW, windowBitmap));
405                 if (DEBUG) {
406                     Log.d(TAG, String.format("CaptionCommand DSW windowBitmap: %d", windowBitmap));
407                 }
408                 break;
409             }
410 
411             case Const.CODE_C1_HDW: {
412                 // HideWindows
413                 int windowBitmap = data[pos] & 0xff;
414                 ++pos;
415                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_HDW, windowBitmap));
416                 if (DEBUG) {
417                     Log.d(TAG, String.format("CaptionCommand HDW windowBitmap: %d", windowBitmap));
418                 }
419                 break;
420             }
421 
422             case Const.CODE_C1_TGW: {
423                 // ToggleWindows
424                 int windowBitmap = data[pos] & 0xff;
425                 ++pos;
426                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_TGW, windowBitmap));
427                 if (DEBUG) {
428                     Log.d(TAG, String.format("CaptionCommand TGW windowBitmap: %d", windowBitmap));
429                 }
430                 break;
431             }
432 
433             case Const.CODE_C1_DLW: {
434                 // DeleteWindows
435                 int windowBitmap = data[pos] & 0xff;
436                 ++pos;
437                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLW, windowBitmap));
438                 if (DEBUG) {
439                     Log.d(TAG, String.format("CaptionCommand DLW windowBitmap: %d", windowBitmap));
440                 }
441                 break;
442             }
443 
444             case Const.CODE_C1_DLY: {
445                 // Delay
446                 int tenthsOfSeconds = data[pos] & 0xff;
447                 ++pos;
448                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLY, tenthsOfSeconds));
449                 if (DEBUG) {
450                     Log.d(TAG, String.format("CaptionCommand DLY %d tenths of seconds",
451                             tenthsOfSeconds));
452                 }
453                 break;
454             }
455             case Const.CODE_C1_DLC: {
456                 // DelayCancel
457                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLC, null));
458                 if (DEBUG) {
459                     Log.d(TAG, "CaptionCommand DLC");
460                 }
461                 break;
462             }
463 
464             case Const.CODE_C1_RST: {
465                 // Reset
466                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_RST, null));
467                 if (DEBUG) {
468                     Log.d(TAG, "CaptionCommand RST");
469                 }
470                 break;
471             }
472 
473             case Const.CODE_C1_SPA: {
474                 // SetPenAttributes
475                 int textTag = (data[pos] & 0xf0) >> 4;
476                 int penSize = data[pos] & 0x03;
477                 int penOffset = (data[pos] & 0x0c) >> 2;
478                 boolean italic = (data[pos + 1] & 0x80) != 0;
479                 boolean underline = (data[pos + 1] & 0x40) != 0;
480                 int edgeType = (data[pos + 1] & 0x38) >> 3;
481                 int fontTag = data[pos + 1] & 0x7;
482                 pos += 2;
483                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPA,
484                         new CaptionPenAttr(penSize, penOffset, textTag, fontTag, edgeType,
485                                 underline, italic)));
486                 if (DEBUG) {
487                     Log.d(TAG, String.format(
488                             "CaptionCommand SPA penSize: %d, penOffset: %d, textTag: %d, "
489                                     + "fontTag: %d, edgeType: %d, underline: %s, italic: %s",
490                             penSize, penOffset, textTag, fontTag, edgeType, underline, italic));
491                 }
492                 break;
493             }
494 
495             case Const.CODE_C1_SPC: {
496                 // SetPenColor
497                 int opacity = (data[pos] & 0xc0) >> 6;
498                 int red = (data[pos] & 0x30) >> 4;
499                 int green = (data[pos] & 0x0c) >> 2;
500                 int blue = data[pos] & 0x03;
501                 CaptionColor foregroundColor = new CaptionColor(opacity, red, green, blue);
502                 ++pos;
503                 opacity = (data[pos] & 0xc0) >> 6;
504                 red = (data[pos] & 0x30) >> 4;
505                 green = (data[pos] & 0x0c) >> 2;
506                 blue = data[pos] & 0x03;
507                 CaptionColor backgroundColor = new CaptionColor(opacity, red, green, blue);
508                 ++pos;
509                 red = (data[pos] & 0x30) >> 4;
510                 green = (data[pos] & 0x0c) >> 2;
511                 blue = data[pos] & 0x03;
512                 CaptionColor edgeColor = new CaptionColor(
513                         CaptionColor.OPACITY_SOLID, red, green, blue);
514                 ++pos;
515                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPC,
516                         new CaptionPenColor(foregroundColor, backgroundColor, edgeColor)));
517                 if (DEBUG) {
518                     Log.d(TAG, String.format(
519                             "CaptionCommand SPC foregroundColor %s backgroundColor %s edgeColor %s",
520                             foregroundColor, backgroundColor, edgeColor));
521                 }
522                 break;
523             }
524 
525             case Const.CODE_C1_SPL: {
526                 // SetPenLocation
527                 // column is normally 0-31 for 4:3 formats, and 0-41 for 16:9 formats
528                 int row = data[pos] & 0x0f;
529                 int column = data[pos + 1] & 0x3f;
530                 pos += 2;
531                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPL,
532                         new CaptionPenLocation(row, column)));
533                 if (DEBUG) {
534                     Log.d(TAG, String.format("CaptionCommand SPL row: %d, column: %d",
535                             row, column));
536                 }
537                 break;
538             }
539 
540             case Const.CODE_C1_SWA: {
541                 // SetWindowAttributes
542                 int opacity = (data[pos] & 0xc0) >> 6;
543                 int red = (data[pos] & 0x30) >> 4;
544                 int green = (data[pos] & 0x0c) >> 2;
545                 int blue = data[pos] & 0x03;
546                 CaptionColor fillColor = new CaptionColor(opacity, red, green, blue);
547                 int borderType = (data[pos + 1] & 0xc0) >> 6 | (data[pos + 2] & 0x80) >> 5;
548                 red = (data[pos + 1] & 0x30) >> 4;
549                 green = (data[pos + 1] & 0x0c) >> 2;
550                 blue = data[pos + 1] & 0x03;
551                 CaptionColor borderColor = new CaptionColor(
552                         CaptionColor.OPACITY_SOLID, red, green, blue);
553                 boolean wordWrap = (data[pos + 2] & 0x40) != 0;
554                 int printDirection = (data[pos + 2] & 0x30) >> 4;
555                 int scrollDirection = (data[pos + 2] & 0x0c) >> 2;
556                 int justify = (data[pos + 2] & 0x03);
557                 int effectSpeed = (data[pos + 3] & 0xf0) >> 4;
558                 int effectDirection = (data[pos + 3] & 0x0c) >> 2;
559                 int displayEffect = data[pos + 3] & 0x3;
560                 pos += 4;
561                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SWA,
562                         new CaptionWindowAttr(fillColor, borderColor, borderType, wordWrap,
563                                 printDirection, scrollDirection, justify,
564                                 effectDirection, effectSpeed, displayEffect)));
565                 if (DEBUG) {
566                     Log.d(TAG, String.format(
567                             "CaptionCommand SWA fillColor: %s, borderColor: %s, borderType: %d"
568                                     + "wordWrap: %s, printDirection: %d, scrollDirection: %d, "
569                                     + "justify: %s, effectDirection: %d, effectSpeed: %d, "
570                                     + "displayEffect: %d",
571                             fillColor, borderColor, borderType, wordWrap, printDirection,
572                             scrollDirection, justify, effectDirection, effectSpeed, displayEffect));
573                 }
574                 break;
575             }
576 
577             case Const.CODE_C1_DF0:
578             case Const.CODE_C1_DF1:
579             case Const.CODE_C1_DF2:
580             case Const.CODE_C1_DF3:
581             case Const.CODE_C1_DF4:
582             case Const.CODE_C1_DF5:
583             case Const.CODE_C1_DF6:
584             case Const.CODE_C1_DF7: {
585                 // DefineWindow0-7
586                 int windowId = mCommand - Const.CODE_C1_DF0;
587                 boolean visible = (data[pos] & 0x20) != 0;
588                 boolean rowLock = (data[pos] & 0x10) != 0;
589                 boolean columnLock = (data[pos] & 0x08) != 0;
590                 int priority = data[pos] & 0x07;
591                 boolean relativePositioning = (data[pos + 1] & 0x80) != 0;
592                 int anchorVertical = data[pos + 1] & 0x7f;
593                 int anchorHorizontal = data[pos + 2] & 0xff;
594                 int anchorId = (data[pos + 3] & 0xf0) >> 4;
595                 int rowCount = data[pos + 3] & 0x0f;
596                 int columnCount = data[pos + 4] & 0x3f;
597                 int windowStyle = (data[pos + 5] & 0x38) >> 3;
598                 int penStyle = data[pos + 5] & 0x07;
599                 pos += 6;
600                 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DFX,
601                         new CaptionWindow(windowId, visible, rowLock, columnLock, priority,
602                                 relativePositioning, anchorVertical, anchorHorizontal, anchorId,
603                                 rowCount, columnCount, penStyle, windowStyle)));
604                 if (DEBUG) {
605                     Log.d(TAG, String.format(
606                             "CaptionCommand DFx windowId: %d, priority: %d, columnLock: %s, "
607                                     + "rowLock: %s, visible: %s, anchorVertical: %d, "
608                                     + "relativePositioning: %s, anchorHorizontal: %d, "
609                                     + "rowCount: %d, anchorId: %d, columnCount: %d, penStyle: %d, "
610                                     + "windowStyle: %d",
611                             windowId, priority, columnLock, rowLock, visible, anchorVertical,
612                             relativePositioning, anchorHorizontal, rowCount, anchorId, columnCount,
613                             penStyle, windowStyle));
614                 }
615                 break;
616             }
617 
618             default:
619                 break;
620         }
621         return pos;
622     }
623 
parseG0(byte[] data, int pos)624     private int parseG0(byte[] data, int pos) {
625         // For the details of G0 code group, see CEA-708B Section 7.4.3.
626         // GL Group: G0 Modified version of ANSI X3.4 Printable Character Set (ASCII)
627         if (mCommand == Const.CODE_G0_MUSICNOTE) {
628             // Music note.
629             mBuffer.append(MUSIC_NOTE_CHAR);
630         } else {
631             // Put ASCII code into buffer.
632             mBuffer.append((char) mCommand);
633         }
634         return pos;
635     }
636 
parseG1(byte[] data, int pos)637     private int parseG1(byte[] data, int pos) {
638         // For the details of G0 code group, see CEA-708B Section 7.4.4.
639         // GR Group: G1 ISO 8859-1 Latin 1 Characters
640         // Put ASCII Extended character set into buffer.
641         mBuffer.append((char) mCommand);
642         return pos;
643     }
644 
645     // Step 4. Extended code groups
parseExt1(byte[] data, int pos)646     private int parseExt1(byte[] data, int pos) {
647         // For the details of EXT1 code group, see CEA-708B Section 7.2.
648         mCommand = data[pos] & 0xff;
649         ++pos;
650         if (mCommand >= Const.CODE_C2_RANGE_START
651                 && mCommand <= Const.CODE_C2_RANGE_END) {
652             pos = parseC2(data, pos);
653         } else if (mCommand >= Const.CODE_C3_RANGE_START
654                 && mCommand <= Const.CODE_C3_RANGE_END) {
655             pos = parseC3(data, pos);
656         } else if (mCommand >= Const.CODE_G2_RANGE_START
657                 && mCommand <= Const.CODE_G2_RANGE_END) {
658             pos = parseG2(data, pos);
659         } else if (mCommand >= Const.CODE_G3_RANGE_START
660                 && mCommand <= Const.CODE_G3_RANGE_END) {
661             pos = parseG3(data ,pos);
662         }
663         return pos;
664     }
665 
parseC2(byte[] data, int pos)666     private int parseC2(byte[] data, int pos) {
667         // For the details of C2 code group, see CEA-708B Section 7.4.7.
668         // Extended Miscellaneous Control Codes
669         // C2 Table : No commands as of CEA-708B. A decoder must skip.
670         if (mCommand >= Const.CODE_C2_SKIP0_RANGE_START
671                 && mCommand <= Const.CODE_C2_SKIP0_RANGE_END) {
672             // Do nothing.
673         } else if (mCommand >= Const.CODE_C2_SKIP1_RANGE_START
674                 && mCommand <= Const.CODE_C2_SKIP1_RANGE_END) {
675             ++pos;
676         } else if (mCommand >= Const.CODE_C2_SKIP2_RANGE_START
677                 && mCommand <= Const.CODE_C2_SKIP2_RANGE_END) {
678             pos += 2;
679         } else if (mCommand >= Const.CODE_C2_SKIP3_RANGE_START
680                 && mCommand <= Const.CODE_C2_SKIP3_RANGE_END) {
681             pos += 3;
682         }
683         return pos;
684     }
685 
parseC3(byte[] data, int pos)686     private int parseC3(byte[] data, int pos) {
687         // For the details of C3 code group, see CEA-708B Section 7.4.8.
688         // Extended Control Code Set 2
689         // C3 Table : No commands as of CEA-708B. A decoder must skip.
690         if (mCommand >= Const.CODE_C3_SKIP4_RANGE_START
691                 && mCommand <= Const.CODE_C3_SKIP4_RANGE_END) {
692             pos += 4;
693         } else if (mCommand >= Const.CODE_C3_SKIP5_RANGE_START
694                 && mCommand <= Const.CODE_C3_SKIP5_RANGE_END) {
695             pos += 5;
696         }
697         return pos;
698     }
699 
parseG2(byte[] data, int pos)700     private int parseG2(byte[] data, int pos) {
701         // For the details of C3 code group, see CEA-708B Section 7.4.5.
702         // Extended Control Code Set 1(G2 Table)
703         switch (mCommand) {
704             case Const.CODE_G2_TSP:
705                 // TODO : TSP is the Transparent space
706                 break;
707             case Const.CODE_G2_NBTSP:
708                 // TODO : NBTSP is Non-Breaking Transparent Space.
709                 break;
710             case Const.CODE_G2_BLK:
711                 // TODO : BLK indicates a solid block which fills the entire character block
712                 // TODO : with a solid foreground color.
713                 break;
714             default:
715                 break;
716         }
717         return pos;
718     }
719 
parseG3(byte[] data, int pos)720     private int parseG3(byte[] data, int pos) {
721         // For the details of C3 code group, see CEA-708B Section 7.4.6.
722         // Future characters and icons(G3 Table)
723         if (mCommand == Const.CODE_G3_CC) {
724             // TODO : [CC] icon with square corners
725         }
726 
727         // Do nothing
728         return pos;
729     }
730 
731     /**
732      * @hide
733      *
734      * Collection of CEA-708 structures.
735      */
736     private static class Const {
737 
Const()738         private Const() {
739         }
740 
741         // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6.
742         public static final int CODE_C0_RANGE_START = 0x00;
743         public static final int CODE_C0_RANGE_END = 0x1f;
744         public static final int CODE_C1_RANGE_START = 0x80;
745         public static final int CODE_C1_RANGE_END = 0x9f;
746         public static final int CODE_G0_RANGE_START = 0x20;
747         public static final int CODE_G0_RANGE_END = 0x7f;
748         public static final int CODE_G1_RANGE_START = 0xa0;
749         public static final int CODE_G1_RANGE_END = 0xff;
750         public static final int CODE_C2_RANGE_START = 0x00;
751         public static final int CODE_C2_RANGE_END = 0x1f;
752         public static final int CODE_C3_RANGE_START = 0x80;
753         public static final int CODE_C3_RANGE_END = 0x9f;
754         public static final int CODE_G2_RANGE_START = 0x20;
755         public static final int CODE_G2_RANGE_END = 0x7f;
756         public static final int CODE_G3_RANGE_START = 0xa0;
757         public static final int CODE_G3_RANGE_END = 0xff;
758 
759         // The following ranges are defined in CEA-708B Section 7.4.1.
760         public static final int CODE_C0_SKIP2_RANGE_START = 0x18;
761         public static final int CODE_C0_SKIP2_RANGE_END = 0x1f;
762         public static final int CODE_C0_SKIP1_RANGE_START = 0x10;
763         public static final int CODE_C0_SKIP1_RANGE_END = 0x17;
764 
765         // The following ranges are defined in CEA-708B Section 7.4.7.
766         public static final int CODE_C2_SKIP0_RANGE_START = 0x00;
767         public static final int CODE_C2_SKIP0_RANGE_END = 0x07;
768         public static final int CODE_C2_SKIP1_RANGE_START = 0x08;
769         public static final int CODE_C2_SKIP1_RANGE_END = 0x0f;
770         public static final int CODE_C2_SKIP2_RANGE_START = 0x10;
771         public static final int CODE_C2_SKIP2_RANGE_END = 0x17;
772         public static final int CODE_C2_SKIP3_RANGE_START = 0x18;
773         public static final int CODE_C2_SKIP3_RANGE_END = 0x1f;
774 
775         // The following ranges are defined in CEA-708B Section 7.4.8.
776         public static final int CODE_C3_SKIP4_RANGE_START = 0x80;
777         public static final int CODE_C3_SKIP4_RANGE_END = 0x87;
778         public static final int CODE_C3_SKIP5_RANGE_START = 0x88;
779         public static final int CODE_C3_SKIP5_RANGE_END = 0x8f;
780 
781         // The following values are the special characters of CEA-708 spec.
782         public static final int CODE_C0_NUL = 0x00;
783         public static final int CODE_C0_ETX = 0x03;
784         public static final int CODE_C0_BS = 0x08;
785         public static final int CODE_C0_FF = 0x0c;
786         public static final int CODE_C0_CR = 0x0d;
787         public static final int CODE_C0_HCR = 0x0e;
788         public static final int CODE_C0_EXT1 = 0x10;
789         public static final int CODE_C0_P16 = 0x18;
790         public static final int CODE_G0_MUSICNOTE = 0x7f;
791         public static final int CODE_G2_TSP = 0x20;
792         public static final int CODE_G2_NBTSP = 0x21;
793         public static final int CODE_G2_BLK = 0x30;
794         public static final int CODE_G3_CC = 0xa0;
795 
796         // The following values are the command bits of CEA-708 spec.
797         public static final int CODE_C1_CW0 = 0x80;
798         public static final int CODE_C1_CW1 = 0x81;
799         public static final int CODE_C1_CW2 = 0x82;
800         public static final int CODE_C1_CW3 = 0x83;
801         public static final int CODE_C1_CW4 = 0x84;
802         public static final int CODE_C1_CW5 = 0x85;
803         public static final int CODE_C1_CW6 = 0x86;
804         public static final int CODE_C1_CW7 = 0x87;
805         public static final int CODE_C1_CLW = 0x88;
806         public static final int CODE_C1_DSW = 0x89;
807         public static final int CODE_C1_HDW = 0x8a;
808         public static final int CODE_C1_TGW = 0x8b;
809         public static final int CODE_C1_DLW = 0x8c;
810         public static final int CODE_C1_DLY = 0x8d;
811         public static final int CODE_C1_DLC = 0x8e;
812         public static final int CODE_C1_RST = 0x8f;
813         public static final int CODE_C1_SPA = 0x90;
814         public static final int CODE_C1_SPC = 0x91;
815         public static final int CODE_C1_SPL = 0x92;
816         public static final int CODE_C1_SWA = 0x97;
817         public static final int CODE_C1_DF0 = 0x98;
818         public static final int CODE_C1_DF1 = 0x99;
819         public static final int CODE_C1_DF2 = 0x9a;
820         public static final int CODE_C1_DF3 = 0x9b;
821         public static final int CODE_C1_DF4 = 0x9c;
822         public static final int CODE_C1_DF5 = 0x9d;
823         public static final int CODE_C1_DF6 = 0x9e;
824         public static final int CODE_C1_DF7 = 0x9f;
825     }
826 
827     /**
828      * @hide
829      *
830      * CEA-708B-specific color.
831      */
832     public static class CaptionColor {
833         public static final int OPACITY_SOLID = 0;
834         public static final int OPACITY_FLASH = 1;
835         public static final int OPACITY_TRANSLUCENT = 2;
836         public static final int OPACITY_TRANSPARENT = 3;
837 
838         private static final int[] COLOR_MAP = new int[] { 0x00, 0x0f, 0xf0, 0xff };
839         private static final int[] OPACITY_MAP = new int[] { 0xff, 0xfe, 0x80, 0x00 };
840 
841         public final int opacity;
842         public final int red;
843         public final int green;
844         public final int blue;
845 
CaptionColor(int opacity, int red, int green, int blue)846         public CaptionColor(int opacity, int red, int green, int blue) {
847             this.opacity = opacity;
848             this.red = red;
849             this.green = green;
850             this.blue = blue;
851         }
852 
getArgbValue()853         public int getArgbValue() {
854             return Color.argb(
855                     OPACITY_MAP[opacity], COLOR_MAP[red], COLOR_MAP[green], COLOR_MAP[blue]);
856         }
857     }
858 
859     /**
860      * @hide
861      *
862      * Caption event generated by {@link Cea708CCParser}.
863      */
864     public static class CaptionEvent {
865         public final int type;
866         public final Object obj;
867 
CaptionEvent(int type, Object obj)868         public CaptionEvent(int type, Object obj) {
869             this.type = type;
870             this.obj = obj;
871         }
872     }
873 
874     /**
875      * @hide
876      *
877      * Pen style information.
878      */
879     public static class CaptionPenAttr {
880         // Pen sizes
881         public static final int PEN_SIZE_SMALL = 0;
882         public static final int PEN_SIZE_STANDARD = 1;
883         public static final int PEN_SIZE_LARGE = 2;
884 
885         // Offsets
886         public static final int OFFSET_SUBSCRIPT = 0;
887         public static final int OFFSET_NORMAL = 1;
888         public static final int OFFSET_SUPERSCRIPT = 2;
889 
890         public final int penSize;
891         public final int penOffset;
892         public final int textTag;
893         public final int fontTag;
894         public final int edgeType;
895         public final boolean underline;
896         public final boolean italic;
897 
CaptionPenAttr(int penSize, int penOffset, int textTag, int fontTag, int edgeType, boolean underline, boolean italic)898         public CaptionPenAttr(int penSize, int penOffset, int textTag, int fontTag, int edgeType,
899                 boolean underline, boolean italic) {
900             this.penSize = penSize;
901             this.penOffset = penOffset;
902             this.textTag = textTag;
903             this.fontTag = fontTag;
904             this.edgeType = edgeType;
905             this.underline = underline;
906             this.italic = italic;
907         }
908     }
909 
910     /**
911      * @hide
912      *
913      * {@link CaptionColor} objects that indicate the foreground, background, and edge color of a
914      * pen.
915      */
916     public static class CaptionPenColor {
917         public final CaptionColor foregroundColor;
918         public final CaptionColor backgroundColor;
919         public final CaptionColor edgeColor;
920 
CaptionPenColor(CaptionColor foregroundColor, CaptionColor backgroundColor, CaptionColor edgeColor)921         public CaptionPenColor(CaptionColor foregroundColor, CaptionColor backgroundColor,
922                 CaptionColor edgeColor) {
923             this.foregroundColor = foregroundColor;
924             this.backgroundColor = backgroundColor;
925             this.edgeColor = edgeColor;
926         }
927     }
928 
929     /**
930      * @hide
931      *
932      * Location information of a pen.
933      */
934     public static class CaptionPenLocation {
935         public final int row;
936         public final int column;
937 
CaptionPenLocation(int row, int column)938         public CaptionPenLocation(int row, int column) {
939             this.row = row;
940             this.column = column;
941         }
942     }
943 
944     /**
945      * @hide
946      *
947      * Attributes of a caption window, which is defined in CEA-708B.
948      */
949     public static class CaptionWindowAttr {
950         public final CaptionColor fillColor;
951         public final CaptionColor borderColor;
952         public final int borderType;
953         public final boolean wordWrap;
954         public final int printDirection;
955         public final int scrollDirection;
956         public final int justify;
957         public final int effectDirection;
958         public final int effectSpeed;
959         public final int displayEffect;
960 
CaptionWindowAttr(CaptionColor fillColor, CaptionColor borderColor, int borderType, boolean wordWrap, int printDirection, int scrollDirection, int justify, int effectDirection, int effectSpeed, int displayEffect)961         public CaptionWindowAttr(CaptionColor fillColor, CaptionColor borderColor, int borderType,
962                 boolean wordWrap, int printDirection, int scrollDirection, int justify,
963                 int effectDirection,
964                 int effectSpeed, int displayEffect) {
965             this.fillColor = fillColor;
966             this.borderColor = borderColor;
967             this.borderType = borderType;
968             this.wordWrap = wordWrap;
969             this.printDirection = printDirection;
970             this.scrollDirection = scrollDirection;
971             this.justify = justify;
972             this.effectDirection = effectDirection;
973             this.effectSpeed = effectSpeed;
974             this.displayEffect = displayEffect;
975         }
976     }
977 
978     /**
979      * @hide
980      *
981      * Construction information of the caption window of CEA-708B.
982      */
983     public static class CaptionWindow {
984         public final int id;
985         public final boolean visible;
986         public final boolean rowLock;
987         public final boolean columnLock;
988         public final int priority;
989         public final boolean relativePositioning;
990         public final int anchorVertical;
991         public final int anchorHorizontal;
992         public final int anchorId;
993         public final int rowCount;
994         public final int columnCount;
995         public final int penStyle;
996         public final int windowStyle;
997 
CaptionWindow(int id, boolean visible, boolean rowLock, boolean columnLock, int priority, boolean relativePositioning, int anchorVertical, int anchorHorizontal, int anchorId, int rowCount, int columnCount, int penStyle, int windowStyle)998         public CaptionWindow(int id, boolean visible,
999                 boolean rowLock, boolean columnLock, int priority, boolean relativePositioning,
1000                 int anchorVertical, int anchorHorizontal, int anchorId,
1001                 int rowCount, int columnCount, int penStyle, int windowStyle) {
1002             this.id = id;
1003             this.visible = visible;
1004             this.rowLock = rowLock;
1005             this.columnLock = columnLock;
1006             this.priority = priority;
1007             this.relativePositioning = relativePositioning;
1008             this.anchorVertical = anchorVertical;
1009             this.anchorHorizontal = anchorHorizontal;
1010             this.anchorId = anchorId;
1011             this.rowCount = rowCount;
1012             this.columnCount = columnCount;
1013             this.penStyle = penStyle;
1014             this.windowStyle = windowStyle;
1015         }
1016     }
1017 }
1018 
1019 /**
1020  * Widget capable of rendering CEA-708 closed captions.
1021  *
1022  * @hide
1023  */
1024 class Cea708CCWidget extends ClosedCaptionWidget implements Cea708CCParser.DisplayListener {
1025     private final CCHandler mCCHandler;
1026 
Cea708CCWidget(Context context)1027     public Cea708CCWidget(Context context) {
1028         this(context, null);
1029     }
1030 
Cea708CCWidget(Context context, AttributeSet attrs)1031     public Cea708CCWidget(Context context, AttributeSet attrs) {
1032         this(context, attrs, 0);
1033     }
1034 
Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr)1035     public Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr) {
1036         this(context, attrs, defStyleAttr, 0);
1037     }
1038 
Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)1039     public Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr,
1040             int defStyleRes) {
1041         super(context, attrs, defStyleAttr, defStyleRes);
1042 
1043         mCCHandler = new CCHandler((CCLayout) mClosedCaptionLayout);
1044     }
1045 
1046     @Override
createCaptionLayout(Context context)1047     public ClosedCaptionLayout createCaptionLayout(Context context) {
1048         return new CCLayout(context);
1049     }
1050 
1051     @Override
emitEvent(Cea708CCParser.CaptionEvent event)1052     public void emitEvent(Cea708CCParser.CaptionEvent event) {
1053         mCCHandler.processCaptionEvent(event);
1054 
1055         setSize(getWidth(), getHeight());
1056 
1057         if (mListener != null) {
1058             mListener.onChanged(this);
1059         }
1060     }
1061 
1062     @Override
onDraw(Canvas canvas)1063     public void onDraw(Canvas canvas) {
1064         super.onDraw(canvas);
1065         ((ViewGroup) mClosedCaptionLayout).draw(canvas);
1066     }
1067 
1068     /**
1069      * @hide
1070      *
1071      * A layout that scales its children using the given percentage value.
1072      */
1073     static class ScaledLayout extends ViewGroup {
1074         private static final String TAG = "ScaledLayout";
1075         private static final boolean DEBUG = false;
1076         private static final Comparator<Rect> mRectTopLeftSorter = new Comparator<Rect>() {
1077             @Override
1078             public int compare(Rect lhs, Rect rhs) {
1079                 if (lhs.top != rhs.top) {
1080                     return lhs.top - rhs.top;
1081                 } else {
1082                     return lhs.left - rhs.left;
1083                 }
1084             }
1085         };
1086 
1087         private Rect[] mRectArray;
1088 
ScaledLayout(Context context)1089         public ScaledLayout(Context context) {
1090             super(context);
1091         }
1092 
1093         /**
1094          * @hide
1095          *
1096          * ScaledLayoutParams stores the four scale factors.
1097          * <br>
1098          * Vertical coordinate system:   (scaleStartRow * 100) % ~ (scaleEndRow * 100) %
1099          * Horizontal coordinate system: (scaleStartCol * 100) % ~ (scaleEndCol * 100) %
1100          * <br>
1101          * In XML, for example,
1102          * <pre>
1103          * {@code
1104          * <View
1105          *     app:layout_scaleStartRow="0.1"
1106          *     app:layout_scaleEndRow="0.5"
1107          *     app:layout_scaleStartCol="0.4"
1108          *     app:layout_scaleEndCol="1" />
1109          * }
1110          * </pre>
1111          */
1112         static class ScaledLayoutParams extends ViewGroup.LayoutParams {
1113             public static final float SCALE_UNSPECIFIED = -1;
1114             public float scaleStartRow;
1115             public float scaleEndRow;
1116             public float scaleStartCol;
1117             public float scaleEndCol;
1118 
ScaledLayoutParams(float scaleStartRow, float scaleEndRow, float scaleStartCol, float scaleEndCol)1119             public ScaledLayoutParams(float scaleStartRow, float scaleEndRow,
1120                     float scaleStartCol, float scaleEndCol) {
1121                 super(MATCH_PARENT, MATCH_PARENT);
1122                 this.scaleStartRow = scaleStartRow;
1123                 this.scaleEndRow = scaleEndRow;
1124                 this.scaleStartCol = scaleStartCol;
1125                 this.scaleEndCol = scaleEndCol;
1126             }
1127 
ScaledLayoutParams(Context context, AttributeSet attrs)1128             public ScaledLayoutParams(Context context, AttributeSet attrs) {
1129                 super(MATCH_PARENT, MATCH_PARENT);
1130             }
1131         }
1132 
1133         @Override
generateLayoutParams(AttributeSet attrs)1134         public LayoutParams generateLayoutParams(AttributeSet attrs) {
1135             return new ScaledLayoutParams(getContext(), attrs);
1136         }
1137 
1138         @Override
checkLayoutParams(LayoutParams p)1139         protected boolean checkLayoutParams(LayoutParams p) {
1140             return (p instanceof ScaledLayoutParams);
1141         }
1142 
1143         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)1144         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1145             int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
1146             int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
1147             int width = widthSpecSize - getPaddingLeft() - getPaddingRight();
1148             int height = heightSpecSize - getPaddingTop() - getPaddingBottom();
1149             if (DEBUG) {
1150                 Log.d(TAG, String.format("onMeasure width: %d, height: %d", width, height));
1151             }
1152             int count = getChildCount();
1153             mRectArray = new Rect[count];
1154             for (int i = 0; i < count; ++i) {
1155                 View child = getChildAt(i);
1156                 ViewGroup.LayoutParams params = child.getLayoutParams();
1157                 float scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol;
1158                 if (!(params instanceof ScaledLayoutParams)) {
1159                     throw new RuntimeException(
1160                             "A child of ScaledLayout cannot have the UNSPECIFIED scale factors");
1161                 }
1162                 scaleStartRow = ((ScaledLayoutParams) params).scaleStartRow;
1163                 scaleEndRow = ((ScaledLayoutParams) params).scaleEndRow;
1164                 scaleStartCol = ((ScaledLayoutParams) params).scaleStartCol;
1165                 scaleEndCol = ((ScaledLayoutParams) params).scaleEndCol;
1166                 if (scaleStartRow < 0 || scaleStartRow > 1) {
1167                     throw new RuntimeException("A child of ScaledLayout should have a range of "
1168                             + "scaleStartRow between 0 and 1");
1169                 }
1170                 if (scaleEndRow < scaleStartRow || scaleStartRow > 1) {
1171                     throw new RuntimeException("A child of ScaledLayout should have a range of "
1172                             + "scaleEndRow between scaleStartRow and 1");
1173                 }
1174                 if (scaleEndCol < 0 || scaleEndCol > 1) {
1175                     throw new RuntimeException("A child of ScaledLayout should have a range of "
1176                             + "scaleStartCol between 0 and 1");
1177                 }
1178                 if (scaleEndCol < scaleStartCol || scaleEndCol > 1) {
1179                     throw new RuntimeException("A child of ScaledLayout should have a range of "
1180                             + "scaleEndCol between scaleStartCol and 1");
1181                 }
1182                 if (DEBUG) {
1183                     Log.d(TAG, String.format("onMeasure child scaleStartRow: %f scaleEndRow: %f "
1184                                     + "scaleStartCol: %f scaleEndCol: %f",
1185                             scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
1186                 }
1187                 mRectArray[i] = new Rect((int) (scaleStartCol * width), (int) (scaleStartRow
1188                         * height), (int) (scaleEndCol * width), (int) (scaleEndRow * height));
1189                 int childWidthSpec = MeasureSpec.makeMeasureSpec(
1190                         (int) (width * (scaleEndCol - scaleStartCol)), MeasureSpec.EXACTLY);
1191                 int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1192                 child.measure(childWidthSpec, childHeightSpec);
1193 
1194                 // If the height of the measured child view is bigger than the height of the
1195                 // calculated region by the given ScaleLayoutParams, the height of the region should
1196                 // be increased to fit the size of the child view.
1197                 if (child.getMeasuredHeight() > mRectArray[i].height()) {
1198                     int overflowedHeight = child.getMeasuredHeight() - mRectArray[i].height();
1199                     overflowedHeight = (overflowedHeight + 1) / 2;
1200                     mRectArray[i].bottom += overflowedHeight;
1201                     mRectArray[i].top -= overflowedHeight;
1202                     if (mRectArray[i].top < 0) {
1203                         mRectArray[i].bottom -= mRectArray[i].top;
1204                         mRectArray[i].top = 0;
1205                     }
1206                     if (mRectArray[i].bottom > height) {
1207                         mRectArray[i].top -= mRectArray[i].bottom - height;
1208                         mRectArray[i].bottom = height;
1209                     }
1210                 }
1211                 childHeightSpec = MeasureSpec.makeMeasureSpec(
1212                         (int) (height * (scaleEndRow - scaleStartRow)), MeasureSpec.EXACTLY);
1213                 child.measure(childWidthSpec, childHeightSpec);
1214             }
1215 
1216             // Avoid overlapping rectangles.
1217             // Step 1. Sort rectangles by position (top-left).
1218             int visibleRectCount = 0;
1219             int[] visibleRectGroup = new int[count];
1220             Rect[] visibleRectArray = new Rect[count];
1221             for (int i = 0; i < count; ++i) {
1222                 if (getChildAt(i).getVisibility() == View.VISIBLE) {
1223                     visibleRectGroup[visibleRectCount] = visibleRectCount;
1224                     visibleRectArray[visibleRectCount] = mRectArray[i];
1225                     ++visibleRectCount;
1226                 }
1227             }
1228             Arrays.sort(visibleRectArray, 0, visibleRectCount, mRectTopLeftSorter);
1229 
1230             // Step 2. Move down if there are overlapping rectangles.
1231             for (int i = 0; i < visibleRectCount - 1; ++i) {
1232                 for (int j = i + 1; j < visibleRectCount; ++j) {
1233                     if (Rect.intersects(visibleRectArray[i], visibleRectArray[j])) {
1234                         visibleRectGroup[j] = visibleRectGroup[i];
1235                         visibleRectArray[j].set(visibleRectArray[j].left,
1236                                 visibleRectArray[i].bottom,
1237                                 visibleRectArray[j].right,
1238                                 visibleRectArray[i].bottom + visibleRectArray[j].height());
1239                     }
1240                 }
1241             }
1242 
1243             // Step 3. Move up if there is any overflowed rectangle.
1244             for (int i = visibleRectCount - 1; i >= 0; --i) {
1245                 if (visibleRectArray[i].bottom > height) {
1246                     int overflowedHeight = visibleRectArray[i].bottom - height;
1247                     for (int j = 0; j <= i; ++j) {
1248                         if (visibleRectGroup[i] == visibleRectGroup[j]) {
1249                             visibleRectArray[j].set(visibleRectArray[j].left,
1250                                     visibleRectArray[j].top - overflowedHeight,
1251                                     visibleRectArray[j].right,
1252                                     visibleRectArray[j].bottom - overflowedHeight);
1253                         }
1254                     }
1255                 }
1256             }
1257             setMeasuredDimension(widthSpecSize, heightSpecSize);
1258         }
1259 
1260         @Override
onLayout(boolean changed, int l, int t, int r, int b)1261         protected void onLayout(boolean changed, int l, int t, int r, int b) {
1262             int paddingLeft = getPaddingLeft();
1263             int paddingTop = getPaddingTop();
1264             int count = getChildCount();
1265             for (int i = 0; i < count; ++i) {
1266                 View child = getChildAt(i);
1267                 if (child.getVisibility() != GONE) {
1268                     int childLeft = paddingLeft + mRectArray[i].left;
1269                     int childTop = paddingTop + mRectArray[i].top;
1270                     int childBottom = paddingLeft + mRectArray[i].bottom;
1271                     int childRight = paddingTop + mRectArray[i].right;
1272                     if (DEBUG) {
1273                         Log.d(TAG, String.format(
1274                                 "child layout bottom: %d left: %d right: %d top: %d",
1275                                 childBottom, childLeft, childRight, childTop));
1276                     }
1277                     child.layout(childLeft, childTop, childRight, childBottom);
1278                 }
1279             }
1280         }
1281 
1282         @Override
dispatchDraw(Canvas canvas)1283         public void dispatchDraw(Canvas canvas) {
1284             int paddingLeft = getPaddingLeft();
1285             int paddingTop = getPaddingTop();
1286             int count = getChildCount();
1287             for (int i = 0; i < count; ++i) {
1288                 View child = getChildAt(i);
1289                 if (child.getVisibility() != GONE) {
1290                     if (i >= mRectArray.length) {
1291                         break;
1292                     }
1293                     int childLeft = paddingLeft + mRectArray[i].left;
1294                     int childTop = paddingTop + mRectArray[i].top;
1295                     final int saveCount = canvas.save();
1296                     canvas.translate(childLeft, childTop);
1297                     child.draw(canvas);
1298                     canvas.restoreToCount(saveCount);
1299                 }
1300             }
1301         }
1302     }
1303 
1304     /**
1305      * @hide
1306      *
1307      * Layout containing the safe title area that helps the closed captions look more prominent.
1308      *
1309      * <p>This is required by CEA-708B.
1310      */
1311     static class CCLayout extends ScaledLayout implements ClosedCaptionLayout {
1312         private static final float SAFE_TITLE_AREA_SCALE_START_X = 0.1f;
1313         private static final float SAFE_TITLE_AREA_SCALE_END_X = 0.9f;
1314         private static final float SAFE_TITLE_AREA_SCALE_START_Y = 0.1f;
1315         private static final float SAFE_TITLE_AREA_SCALE_END_Y = 0.9f;
1316 
1317         private final ScaledLayout mSafeTitleAreaLayout;
1318 
CCLayout(Context context)1319         public CCLayout(Context context) {
1320             super(context);
1321 
1322             mSafeTitleAreaLayout = new ScaledLayout(context);
1323             addView(mSafeTitleAreaLayout, new ScaledLayout.ScaledLayoutParams(
1324                     SAFE_TITLE_AREA_SCALE_START_X, SAFE_TITLE_AREA_SCALE_END_X,
1325                     SAFE_TITLE_AREA_SCALE_START_Y, SAFE_TITLE_AREA_SCALE_END_Y));
1326         }
1327 
addOrUpdateViewToSafeTitleArea(CCWindowLayout captionWindowLayout, ScaledLayoutParams scaledLayoutParams)1328         public void addOrUpdateViewToSafeTitleArea(CCWindowLayout captionWindowLayout,
1329                 ScaledLayoutParams scaledLayoutParams) {
1330             int index = mSafeTitleAreaLayout.indexOfChild(captionWindowLayout);
1331             if (index < 0) {
1332                 mSafeTitleAreaLayout.addView(captionWindowLayout, scaledLayoutParams);
1333                 return;
1334             }
1335             mSafeTitleAreaLayout.updateViewLayout(captionWindowLayout, scaledLayoutParams);
1336         }
1337 
removeViewFromSafeTitleArea(CCWindowLayout captionWindowLayout)1338         public void removeViewFromSafeTitleArea(CCWindowLayout captionWindowLayout) {
1339             mSafeTitleAreaLayout.removeView(captionWindowLayout);
1340         }
1341 
setCaptionStyle(CaptionStyle style)1342         public void setCaptionStyle(CaptionStyle style) {
1343             final int count = mSafeTitleAreaLayout.getChildCount();
1344             for (int i = 0; i < count; ++i) {
1345                 final CCWindowLayout windowLayout =
1346                         (CCWindowLayout) mSafeTitleAreaLayout.getChildAt(i);
1347                 windowLayout.setCaptionStyle(style);
1348             }
1349         }
1350 
setFontScale(float fontScale)1351         public void setFontScale(float fontScale) {
1352             final int count = mSafeTitleAreaLayout.getChildCount();
1353             for (int i = 0; i < count; ++i) {
1354                 final CCWindowLayout windowLayout =
1355                         (CCWindowLayout) mSafeTitleAreaLayout.getChildAt(i);
1356                 windowLayout.setFontScale(fontScale);
1357             }
1358         }
1359     }
1360 
1361     /**
1362      * @hide
1363      *
1364      * Renders the selected CC track.
1365      */
1366     static class CCHandler implements Handler.Callback {
1367         // TODO: Remaining works
1368         // CaptionTrackRenderer does not support the full spec of CEA-708. The remaining works are
1369         // described in the follows.
1370         // C0 Table: Backspace, FF, and HCR are not supported. The rule for P16 is not standardized
1371         //           but it is handled as EUC-KR charset for Korea broadcasting.
1372         // C1 Table: All the styles of windows and pens except underline, italic, pen size, and pen
1373         //           offset specified in CEA-708 are ignored and this follows system wide CC
1374         //           preferences for look and feel. SetPenLocation is not implemented.
1375         // G2 Table: TSP, NBTSP and BLK are not supported.
1376         // Text/commands: Word wrapping, fonts, row and column locking are not supported.
1377 
1378         private static final String TAG = "CCHandler";
1379         private static final boolean DEBUG = false;
1380 
1381         private static final int TENTHS_OF_SECOND_IN_MILLIS = 100;
1382 
1383         // According to CEA-708B, there can exist up to 8 caption windows.
1384         private static final int CAPTION_WINDOWS_MAX = 8;
1385         private static final int CAPTION_ALL_WINDOWS_BITMAP = 255;
1386 
1387         private static final int MSG_DELAY_CANCEL = 1;
1388         private static final int MSG_CAPTION_CLEAR = 2;
1389 
1390         private static final long CAPTION_CLEAR_INTERVAL_MS = 60000;
1391 
1392         private final CCLayout mCCLayout;
1393         private boolean mIsDelayed = false;
1394         private CCWindowLayout mCurrentWindowLayout;
1395         private final CCWindowLayout[] mCaptionWindowLayouts =
1396                 new CCWindowLayout[CAPTION_WINDOWS_MAX];
1397         private final ArrayList<Cea708CCParser.CaptionEvent> mPendingCaptionEvents
1398                 = new ArrayList<>();
1399         private final Handler mHandler;
1400 
CCHandler(CCLayout ccLayout)1401         public CCHandler(CCLayout ccLayout) {
1402             mCCLayout = ccLayout;
1403             mHandler = new Handler(this);
1404         }
1405 
1406         @Override
handleMessage(Message msg)1407         public boolean handleMessage(Message msg) {
1408             switch (msg.what) {
1409                 case MSG_DELAY_CANCEL:
1410                     delayCancel();
1411                     return true;
1412                 case MSG_CAPTION_CLEAR:
1413                     clearWindows(CAPTION_ALL_WINDOWS_BITMAP);
1414                     return true;
1415             }
1416             return false;
1417         }
1418 
processCaptionEvent(Cea708CCParser.CaptionEvent event)1419         public void processCaptionEvent(Cea708CCParser.CaptionEvent event) {
1420             if (mIsDelayed) {
1421                 mPendingCaptionEvents.add(event);
1422                 return;
1423             }
1424             switch (event.type) {
1425                 case Cea708CCParser.CAPTION_EMIT_TYPE_BUFFER:
1426                     sendBufferToCurrentWindow((String) event.obj);
1427                     break;
1428                 case Cea708CCParser.CAPTION_EMIT_TYPE_CONTROL:
1429                     sendControlToCurrentWindow((char) event.obj);
1430                     break;
1431                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_CWX:
1432                     setCurrentWindowLayout((int) event.obj);
1433                     break;
1434                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_CLW:
1435                     clearWindows((int) event.obj);
1436                     break;
1437                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DSW:
1438                     displayWindows((int) event.obj);
1439                     break;
1440                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_HDW:
1441                     hideWindows((int) event.obj);
1442                     break;
1443                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_TGW:
1444                     toggleWindows((int) event.obj);
1445                     break;
1446                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLW:
1447                     deleteWindows((int) event.obj);
1448                     break;
1449                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLY:
1450                     delay((int) event.obj);
1451                     break;
1452                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLC:
1453                     delayCancel();
1454                     break;
1455                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_RST:
1456                     reset();
1457                     break;
1458                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPA:
1459                     setPenAttr((Cea708CCParser.CaptionPenAttr) event.obj);
1460                     break;
1461                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPC:
1462                     setPenColor((Cea708CCParser.CaptionPenColor) event.obj);
1463                     break;
1464                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPL:
1465                     setPenLocation((Cea708CCParser.CaptionPenLocation) event.obj);
1466                     break;
1467                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SWA:
1468                     setWindowAttr((Cea708CCParser.CaptionWindowAttr) event.obj);
1469                     break;
1470                 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DFX:
1471                     defineWindow((Cea708CCParser.CaptionWindow) event.obj);
1472                     break;
1473             }
1474         }
1475 
1476         // The window related caption commands
setCurrentWindowLayout(int windowId)1477         private void setCurrentWindowLayout(int windowId) {
1478             if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) {
1479                 return;
1480             }
1481             CCWindowLayout windowLayout = mCaptionWindowLayouts[windowId];
1482             if (windowLayout == null) {
1483                 return;
1484             }
1485             if (DEBUG) {
1486                 Log.d(TAG, "setCurrentWindowLayout to " + windowId);
1487             }
1488             mCurrentWindowLayout = windowLayout;
1489         }
1490 
1491         // Each bit of windowBitmap indicates a window.
1492         // If a bit is set, the window id is the same as the number of the trailing zeros of the
1493         // bit.
getWindowsFromBitmap(int windowBitmap)1494         private ArrayList<CCWindowLayout> getWindowsFromBitmap(int windowBitmap) {
1495             ArrayList<CCWindowLayout> windows = new ArrayList<>();
1496             for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) {
1497                 if ((windowBitmap & (1 << i)) != 0) {
1498                     CCWindowLayout windowLayout = mCaptionWindowLayouts[i];
1499                     if (windowLayout != null) {
1500                         windows.add(windowLayout);
1501                     }
1502                 }
1503             }
1504             return windows;
1505         }
1506 
clearWindows(int windowBitmap)1507         private void clearWindows(int windowBitmap) {
1508             if (windowBitmap == 0) {
1509                 return;
1510             }
1511             for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
1512                 windowLayout.clear();
1513             }
1514         }
1515 
displayWindows(int windowBitmap)1516         private void displayWindows(int windowBitmap) {
1517             if (windowBitmap == 0) {
1518                 return;
1519             }
1520             for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
1521                 windowLayout.show();
1522             }
1523         }
1524 
hideWindows(int windowBitmap)1525         private void hideWindows(int windowBitmap) {
1526             if (windowBitmap == 0) {
1527                 return;
1528             }
1529             for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
1530                 windowLayout.hide();
1531             }
1532         }
1533 
toggleWindows(int windowBitmap)1534         private void toggleWindows(int windowBitmap) {
1535             if (windowBitmap == 0) {
1536                 return;
1537             }
1538             for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
1539                 if (windowLayout.isShown()) {
1540                     windowLayout.hide();
1541                 } else {
1542                     windowLayout.show();
1543                 }
1544             }
1545         }
1546 
deleteWindows(int windowBitmap)1547         private void deleteWindows(int windowBitmap) {
1548             if (windowBitmap == 0) {
1549                 return;
1550             }
1551             for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
1552                 windowLayout.removeFromCaptionView();
1553                 mCaptionWindowLayouts[windowLayout.getCaptionWindowId()] = null;
1554             }
1555         }
1556 
reset()1557         public void reset() {
1558             mCurrentWindowLayout = null;
1559             mIsDelayed = false;
1560             mPendingCaptionEvents.clear();
1561             for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) {
1562                 if (mCaptionWindowLayouts[i] != null) {
1563                     mCaptionWindowLayouts[i].removeFromCaptionView();
1564                 }
1565                 mCaptionWindowLayouts[i] = null;
1566             }
1567             mCCLayout.setVisibility(View.INVISIBLE);
1568             mHandler.removeMessages(MSG_CAPTION_CLEAR);
1569         }
1570 
setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr)1571         private void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) {
1572             if (mCurrentWindowLayout != null) {
1573                 mCurrentWindowLayout.setWindowAttr(windowAttr);
1574             }
1575         }
1576 
defineWindow(Cea708CCParser.CaptionWindow window)1577         private void defineWindow(Cea708CCParser.CaptionWindow window) {
1578             if (window == null) {
1579                 return;
1580             }
1581             int windowId = window.id;
1582             if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) {
1583                 return;
1584             }
1585             CCWindowLayout windowLayout = mCaptionWindowLayouts[windowId];
1586             if (windowLayout == null) {
1587                 windowLayout = new CCWindowLayout(mCCLayout.getContext());
1588             }
1589             windowLayout.initWindow(mCCLayout, window);
1590             mCurrentWindowLayout = mCaptionWindowLayouts[windowId] = windowLayout;
1591         }
1592 
1593         // The job related caption commands
delay(int tenthsOfSeconds)1594         private void delay(int tenthsOfSeconds) {
1595             if (tenthsOfSeconds < 0 || tenthsOfSeconds > 255) {
1596                 return;
1597             }
1598             mIsDelayed = true;
1599             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DELAY_CANCEL),
1600                     tenthsOfSeconds * TENTHS_OF_SECOND_IN_MILLIS);
1601         }
1602 
delayCancel()1603         private void delayCancel() {
1604             mIsDelayed = false;
1605             processPendingBuffer();
1606         }
1607 
processPendingBuffer()1608         private void processPendingBuffer() {
1609             for (Cea708CCParser.CaptionEvent event : mPendingCaptionEvents) {
1610                 processCaptionEvent(event);
1611             }
1612             mPendingCaptionEvents.clear();
1613         }
1614 
1615         // The implicit write caption commands
sendControlToCurrentWindow(char control)1616         private void sendControlToCurrentWindow(char control) {
1617             if (mCurrentWindowLayout != null) {
1618                 mCurrentWindowLayout.sendControl(control);
1619             }
1620         }
1621 
sendBufferToCurrentWindow(String buffer)1622         private void sendBufferToCurrentWindow(String buffer) {
1623             if (mCurrentWindowLayout != null) {
1624                 mCurrentWindowLayout.sendBuffer(buffer);
1625                 mHandler.removeMessages(MSG_CAPTION_CLEAR);
1626                 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CAPTION_CLEAR),
1627                         CAPTION_CLEAR_INTERVAL_MS);
1628             }
1629         }
1630 
1631         // The pen related caption commands
setPenAttr(Cea708CCParser.CaptionPenAttr attr)1632         private void setPenAttr(Cea708CCParser.CaptionPenAttr attr) {
1633             if (mCurrentWindowLayout != null) {
1634                 mCurrentWindowLayout.setPenAttr(attr);
1635             }
1636         }
1637 
setPenColor(Cea708CCParser.CaptionPenColor color)1638         private void setPenColor(Cea708CCParser.CaptionPenColor color) {
1639             if (mCurrentWindowLayout != null) {
1640                 mCurrentWindowLayout.setPenColor(color);
1641             }
1642         }
1643 
setPenLocation(Cea708CCParser.CaptionPenLocation location)1644         private void setPenLocation(Cea708CCParser.CaptionPenLocation location) {
1645             if (mCurrentWindowLayout != null) {
1646                 mCurrentWindowLayout.setPenLocation(location.row, location.column);
1647             }
1648         }
1649     }
1650 
1651     /**
1652      * @hide
1653      *
1654      * Layout which renders a caption window of CEA-708B. It contains a {@link TextView} that takes
1655      * care of displaying the actual CC text.
1656      */
1657     static class CCWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener {
1658         private static final String TAG = "CCWindowLayout";
1659 
1660         private static final float PROPORTION_PEN_SIZE_SMALL = .75f;
1661         private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f;
1662 
1663         // The following values indicates the maximum cell number of a window.
1664         private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99;
1665         private static final int ANCHOR_VERTICAL_MAX = 74;
1666         private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209;
1667         private static final int MAX_COLUMN_COUNT_16_9 = 42;
1668 
1669         // The following values indicates a gravity of a window.
1670         private static final int ANCHOR_MODE_DIVIDER = 3;
1671         private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0;
1672         private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1;
1673         private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2;
1674         private static final int ANCHOR_VERTICAL_MODE_TOP = 0;
1675         private static final int ANCHOR_VERTICAL_MODE_CENTER = 1;
1676         private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2;
1677 
1678         private CCLayout mCCLayout;
1679 
1680         private CCView mCCView;
1681         private CaptionStyle mCaptionStyle;
1682         private int mRowLimit = 0;
1683         private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
1684         private final List<CharacterStyle> mCharacterStyles = new ArrayList<>();
1685         private int mCaptionWindowId;
1686         private int mRow = -1;
1687         private float mFontScale;
1688         private float mTextSize;
1689         private String mWidestChar;
1690         private int mLastCaptionLayoutWidth;
1691         private int mLastCaptionLayoutHeight;
1692 
CCWindowLayout(Context context)1693         public CCWindowLayout(Context context) {
1694             this(context, null);
1695         }
1696 
CCWindowLayout(Context context, AttributeSet attrs)1697         public CCWindowLayout(Context context, AttributeSet attrs) {
1698             this(context, attrs, 0);
1699         }
1700 
CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr)1701         public CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
1702             this(context, attrs, defStyleAttr, 0);
1703         }
1704 
CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)1705         public CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr,
1706                 int defStyleRes) {
1707             super(context, attrs, defStyleAttr, defStyleRes);
1708 
1709             // Add a subtitle view to the layout.
1710             mCCView = new CCView(context);
1711             LayoutParams params = new RelativeLayout.LayoutParams(
1712                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
1713             addView(mCCView, params);
1714 
1715             // Set the system wide CC preferences to the subtitle view.
1716             CaptioningManager captioningManager =
1717                     (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
1718             mFontScale = captioningManager.getFontScale();
1719             setCaptionStyle(captioningManager.getUserStyle());
1720             mCCView.setText("");
1721             updateWidestChar();
1722         }
1723 
setCaptionStyle(CaptionStyle style)1724         public void setCaptionStyle(CaptionStyle style) {
1725             mCaptionStyle = style;
1726             mCCView.setCaptionStyle(style);
1727         }
1728 
setFontScale(float fontScale)1729         public void setFontScale(float fontScale) {
1730             mFontScale = fontScale;
1731             updateTextSize();
1732         }
1733 
getCaptionWindowId()1734         public int getCaptionWindowId() {
1735             return mCaptionWindowId;
1736         }
1737 
setCaptionWindowId(int captionWindowId)1738         public void setCaptionWindowId(int captionWindowId) {
1739             mCaptionWindowId = captionWindowId;
1740         }
1741 
clear()1742         public void clear() {
1743             clearText();
1744             hide();
1745         }
1746 
show()1747         public void show() {
1748             setVisibility(View.VISIBLE);
1749             requestLayout();
1750         }
1751 
hide()1752         public void hide() {
1753             setVisibility(View.INVISIBLE);
1754             requestLayout();
1755         }
1756 
setPenAttr(Cea708CCParser.CaptionPenAttr penAttr)1757         public void setPenAttr(Cea708CCParser.CaptionPenAttr penAttr) {
1758             mCharacterStyles.clear();
1759             if (penAttr.italic) {
1760                 mCharacterStyles.add(new StyleSpan(Typeface.ITALIC));
1761             }
1762             if (penAttr.underline) {
1763                 mCharacterStyles.add(new UnderlineSpan());
1764             }
1765             switch (penAttr.penSize) {
1766                 case Cea708CCParser.CaptionPenAttr.PEN_SIZE_SMALL:
1767                     mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL));
1768                     break;
1769                 case Cea708CCParser.CaptionPenAttr.PEN_SIZE_LARGE:
1770                     mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE));
1771                     break;
1772             }
1773             switch (penAttr.penOffset) {
1774                 case Cea708CCParser.CaptionPenAttr.OFFSET_SUBSCRIPT:
1775                     mCharacterStyles.add(new SubscriptSpan());
1776                     break;
1777                 case Cea708CCParser.CaptionPenAttr.OFFSET_SUPERSCRIPT:
1778                     mCharacterStyles.add(new SuperscriptSpan());
1779                     break;
1780             }
1781         }
1782 
setPenColor(Cea708CCParser.CaptionPenColor penColor)1783         public void setPenColor(Cea708CCParser.CaptionPenColor penColor) {
1784             // TODO: apply pen colors or skip this and use the style of system wide CC style as is.
1785         }
1786 
setPenLocation(int row, int column)1787         public void setPenLocation(int row, int column) {
1788             // TODO: change the location of pen based on row and column both.
1789             if (mRow >= 0) {
1790                 for (int r = mRow; r < row; ++r) {
1791                     appendText("\n");
1792                 }
1793             }
1794             mRow = row;
1795         }
1796 
setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr)1797         public void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) {
1798             // TODO: apply window attrs or skip this and use the style of system wide CC style as
1799             // is.
1800         }
1801 
sendBuffer(String buffer)1802         public void sendBuffer(String buffer) {
1803             appendText(buffer);
1804         }
1805 
sendControl(char control)1806         public void sendControl(char control) {
1807             // TODO: there are a bunch of ASCII-style control codes.
1808         }
1809 
1810         /**
1811          * This method places the window on a given CaptionLayout along with the anchor of the
1812          * window.
1813          * <p>
1814          * According to CEA-708B, the anchor id indicates the gravity of the window as the follows.
1815          * For example, A value 7 of a anchor id says that a window is align with its parent bottom
1816          * and is located at the center horizontally of its parent.
1817          * </p>
1818          * <h4>Anchor id and the gravity of a window</h4>
1819          * <table>
1820          *     <tr>
1821          *         <th>GRAVITY</th>
1822          *         <th>LEFT</th>
1823          *         <th>CENTER_HORIZONTAL</th>
1824          *         <th>RIGHT</th>
1825          *     </tr>
1826          *     <tr>
1827          *         <th>TOP</th>
1828          *         <td>0</td>
1829          *         <td>1</td>
1830          *         <td>2</td>
1831          *     </tr>
1832          *     <tr>
1833          *         <th>CENTER_VERTICAL</th>
1834          *         <td>3</td>
1835          *         <td>4</td>
1836          *         <td>5</td>
1837          *     </tr>
1838          *     <tr>
1839          *         <th>BOTTOM</th>
1840          *         <td>6</td>
1841          *         <td>7</td>
1842          *         <td>8</td>
1843          *     </tr>
1844          * </table>
1845          * <p>
1846          * In order to handle the gravity of a window, there are two steps. First, set the size of
1847          * the window. Since the window will be positioned at ScaledLayout, the size factors are
1848          * determined in a ratio. Second, set the gravity of the window. CaptionWindowLayout is
1849          * inherited from RelativeLayout. Hence, we could set the gravity of its child view,
1850          * SubtitleView.
1851          * </p>
1852          * <p>
1853          * The gravity of the window is also related to its size. When it should be pushed to a one
1854          * of the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a
1855          * boundary of the window. When it should be pushed in the horizontal/vertical center of its
1856          * container, the horizontal/vertical center point of the window should be the same as the
1857          * anchor point.
1858          * </p>
1859          *
1860          * @param ccLayout a given CaptionLayout, which contains a safe title area.
1861          * @param captionWindow a given CaptionWindow, which stores the construction info of the
1862          *                      window.
1863          */
initWindow(CCLayout ccLayout, Cea708CCParser.CaptionWindow captionWindow)1864         public void initWindow(CCLayout ccLayout, Cea708CCParser.CaptionWindow captionWindow) {
1865             if (mCCLayout != ccLayout) {
1866                 if (mCCLayout != null) {
1867                     mCCLayout.removeOnLayoutChangeListener(this);
1868                 }
1869                 mCCLayout = ccLayout;
1870                 mCCLayout.addOnLayoutChangeListener(this);
1871                 updateWidestChar();
1872             }
1873 
1874             // Both anchor vertical and horizontal indicates the position cell number of the window.
1875             float scaleRow = (float) captionWindow.anchorVertical /
1876                     (captionWindow.relativePositioning
1877                             ? ANCHOR_RELATIVE_POSITIONING_MAX : ANCHOR_VERTICAL_MAX);
1878 
1879             // Assumes it has a wide aspect ratio track.
1880             float scaleCol = (float) captionWindow.anchorHorizontal /
1881                     (captionWindow.relativePositioning ? ANCHOR_RELATIVE_POSITIONING_MAX
1882                             : ANCHOR_HORIZONTAL_16_9_MAX);
1883 
1884             // The range of scaleRow/Col need to be verified to be in [0, 1].
1885             // Otherwise a RuntimeException will be raised in ScaledLayout.
1886             if (scaleRow < 0 || scaleRow > 1) {
1887                 Log.i(TAG, "The vertical position of the anchor point should be at the range of 0 "
1888                         + "and 1 but " + scaleRow);
1889                 scaleRow = Math.max(0, Math.min(scaleRow, 1));
1890             }
1891             if (scaleCol < 0 || scaleCol > 1) {
1892                 Log.i(TAG, "The horizontal position of the anchor point should be at the range of 0"
1893                         + " and 1 but " + scaleCol);
1894                 scaleCol = Math.max(0, Math.min(scaleCol, 1));
1895             }
1896             int gravity = Gravity.CENTER;
1897             int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER;
1898             int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER;
1899             float scaleStartRow = 0;
1900             float scaleEndRow = 1;
1901             float scaleStartCol = 0;
1902             float scaleEndCol = 1;
1903             switch (horizontalMode) {
1904                 case ANCHOR_HORIZONTAL_MODE_LEFT:
1905                     gravity = Gravity.LEFT;
1906                     mCCView.setAlignment(Alignment.ALIGN_NORMAL);
1907                     scaleStartCol = scaleCol;
1908                     break;
1909                 case ANCHOR_HORIZONTAL_MODE_CENTER:
1910                     float gap = Math.min(1 - scaleCol, scaleCol);
1911 
1912                     // Since all TV sets use left text alignment instead of center text alignment
1913                     // for this case, we follow the industry convention if possible.
1914                     int columnCount = captionWindow.columnCount + 1;
1915                     columnCount = Math.min(getScreenColumnCount(), columnCount);
1916                     StringBuilder widestTextBuilder = new StringBuilder();
1917                     for (int i = 0; i < columnCount; ++i) {
1918                         widestTextBuilder.append(mWidestChar);
1919                     }
1920                     Paint paint = new Paint();
1921                     paint.setTypeface(mCaptionStyle.getTypeface());
1922                     paint.setTextSize(mTextSize);
1923                     float maxWindowWidth = paint.measureText(widestTextBuilder.toString());
1924                     float halfMaxWidthScale = mCCLayout.getWidth() > 0
1925                             ? maxWindowWidth / 2.0f / (mCCLayout.getWidth() * 0.8f) : 0.0f;
1926                     if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) {
1927                         // Calculate the expected max window size based on the column count of the
1928                         // caption window multiplied by average alphabets char width, then align the
1929                         // left side of the window with the left side of the expected max window.
1930                         gravity = Gravity.LEFT;
1931                         mCCView.setAlignment(Alignment.ALIGN_NORMAL);
1932                         scaleStartCol = scaleCol - halfMaxWidthScale;
1933                         scaleEndCol = 1.0f;
1934                     } else {
1935                         // The gap will be the minimum distance value of the distances from both
1936                         // horizontal end points to the anchor point.
1937                         // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2].
1938                         // If scaleCol > 0.5, the range of scaleCol is
1939                         // [(1 - the anchor point) * 2, 1].
1940                         // The anchor point is located at the horizontal center of the window in
1941                         // both cases.
1942                         gravity = Gravity.CENTER_HORIZONTAL;
1943                         mCCView.setAlignment(Alignment.ALIGN_CENTER);
1944                         scaleStartCol = scaleCol - gap;
1945                         scaleEndCol = scaleCol + gap;
1946                     }
1947                     break;
1948                 case ANCHOR_HORIZONTAL_MODE_RIGHT:
1949                     gravity = Gravity.RIGHT;
1950                     mCCView.setAlignment(Alignment.ALIGN_RIGHT);
1951                     scaleEndCol = scaleCol;
1952                     break;
1953             }
1954             switch (verticalMode) {
1955                 case ANCHOR_VERTICAL_MODE_TOP:
1956                     gravity |= Gravity.TOP;
1957                     scaleStartRow = scaleRow;
1958                     break;
1959                 case ANCHOR_VERTICAL_MODE_CENTER:
1960                     gravity |= Gravity.CENTER_VERTICAL;
1961 
1962                     // See the above comment.
1963                     float gap = Math.min(1 - scaleRow, scaleRow);
1964                     scaleStartRow = scaleRow - gap;
1965                     scaleEndRow = scaleRow + gap;
1966                     break;
1967                 case ANCHOR_VERTICAL_MODE_BOTTOM:
1968                     gravity |= Gravity.BOTTOM;
1969                     scaleEndRow = scaleRow;
1970                     break;
1971             }
1972             mCCLayout.addOrUpdateViewToSafeTitleArea(this, new ScaledLayout
1973                     .ScaledLayoutParams(scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
1974             setCaptionWindowId(captionWindow.id);
1975             setRowLimit(captionWindow.rowCount);
1976             setGravity(gravity);
1977             if (captionWindow.visible) {
1978                 show();
1979             } else {
1980                 hide();
1981             }
1982         }
1983 
1984         @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)1985         public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
1986                 int oldTop, int oldRight, int oldBottom) {
1987             int width = right - left;
1988             int height = bottom - top;
1989             if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) {
1990                 mLastCaptionLayoutWidth = width;
1991                 mLastCaptionLayoutHeight = height;
1992                 updateTextSize();
1993             }
1994         }
1995 
updateWidestChar()1996         private void updateWidestChar() {
1997             Paint paint = new Paint();
1998             paint.setTypeface(mCaptionStyle.getTypeface());
1999             Charset latin1 = Charset.forName("ISO-8859-1");
2000             float widestCharWidth = 0f;
2001             for (int i = 0; i < 256; ++i) {
2002                 String ch = new String(new byte[]{(byte) i}, latin1);
2003                 float charWidth = paint.measureText(ch);
2004                 if (widestCharWidth < charWidth) {
2005                     widestCharWidth = charWidth;
2006                     mWidestChar = ch;
2007                 }
2008             }
2009             updateTextSize();
2010         }
2011 
updateTextSize()2012         private void updateTextSize() {
2013             if (mCCLayout == null) return;
2014 
2015             // Calculate text size based on the max window size.
2016             StringBuilder widestTextBuilder = new StringBuilder();
2017             int screenColumnCount = getScreenColumnCount();
2018             for (int i = 0; i < screenColumnCount; ++i) {
2019                 widestTextBuilder.append(mWidestChar);
2020             }
2021             String widestText = widestTextBuilder.toString();
2022             Paint paint = new Paint();
2023             paint.setTypeface(mCaptionStyle.getTypeface());
2024             float startFontSize = 0f;
2025             float endFontSize = 255f;
2026             while (startFontSize < endFontSize) {
2027                 float testTextSize = (startFontSize + endFontSize) / 2f;
2028                 paint.setTextSize(testTextSize);
2029                 float width = paint.measureText(widestText);
2030                 if (mCCLayout.getWidth() * 0.8f > width) {
2031                     startFontSize = testTextSize + 0.01f;
2032                 } else {
2033                     endFontSize = testTextSize - 0.01f;
2034                 }
2035             }
2036             mTextSize = endFontSize * mFontScale;
2037             mCCView.setTextSize(mTextSize);
2038         }
2039 
getScreenColumnCount()2040         private int getScreenColumnCount() {
2041             // Assume it has a wide aspect ratio track.
2042             return MAX_COLUMN_COUNT_16_9;
2043         }
2044 
removeFromCaptionView()2045         public void removeFromCaptionView() {
2046             if (mCCLayout != null) {
2047                 mCCLayout.removeViewFromSafeTitleArea(this);
2048                 mCCLayout.removeOnLayoutChangeListener(this);
2049                 mCCLayout = null;
2050             }
2051         }
2052 
setText(String text)2053         public void setText(String text) {
2054             updateText(text, false);
2055         }
2056 
appendText(String text)2057         public void appendText(String text) {
2058             updateText(text, true);
2059         }
2060 
clearText()2061         public void clearText() {
2062             mBuilder.clear();
2063             mCCView.setText("");
2064         }
2065 
updateText(String text, boolean appended)2066         private void updateText(String text, boolean appended) {
2067             if (!appended) {
2068                 mBuilder.clear();
2069             }
2070             if (text != null && text.length() > 0) {
2071                 int length = mBuilder.length();
2072                 mBuilder.append(text);
2073                 for (CharacterStyle characterStyle : mCharacterStyles) {
2074                     mBuilder.setSpan(characterStyle, length, mBuilder.length(),
2075                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2076                 }
2077             }
2078             String[] lines = TextUtils.split(mBuilder.toString(), "\n");
2079 
2080             // Truncate text not to exceed the row limit.
2081             // Plus one here since the range of the rows is [0, mRowLimit].
2082             String truncatedText = TextUtils.join("\n", Arrays.copyOfRange(
2083                     lines, Math.max(0, lines.length - (mRowLimit + 1)), lines.length));
2084             mBuilder.delete(0, mBuilder.length() - truncatedText.length());
2085 
2086             // Trim the buffer first then set text to CCView.
2087             int start = 0, last = mBuilder.length() - 1;
2088             int end = last;
2089             while ((start <= end) && (mBuilder.charAt(start) <= ' ')) {
2090                 ++start;
2091             }
2092             while ((end >= start) && (mBuilder.charAt(end) <= ' ')) {
2093                 --end;
2094             }
2095             if (start == 0 && end == last) {
2096                 mCCView.setText(mBuilder);
2097             } else {
2098                 SpannableStringBuilder trim = new SpannableStringBuilder();
2099                 trim.append(mBuilder);
2100                 if (end < last) {
2101                     trim.delete(end + 1, last + 1);
2102                 }
2103                 if (start > 0) {
2104                     trim.delete(0, start);
2105                 }
2106                 mCCView.setText(trim);
2107             }
2108         }
2109 
setRowLimit(int rowLimit)2110         public void setRowLimit(int rowLimit) {
2111             if (rowLimit < 0) {
2112                 throw new IllegalArgumentException("A rowLimit should have a positive number");
2113             }
2114             mRowLimit = rowLimit;
2115         }
2116     }
2117 
2118     /** @hide */
2119     static class CCView extends SubtitleView {
2120         private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT;
2121 
CCView(Context context)2122         public CCView(Context context) {
2123             this(context, null);
2124         }
2125 
CCView(Context context, AttributeSet attrs)2126         public CCView(Context context, AttributeSet attrs) {
2127             this(context, attrs, 0);
2128         }
2129 
CCView(Context context, AttributeSet attrs, int defStyleAttr)2130         public CCView(Context context, AttributeSet attrs, int defStyleAttr) {
2131             this(context, attrs, defStyleAttr, 0);
2132         }
2133 
CCView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)2134         public CCView(Context context, AttributeSet attrs, int defStyleAttr,
2135                 int defStyleRes) {
2136             super(context, attrs, defStyleAttr, defStyleRes);
2137         }
2138 
setCaptionStyle(CaptionStyle style)2139         public void setCaptionStyle(CaptionStyle style) {
2140             setForegroundColor(style.hasForegroundColor()
2141                     ? style.foregroundColor : DEFAULT_CAPTION_STYLE.foregroundColor);
2142             setBackgroundColor(style.hasBackgroundColor()
2143                     ? style.backgroundColor : DEFAULT_CAPTION_STYLE.backgroundColor);
2144             setEdgeType(style.hasEdgeType()
2145                     ? style.edgeType : DEFAULT_CAPTION_STYLE.edgeType);
2146             setEdgeColor(style.hasEdgeColor()
2147                     ? style.edgeColor : DEFAULT_CAPTION_STYLE.edgeColor);
2148             setTypeface(style.getTypeface());
2149         }
2150     }
2151 }
2152