aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/tuner/cc/CaptionWindowLayout.java
blob: 6f42b5064180faffca8d63a743c648e6ac1c08b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tv.tuner.cc;

import android.content.Context;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.CaptioningManager;
import android.view.accessibility.CaptioningManager.CaptionStyle;
import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
import android.widget.RelativeLayout;

import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.SubtitleView;
import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr;
import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
import com.android.tv.tuner.layout.ScaledLayout;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Layout which renders a caption window of CEA-708B. It contains a {@link SubtitleView} that
 * takes care of displaying the actual cc text.
 */
public class CaptionWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener {
    private static final String TAG = "CaptionWindowLayout";
    private static final boolean DEBUG = false;

    private static final float PROPORTION_PEN_SIZE_SMALL = .75f;
    private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f;

    // The following values indicates the maximum cell number of a window.
    private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99;
    private static final int ANCHOR_VERTICAL_MAX = 74;
    private static final int ANCHOR_HORIZONTAL_4_3_MAX = 159;
    private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209;

    // The following values indicates a gravity of a window.
    private static final int ANCHOR_MODE_DIVIDER = 3;
    private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0;
    private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1;
    private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2;
    private static final int ANCHOR_VERTICAL_MODE_TOP = 0;
    private static final int ANCHOR_VERTICAL_MODE_CENTER = 1;
    private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2;

    private static final int US_MAX_COLUMN_COUNT_16_9 = 42;
    private static final int US_MAX_COLUMN_COUNT_4_3 = 32;
    private static final int KR_MAX_COLUMN_COUNT_16_9 = 52;
    private static final int KR_MAX_COLUMN_COUNT_4_3 = 40;
    private static final int MAX_ROW_COUNT = 15;

