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