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