1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.media;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.Rect;
25import android.graphics.Typeface;
26import android.text.Spannable;
27import android.text.SpannableStringBuilder;
28import android.text.TextPaint;
29import android.text.style.CharacterStyle;
30import android.text.style.StyleSpan;
31import android.text.style.UnderlineSpan;
32import android.text.style.UpdateAppearance;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.util.TypedValue;
36import android.view.Gravity;
37import android.view.View;
38import android.view.ViewGroup;
39import android.view.accessibility.CaptioningManager;
40import android.view.accessibility.CaptioningManager.CaptionStyle;
41import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
42import android.widget.LinearLayout;
43import android.widget.TextView;
44
45import java.util.ArrayList;
46import java.util.Arrays;
47import java.util.Vector;
48
49/** @hide */
50public class ClosedCaptionRenderer extends SubtitleController.Renderer {
51    private final Context mContext;
52    private Cea608CCWidget mCCWidget;
53
54    public ClosedCaptionRenderer(Context context) {
55        mContext = context;
56    }
57
58    @Override
59    public boolean supports(MediaFormat format) {
60        if (format.containsKey(MediaFormat.KEY_MIME)) {
61            String mimeType = format.getString(MediaFormat.KEY_MIME);
62            return MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608.equals(mimeType);
63        }
64        return false;
65    }
66
67    @Override
68    public SubtitleTrack createTrack(MediaFormat format) {
69        String mimeType = format.getString(MediaFormat.KEY_MIME);
70        if (MediaPlayer.MEDIA_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 */
81class Cea608CaptionTrack extends SubtitleTrack {
82    private final Cea608CCParser mCCParser;
83    private final Cea608CCWidget mRenderingWidget;
84
85    Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) {
86        super(format);
87
88        mRenderingWidget = renderingWidget;
89        mCCParser = new Cea608CCParser(mRenderingWidget);
90    }
91
92    @Override
93    public void onData(byte[] data, boolean eos, long runID) {
94        mCCParser.parse(data);
95    }
96
97    @Override
98    public RenderingWidget getRenderingWidget() {
99        return mRenderingWidget;
100    }
101
102    @Override
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 */
113abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
114
115    /** @hide */
116    interface ClosedCaptionLayout {
117        void setCaptionStyle(CaptionStyle captionStyle);
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
138    public ClosedCaptionWidget(Context context) {
139        this(context, null);
140    }
141
142    public ClosedCaptionWidget(Context context, AttributeSet attrs) {
143        this(context, attrs, 0);
144    }
145
146    public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
147        this(context, attrs, defStyle, 0);
148    }
149
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
169    public abstract ClosedCaptionLayout createCaptionLayout(Context context);
170
171    @Override
172    public void setOnChangedListener(OnChangedListener listener) {
173        mListener = listener;
174    }
175
176    @Override
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
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
197    public void onAttachedToWindow() {
198        super.onAttachedToWindow();
199
200        manageChangeListener();
201    }
202
203    @Override
204    public void onDetachedFromWindow() {
205        super.onDetachedFromWindow();
206
207        manageChangeListener();
208    }
209
210    @Override
211    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
212        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
213        ((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec);
214    }
215
216    @Override
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
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 */
260class 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
307    Cea608CCParser(DisplayListener listener) {
308        mListener = listener;
309    }
310
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 {
331        void onDisplayChanged(SpannableStringBuilder[] styledTexts);
332        CaptionStyle getCaptionStyle();
333    }
334
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
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
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
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
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
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
490    private void updateDisplay() {
491        if (mListener != null) {
492            CaptionStyle captionStyle = mListener.getCaptionStyle();
493            mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
494        }
495    }
496
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
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
540        StyleCode(int style, int color) {
541            mStyle = style;
542            mColor = color;
543        }
544
545        boolean isItalics() {
546            return (mStyle & STYLE_ITALICS) != 0;
547        }
548
549        boolean isUnderline() {
550            return (mStyle & STYLE_UNDERLINE) != 0;
551        }
552
553        int getColor() {
554            return mColor;
555        }
556
557        @Override
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
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
602        PAC(int row, int col, int style, int color) {
603            super(style, color);
604            mRow = row;
605            mCol = col;
606        }
607
608        boolean isIndentPAC() {
609            return (mCol >= 0);
610        }
611
612        int getRow() {
613            return mRow;
614        }
615
616        int getCol() {
617            return mCol;
618        }
619
620        @Override
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
636        public MutableBackgroundColorSpan(int color) {
637            mColor = color;
638        }
639
640        public void setBackgroundColor(int color) {
641            mColor = color;
642        }
643
644        public int getBackgroundColor() {
645            return mColor;
646        }
647
648        @Override
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
664        CCLineBuilder(String str) {
665            mDisplayChars = new StringBuilder(str);
666            mMidRowStyles = new StyleCode[mDisplayChars.length()];
667            mPACStyles = new StyleCode[mDisplayChars.length()];
668        }
669
670        void setCharAt(int index, char ch) {
671            mDisplayChars.setCharAt(index, ch);
672            mMidRowStyles[index] = null;
673        }
674
675        void setMidRowAt(int index, StyleCode m) {
676            mDisplayChars.setCharAt(index, ' ');
677            mMidRowStyles[index] = m;
678        }
679
680        void setPACAt(int index, PAC pac) {
681            mPACStyles[index] = pac;
682        }
683
684        char charAt(int index) {
685            return mDisplayChars.charAt(index);
686        }
687
688        int length() {
689            return mDisplayChars.length();
690        }
691
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
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
764        CCMemory() {
765            char[] blank = new char[MAX_COLS + 2];
766            Arrays.fill(blank, TS);
767            mBlankLine = new String(blank);
768        }
769
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
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
793        void tab(int tabs) {
794            moveCursorByCol(tabs);
795        }
796
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
810        void cr() {
811            moveCursorTo(mRow + 1, 1);
812        }
813
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
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
841        void writeMidRowCode(StyleCode m) {
842            getLineBuffer(mRow).setMidRowAt(mCol, m);
843            moveCursorByCol(1);
844        }
845
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
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
864        private static int clamp(int x, int min, int max) {
865            return x < min ? min : (x > max ? max : x);
866        }
867
868        private void moveCursorTo(int row, int col) {
869            mRow = clamp(row, 1, MAX_ROWS);
870            mCol = clamp(col, 1, MAX_COLS);
871        }
872
873        private void moveCursorToRow(int row) {
874            mRow = clamp(row, 1, MAX_ROWS);
875        }
876
877        private void moveCursorByCol(int col) {
878            mCol = clamp(mCol + col, 1, MAX_COLS);
879        }
880
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
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
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
1044        CCData(byte type, byte data1, byte data2) {
1045            mType = type;
1046            mData1 = data1;
1047            mData2 = data2;
1048        }
1049
1050        int getCtrlCode() {
1051            if ((mData1 == 0x14 || mData1 == 0x1c)
1052                    && mData2 >= 0x20 && mData2 <= 0x2f) {
1053                return mData2;
1054            }
1055            return INVALID;
1056        }
1057
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
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
1077        int getTabOffset() {
1078            if ((mData1 == 0x17 || mData1 == 0x1f)
1079                    && mData2 >= 0x21 && mData2 <= 0x23) {
1080                return mData2 & 0x3;
1081            }
1082            return 0;
1083        }
1084
1085        boolean isDisplayableChar() {
1086            return isBasicChar() || isSpecialChar() || isExtendedChar();
1087        }
1088
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
1103        private String ctrlCodeToString(int ctrlCode) {
1104            return mCtrlCodeMap[ctrlCode - 0x20];
1105        }
1106
1107        private boolean isBasicChar() {
1108            return mData1 >= 0x20 && mData1 <= 0x7f;
1109        }
1110
1111        private boolean isSpecialChar() {
1112            return ((mData1 == 0x11 || mData1 == 0x19)
1113                    && mData2 >= 0x30 && mData2 <= 0x3f);
1114        }
1115
1116        private boolean isExtendedChar() {
1117            return ((mData1 == 0x12 || mData1 == 0x1A
1118                    || mData1 == 0x13 || mData1 == 0x1B)
1119                    && mData2 >= 0x20 && mData2 <= 0x3f);
1120        }
1121
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
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
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
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
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 */
1221class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener {
1222    private static final Rect mTextBounds = new Rect();
1223    private static final String mDummyText = "1234567890123456789012345678901234";
1224
1225    public Cea608CCWidget(Context context) {
1226        this(context, null);
1227    }
1228
1229    public Cea608CCWidget(Context context, AttributeSet attrs) {
1230        this(context, attrs, 0);
1231    }
1232
1233    public Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) {
1234        this(context, attrs, defStyle, 0);
1235    }
1236
1237    public Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr,
1238            int defStyleRes) {
1239        super(context, attrs, defStyleAttr, defStyleRes);
1240    }
1241
1242    @Override
1243    public ClosedCaptionLayout createCaptionLayout(Context context) {
1244        return new CCLayout(context);
1245    }
1246
1247    @Override
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
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
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
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
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
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
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
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
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
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
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
1440        public void setFontScale(float fontScale) {
1441            // Ignores the font scale changes of the system wide CC preference.
1442        }
1443
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
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
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