1/*
2 * Copyright (C) 2015 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.tv.tuner.cc;
18
19import android.content.Context;
20import android.graphics.Paint;
21import android.graphics.Rect;
22import android.graphics.Typeface;
23import android.text.Layout.Alignment;
24import android.text.SpannableStringBuilder;
25import android.text.Spanned;
26import android.text.TextUtils;
27import android.text.style.CharacterStyle;
28import android.text.style.RelativeSizeSpan;
29import android.text.style.StyleSpan;
30import android.text.style.SubscriptSpan;
31import android.text.style.SuperscriptSpan;
32import android.text.style.UnderlineSpan;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.view.Gravity;
36import android.view.View;
37import android.view.ViewGroup;
38import android.view.accessibility.CaptioningManager;
39import android.view.accessibility.CaptioningManager.CaptionStyle;
40import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
41import android.widget.RelativeLayout;
42import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr;
43import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
44import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
45import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
46import com.android.tv.tuner.exoplayer.text.SubtitleView;
47import com.android.tv.tuner.layout.ScaledLayout;
48import com.google.android.exoplayer.text.CaptionStyleCompat;
49import java.nio.charset.Charset;
50import java.nio.charset.StandardCharsets;
51import java.util.ArrayList;
52import java.util.Arrays;
53import java.util.List;
54
55/**
56 * Layout which renders a caption window of CEA-708B. It contains a {@link SubtitleView} that takes
57 * care of displaying the actual cc text.
58 */
59public class CaptionWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener {
60    private static final String TAG = "CaptionWindowLayout";
61    private static final boolean DEBUG = false;
62
63    private static final float PROPORTION_PEN_SIZE_SMALL = .75f;
64    private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f;
65
66    // The following values indicates the maximum cell number of a window.
67    private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99;
68    private static final int ANCHOR_VERTICAL_MAX = 74;
69    private static final int ANCHOR_HORIZONTAL_4_3_MAX = 159;
70    private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209;
71
72    // The following values indicates a gravity of a window.
73    private static final int ANCHOR_MODE_DIVIDER = 3;
74    private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0;
75    private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1;
76    private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2;
77    private static final int ANCHOR_VERTICAL_MODE_TOP = 0;
78    private static final int ANCHOR_VERTICAL_MODE_CENTER = 1;
79    private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2;
80
81    private static final int US_MAX_COLUMN_COUNT_16_9 = 42;
82    private static final int US_MAX_COLUMN_COUNT_4_3 = 32;
83    private static final int KR_MAX_COLUMN_COUNT_16_9 = 52;
84    private static final int KR_MAX_COLUMN_COUNT_4_3 = 40;
85    private static final int MAX_ROW_COUNT = 15;
86
87    private static final String KOR_ALPHABET =
88            new String("\uAC00".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
89    private static final float WIDE_SCREEN_ASPECT_RATIO_THRESHOLD = 1.6f;
90
91    private CaptionLayout mCaptionLayout;
92    private CaptionStyleCompat mCaptionStyleCompat;
93
94    // TODO: Replace SubtitleView to {@link com.google.android.exoplayer.text.SubtitleLayout}.
95    private final SubtitleView mSubtitleView;
96    private int mRowLimit = 0;
97    private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
98    private final List<CharacterStyle> mCharacterStyles = new ArrayList<>();
99    private int mCaptionWindowId;
100    private int mCurrentTextRow = -1;
101    private float mFontScale;
102    private float mTextSize;
103    private String mWidestChar;
104    private int mLastCaptionLayoutWidth;
105    private int mLastCaptionLayoutHeight;
106    private int mWindowJustify;
107    private int mPrintDirection;
108
109    private class SystemWideCaptioningChangeListener extends CaptioningChangeListener {
110        @Override
111        public void onUserStyleChanged(CaptionStyle userStyle) {
112            mCaptionStyleCompat = CaptionStyleCompat.createFromCaptionStyle(userStyle);
113            mSubtitleView.setStyle(mCaptionStyleCompat);
114            updateWidestChar();
115        }
116
117        @Override
118        public void onFontScaleChanged(float fontScale) {
119            mFontScale = fontScale;
120            updateTextSize();
121        }
122    }
123
124    public CaptionWindowLayout(Context context) {
125        this(context, null);
126    }
127
128    public CaptionWindowLayout(Context context, AttributeSet attrs) {
129        this(context, attrs, 0);
130    }
131
132    public CaptionWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
133        super(context, attrs, defStyleAttr);
134
135        // Add a subtitle view to the layout.
136        mSubtitleView = new SubtitleView(context);
137        LayoutParams params =
138                new RelativeLayout.LayoutParams(
139                        ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
140        addView(mSubtitleView, params);
141
142        // Set the system wide cc preferences to the subtitle view.
143        CaptioningManager captioningManager =
144                (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
145        mFontScale = captioningManager.getFontScale();
146        mCaptionStyleCompat =
147                CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
148        mSubtitleView.setStyle(mCaptionStyleCompat);
149        mSubtitleView.setText("");
150        captioningManager.addCaptioningChangeListener(new SystemWideCaptioningChangeListener());
151        updateWidestChar();
152    }
153
154    public int getCaptionWindowId() {
155        return mCaptionWindowId;
156    }
157
158    public void setCaptionWindowId(int captionWindowId) {
159        mCaptionWindowId = captionWindowId;
160    }
161
162    public void clear() {
163        clearText();
164        hide();
165    }
166
167    public void show() {
168        setVisibility(View.VISIBLE);
169        requestLayout();
170    }
171
172    public void hide() {
173        setVisibility(View.INVISIBLE);
174        requestLayout();
175    }
176
177    public void setPenAttr(CaptionPenAttr penAttr) {
178        mCharacterStyles.clear();
179        if (penAttr.italic) {
180            mCharacterStyles.add(new StyleSpan(Typeface.ITALIC));
181        }
182        if (penAttr.underline) {
183            mCharacterStyles.add(new UnderlineSpan());
184        }
185        switch (penAttr.penSize) {
186            case CaptionPenAttr.PEN_SIZE_SMALL:
187                mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL));
188                break;
189            case CaptionPenAttr.PEN_SIZE_LARGE:
190                mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE));
191                break;
192        }
193        switch (penAttr.penOffset) {
194            case CaptionPenAttr.OFFSET_SUBSCRIPT:
195                mCharacterStyles.add(new SubscriptSpan());
196                break;
197            case CaptionPenAttr.OFFSET_SUPERSCRIPT:
198                mCharacterStyles.add(new SuperscriptSpan());
199                break;
200        }
201    }
202
203    public void setPenColor(CaptionPenColor penColor) {
204        // TODO: apply pen colors or skip this and use the style of system wide cc style as is.
205    }
206
207    public void setPenLocation(int row, int column) {
208        // TODO: change the location of pen when window's justify isn't left.
209        // According to the CEA708B spec 8.7, setPenLocation means set the pen cursor within
210        // window's text buffer. When row > mCurrentTextRow, we add "\n" to make the cursor locate
211        // at row. Adding white space to make cursor locate at column.
212        if (mWindowJustify == CaptionWindowAttr.JUSTIFY_LEFT) {
213            if (mCurrentTextRow >= 0) {
214                for (int r = mCurrentTextRow; r < row; ++r) {
215                    appendText("\n");
216                }
217                if (mCurrentTextRow <= row) {
218                    for (int i = 0; i < column; ++i) {
219                        appendText(" ");
220                    }
221                }
222            }
223        }
224        mCurrentTextRow = row;
225    }
226
227    public void setWindowAttr(CaptionWindowAttr windowAttr) {
228        // TODO: apply window attrs or skip this and use the style of system wide cc style as is.
229        mWindowJustify = windowAttr.justify;
230        mPrintDirection = windowAttr.printDirection;
231    }
232
233    public void sendBuffer(String buffer) {
234        appendText(buffer);
235    }
236
237    public void sendControl(char control) {
238        // TODO: there are a bunch of ASCII-style control codes.
239    }
240
241    /**
242     * This method places the window on a given CaptionLayout along with the anchor of the window.
243     *
244     * <p>According to CEA-708B, the anchor id indicates the gravity of the window as the follows.
245     * For example, A value 7 of a anchor id says that a window is align with its parent bottom and
246     * is located at the center horizontally of its parent.
247     *
248     * <h4>Anchor id and the gravity of a window</h4>
249     *
250     * <table>
251     *     <tr>
252     *         <th>GRAVITY</th>
253     *         <th>LEFT</th>
254     *         <th>CENTER_HORIZONTAL</th>
255     *         <th>RIGHT</th>
256     *     </tr>
257     *     <tr>
258     *         <th>TOP</th>
259     *         <td>0</td>
260     *         <td>1</td>
261     *         <td>2</td>
262     *     </tr>
263     *     <tr>
264     *         <th>CENTER_VERTICAL</th>
265     *         <td>3</td>
266     *         <td>4</td>
267     *         <td>5</td>
268     *     </tr>
269     *     <tr>
270     *         <th>BOTTOM</th>
271     *         <td>6</td>
272     *         <td>7</td>
273     *         <td>8</td>
274     *     </tr>
275     * </table>
276     *
277     * <p>In order to handle the gravity of a window, there are two steps. First, set the size of
278     * the window. Since the window will be positioned at {@link ScaledLayout}, the size factors are
279     * determined in a ratio. Second, set the gravity of the window. {@link CaptionWindowLayout} is
280     * inherited from {@link RelativeLayout}. Hence, we could set the gravity of its child view,
281     * {@link SubtitleView}.
282     *
283     * <p>The gravity of the window is also related to its size. When it should be pushed to a one
284     * of the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a
285     * boundary of the window. When it should be pushed in the horizontal/vertical center of its
286     * container, the horizontal/vertical center point of the window should be the same as the
287     * anchor point.
288     *
289     * @param captionLayout a given {@link CaptionLayout}, which contains a safe title area
290     * @param captionWindow a given {@link CaptionWindow}, which stores the construction info of the
291     *     window
292     */
293    public void initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow) {
294        if (DEBUG) {
295            Log.d(
296                    TAG,
297                    "initWindow with "
298                            + (captionLayout != null ? captionLayout.getCaptionTrack() : null));
299        }
300        if (mCaptionLayout != captionLayout) {
301            if (mCaptionLayout != null) {
302                mCaptionLayout.removeOnLayoutChangeListener(this);
303            }
304            mCaptionLayout = captionLayout;
305            mCaptionLayout.addOnLayoutChangeListener(this);
306            updateWidestChar();
307        }
308
309        // Both anchor vertical and horizontal indicates the position cell number of the window.
310        float scaleRow =
311                (float) captionWindow.anchorVertical
312                        / (captionWindow.relativePositioning
313                                ? ANCHOR_RELATIVE_POSITIONING_MAX
314                                : ANCHOR_VERTICAL_MAX);
315        float scaleCol =
316                (float) captionWindow.anchorHorizontal
317                        / (captionWindow.relativePositioning
318                                ? ANCHOR_RELATIVE_POSITIONING_MAX
319                                : (isWideAspectRatio()
320                                        ? ANCHOR_HORIZONTAL_16_9_MAX
321                                        : ANCHOR_HORIZONTAL_4_3_MAX));
322
323        // The range of scaleRow/Col need to be verified to be in [0, 1].
324        // Otherwise a {@link RuntimeException} will be raised in {@link ScaledLayout}.
325        if (scaleRow < 0 || scaleRow > 1) {
326            Log.i(
327                    TAG,
328                    "The vertical position of the anchor point should be at the range of 0 and 1"
329                            + " but "
330                            + scaleRow);
331            scaleRow = Math.max(0, Math.min(scaleRow, 1));
332        }
333        if (scaleCol < 0 || scaleCol > 1) {
334            Log.i(
335                    TAG,
336                    "The horizontal position of the anchor point should be at the range of 0 and"
337                            + " 1 but "
338                            + scaleCol);
339            scaleCol = Math.max(0, Math.min(scaleCol, 1));
340        }
341        int gravity = Gravity.CENTER;
342        int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER;
343        int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER;
344        float scaleStartRow = 0;
345        float scaleEndRow = 1;
346        float scaleStartCol = 0;
347        float scaleEndCol = 1;
348        switch (horizontalMode) {
349            case ANCHOR_HORIZONTAL_MODE_LEFT:
350                gravity = Gravity.LEFT;
351                mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL);
352                scaleStartCol = scaleCol;
353                break;
354            case ANCHOR_HORIZONTAL_MODE_CENTER:
355                float gap = Math.min(1 - scaleCol, scaleCol);
356
357                // Since all TV sets use left text alignment instead of center text alignment
358                // for this case, we follow the industry convention if possible.
359                int columnCount = captionWindow.columnCount + 1;
360                if (isKoreanLanguageTrack()) {
361                    columnCount /= 2;
362                }
363                columnCount = Math.min(getScreenColumnCount(), columnCount);
364                StringBuilder widestTextBuilder = new StringBuilder();
365                for (int i = 0; i < columnCount; ++i) {
366                    widestTextBuilder.append(mWidestChar);
367                }
368                Paint paint = new Paint();
369                paint.setTypeface(mCaptionStyleCompat.typeface);
370                paint.setTextSize(mTextSize);
371                float maxWindowWidth = paint.measureText(widestTextBuilder.toString());
372                float halfMaxWidthScale =
373                        mCaptionLayout.getWidth() > 0
374                                ? maxWindowWidth / 2.0f / (mCaptionLayout.getWidth() * 0.8f)
375                                : 0.0f;
376                if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) {
377                    // Calculate the expected max window size based on the column count of the
378                    // caption window multiplied by average alphabets char width, then align the
379                    // left side of the window with the left side of the expected max window.
380                    gravity = Gravity.LEFT;
381                    mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL);
382                    scaleStartCol = scaleCol - halfMaxWidthScale;
383                    scaleEndCol = 1.0f;
384                } else {
385                    // The gap will be the minimum distance value of the distances from both
386                    // horizontal end points to the anchor point.
387                    // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2].
388                    // If scaleCol > 0.5, the range of scaleCol is [(1 - the anchor point) * 2, 1].
389                    // The anchor point is located at the horizontal center of the window in both
390                    // cases.
391                    gravity = Gravity.CENTER_HORIZONTAL;
392                    mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER);
393                    scaleStartCol = scaleCol - gap;
394                    scaleEndCol = scaleCol + gap;
395                }
396                break;
397            case ANCHOR_HORIZONTAL_MODE_RIGHT:
398                gravity = Gravity.RIGHT;
399                mSubtitleView.setTextAlignment(Alignment.ALIGN_OPPOSITE);
400                scaleEndCol = scaleCol;
401                break;
402        }
403        switch (verticalMode) {
404            case ANCHOR_VERTICAL_MODE_TOP:
405                gravity |= Gravity.TOP;
406                scaleStartRow = scaleRow;
407                break;
408            case ANCHOR_VERTICAL_MODE_CENTER:
409                gravity |= Gravity.CENTER_VERTICAL;
410
411                // See the above comment.
412                float gap = Math.min(1 - scaleRow, scaleRow);
413                scaleStartRow = scaleRow - gap;
414                scaleEndRow = scaleRow + gap;
415                break;
416            case ANCHOR_VERTICAL_MODE_BOTTOM:
417                gravity |= Gravity.BOTTOM;
418                scaleEndRow = scaleRow;
419                break;
420        }
421        mCaptionLayout.addOrUpdateViewToSafeTitleArea(
422                this,
423                new ScaledLayout.ScaledLayoutParams(
424                        scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
425        setCaptionWindowId(captionWindow.id);
426        setRowLimit(captionWindow.rowCount);
427        setGravity(gravity);
428        setWindowStyle(captionWindow.windowStyle);
429        if (mWindowJustify == CaptionWindowAttr.JUSTIFY_CENTER) {
430            mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER);
431        }
432        if (captionWindow.visible) {
433            show();
434        } else {
435            hide();
436        }
437    }
438
439    @Override
440    public void onLayoutChange(
441            View v,
442            int left,
443            int top,
444            int right,
445            int bottom,
446            int oldLeft,
447            int oldTop,
448            int oldRight,
449            int oldBottom) {
450        int width = right - left;
451        int height = bottom - top;
452        if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) {
453            mLastCaptionLayoutWidth = width;
454            mLastCaptionLayoutHeight = height;
455            updateTextSize();
456        }
457    }
458
459    private boolean isKoreanLanguageTrack() {
460        return mCaptionLayout != null
461                && mCaptionLayout.getCaptionTrack() != null
462                && mCaptionLayout.getCaptionTrack().language != null
463                && "KOR".compareToIgnoreCase(mCaptionLayout.getCaptionTrack().language) == 0;
464    }
465
466    private boolean isWideAspectRatio() {
467        return mCaptionLayout != null
468                && mCaptionLayout.getCaptionTrack() != null
469                && mCaptionLayout.getCaptionTrack().wideAspectRatio;
470    }
471
472    private void updateWidestChar() {
473        if (isKoreanLanguageTrack()) {
474            mWidestChar = KOR_ALPHABET;
475        } else {
476            Paint paint = new Paint();
477            paint.setTypeface(mCaptionStyleCompat.typeface);
478            Charset latin1 = Charset.forName("ISO-8859-1");
479            float widestCharWidth = 0f;
480            for (int i = 0; i < 256; ++i) {
481                String ch = new String(new byte[] {(byte) i}, latin1);
482                float charWidth = paint.measureText(ch);
483                if (widestCharWidth < charWidth) {
484                    widestCharWidth = charWidth;
485                    mWidestChar = ch;
486                }
487            }
488        }
489        updateTextSize();
490    }
491
492    private void updateTextSize() {
493        if (mCaptionLayout == null) return;
494
495        // Calculate text size based on the max window size.
496        StringBuilder widestTextBuilder = new StringBuilder();
497        int screenColumnCount = getScreenColumnCount();
498        for (int i = 0; i < screenColumnCount; ++i) {
499            widestTextBuilder.append(mWidestChar);
500        }
501        String widestText = widestTextBuilder.toString();
502        Paint paint = new Paint();
503        paint.setTypeface(mCaptionStyleCompat.typeface);
504        float startFontSize = 0f;
505        float endFontSize = 255f;
506        Rect boundRect = new Rect();
507        while (startFontSize < endFontSize) {
508            float testTextSize = (startFontSize + endFontSize) / 2f;
509            paint.setTextSize(testTextSize);
510            float width = paint.measureText(widestText);
511            paint.getTextBounds(widestText, 0, widestText.length(), boundRect);
512            float height = boundRect.height() + width - boundRect.width();
513            // According to CEA-708B Section 9.13, the height of standard font size shouldn't taller
514            // than 1/15 of the height of the safe-title area, and the width shouldn't wider than
515            // 1/{@code getScreenColumnCount()} of the width of the safe-title area.
516            if (mCaptionLayout.getWidth() * 0.8f > width
517                    && mCaptionLayout.getHeight() * 0.8f / MAX_ROW_COUNT > height) {
518                startFontSize = testTextSize + 0.01f;
519            } else {
520                endFontSize = testTextSize - 0.01f;
521            }
522        }
523        mTextSize = endFontSize * mFontScale;
524        paint.setTextSize(mTextSize);
525        float whiteSpaceWidth = paint.measureText(" ");
526        mSubtitleView.setWhiteSpaceWidth(whiteSpaceWidth);
527        mSubtitleView.setTextSize(mTextSize);
528    }
529
530    private int getScreenColumnCount() {
531        float screenAspectRatio = (float) mCaptionLayout.getWidth() / mCaptionLayout.getHeight();
532        boolean isWideAspectRationScreen = screenAspectRatio > WIDE_SCREEN_ASPECT_RATIO_THRESHOLD;
533        if (isKoreanLanguageTrack()) {
534            // Each korean character consumes two slots.
535            if (isWideAspectRationScreen || isWideAspectRatio()) {
536                return KR_MAX_COLUMN_COUNT_16_9 / 2;
537            } else {
538                return KR_MAX_COLUMN_COUNT_4_3 / 2;
539            }
540        } else {
541            if (isWideAspectRationScreen || isWideAspectRatio()) {
542                return US_MAX_COLUMN_COUNT_16_9;
543            } else {
544                return US_MAX_COLUMN_COUNT_4_3;
545            }
546        }
547    }
548
549    public void removeFromCaptionView() {
550        if (mCaptionLayout != null) {
551            mCaptionLayout.removeViewFromSafeTitleArea(this);
552            mCaptionLayout.removeOnLayoutChangeListener(this);
553            mCaptionLayout = null;
554        }
555    }
556
557    public void setText(String text) {
558        updateText(text, false);
559    }
560
561    public void appendText(String text) {
562        updateText(text, true);
563    }
564
565    public void clearText() {
566        mBuilder.clear();
567        mSubtitleView.setText("");
568    }
569
570    private void updateText(String text, boolean appended) {
571        if (!appended) {
572            mBuilder.clear();
573        }
574        if (text != null && text.length() > 0) {
575            int length = mBuilder.length();
576            mBuilder.append(text);
577            for (CharacterStyle characterStyle : mCharacterStyles) {
578                mBuilder.setSpan(
579                        characterStyle,
580                        length,
581                        mBuilder.length(),
582                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
583            }
584        }
585        String[] lines = TextUtils.split(mBuilder.toString(), "\n");
586
587        // Truncate text not to exceed the row limit.
588        // Plus one here since the range of the rows is [0, mRowLimit].
589        int startRow = Math.max(0, lines.length - (mRowLimit + 1));
590        String truncatedText =
591                TextUtils.join("\n", Arrays.copyOfRange(lines, startRow, lines.length));
592        mBuilder.delete(0, mBuilder.length() - truncatedText.length());
593        mCurrentTextRow = lines.length - startRow - 1;
594
595        // Trim the buffer first then set text to {@link SubtitleView}.
596        int start = 0, last = mBuilder.length() - 1;
597        int end = last;
598        while ((start <= end) && (mBuilder.charAt(start) <= ' ')) {
599            ++start;
600        }
601        while (start - 1 >= 0 && start <= end && mBuilder.charAt(start - 1) != '\n') {
602            --start;
603        }
604        while ((end >= start) && (mBuilder.charAt(end) <= ' ')) {
605            --end;
606        }
607        if (start == 0 && end == last) {
608            mSubtitleView.setPrefixSpaces(getPrefixSpaces(mBuilder));
609            mSubtitleView.setText(mBuilder);
610        } else {
611            SpannableStringBuilder trim = new SpannableStringBuilder();
612            trim.append(mBuilder);
613            if (end < last) {
614                trim.delete(end + 1, last + 1);
615            }
616            if (start > 0) {
617                trim.delete(0, start);
618            }
619            mSubtitleView.setPrefixSpaces(getPrefixSpaces(trim));
620            mSubtitleView.setText(trim);
621        }
622    }
623
624    private static ArrayList<Integer> getPrefixSpaces(SpannableStringBuilder builder) {
625        ArrayList<Integer> prefixSpaces = new ArrayList<>();
626        String[] lines = TextUtils.split(builder.toString(), "\n");
627        for (String line : lines) {
628            int start = 0;
629            while (start < line.length() && line.charAt(start) <= ' ') {
630                start++;
631            }
632            prefixSpaces.add(start);
633        }
634        return prefixSpaces;
635    }
636
637    public void setRowLimit(int rowLimit) {
638        if (rowLimit < 0) {
639            throw new IllegalArgumentException("A rowLimit should have a positive number");
640        }
641        mRowLimit = rowLimit;
642    }
643
644    private void setWindowStyle(int windowStyle) {
645        // TODO: Set other attributes of window style. Like fill opacity and fill color.
646        switch (windowStyle) {
647            case 2:
648                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
649                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
650                break;
651            case 3:
652                mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER;
653                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
654                break;
655            case 4:
656                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
657                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
658                break;
659            case 5:
660                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
661                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
662                break;
663            case 6:
664                mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER;
665                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
666                break;
667            case 7:
668                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
669                mPrintDirection = CaptionWindowAttr.PRINT_TOP_TO_BOTTOM;
670                break;
671            default:
672                if (windowStyle != 0 && windowStyle != 1) {
673                    Log.e(TAG, "Error predefined window style:" + windowStyle);
674                }
675                mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT;
676                mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT;
677                break;
678        }
679    }
680}
681