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
17package android.media;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Color;
22import android.graphics.Paint;
23import android.graphics.Rect;
24import android.graphics.Typeface;
25import android.os.Handler;
26import android.os.Message;
27import android.text.SpannableStringBuilder;
28import android.text.Spanned;
29import android.text.style.CharacterStyle;
30import android.text.style.RelativeSizeSpan;
31import android.text.style.StyleSpan;
32import android.text.style.SubscriptSpan;
33import android.text.style.SuperscriptSpan;
34import android.text.style.UnderlineSpan;
35import android.util.AttributeSet;
36import android.text.Layout.Alignment;
37import android.util.Log;
38import android.text.TextUtils;
39import android.view.Gravity;
40import android.view.View;
41import android.view.ViewGroup;
42import android.view.accessibility.CaptioningManager;
43import android.view.accessibility.CaptioningManager.CaptionStyle;
44import android.widget.RelativeLayout;
45import android.widget.TextView;
46
47import java.io.UnsupportedEncodingException;
48import java.nio.charset.Charset;
49import java.nio.charset.StandardCharsets;
50import java.util.ArrayList;
51import java.util.Arrays;
52import java.util.Comparator;
53import java.util.List;
54import java.util.Vector;
55
56import com.android.internal.widget.SubtitleView;
57
58/** @hide */
59public class Cea708CaptionRenderer extends SubtitleController.Renderer {
60    private final Context mContext;
61    private Cea708CCWidget mCCWidget;
62
63    public Cea708CaptionRenderer(Context context) {
64        mContext = context;
65    }
66
67    @Override
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
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 */
90class Cea708CaptionTrack extends SubtitleTrack {
91    private final Cea708CCParser mCCParser;
92    private final Cea708CCWidget mRenderingWidget;
93
94    Cea708CaptionTrack(Cea708CCWidget renderingWidget, MediaFormat format) {
95        super(format);
96
97        mRenderingWidget = renderingWidget;
98        mCCParser = new Cea708CCParser(mRenderingWidget);
99    }
100
101    @Override
102    public void onData(byte[] data, boolean eos, long runID) {
103        mCCParser.parse(data);
104    }
105
106    @Override
107    public RenderingWidget getRenderingWidget() {
108        return mRenderingWidget;
109    }
110
111    @Override
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 */
187class 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
240    Cea708CCParser(DisplayListener listener) {
241        if (listener != null) {
242            mListener = listener;
243        }
244    }
245
246    interface DisplayListener {
247        void emitEvent(CaptionEvent event);
248    }
249
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 */
1024class Cea708CCWidget extends ClosedCaptionWidget implements Cea708CCParser.DisplayListener {
1025    private final CCHandler mCCHandler;
1026
1027    public Cea708CCWidget(Context context) {
1028        this(context, null);
1029    }
1030
1031    public Cea708CCWidget(Context context, AttributeSet attrs) {
1032        this(context, attrs, 0);
1033    }
1034
1035    public Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr) {
1036        this(context, attrs, defStyleAttr, 0);
1037    }
1038
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
1047    public ClosedCaptionLayout createCaptionLayout(Context context) {
1048        return new CCLayout(context);
1049    }
1050
1051    @Override
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
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
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
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
1128            public ScaledLayoutParams(Context context, AttributeSet attrs) {
1129                super(MATCH_PARENT, MATCH_PARENT);
1130            }
1131        }
1132
1133        @Override
1134        public LayoutParams generateLayoutParams(AttributeSet attrs) {
1135            return new ScaledLayoutParams(getContext(), attrs);
1136        }
1137
1138        @Override
1139        protected boolean checkLayoutParams(LayoutParams p) {
1140            return (p instanceof ScaledLayoutParams);
1141        }
1142
1143        @Override
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
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
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
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
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
1338        public void removeViewFromSafeTitleArea(CCWindowLayout captionWindowLayout) {
1339            mSafeTitleAreaLayout.removeView(captionWindowLayout);
1340        }
1341
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
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
1401        public CCHandler(CCLayout ccLayout) {
1402            mCCLayout = ccLayout;
1403            mHandler = new Handler(this);
1404        }
1405
1406        @Override
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
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
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.
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
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
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
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
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
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
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
1571        private void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) {
1572            if (mCurrentWindowLayout != null) {
1573                mCurrentWindowLayout.setWindowAttr(windowAttr);
1574            }
1575        }
1576
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
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
1603        private void delayCancel() {
1604            mIsDelayed = false;
1605            processPendingBuffer();
1606        }
1607
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
1616        private void sendControlToCurrentWindow(char control) {
1617            if (mCurrentWindowLayout != null) {
1618                mCurrentWindowLayout.sendControl(control);
1619            }
1620        }
1621
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
1632        private void setPenAttr(Cea708CCParser.CaptionPenAttr attr) {
1633            if (mCurrentWindowLayout != null) {
1634                mCurrentWindowLayout.setPenAttr(attr);
1635            }
1636        }
1637
1638        private void setPenColor(Cea708CCParser.CaptionPenColor color) {
1639            if (mCurrentWindowLayout != null) {
1640                mCurrentWindowLayout.setPenColor(color);
1641            }
1642        }
1643
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
1693        public CCWindowLayout(Context context) {
1694            this(context, null);
1695        }
1696
1697        public CCWindowLayout(Context context, AttributeSet attrs) {
1698            this(context, attrs, 0);
1699        }
1700
1701        public CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
1702            this(context, attrs, defStyleAttr, 0);
1703        }
1704
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
1724        public void setCaptionStyle(CaptionStyle style) {
1725            mCaptionStyle = style;
1726            mCCView.setCaptionStyle(style);
1727        }
1728
1729        public void setFontScale(float fontScale) {
1730            mFontScale = fontScale;
1731            updateTextSize();
1732        }
1733
1734        public int getCaptionWindowId() {
1735            return mCaptionWindowId;
1736        }
1737
1738        public void setCaptionWindowId(int captionWindowId) {
1739            mCaptionWindowId = captionWindowId;
1740        }
1741
1742        public void clear() {
1743            clearText();
1744            hide();
1745        }
1746
1747        public void show() {
1748            setVisibility(View.VISIBLE);
1749            requestLayout();
1750        }
1751
1752        public void hide() {
1753            setVisibility(View.INVISIBLE);
1754            requestLayout();
1755        }
1756
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
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
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
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
1802        public void sendBuffer(String buffer) {
1803            appendText(buffer);
1804        }
1805
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         */
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
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
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
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
2040        private int getScreenColumnCount() {
2041            // Assume it has a wide aspect ratio track.
2042            return MAX_COLUMN_COUNT_16_9;
2043        }
2044
2045        public void removeFromCaptionView() {
2046            if (mCCLayout != null) {
2047                mCCLayout.removeViewFromSafeTitleArea(this);
2048                mCCLayout.removeOnLayoutChangeListener(this);
2049                mCCLayout = null;
2050            }
2051        }
2052
2053        public void setText(String text) {
2054            updateText(text, false);
2055        }
2056
2057        public void appendText(String text) {
2058            updateText(text, true);
2059        }
2060
2061        public void clearText() {
2062            mBuilder.clear();
2063            mCCView.setText("");
2064        }
2065
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
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
2122        public CCView(Context context) {
2123            this(context, null);
2124        }
2125
2126        public CCView(Context context, AttributeSet attrs) {
2127            this(context, attrs, 0);
2128        }
2129
2130        public CCView(Context context, AttributeSet attrs, int defStyleAttr) {
2131            this(context, attrs, defStyleAttr, 0);
2132        }
2133
2134        public CCView(Context context, AttributeSet attrs, int defStyleAttr,
2135                int defStyleRes) {
2136            super(context, attrs, defStyleAttr, defStyleRes);
2137        }
2138
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