ClosedCaptionRenderer.java revision bdfd91024767893829017918ab565f224ccd35ec
1/*
2 * Copyright (C) 2014 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.Color;
21import android.graphics.Rect;
22import android.graphics.Typeface;
23import android.util.AttributeSet;
24import android.util.Log;
25import android.util.TypedValue;
26import android.view.Gravity;
27import android.view.View;
28import android.view.ViewGroup;
29import android.view.accessibility.CaptioningManager;
30import android.view.accessibility.CaptioningManager.CaptionStyle;
31import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
32import android.widget.LinearLayout;
33import android.widget.TextView;
34
35import java.util.ArrayList;
36import java.util.Arrays;
37import java.util.Vector;
38
39import android.text.SpannableStringBuilder;
40import android.text.Spannable;
41import android.text.style.BackgroundColorSpan;
42import android.text.style.StyleSpan;
43
44/** @hide */
45public class ClosedCaptionRenderer extends SubtitleController.Renderer {
46    private final Context mContext;
47    private ClosedCaptionWidget mRenderingWidget;
48
49    public ClosedCaptionRenderer(Context context) {
50        mContext = context;
51    }
52
53    @Override
54    public boolean supports(MediaFormat format) {
55        if (format.containsKey(MediaFormat.KEY_MIME)) {
56            return format.getString(MediaFormat.KEY_MIME).equals(
57                    MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608);
58        }
59        return false;
60    }
61
62    @Override
63    public SubtitleTrack createTrack(MediaFormat format) {
64        if (mRenderingWidget == null) {
65            mRenderingWidget = new ClosedCaptionWidget(mContext);
66        }
67        return new ClosedCaptionTrack(mRenderingWidget, format);
68    }
69}
70
71/** @hide */
72class ClosedCaptionTrack extends SubtitleTrack {
73    private final ClosedCaptionWidget mRenderingWidget;
74    private final CCParser mCCParser;
75
76    ClosedCaptionTrack(ClosedCaptionWidget renderingWidget, MediaFormat format) {
77        super(format);
78
79        mRenderingWidget = renderingWidget;
80        mCCParser = new CCParser(renderingWidget);
81    }
82
83    @Override
84    public void onData(byte[] data, boolean eos, long runID) {
85        mCCParser.parse(data);
86    }
87
88    @Override
89    public RenderingWidget getRenderingWidget() {
90        return mRenderingWidget;
91    }
92
93    @Override
94    public void updateView(Vector<Cue> activeCues) {
95        // Overriding with NO-OP, CC rendering by-passes this
96    }
97}
98
99/**
100 * @hide
101 *
102 * CCParser processes CEA-608 closed caption data.
103 *
104 * It calls back into OnDisplayChangedListener upon
105 * display change with styled text for rendering.
106 *
107 */
108class CCParser {
109    public static final int MAX_ROWS = 15;
110    public static final int MAX_COLS = 32;
111
112    private static final String TAG = "CCParser";
113    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
114
115    private static final int INVALID = -1;
116
117    // EIA-CEA-608: Table 70 - Control Codes
118    private static final int RCL = 0x20;
119    private static final int BS  = 0x21;
120    private static final int AOF = 0x22;
121    private static final int AON = 0x23;
122    private static final int DER = 0x24;
123    private static final int RU2 = 0x25;
124    private static final int RU3 = 0x26;
125    private static final int RU4 = 0x27;
126    private static final int FON = 0x28;
127    private static final int RDC = 0x29;
128    private static final int TR  = 0x2a;
129    private static final int RTD = 0x2b;
130    private static final int EDM = 0x2c;
131    private static final int CR  = 0x2d;
132    private static final int ENM = 0x2e;
133    private static final int EOC = 0x2f;
134
135    // Transparent Space
136    private static final char TS = '\u00A0';
137
138    // Captioning Modes
139    private static final int MODE_UNKNOWN = 0;
140    private static final int MODE_PAINT_ON = 1;
141    private static final int MODE_ROLL_UP = 2;
142    private static final int MODE_POP_ON = 3;
143    private static final int MODE_TEXT = 4;
144
145    private final DisplayListener mListener;
146
147    private int mMode = MODE_PAINT_ON;
148    private int mRollUpSize = 4;
149
150    private CCMemory mDisplay = new CCMemory();
151    private CCMemory mNonDisplay = new CCMemory();
152    private CCMemory mTextMem = new CCMemory();
153
154    CCParser(DisplayListener listener) {
155        mListener = listener;
156    }
157
158    void parse(byte[] data) {
159        CCData[] ccData = CCData.fromByteArray(data);
160
161        for (int i = 0; i < ccData.length; i++) {
162            if (DEBUG) {
163                Log.d(TAG, ccData[i].toString());
164            }
165
166            if (handleCtrlCode(ccData[i])
167                    || handleTabOffsets(ccData[i])
168                    || handlePACCode(ccData[i])
169                    || handleMidRowCode(ccData[i])) {
170                continue;
171            }
172
173            handleDisplayableChars(ccData[i]);
174        }
175    }
176
177    interface DisplayListener {
178        public void onDisplayChanged(SpannableStringBuilder[] styledTexts);
179        public CaptionStyle getCaptionStyle();
180    }
181
182    private CCMemory getMemory() {
183        // get the CC memory to operate on for current mode
184        switch (mMode) {
185        case MODE_POP_ON:
186            return mNonDisplay;
187        case MODE_TEXT:
188            // TODO(chz): support only caption mode for now,
189            // in text mode, dump everything to text mem.
190            return mTextMem;
191        case MODE_PAINT_ON:
192        case MODE_ROLL_UP:
193            return mDisplay;
194        default:
195            Log.w(TAG, "unrecoginized mode: " + mMode);
196        }
197        return mDisplay;
198    }
199
200    private boolean handleDisplayableChars(CCData ccData) {
201        if (!ccData.isDisplayableChar()) {
202            return false;
203        }
204
205        // Extended char includes 1 automatic backspace
206        if (ccData.isExtendedChar()) {
207            getMemory().bs();
208        }
209
210        getMemory().writeText(ccData.getDisplayText());
211
212        if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
213            updateDisplay();
214        }
215
216        return true;
217    }
218
219    private boolean handleMidRowCode(CCData ccData) {
220        StyleCode m = ccData.getMidRow();
221        if (m != null) {
222            getMemory().writeMidRowCode(m);
223            return true;
224        }
225        return false;
226    }
227
228    private boolean handlePACCode(CCData ccData) {
229        PAC pac = ccData.getPAC();
230
231        if (pac != null) {
232            if (mMode == MODE_ROLL_UP) {
233                getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
234            }
235            getMemory().writePAC(pac);
236            return true;
237        }
238
239        return false;
240    }
241
242    private boolean handleTabOffsets(CCData ccData) {
243        int tabs = ccData.getTabOffset();
244
245        if (tabs > 0) {
246            getMemory().tab(tabs);
247            return true;
248        }
249
250        return false;
251    }
252
253    private boolean handleCtrlCode(CCData ccData) {
254        int ctrlCode = ccData.getCtrlCode();
255        switch(ctrlCode) {
256        case RCL:
257            // select pop-on style
258            mMode = MODE_POP_ON;
259            break;
260        case BS:
261            getMemory().bs();
262            break;
263        case DER:
264            getMemory().der();
265            break;
266        case RU2:
267        case RU3:
268        case RU4:
269            mRollUpSize = (ctrlCode - 0x23);
270            // erase memory if currently in other style
271            if (mMode != MODE_ROLL_UP) {
272                mDisplay.erase();
273                mNonDisplay.erase();
274            }
275            // select roll-up style
276            mMode = MODE_ROLL_UP;
277            break;
278        case FON:
279            Log.i(TAG, "Flash On");
280            break;
281        case RDC:
282            // select paint-on style
283            mMode = MODE_PAINT_ON;
284            break;
285        case TR:
286            mMode = MODE_TEXT;
287            mTextMem.erase();
288            break;
289        case RTD:
290            mMode = MODE_TEXT;
291            break;
292        case EDM:
293            // erase display memory
294            mDisplay.erase();
295            updateDisplay();
296            break;
297        case CR:
298            if (mMode == MODE_ROLL_UP) {
299                getMemory().rollUp(mRollUpSize);
300            } else {
301                getMemory().cr();
302            }
303            if (mMode == MODE_ROLL_UP) {
304                updateDisplay();
305            }
306            break;
307        case ENM:
308            // erase non-display memory
309            mNonDisplay.erase();
310            break;
311        case EOC:
312            // swap display/non-display memory
313            swapMemory();
314            // switch to pop-on style
315            mMode = MODE_POP_ON;
316            updateDisplay();
317            break;
318        case INVALID:
319        default:
320            // not handled
321            return false;
322        }
323
324        // handled
325        return true;
326    }
327
328    private void updateDisplay() {
329        if (mListener != null) {
330            CaptionStyle captionStyle = mListener.getCaptionStyle();
331            mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
332        }
333    }
334
335    private void swapMemory() {
336        CCMemory temp = mDisplay;
337        mDisplay = mNonDisplay;
338        mNonDisplay = temp;
339    }
340
341    private static class StyleCode {
342        static final int COLOR_WHITE = 0;
343        static final int COLOR_GREEN = 1;
344        static final int COLOR_BLUE = 2;
345        static final int COLOR_CYAN = 3;
346        static final int COLOR_RED = 4;
347        static final int COLOR_YELLOW = 5;
348        static final int COLOR_MAGENTA = 6;
349        static final int COLOR_INVALID = 7;
350
351        static final int STYLE_ITALICS   = 0x00000001;
352        static final int STYLE_UNDERLINE = 0x00000002;
353
354        static final String[] mColorMap = {
355            "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
356        };
357
358        final int mStyle;
359        final int mColor;
360
361        static StyleCode fromByte(byte data2) {
362            int style = 0;
363            int color = (data2 >> 1) & 0x7;
364
365            if ((data2 & 0x1) != 0) {
366                style |= STYLE_UNDERLINE;
367            }
368
369            if (color == COLOR_INVALID) {
370                // WHITE ITALICS
371                color = COLOR_WHITE;
372                style |= STYLE_ITALICS;
373            }
374
375            return new StyleCode(style, color);
376        }
377
378        StyleCode(int style, int color) {
379            mStyle = style;
380            mColor = color;
381        }
382
383        boolean isItalics() {
384            return (mStyle & STYLE_ITALICS) != 0;
385        }
386
387        int getColor() {
388            return mColor;
389        }
390
391        @Override
392        public String toString() {
393            StringBuilder str = new StringBuilder();
394            str.append("{");
395            str.append(mColorMap[mColor]);
396            if ((mStyle & STYLE_ITALICS) != 0) {
397                str.append(", ITALICS");
398            }
399            if ((mStyle & STYLE_UNDERLINE) != 0) {
400                str.append(", UNDERLINE");
401            }
402            str.append("}");
403
404            return str.toString();
405        }
406    }
407
408    private static class PAC extends StyleCode {
409        final int mRow;
410        final int mCol;
411
412        static PAC fromBytes(byte data1, byte data2) {
413            int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
414            int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
415            int style = 0;
416            if ((data2 & 1) != 0) {
417                style |= STYLE_UNDERLINE;
418            }
419            if ((data2 & 0x10) != 0) {
420                // indent code
421                int indent = (data2 >> 1) & 0x7;
422                return new PAC(row, indent * 4, style, COLOR_WHITE);
423            } else {
424                // style code
425                int color = (data2 >> 1) & 0x7;
426
427                if (color == COLOR_INVALID) {
428                    // WHITE ITALICS
429                    color = COLOR_WHITE;
430                    style |= STYLE_ITALICS;
431                }
432                return new PAC(row, -1, style, color);
433            }
434        }
435
436        PAC(int row, int col, int style, int color) {
437            super(style, color);
438            mRow = row;
439            mCol = col;
440        }
441
442        boolean isIndentPAC() {
443            return (mCol >= 0);
444        }
445
446        int getRow() {
447            return mRow;
448        }
449
450        int getCol() {
451            return mCol;
452        }
453
454        @Override
455        public String toString() {
456            return String.format("{%d, %d}, %s",
457                    mRow, mCol, super.toString());
458        }
459    }
460
461    /* CCLineBuilder keeps track of displayable chars, as well as
462     * MidRow styles and PACs, for a single line of CC memory.
463     *
464     * It generates styled text via getStyledText() method.
465     */
466    private static class CCLineBuilder {
467        private final StringBuilder mDisplayChars;
468        private final StyleCode[] mMidRowStyles;
469        private final StyleCode[] mPACStyles;
470
471        CCLineBuilder(String str) {
472            mDisplayChars = new StringBuilder(str);
473            mMidRowStyles = new StyleCode[mDisplayChars.length()];
474            mPACStyles = new StyleCode[mDisplayChars.length()];
475        }
476
477        void setCharAt(int index, char ch) {
478            mDisplayChars.setCharAt(index, ch);
479            mMidRowStyles[index] = null;
480        }
481
482        void setMidRowAt(int index, StyleCode m) {
483            mDisplayChars.setCharAt(index, ' ');
484            mMidRowStyles[index] = m;
485        }
486
487        void setPACAt(int index, PAC pac) {
488            mPACStyles[index] = pac;
489        }
490
491        char charAt(int index) {
492            return mDisplayChars.charAt(index);
493        }
494
495        int length() {
496            return mDisplayChars.length();
497        }
498
499        void applyStyleSpan(
500                SpannableStringBuilder styledText,
501                StyleCode s, int start, int end) {
502            if (s.isItalics()) {
503                styledText.setSpan(
504                        new StyleSpan(android.graphics.Typeface.ITALIC),
505                        start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
506            }
507        }
508
509        SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
510            SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
511            int start = -1, next = 0;
512            int styleStart = -1;
513            StyleCode curStyle = null;
514            while (next < mDisplayChars.length()) {
515                StyleCode newStyle = null;
516                if (mMidRowStyles[next] != null) {
517                    // apply mid-row style change
518                    newStyle = mMidRowStyles[next];
519                } else if (mPACStyles[next] != null
520                    && (styleStart < 0 || start < 0)) {
521                    // apply PAC style change, only if:
522                    // 1. no style set, or
523                    // 2. style set, but prev char is none-displayable
524                    newStyle = mPACStyles[next];
525                }
526                if (newStyle != null) {
527                    curStyle = newStyle;
528                    if (styleStart >= 0 && start >= 0) {
529                        applyStyleSpan(styledText, newStyle, styleStart, next);
530                    }
531                    styleStart = next;
532                }
533
534                if (mDisplayChars.charAt(next) != TS) {
535                    if (start < 0) {
536                        start = next;
537                    }
538                } else if (start >= 0) {
539                    int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
540                    int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
541                    styledText.setSpan(
542                            new BackgroundColorSpan(captionStyle.backgroundColor),
543                            expandedStart, expandedEnd,
544                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
545                    if (styleStart >= 0) {
546                        applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
547                    }
548                    start = -1;
549                }
550                next++;
551            }
552
553            return styledText;
554        }
555    }
556
557    /*
558     * CCMemory models a console-style display.
559     */
560    private static class CCMemory {
561        private final String mBlankLine;
562        private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
563        private int mRow;
564        private int mCol;
565
566        CCMemory() {
567            char[] blank = new char[MAX_COLS + 2];
568            Arrays.fill(blank, TS);
569            mBlankLine = new String(blank);
570        }
571
572        void erase() {
573            // erase all lines
574            for (int i = 0; i < mLines.length; i++) {
575                mLines[i] = null;
576            }
577            mRow = MAX_ROWS;
578            mCol = 1;
579        }
580
581        void der() {
582            if (mLines[mRow] != null) {
583                for (int i = 0; i < mCol; i++) {
584                    if (mLines[mRow].charAt(i) != TS) {
585                        for (int j = mCol; j < mLines[mRow].length(); j++) {
586                            mLines[j].setCharAt(j, TS);
587                        }
588                        return;
589                    }
590                }
591                mLines[mRow] = null;
592            }
593        }
594
595        void tab(int tabs) {
596            moveCursorByCol(tabs);
597        }
598
599        void bs() {
600            moveCursorByCol(-1);
601            if (mLines[mRow] != null) {
602                mLines[mRow].setCharAt(mCol, TS);
603                if (mCol == MAX_COLS - 1) {
604                    // Spec recommendation:
605                    // if cursor was at col 32, move cursor
606                    // back to col 31 and erase both col 31&32
607                    mLines[mRow].setCharAt(MAX_COLS, TS);
608                }
609            }
610        }
611
612        void cr() {
613            moveCursorTo(mRow + 1, 1);
614        }
615
616        void rollUp(int windowSize) {
617            int i;
618            for (i = 0; i <= mRow - windowSize; i++) {
619                mLines[i] = null;
620            }
621            int startRow = mRow - windowSize + 1;
622            if (startRow < 1) {
623                startRow = 1;
624            }
625            for (i = startRow; i < mRow; i++) {
626                mLines[i] = mLines[i + 1];
627            }
628            for (i = mRow; i < mLines.length; i++) {
629                // clear base row
630                mLines[i] = null;
631            }
632            // default to col 1, in case PAC is not sent
633            mCol = 1;
634        }
635
636        void writeText(String text) {
637            for (int i = 0; i < text.length(); i++) {
638                getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
639                moveCursorByCol(1);
640            }
641        }
642
643        void writeMidRowCode(StyleCode m) {
644            getLineBuffer(mRow).setMidRowAt(mCol, m);
645            moveCursorByCol(1);
646        }
647
648        void writePAC(PAC pac) {
649            if (pac.isIndentPAC()) {
650                moveCursorTo(pac.getRow(), pac.getCol());
651            } else {
652                moveCursorToRow(pac.getRow());
653            }
654            getLineBuffer(mRow).setPACAt(mCol, pac);
655        }
656
657        SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
658            ArrayList<SpannableStringBuilder> rows =
659                    new ArrayList<SpannableStringBuilder>(MAX_ROWS);
660            for (int i = 1; i <= MAX_ROWS; i++) {
661                rows.add(mLines[i] != null ?
662                        mLines[i].getStyledText(captionStyle) : null);
663            }
664            return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
665        }
666
667        private static int clamp(int x, int min, int max) {
668            return x < min ? min : (x > max ? max : x);
669        }
670
671        private void moveCursorTo(int row, int col) {
672            mRow = clamp(row, 1, MAX_ROWS);
673            mCol = clamp(col, 1, MAX_COLS);
674        }
675
676        private void moveCursorToRow(int row) {
677            mRow = clamp(row, 1, MAX_ROWS);
678        }
679
680        private void moveCursorByCol(int col) {
681            mCol = clamp(mCol + col, 1, MAX_COLS);
682        }
683
684        private void moveBaselineTo(int baseRow, int windowSize) {
685            if (mRow == baseRow) {
686                return;
687            }
688            int actualWindowSize = windowSize;
689            if (baseRow < actualWindowSize) {
690                actualWindowSize = baseRow;
691            }
692            if (mRow < actualWindowSize) {
693                actualWindowSize = mRow;
694            }
695
696            int i;
697            if (baseRow < mRow) {
698                // copy from bottom to top row
699                for (i = actualWindowSize - 1; i >= 0; i--) {
700                    mLines[baseRow - i] = mLines[mRow - i];
701                }
702            } else {
703                // copy from top to bottom row
704                for (i = 0; i < actualWindowSize; i++) {
705                    mLines[baseRow - i] = mLines[mRow - i];
706                }
707            }
708            // clear rest of the rows
709            for (i = 0; i <= baseRow - windowSize; i++) {
710                mLines[i] = null;
711            }
712            for (i = baseRow + 1; i < mLines.length; i++) {
713                mLines[i] = null;
714            }
715        }
716
717        private CCLineBuilder getLineBuffer(int row) {
718            if (mLines[row] == null) {
719                mLines[row] = new CCLineBuilder(mBlankLine);
720            }
721            return mLines[row];
722        }
723    }
724
725    /*
726     * CCData parses the raw CC byte pair into displayable chars,
727     * misc control codes, Mid-Row or Preamble Address Codes.
728     */
729    private static class CCData {
730        private final byte mType;
731        private final byte mData1;
732        private final byte mData2;
733
734        private static final String[] mCtrlCodeMap = {
735            "RCL", "BS" , "AOF", "AON",
736            "DER", "RU2", "RU3", "RU4",
737            "FON", "RDC", "TR" , "RTD",
738            "EDM", "CR" , "ENM", "EOC",
739        };
740
741        private static final String[] mSpecialCharMap = {
742            "\u00AE",
743            "\u00B0",
744            "\u00BD",
745            "\u00BF",
746            "\u2122",
747            "\u00A2",
748            "\u00A3",
749            "\u266A", // Eighth note
750            "\u00E0",
751            "\u00A0", // Transparent space
752            "\u00E8",
753            "\u00E2",
754            "\u00EA",
755            "\u00EE",
756            "\u00F4",
757            "\u00FB",
758        };
759
760        private static final String[] mSpanishCharMap = {
761            // Spanish and misc chars
762            "\u00C1", // A
763            "\u00C9", // E
764            "\u00D3", // I
765            "\u00DA", // O
766            "\u00DC", // U
767            "\u00FC", // u
768            "\u2018", // opening single quote
769            "\u00A1", // inverted exclamation mark
770            "*",
771            "'",
772            "\u2014", // em dash
773            "\u00A9", // Copyright
774            "\u2120", // Servicemark
775            "\u2022", // round bullet
776            "\u201C", // opening double quote
777            "\u201D", // closing double quote
778            // French
779            "\u00C0",
780            "\u00C2",
781            "\u00C7",
782            "\u00C8",
783            "\u00CA",
784            "\u00CB",
785            "\u00EB",
786            "\u00CE",
787            "\u00CF",
788            "\u00EF",
789            "\u00D4",
790            "\u00D9",
791            "\u00F9",
792            "\u00DB",
793            "\u00AB",
794            "\u00BB"
795        };
796
797        private static final String[] mProtugueseCharMap = {
798            // Portuguese
799            "\u00C3",
800            "\u00E3",
801            "\u00CD",
802            "\u00CC",
803            "\u00EC",
804            "\u00D2",
805            "\u00F2",
806            "\u00D5",
807            "\u00F5",
808            "{",
809            "}",
810            "\\",
811            "^",
812            "_",
813            "|",
814            "~",
815            // German and misc chars
816            "\u00C4",
817            "\u00E4",
818            "\u00D6",
819            "\u00F6",
820            "\u00DF",
821            "\u00A5",
822            "\u00A4",
823            "\u2502", // vertical bar
824            "\u00C5",
825            "\u00E5",
826            "\u00D8",
827            "\u00F8",
828            "\u250C", // top-left corner
829            "\u2510", // top-right corner
830            "\u2514", // lower-left corner
831            "\u2518", // lower-right corner
832        };
833
834        static CCData[] fromByteArray(byte[] data) {
835            CCData[] ccData = new CCData[data.length / 3];
836
837            for (int i = 0; i < ccData.length; i++) {
838                ccData[i] = new CCData(
839                        data[i * 3],
840                        data[i * 3 + 1],
841                        data[i * 3 + 2]);
842            }
843
844            return ccData;
845        }
846
847        CCData(byte type, byte data1, byte data2) {
848            mType = type;
849            mData1 = data1;
850            mData2 = data2;
851        }
852
853        int getCtrlCode() {
854            if ((mData1 == 0x14 || mData1 == 0x1c)
855                    && mData2 >= 0x20 && mData2 <= 0x2f) {
856                return mData2;
857            }
858            return INVALID;
859        }
860
861        StyleCode getMidRow() {
862            // only support standard Mid-row codes, ignore
863            // optional background/foreground mid-row codes
864            if ((mData1 == 0x11 || mData1 == 0x19)
865                    && mData2 >= 0x20 && mData2 <= 0x2f) {
866                return StyleCode.fromByte(mData2);
867            }
868            return null;
869        }
870
871        PAC getPAC() {
872            if ((mData1 & 0x70) == 0x10
873                    && (mData2 & 0x40) == 0x40
874                    && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
875                return PAC.fromBytes(mData1, mData2);
876            }
877            return null;
878        }
879
880        int getTabOffset() {
881            if ((mData1 == 0x17 || mData1 == 0x1f)
882                    && mData2 >= 0x21 && mData2 <= 0x23) {
883                return mData2 & 0x3;
884            }
885            return 0;
886        }
887
888        boolean isDisplayableChar() {
889            return isBasicChar() || isSpecialChar() || isExtendedChar();
890        }
891
892        String getDisplayText() {
893            String str = getBasicChars();
894
895            if (str == null) {
896                str =  getSpecialChar();
897
898                if (str == null) {
899                    str = getExtendedChar();
900                }
901            }
902
903            return str;
904        }
905
906        private String ctrlCodeToString(int ctrlCode) {
907            return mCtrlCodeMap[ctrlCode - 0x20];
908        }
909
910        private boolean isBasicChar() {
911            return mData1 >= 0x20 && mData1 <= 0x7f;
912        }
913
914        private boolean isSpecialChar() {
915            return ((mData1 == 0x11 || mData1 == 0x19)
916                    && mData2 >= 0x30 && mData2 <= 0x3f);
917        }
918
919        private boolean isExtendedChar() {
920            return ((mData1 == 0x12 || mData1 == 0x1A
921                    || mData1 == 0x13 || mData1 == 0x1B)
922                    && mData2 >= 0x20 && mData2 <= 0x3f);
923        }
924
925        private char getBasicChar(byte data) {
926            char c;
927            // replace the non-ASCII ones
928            switch (data) {
929                case 0x2A: c = '\u00E1'; break;
930                case 0x5C: c = '\u00E9'; break;
931                case 0x5E: c = '\u00ED'; break;
932                case 0x5F: c = '\u00F3'; break;
933                case 0x60: c = '\u00FA'; break;
934                case 0x7B: c = '\u00E7'; break;
935                case 0x7C: c = '\u00F7'; break;
936                case 0x7D: c = '\u00D1'; break;
937                case 0x7E: c = '\u00F1'; break;
938                case 0x7F: c = '\u2588'; break; // Full block
939                default: c = (char) data; break;
940            }
941            return c;
942        }
943
944        private String getBasicChars() {
945            if (mData1 >= 0x20 && mData1 <= 0x7f) {
946                StringBuilder builder = new StringBuilder(2);
947                builder.append(getBasicChar(mData1));
948                if (mData2 >= 0x20 && mData2 <= 0x7f) {
949                    builder.append(getBasicChar(mData2));
950                }
951                return builder.toString();
952            }
953
954            return null;
955        }
956
957        private String getSpecialChar() {
958            if ((mData1 == 0x11 || mData1 == 0x19)
959                    && mData2 >= 0x30 && mData2 <= 0x3f) {
960                return mSpecialCharMap[mData2 - 0x30];
961            }
962
963            return null;
964        }
965
966        private String getExtendedChar() {
967            if ((mData1 == 0x12 || mData1 == 0x1A)
968                    && mData2 >= 0x20 && mData2 <= 0x3f){
969                // 1 Spanish/French char
970                return mSpanishCharMap[mData2 - 0x20];
971            } else if ((mData1 == 0x13 || mData1 == 0x1B)
972                    && mData2 >= 0x20 && mData2 <= 0x3f){
973                // 1 Portuguese/German/Danish char
974                return mProtugueseCharMap[mData2 - 0x20];
975            }
976
977            return null;
978        }
979
980        @Override
981        public String toString() {
982            String str;
983
984            if (mData1 < 0x10 && mData2 < 0x10) {
985                // Null Pad, ignore
986                return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
987            }
988
989            int ctrlCode = getCtrlCode();
990            if (ctrlCode != INVALID) {
991                return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
992            }
993
994            int tabOffset = getTabOffset();
995            if (tabOffset > 0) {
996                return String.format("[%d]Tab%d", mType, tabOffset);
997            }
998
999            PAC pac = getPAC();
1000            if (pac != null) {
1001                return String.format("[%d]PAC: %s", mType, pac.toString());
1002            }
1003
1004            StyleCode m = getMidRow();
1005            if (m != null) {
1006                return String.format("[%d]Mid-row: %s", mType, m.toString());
1007            }
1008
1009            if (isDisplayableChar()) {
1010                return String.format("[%d]Displayable: %s (%02x %02x)",
1011                        mType, getDisplayText(), mData1, mData2);
1012            }
1013
1014            return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
1015        }
1016    }
1017}
1018
1019/**
1020 * Widget capable of rendering CEA-608 closed captions.
1021 *
1022 * @hide
1023 */
1024class ClosedCaptionWidget extends ViewGroup implements
1025        SubtitleTrack.RenderingWidget,
1026        CCParser.DisplayListener {
1027    private static final String TAG = "ClosedCaptionWidget";
1028
1029    private static final Rect mTextBounds = new Rect();
1030    private static final String mDummyText = "1234567890123456789012345678901234";
1031    private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT;
1032
1033    /** Captioning manager, used to obtain and track caption properties. */
1034    private final CaptioningManager mManager;
1035
1036    /** Callback for rendering changes. */
1037    private OnChangedListener mListener;
1038
1039    /** Current caption style. */
1040    private CaptionStyle mCaptionStyle;
1041
1042    /* Closed caption layout. */
1043    private CCLayout mClosedCaptionLayout;
1044
1045    /** Whether a caption style change listener is registered. */
1046    private boolean mHasChangeListener;
1047
1048    public ClosedCaptionWidget(Context context) {
1049        this(context, null);
1050    }
1051
1052    public ClosedCaptionWidget(Context context, AttributeSet attrs) {
1053        this(context, null, 0);
1054    }
1055
1056    public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
1057        super(context, attrs, defStyle);
1058
1059        // Cannot render text over video when layer type is hardware.
1060        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
1061
1062        mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
1063        mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle());
1064
1065        mClosedCaptionLayout = new CCLayout(context);
1066        mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
1067        addView(mClosedCaptionLayout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
1068
1069        requestLayout();
1070    }
1071
1072    @Override
1073    public void setOnChangedListener(OnChangedListener listener) {
1074        mListener = listener;
1075    }
1076
1077    @Override
1078    public void setSize(int width, int height) {
1079        final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
1080        final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
1081
1082        measure(widthSpec, heightSpec);
1083        layout(0, 0, width, height);
1084    }
1085
1086    @Override
1087    public void setVisible(boolean visible) {
1088        if (visible) {
1089            setVisibility(View.VISIBLE);
1090        } else {
1091            setVisibility(View.GONE);
1092        }
1093
1094        manageChangeListener();
1095    }
1096
1097    @Override
1098    public void onAttachedToWindow() {
1099        super.onAttachedToWindow();
1100
1101        manageChangeListener();
1102    }
1103
1104    @Override
1105    public void onDetachedFromWindow() {
1106        super.onDetachedFromWindow();
1107
1108        manageChangeListener();
1109    }
1110
1111    @Override
1112    public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
1113        mClosedCaptionLayout.update(styledTexts);
1114
1115        if (mListener != null) {
1116            mListener.onChanged(this);
1117        }
1118    }
1119
1120    @Override
1121    public CaptionStyle getCaptionStyle() {
1122        return mCaptionStyle;
1123    }
1124
1125    @Override
1126    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1127        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1128        mClosedCaptionLayout.measure(widthMeasureSpec, heightMeasureSpec);
1129    }
1130
1131    @Override
1132    protected void onLayout(boolean changed, int l, int t, int r, int b) {
1133        mClosedCaptionLayout.layout(l, t, r, b);
1134    }
1135
1136    /**
1137     * Manages whether this renderer is listening for caption style changes.
1138     */
1139    private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
1140        @Override
1141        public void onUserStyleChanged(CaptionStyle userStyle) {
1142            mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle);
1143            mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
1144        }
1145    };
1146
1147    private void manageChangeListener() {
1148        final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
1149        if (mHasChangeListener != needsListener) {
1150            mHasChangeListener = needsListener;
1151
1152            if (needsListener) {
1153                mManager.addCaptioningChangeListener(mCaptioningListener);
1154            } else {
1155                mManager.removeCaptioningChangeListener(mCaptioningListener);
1156            }
1157        }
1158    }
1159
1160    private static class CCLineBox extends TextView {
1161        private static final float FONT_PADDING_RATIO = 0.75f;
1162
1163        CCLineBox(Context context) {
1164            super(context);
1165            setGravity(Gravity.CENTER);
1166            setBackgroundColor(Color.TRANSPARENT);
1167            setTextColor(Color.WHITE);
1168            setTypeface(Typeface.MONOSPACE);
1169            setVisibility(View.INVISIBLE);
1170        }
1171
1172        void setCaptionStyle(CaptionStyle captionStyle) {
1173            setTextColor(captionStyle.foregroundColor);
1174            // TODO: edge color?
1175        }
1176
1177        @Override
1178        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1179            float fontSize = MeasureSpec.getSize(heightMeasureSpec)
1180                    * FONT_PADDING_RATIO;
1181            setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
1182
1183            // set font scale in the X direction to match the required width
1184            setScaleX(1.0f);
1185            getPaint().getTextBounds(mDummyText, 0, mDummyText.length(), mTextBounds);
1186            float actualTextWidth = mTextBounds.width();
1187            float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec);
1188            setScaleX(requiredTextWidth / actualTextWidth);
1189
1190            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1191        }
1192    }
1193
1194    private static class CCLayout extends LinearLayout {
1195        private static final int MAX_ROWS = CCParser.MAX_ROWS;
1196        private static final float SAFE_AREA_RATIO = 0.9f;
1197
1198        private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS];
1199
1200        CCLayout(Context context) {
1201            super(context);
1202            setGravity(Gravity.START);
1203            setOrientation(LinearLayout.VERTICAL);
1204            for (int i = 0; i < MAX_ROWS; i++) {
1205                mLineBoxes[i] = new CCLineBox(getContext());
1206                addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
1207            }
1208        }
1209
1210        void setCaptionStyle(CaptionStyle captionStyle) {
1211            for (int i = 0; i < MAX_ROWS; i++) {
1212                mLineBoxes[i].setCaptionStyle(captionStyle);
1213            }
1214        }
1215
1216        void update(SpannableStringBuilder[] textBuffer) {
1217            for (int i = 0; i < MAX_ROWS; i++) {
1218                if (textBuffer[i] != null) {
1219                    mLineBoxes[i].setText(textBuffer[i]);
1220                    mLineBoxes[i].setVisibility(View.VISIBLE);
1221                } else {
1222                    mLineBoxes[i].setVisibility(View.INVISIBLE);
1223                }
1224            }
1225        }
1226
1227        @Override
1228        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1229            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1230
1231            int safeWidth = getMeasuredWidth();
1232            int safeHeight = getMeasuredHeight();
1233
1234            // CEA-608 assumes 4:3 video
1235            if (safeWidth * 3 >= safeHeight * 4) {
1236                safeWidth = safeHeight * 4 / 3;
1237            } else {
1238                safeHeight = safeWidth * 3 / 4;
1239            }
1240            safeWidth *= SAFE_AREA_RATIO;
1241            safeHeight *= SAFE_AREA_RATIO;
1242
1243            int lineHeight = safeHeight / MAX_ROWS;
1244            int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
1245                    lineHeight, MeasureSpec.EXACTLY);
1246            int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
1247                    safeWidth, MeasureSpec.EXACTLY);
1248
1249            for (int i = 0; i < MAX_ROWS; i++) {
1250                mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec);
1251            }
1252        }
1253
1254        @Override
1255        protected void onLayout(boolean changed, int l, int t, int r, int b) {
1256            // safe caption area
1257            int viewPortWidth = r - l;
1258            int viewPortHeight = b - t;
1259            int safeWidth, safeHeight;
1260            // CEA-608 assumes 4:3 video
1261            if (viewPortWidth * 3 >= viewPortHeight * 4) {
1262                safeWidth = viewPortHeight * 4 / 3;
1263                safeHeight = viewPortHeight;
1264            } else {
1265                safeWidth = viewPortWidth;
1266                safeHeight = viewPortWidth * 3 / 4;
1267            }
1268            safeWidth *= SAFE_AREA_RATIO;
1269            safeHeight *= SAFE_AREA_RATIO;
1270            int left = (viewPortWidth - safeWidth) / 2;
1271            int top = (viewPortHeight - safeHeight) / 2;
1272
1273            for (int i = 0; i < MAX_ROWS; i++) {
1274                mLineBoxes[i].layout(
1275                        left,
1276                        top + safeHeight * i / MAX_ROWS,
1277                        left + safeWidth,
1278                        top + safeHeight * (i + 1) / MAX_ROWS);
1279            }
1280        }
1281    }
1282};
1283