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