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