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