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