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