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 
17 package android.media;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Canvas;
22 import android.graphics.Color;
23 import android.graphics.Paint;
24 import android.graphics.Rect;
25 import android.graphics.Typeface;
26 import android.os.Parcel;
27 import android.text.ParcelableSpan;
28 import android.text.Spannable;
29 import android.text.SpannableStringBuilder;
30 import android.text.TextPaint;
31 import android.text.TextUtils;
32 import android.text.style.CharacterStyle;
33 import android.text.style.StyleSpan;
34 import android.text.style.UnderlineSpan;
35 import android.text.style.UpdateAppearance;
36 import android.util.AttributeSet;
37 import android.util.Log;
38 import android.util.TypedValue;
39 import android.view.Gravity;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.accessibility.CaptioningManager;
43 import android.view.accessibility.CaptioningManager.CaptionStyle;
44 import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
45 import android.widget.LinearLayout;
46 import android.widget.TextView;
47 
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Vector;
51 
52 /** @hide */
53 public class ClosedCaptionRenderer extends SubtitleController.Renderer {
54     private final Context mContext;
55     private ClosedCaptionWidget mRenderingWidget;
56 
ClosedCaptionRenderer(Context context)57     public ClosedCaptionRenderer(Context context) {
58         mContext = context;
59     }
60 
61     @Override
supports(MediaFormat format)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
createTrack(MediaFormat format)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 */
80 class ClosedCaptionTrack extends SubtitleTrack {
81     private final ClosedCaptionWidget mRenderingWidget;
82     private final CCParser mCCParser;
83 
ClosedCaptionTrack(ClosedCaptionWidget renderingWidget, MediaFormat format)84     ClosedCaptionTrack(ClosedCaptionWidget renderingWidget, MediaFormat format) {
85         super(format);
86 
87         mRenderingWidget = renderingWidget;
88         mCCParser = new CCParser(renderingWidget);
89     }
90 
91     @Override
onData(byte[] data, boolean eos, long runID)92     public void onData(byte[] data, boolean eos, long runID) {
93         mCCParser.parse(data);
94     }
95 
96     @Override
getRenderingWidget()97     public RenderingWidget getRenderingWidget() {
98         return mRenderingWidget;
99     }
100 
101     @Override
updateView(Vector<Cue> activeCues)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  */
116 class 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 
CCParser(DisplayListener listener)162     CCParser(DisplayListener listener) {
163         mListener = listener;
164     }
165 
parse(byte[] data)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 {
onDisplayChanged(SpannableStringBuilder[] styledTexts)186         public void onDisplayChanged(SpannableStringBuilder[] styledTexts);
getCaptionStyle()187         public CaptionStyle getCaptionStyle();
188     }
189 
getMemory()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 
handleDisplayableChars(CCData ccData)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 
handleMidRowCode(CCData ccData)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 
handlePACCode(CCData ccData)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 
handleTabOffsets(CCData ccData)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 
handleCtrlCode(CCData ccData)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 
updateDisplay()336     private void updateDisplay() {
337         if (mListener != null) {
338             CaptionStyle captionStyle = mListener.getCaptionStyle();
339             mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
340         }
341     }
342 
swapMemory()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 
fromByte(byte data2)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 
StyleCode(int style, int color)386         StyleCode(int style, int color) {
387             mStyle = style;
388             mColor = color;
389         }
390 
isItalics()391         boolean isItalics() {
392             return (mStyle & STYLE_ITALICS) != 0;
393         }
394 
isUnderline()395         boolean isUnderline() {
396             return (mStyle & STYLE_UNDERLINE) != 0;
397         }
398 
getColor()399         int getColor() {
400             return mColor;
401         }
402 
403         @Override
toString()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 
fromBytes(byte data1, byte data2)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 
PAC(int row, int col, int style, int color)448         PAC(int row, int col, int style, int color) {
449             super(style, color);
450             mRow = row;
451             mCol = col;
452         }
453 
isIndentPAC()454         boolean isIndentPAC() {
455             return (mCol >= 0);
456         }
457 
getRow()458         int getRow() {
459             return mRow;
460         }
461 
getCol()462         int getCol() {
463             return mCol;
464         }
465 
466         @Override
toString()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 
CCLineBuilder(String str)483         CCLineBuilder(String str) {
484             mDisplayChars = new StringBuilder(str);
485             mMidRowStyles = new StyleCode[mDisplayChars.length()];
486             mPACStyles = new StyleCode[mDisplayChars.length()];
487         }
488 
setCharAt(int index, char ch)489         void setCharAt(int index, char ch) {
490             mDisplayChars.setCharAt(index, ch);
491             mMidRowStyles[index] = null;
492         }
493 
setMidRowAt(int index, StyleCode m)494         void setMidRowAt(int index, StyleCode m) {
495             mDisplayChars.setCharAt(index, ' ');
496             mMidRowStyles[index] = m;
497         }
498 
setPACAt(int index, PAC pac)499         void setPACAt(int index, PAC pac) {
500             mPACStyles[index] = pac;
501         }
502 
charAt(int index)503         char charAt(int index) {
504             return mDisplayChars.charAt(index);
505         }
506 
length()507         int length() {
508             return mDisplayChars.length();
509         }
510 
applyStyleSpan( SpannableStringBuilder styledText, StyleCode s, int start, int end)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 
getStyledText(CaptionStyle captionStyle)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 
CCMemory()583         CCMemory() {
584             char[] blank = new char[MAX_COLS + 2];
585             Arrays.fill(blank, TS);
586             mBlankLine = new String(blank);
587         }
588 
erase()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 
der()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 
tab(int tabs)612         void tab(int tabs) {
613             moveCursorByCol(tabs);
614         }
615 
bs()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 
cr()629         void cr() {
630             moveCursorTo(mRow + 1, 1);
631         }
632 
rollUp(int windowSize)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 
writeText(String text)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 
writeMidRowCode(StyleCode m)660         void writeMidRowCode(StyleCode m) {
661             getLineBuffer(mRow).setMidRowAt(mCol, m);
662             moveCursorByCol(1);
663         }
664 
writePAC(PAC pac)665         void writePAC(PAC pac) {
666             if (pac.isIndentPAC()) {
667                 moveCursorTo(pac.getRow(), pac.getCol());
668             } else {
669                 moveCursorTo(pac.getRow(), 1);
670             }
671             getLineBuffer(mRow).setPACAt(mCol, pac);
672         }
673 
getStyledText(CaptionStyle captionStyle)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 
clamp(int x, int min, int max)684         private static int clamp(int x, int min, int max) {
685             return x < min ? min : (x > max ? max : x);
686         }
687 
moveCursorTo(int row, int col)688         private void moveCursorTo(int row, int col) {
689             mRow = clamp(row, 1, MAX_ROWS);
690             mCol = clamp(col, 1, MAX_COLS);
691         }
692 
moveCursorToRow(int row)693         private void moveCursorToRow(int row) {
694             mRow = clamp(row, 1, MAX_ROWS);
695         }
696 
moveCursorByCol(int col)697         private void moveCursorByCol(int col) {
698             mCol = clamp(mCol + col, 1, MAX_COLS);
699         }
700 
moveBaselineTo(int baseRow, int windowSize)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 
getLineBuffer(int row)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 
fromByteArray(byte[] data)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 
CCData(byte type, byte data1, byte data2)864         CCData(byte type, byte data1, byte data2) {
865             mType = type;
866             mData1 = data1;
867             mData2 = data2;
868         }
869 
getCtrlCode()870         int getCtrlCode() {
871             if ((mData1 == 0x14 || mData1 == 0x1c)
872                     && mData2 >= 0x20 && mData2 <= 0x2f) {
873                 return mData2;
874             }
875             return INVALID;
876         }
877 
getMidRow()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 
getPAC()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 
getTabOffset()897         int getTabOffset() {
898             if ((mData1 == 0x17 || mData1 == 0x1f)
899                     && mData2 >= 0x21 && mData2 <= 0x23) {
900                 return mData2 & 0x3;
901             }
902             return 0;
903         }
904 
isDisplayableChar()905         boolean isDisplayableChar() {
906             return isBasicChar() || isSpecialChar() || isExtendedChar();
907         }
908 
getDisplayText()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 
ctrlCodeToString(int ctrlCode)923         private String ctrlCodeToString(int ctrlCode) {
924             return mCtrlCodeMap[ctrlCode - 0x20];
925         }
926 
isBasicChar()927         private boolean isBasicChar() {
928             return mData1 >= 0x20 && mData1 <= 0x7f;
929         }
930 
isSpecialChar()931         private boolean isSpecialChar() {
932             return ((mData1 == 0x11 || mData1 == 0x19)
933                     && mData2 >= 0x30 && mData2 <= 0x3f);
934         }
935 
isExtendedChar()936         private boolean isExtendedChar() {
937             return ((mData1 == 0x12 || mData1 == 0x1A
938                     || mData1 == 0x13 || mData1 == 0x1B)
939                     && mData2 >= 0x20 && mData2 <= 0x3f);
940         }
941 
getBasicChar(byte data)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 
getBasicChars()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 
getSpecialChar()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 
getExtendedChar()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
toString()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  */
1045 class MutableBackgroundColorSpan extends CharacterStyle
1046         implements UpdateAppearance, ParcelableSpan {
1047     private int mColor;
1048 
MutableBackgroundColorSpan(int color)1049     public MutableBackgroundColorSpan(int color) {
1050         mColor = color;
1051     }
MutableBackgroundColorSpan(Parcel src)1052     public MutableBackgroundColorSpan(Parcel src) {
1053         mColor = src.readInt();
1054     }
setBackgroundColor(int color)1055     public void setBackgroundColor(int color) {
1056         mColor = color;
1057     }
getBackgroundColor()1058     public int getBackgroundColor() {
1059         return mColor;
1060     }
1061     @Override
getSpanTypeId()1062     public int getSpanTypeId() {
1063         return TextUtils.BACKGROUND_COLOR_SPAN;
1064     }
1065     @Override
describeContents()1066     public int describeContents() {
1067         return 0;
1068     }
1069     @Override
writeToParcel(Parcel dest, int flags)1070     public void writeToParcel(Parcel dest, int flags) {
1071         dest.writeInt(mColor);
1072     }
1073     @Override
updateDrawState(TextPaint ds)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  */
1084 class 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 
ClosedCaptionWidget(Context context)1108     public ClosedCaptionWidget(Context context) {
1109         this(context, null);
1110     }
1111 
ClosedCaptionWidget(Context context, AttributeSet attrs)1112     public ClosedCaptionWidget(Context context, AttributeSet attrs) {
1113         this(context, null, 0);
1114     }
1115 
ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle)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
setOnChangedListener(OnChangedListener listener)1133     public void setOnChangedListener(OnChangedListener listener) {
1134         mListener = listener;
1135     }
1136 
1137     @Override
setSize(int width, int height)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
setVisible(boolean visible)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
onAttachedToWindow()1158     public void onAttachedToWindow() {
1159         super.onAttachedToWindow();
1160 
1161         manageChangeListener();
1162     }
1163 
1164     @Override
onDetachedFromWindow()1165     public void onDetachedFromWindow() {
1166         super.onDetachedFromWindow();
1167 
1168         manageChangeListener();
1169     }
1170 
1171     @Override
onDisplayChanged(SpannableStringBuilder[] styledTexts)1172     public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
1173         mClosedCaptionLayout.update(styledTexts);
1174 
1175         if (mListener != null) {
1176             mListener.onChanged(this);
1177         }
1178     }
1179 
1180     @Override
getCaptionStyle()1181     public CaptionStyle getCaptionStyle() {
1182         return mCaptionStyle;
1183     }
1184 
1185     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)1186     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1187         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1188         mClosedCaptionLayout.measure(widthMeasureSpec, heightMeasureSpec);
1189     }
1190 
1191     @Override
onLayout(boolean changed, int l, int t, int r, int b)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 
manageChangeListener()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 
CCLineBox(Context context)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 
setCaptionStyle(CaptionStyle captionStyle)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onDraw(Canvas c)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 
drawEdgeOutline(Canvas c)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 
drawEdgeRaisedOrDepressed(Canvas c)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 
setBackgroundSpans(int color)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 
CCLayout(Context context)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 
setCaptionStyle(CaptionStyle captionStyle)1392         void setCaptionStyle(CaptionStyle captionStyle) {
1393             for (int i = 0; i < MAX_ROWS; i++) {
1394                 mLineBoxes[i].setCaptionStyle(captionStyle);
1395             }
1396         }
1397 
update(SpannableStringBuilder[] textBuffer)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onLayout(boolean changed, int l, int t, int r, int b)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