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