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