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