    private static final String KOR_ALPHABET =
            new String("\uAC00".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
    private static final float WIDE_SCREEN_ASPECT_RATIO_THRESHOLD = 1.6f;

    private CaptionLayout mCaptionLayout;
    private CaptionStyleCompat mCaptionStyleCompat;

    // TODO: Replace SubtitleView to {@link com.google.android.exoplayer.text.SubtitleLayout}.
    private final SubtitleView mSubtitleView;
    private int mRowLimit = 0;
    private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
    private final List<CharacterStyle> mCharacterStyles = new ArrayList<>();
    private int mCaptionWindowId;
    private int mCurrentTextRow = -1;
    private float mFontScale;
    private float mTextSize;
    private String mWidestChar;
    private int mLastCaptionLayoutWidth;
    private int mLastCaptionLayoutHeight;
    private int mWindowJustify;
    private int mPrintDirection;

    private class SystemWideCaptioningChangeListener extends CaptioningChangeListener {
        @Override
        public void onUserStyleChanged(CaptionStyle userStyle) {
            mCaptionStyleCompat = CaptionStyleCompat.createFromCaptionStyle(userStyle);
            mSubtitleView.setStyle(mCaptionStyleCompat);
            updateWidestChar();
        }

        @Override
        public void onFontScaleChanged(float fontScale) {
            mFontScale = fontScale;
            updateTextSize();
        }
    }

    public CaptionWindowLayout(Context context) {
        this(context, null);
    }

    public CaptionWindowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CaptionWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // Add a subtitle view to the layout.
        mSubtitleView = new SubtitleView(context);
        LayoutParams params = new RelativeLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        addView(mSubtitleView, params);

        // Set the system wide cc preferences to the subtitle view.
        CaptioningManager captioningManager =
                (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
        mFontScale = captioningManager.getFontScale();
        mCaptionStyleCompat =
                CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
        mSubtitleView.setStyle(mCaptionStyleCompat);
        mSubtitleView.setText("");
        captioningManager.addCaptioningChangeListener(new SystemWideCaptioningChangeListener());
        updateWidestChar();
    }

    public int getCaptionWindowId() {
        return mCaptionWindowId;
    }

    public void setCaptionWindowId(int captionWindowId) {
        mCaptionWindowId = captionWindowId;
    }

    public void clear() {
        clearText();
        hide();
    }

    public void show() {
        setVisibility(View.VISIBLE);
        requestLayout();
    }

    public void hide() {
        setVisibility(View.INVISIBLE);
        requestLayout();
    }

    public void setPenAttr(CaptionPenAttr penAttr) {
        mCharacterStyles.clear();
        if (penAttr.italic) {
            mCharacterStyles.add(new StyleSpan(Typeface.ITALIC));
        }
        if (penAttr.underline) {
            mCharacterStyles.add(new UnderlineSpan());
        }
        switch (penAttr.penSize) {
            case CaptionPenAttr.PEN_SIZE_SMALL:
                mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL));
                break;
            case CaptionPenAttr.PEN_SIZE_LARGE:
                mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE));
                break;
        }
        switch (penAttr.penOffset) {
            case CaptionPenAttr.OFFSET_SUBSCRIPT:
                mCharacterStyles.add(new SubscriptSpan());
                break;
            case CaptionPenAttr.OFFSET_SUPERSCRIPT:
                mCharacterStyles.add(new SuperscriptSpan());
                break;
        }
    }

    public void setPenColor(CaptionPenColor penColor) {
        // TODO: apply pen colors or skip this and use the style of system wide cc style as is.
    }

    public void setPenLocation(int row, int column) {
        // TODO: change the location of pen when window's justify isn't left.
        // According to the CEA708B spec 8.7, setPenLocation means set the pen cursor within
        // window's text buffer. When row > mCurrentTextRow, we add "\n" to make the cursor locate
        // at row. Adding white space to make cursor locate at column.
        if (mWindowJustify == CaptionWindowAttr.JUSTIFY_LEFT) {
            if (mCurrentTextRow >= 0) {
                for (int r = mCurrentTextRow; r < row; ++r) {
                    appendText("\n");
                }
                if (mCurrentTextRow <= row) {
                    for (int i = 0; i < column; ++i) {
                        appendText(" ");
                    }
                }
            }
        }
        mCurrentTextRow = row;
    }

    public void setWindowAttr(CaptionWindowAttr windowAttr) {
        // TODO: apply window attrs or skip this and use the style of system wide cc style as is.
        mWindowJustify = windowAttr.justify;
        mPrintDirection = windowAttr.printDirection;
    }

    public void sendBuffer(String buffer) {
        appendText(buffer);
    }

    public void sendControl(char control) {
        // TODO: there are a bunch of ASCII-style control codes.
    }

    /**
     * This method places the window on a given CaptionLayout along with the anchor of the window.
     * <p>
     * According to CEA-708B, the anchor id indicates the gravity of the window as the follows.
     * For example, A value 7 of a anchor id says that a window is align with its parent bottom and
     * is located at the center horizontally of its parent.
     * </p>
     * <h4>Anchor id and the gravity of a window</h4>
     * <table>
     *     <tr>
     *         <th>GRAVITY</th>
     *         <th>LEFT</th>
     *         <th>CENTER_HORIZONTAL</th>
     *         <th>RIGHT</th>
     *     </tr>
     *     <tr>
     *         <th>TOP</th>
     *         <td>0</td>
     *         <td>1</td>
     *         <td>2</td>
     *     </tr>
     *     <tr>
     *         <th>CENTER_VERTICAL</th>
     *         <td>3</td>
     *         <td>4</td>
     *         <td>5</td>
     *     </tr>
     *     <tr>
     *         <th>BOTTOM</th>
     *         <td>6</td>
     *         <td>7</td>
     *         <td>8</td>
     *     </tr>
     * </table>
     * <p>
     * In order to handle the gravity of a window, there are two steps. First, set the size of the
     * window. Since the window will be positioned at {@link ScaledLayout}, the size factors are
     * determined in a ratio. Second, set the gravity of the window. {@link CaptionWindowLayout} is
     * inherited from {@link RelativeLayout}. Hence, we could set the gravity of its child view,
     * {@link SubtitleView}.
     * </p>
     * <p>
     * The gravity of the window is also related to its size. When it should be pushed to a one of
     * the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a boundary
     * of the window. When it should be pushed in the horizontal/vertical center of its container,
     * the horizontal/vertical center point of the window should be the same as the anchor point.
     * </p>
     *
     * @param captionLayout a given {@link CaptionLayout}, which contains a safe title area
     * @param captionWindow a given {@link CaptionWindow}, which stores the construction info of the
     *                      window
     */
    public void initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow) {
        if (DEBUG) {
            Log.d(TAG, "initWindow with "
                    + (captionLayout != null ? captionLayout.getCaptionTrack() : null));
        }
        if (mCaptionLayout != captionLayout) {
            if (mCaptionLayout != null) {
                mCaptionLayout.removeOnLayoutChangeListener(this);
            }
            mCaptionLayout = captionLayout;
            mCaptionLayout.addOnLayoutChangeListener(this);
            updateWidestChar();
        }

        // Both anchor vertical and horizontal indicates the position cell number of the window.
        float scaleRow = (float) captionWindow.anchorVertical / (captionWindow.relativePositioning
                ? ANCHOR_RELATIVE_POSITIONING_MAX : ANCHOR_VERTICAL_MAX);
        float scaleCol = (float) captionWindow.anchorHorizontal /
                (captionWindow.relativePositioning ? ANCHOR_RELATIVE_POSITIONING_MAX
                        : (isWideAspectRatio()
                                ? ANCHOR_HORIZONTAL_16_9_MAX : ANCHOR_HORIZONTAL_4_3_MAX));

        // The range of scaleRow/Col need to be verified to be in [0, 1].
        // Otherwise a {@link RuntimeException} will be raised in {@link ScaledLayout}.
        if (scaleRow < 0 || scaleRow > 1) {
            Log.i(TAG, "The vertical position of the anchor point should be at the range of 0 and 1"
                    + " but " + scaleRow);
            scaleRow = Math.max(0, Math.min(scaleRow, 1));
        }
        if (scaleCol < 0 || scaleCol > 1) {
            Log.i(TAG, "The horizontal position of the anchor point should be at the range of 0 and"
                    + " 1 but " + scaleCol);
            scaleCol = Math.max(0, Math.min(scaleCol, 1));
        }
        int gravity = Gravity.CENTER;
        int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER;
        int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER;
        float scaleStartRow = 0;
        float scaleEndRow = 1;
        float scaleStartCol = 0;
        float scaleEndCol = 1;
        switch (horizontalMode) {
            case ANCHOR_HORIZONTAL_MODE_LEFT:
                gravity = Gravity.LEFT;
                mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL);
                scaleStartCol = scaleCol;
                break;
            case ANCHOR_HORIZONTAL_MODE_CENTER:
                float gap = Math.min(1 - scaleCol, scaleCol);

                // Since all TV sets use left text alignment instead of center text alignment
                // for this case, we follow the industry convention if possible.
                int columnCount = captionWindow.columnCount + 1;
                if (isKoreanLanguageTrack()) {
                    columnCount /= 2;
                }
                columnCount = Math.min(getScreenColumnCount(), columnCount);
                StringBuilder widestTextBuilder = new StringBuilder();
                for (int i = 0; i < columnCount; ++i) {
                    widestTextBuilder.append(mWidestChar);
                }
                Paint paint = new Paint();
                paint.setTypeface(mCaptionStyleCompat.typeface);
                paint.setTextSize(mTextSize);
                float maxWindowWidth = paint.measureText(widestTextBuilder.toString());
                float halfMaxWidthScale = mCaptionLayout.getWidth() > 0
                        ? maxWindowWidth / 2.0f / (mCaptionLayout.getWidth() * 0.8f) : 0.0f;
                if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) {
                    // Calculate the expected max window size based on the column count of the
                    // caption window multiplied by average alphabets char width, then align the
                    // left side of the window with the left side of the expected max window.
                    gravity = Gravity.LEFT;
                    mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL);
                    scaleStartCol = scaleCol - halfMaxWidthScale;
                    scaleEndCol = 1.0f;
                } else {
                    // The gap will be the minimum distance value of the distances from both
                    // horizontal end points to the anchor point.
                    // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2].
                    // If scaleCol > 0.5, the range of scaleCol is [(1 - the anchor point) * 2, 1].
                    // The anchor point is located at the horizontal center of the window in both
                    // cases.
                    gravity = Gravity.CENTER_HORIZONTAL;
                    mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER);
                    scaleStartCol = scaleCol - gap;
                    scaleEndCol = scaleCol + gap;
                }
                break;
            case ANCHOR_HORIZONTAL_MODE_RIGHT:
                gravity = Gravity.RIGHT;
                mSubtitleView.setTextAlignment(Alignment.ALIGN_OPPOSITE);
                scaleEndCol = scaleCol;
                break;
        }
        switch (verticalMode) {
            case ANCHOR_VERTICAL_MODE_TOP:
                gravity |= Gravity.TOP;
                scaleStartRow = scaleRow;
                break;
            case ANCHOR_VERTICAL_MODE_CENTER:
                gravity |= Gravity.CENTER_VERTICAL;

                // See the above comment.
                float gap = Math.min(1 - scaleRow, scaleRow);
                scaleStartRow = scaleRow - gap;
                scaleEndRow = scaleRow + gap;
                break;
            case ANCHOR_VERTICAL_MODE_BOTTOM:
                gravity |= Gravity.BOTTOM;
                scaleEndRow = scaleRow;
                break;
        }
        mCaptionLayout.addOrUpdateViewToSafeTitleArea(this, new ScaledLayout
                .ScaledLayoutParams(scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
        setCaptionWindowId(captionWindow.id);
        setRowLimit(captionWindow.rowCount);
        setGravity(gravity);
        setWindowStyle(captionWindow.windowStyle);
        if (mWindowJustify == CaptionWindowAttr.JUSTIFY_CENTER) {
            mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER);
        }
        if (captionWindow.visible) {
            show();
        } else {
            hide();
        }
    }

    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
            int oldTop, int oldRight, int oldBottom) {
        int width = right - left;
        int height = bottom - top;
        if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) {
            mLastCaptionLayoutWidth = width;
            mLastCaptionLayoutHeight = height;
            updateTextSize();
        }
    }

    private boolean isKoreanLanguageTrack() {
        return mCaptionLayout != null && mCaptionLayout.getCaptionTrack() != null
                && mCaptionLayout.getCaptionTrack().language != null
                && "KOR".compareToIgnoreCase(mCaptionLayout.getCaptionTrack().language) == 0;
    }

    private boolean isWideAspectRatio() {
        return mCaptionLayout != null && mCaptionLayout.getCaptionTrack() != null
                && mCaptionLayout.getCaptionTrack().wideAspectRatio;
    }

    private void updateWidestChar() {
        if (isKoreanLanguageTrack()) {
            mWidestChar = KOR_ALPHABET;
        } else {
            Paint paint = new Paint();
            paint.setTypeface(mCaptionStyleCompat.typeface);
            Charset latin1 = Charset.forName("ISO-8859-1");
            float widestCharWidth = 0f;
            for (int i = 0; i < 256; ++i) {
                String ch = new String(new byte[]{(byte) i}, latin1);
                float charWidth = paint.measureText(ch);
                if (widestCharWidth < charWidth) {
                    widestCharWidth = charWidth;
                    mWidestChar = ch;
                }
            }
        }
        updateTextSize();
    }

    private void updateTextSize() {
        if (mCaptionLayout == null) return;

        // Calculate text size based on the max window size.
        StringBuilder widestTextBuilder = new StringBuilder();
        int screenColumnCount = getScreenColumnCount();
        for (int i = 0; i < screenColumnCount; ++i) {
            widestTextBuilder.append(mWidestChar);
        }
        String widestText = widestTextBuilder.toString();
        Paint paint = new Paint();
        paint.setTypeface(mCaptionStyleCompat.typeface);
        float startFontSize = 0f;
        float endFontSize = 255f;
        Rect boundRect = new Rect();
        while (startFontSize < endFontSize) {
            float testTextSize = (startFontSize + endFontSize) / 2f;
            paint.setTextSize(testTextSize);
            float width = paint.measureText(widestText);
            paint.getTextBounds(widestText, 0, widestText.length(), boundRect);
            float height = boundRect.height() + width - boundRect.width();
            // According to CEA-708B Section 9.13, the height of standard font size shouldn't taller
            // than 1/15 of the height of the safe-title area, and the width shouldn't wider than
            // 1/{@code getScreenColumnCount()} of the width of the safe-title area.
            if (mCaptionLayout.getWidth() * 0.8f > width
                    && mCaptionLayout.getHeight() * 0.8f / MAX_ROW_COUNT > height) {
                startFontSize = testTextSize + 0.01f;
            } else {
                endFontSize = testTextSize - 0.01f;
            }
        }
        mTextSize = endFontSize * mFontScale;
        paint.setTextSize(mTextSize);
        float whiteSpaceWidth = paint.measureText(" ");
        mSubtitleView.setWhiteSpaceWidth(whiteSpaceWidth);
        mSubtitleView.setTextSize(mTextSize);
    }

    private int getScreenColumnCount() {
        float screenAspectRatio = (float) mCaptionLayout.getWidth() / mCaptionLayout.getHeight();
        boolean isWideAspectRationScreen = screenAspectRatio > WIDE_SCREEN_ASPECT_RATIO_THRESHOLD;
        if (isKoreanLanguageTrack()) {
            // Each korean character consumes two slots.
            if (isWideAspectRationScreen || isWideAspectRatio()) {
                return KR_MAX_COLUMN_COUNT_16_9 / 2;
            } else {
                return KR_MAX_COLUMN_COUNT_4_3 / 2;
            }
        } else {
            if (isWideAspectRationScreen || isWideAspectRatio()) {
                return US_MAX_COLUMN_COUNT_16_9;
            } else {
                return US_MAX_COLUMN_COUNT_4_3;
            }
        }
    }

    public void removeFromCaptionView() {
        if (mCaptionLayout != null) {
            mCaptionLayout.removeViewFromSafeTitleArea(this);
            mCaptionLayout.removeOnLayoutChangeListener(this);
            mCaptionLayout = null;
        }
    }

    public void setText(String text) {
        updateText(text, false);
    }

    public void appendText(String text) {
        updateText(text, true);
    }

    public void clearText() {
        mBuilder.clear();
        mSubtitleView.setText("");
    }

    private void updateText(String text, boolean appended) {
        if (!appended) {
            mBuilder.clear();
        }
        if (text != null && text.length() > 0) {
            int length = mBuilder.length();
            mBuilder.append(text);
            for (CharacterStyle characterStyle : mCharacterStyles) {
                mBuilder.setSpan(characterStyle, length, mBuilder.length(),
                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
        String[] lines = TextUtils.split(mBuilder.toString(), "\n");

        // Truncate text not to exceed the row limit.
        // Plus one here since the range of the rows is [0, mRowLimit].
        int startRow = Math.max(0, lines.length - (mRowLimit + 1));
        String truncatedText = TextUtils.join("\n", Arrays.copyOfRange(
                lines, startRow, lines.length));
        mBuilder.delete(0, mBuilder.length() - truncatedText.length());
        mCurrentTextRow = lines.length - startRow - 1;

        // Trim the buffer first then set text to {@link SubtitleView}.
        int start = 0, last = mBuilder.length() - 1;
        int end = last;
        while ((start <= end) && (mBuilder.charAt(start) <= ' ')) {
            ++start;
        }
        while (start - 1 >= 0 && start <= end && mBuilder.charAt(start - 1) != '\n') {
            --start;
        }
        while ((end >= start) && (mBuilder.charAt(end) <= ' ')) {
            --end;
        }
        if (start == 0 && end == last) {
            mSubtitleView.setPrefixSpaces(getPrefixSpaces(mBuilder));
            mSubtitleView.setText(mBuilder);
        } else {
            SpannableStringBuilder trim = new SpannableStringBuilder();
            trim.append(mBuilder);
            if (end < last) {
                trim.delete(end + 1, last + 1);
            }
            if (start > 0) {
                trim.delete(0, start);
            }
            mSubtitleView.setPrefixSpaces(getPrefixSpaces(trim));
            mSubtitleView.setText(trim);
        }
    }

    private static ArrayList<Integer> getPrefixSpaces(SpannableStringBuilder builder) {
        ArrayList<Integer> prefixSpaces = new ArrayList<>();
        String[] lines = TextUtils.split(builder.toString(), "\n");
        for (String line : lines) {
            int start = 0;
            while (start < line.length() && line.charAt(start) <= ' ') {
                start++;
            }
            prefixSpaces.add(start);
        }
        return prefixSpaces;
    }

    public void setRowLimit(int rowLimit) {
        if (rowLimit < 0) {
            throw new IllegalArgumentException("A rowLimit should have a positive number");
        }
        mRowLimit = rowLimit;
    }

    private void setWindowStyle(int windowStyle) {
        // TODO: Set other attributes of window style. Like fill opacity and fill color.
        switch (windowStyle) {
            case 2:
                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
                break;
            case 3:
                mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER;
                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
                break;
            case 4:
                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
                break;
            case 5:
                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
                break;
            case 6:
                mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER;
                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
                break;
            case 7:
                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
                mPrintDirection = CaptionWindowAttr.PRINT_TOP_TO_BOTTOM;
                break;
            default:
                if (windowStyle != 0 && windowStyle != 1) {
                    Log.e(TAG, "Error predefined window style:" + windowStyle);
                }
                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
                break;
        }
    }
}