1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media; 18 19 import android.content.Context; 20 import android.graphics.Canvas; 21 import android.graphics.Color; 22 import android.graphics.Paint; 23 import android.graphics.Rect; 24 import android.graphics.Typeface; 25 import android.os.Handler; 26 import android.os.Message; 27 import android.text.SpannableStringBuilder; 28 import android.text.Spanned; 29 import android.text.style.CharacterStyle; 30 import android.text.style.RelativeSizeSpan; 31 import android.text.style.StyleSpan; 32 import android.text.style.SubscriptSpan; 33 import android.text.style.SuperscriptSpan; 34 import android.text.style.UnderlineSpan; 35 import android.util.AttributeSet; 36 import android.text.Layout.Alignment; 37 import android.util.Log; 38 import android.text.TextUtils; 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.widget.RelativeLayout; 45 import android.widget.TextView; 46 47 import java.io.UnsupportedEncodingException; 48 import java.nio.charset.Charset; 49 import java.nio.charset.StandardCharsets; 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Comparator; 53 import java.util.List; 54 import java.util.Vector; 55 56 import com.android.internal.widget.SubtitleView; 57 58 /** @hide */ 59 public class Cea708CaptionRenderer extends SubtitleController.Renderer { 60 private final Context mContext; 61 private Cea708CCWidget mCCWidget; 62 Cea708CaptionRenderer(Context context)63 public Cea708CaptionRenderer(Context context) { 64 mContext = context; 65 } 66 67 @Override supports(MediaFormat format)68 public boolean supports(MediaFormat format) { 69 if (format.containsKey(MediaFormat.KEY_MIME)) { 70 String mimeType = format.getString(MediaFormat.KEY_MIME); 71 return MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_708.equals(mimeType); 72 } 73 return false; 74 } 75 76 @Override createTrack(MediaFormat format)77 public SubtitleTrack createTrack(MediaFormat format) { 78 String mimeType = format.getString(MediaFormat.KEY_MIME); 79 if (MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_708.equals(mimeType)) { 80 if (mCCWidget == null) { 81 mCCWidget = new Cea708CCWidget(mContext); 82 } 83 return new Cea708CaptionTrack(mCCWidget, format); 84 } 85 throw new RuntimeException("No matching format: " + format.toString()); 86 } 87 } 88 89 /** @hide */ 90 class Cea708CaptionTrack extends SubtitleTrack { 91 private final Cea708CCParser mCCParser; 92 private final Cea708CCWidget mRenderingWidget; 93 Cea708CaptionTrack(Cea708CCWidget renderingWidget, MediaFormat format)94 Cea708CaptionTrack(Cea708CCWidget renderingWidget, MediaFormat format) { 95 super(format); 96 97 mRenderingWidget = renderingWidget; 98 mCCParser = new Cea708CCParser(mRenderingWidget); 99 } 100 101 @Override onData(byte[] data, boolean eos, long runID)102 public void onData(byte[] data, boolean eos, long runID) { 103 mCCParser.parse(data); 104 } 105 106 @Override getRenderingWidget()107 public RenderingWidget getRenderingWidget() { 108 return mRenderingWidget; 109 } 110 111 @Override updateView(Vector<Cue> activeCues)112 public void updateView(Vector<Cue> activeCues) { 113 // Overriding with NO-OP, CC rendering by-passes this 114 } 115 } 116 117 /** 118 * @hide 119 * 120 * A class for parsing CEA-708, which is the standard for closed captioning for ATSC DTV. 121 * 122 * <p>ATSC DTV closed caption data are carried on picture user data of video streams. 123 * This class starts to parse from picture user data payload, so extraction process of user_data 124 * from video streams is up to outside of this code. 125 * 126 * <p>There are 4 steps to decode user_data to provide closed caption services. Step 1 and 2 are 127 * done in NuPlayer and libstagefright. 128 * 129 * <h3>Step 1. user_data -> CcPacket</h3> 130 * 131 * <p>First, user_data consists of cc_data packets, which are 3-byte segments. Here, CcPacket is a 132 * collection of cc_data packets in a frame along with same presentation timestamp. Because cc_data 133 * packets must be reassembled in the frame display order, CcPackets are reordered. 134 * 135 * <h3>Step 2. CcPacket -> DTVCC packet</h3> 136 * 137 * <p>Each cc_data packet has a one byte for declaring a type of itself and data validity, and the 138 * subsequent two bytes for input data of a DTVCC packet. There are 4 types for cc_data packet. 139 * We're interested in DTVCC_PACKET_START(type 3) and DTVCC_PACKET_DATA(type 2). Each DTVCC packet 140 * begins with DTVCC_PACKET_START(type 3) and the following cc_data packets which has 141 * DTVCC_PACKET_DATA(type 2) are appended into the DTVCC packet being assembled. 142 * 143 * <h3>Step 3. DTVCC packet -> Service Blocks</h3> 144 * 145 * <p>A DTVCC packet consists of multiple service blocks. Each service block represents a caption 146 * track and has a service number, which ranges from 1 to 63, that denotes caption track identity. 147 * In here, we listen at most one chosen caption track by service number. Otherwise, just skip the 148 * other service blocks. 149 * 150 * <h3>Step 4. Interpreting Service Block Data ({@link #parseServiceBlockData}, {@code parseXX}, 151 * and {@link #parseExt1} methods)</h3> 152 * 153 * <p>Service block data is actual caption stream. it looks similar to telnet. It uses most parts of 154 * ASCII table and consists of specially defined commands and some ASCII control codes which work 155 * in a behavior slightly different from their original purpose. ASCII control codes and caption 156 * commands are explicit instructions that control the state of a closed caption service and the 157 * other ASCII and text codes are implicit instructions that send their characters to buffer. 158 * 159 * <p>There are 4 main code groups and 4 extended code groups. Both the range of code groups are the 160 * same as the range of a byte. 161 * 162 * <p>4 main code groups: C0, C1, G0, G1 163 * <br>4 extended code groups: C2, C3, G2, G3 164 * 165 * <p>Each code group has its own handle method. For example, {@link #parseC0} handles C0 code group 166 * and so on. And {@link #parseServiceBlockData} method maps a stream on the main code groups while 167 * {@link #parseExt1} method maps on the extended code groups. 168 * 169 * <p>The main code groups: 170 * <ul> 171 * <li>C0 - contains modified ASCII control codes. It is not intended by CEA-708 but Korea TTA 172 * standard for ATSC CC uses P16 character heavily, which is unclear entity in CEA-708 doc, 173 * even for the alphanumeric characters instead of ASCII characters.</li> 174 * <li>C1 - contains the caption commands. There are 3 categories of a caption command.</li> 175 * <ul> 176 * <li>Window commands: The window commands control a caption window which is addressable area being 177 * with in the Safe title area. (CWX, CLW, DSW, HDW, TGW, DLW, SWA, DFX)</li> 178 * <li>Pen commands: Th pen commands control text style and location. (SPA, SPC, SPL)</li> 179 * <li>Job commands: The job commands make a delay and recover from the delay. (DLY, DLC, RST)</li> 180 * </ul> 181 * <li>G0 - same as printable ASCII character set except music note character.</li> 182 * <li>G1 - same as ISO 8859-1 Latin 1 character set.</li> 183 * </ul> 184 * <p>Most of the extended code groups are being skipped. 185 * 186 */ 187 class Cea708CCParser { 188 private static final String TAG = "Cea708CCParser"; 189 private static final boolean DEBUG = false; 190 191 private static final String MUSIC_NOTE_CHAR = new String( 192 "\u266B".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); 193 194 private final StringBuffer mBuffer = new StringBuffer(); 195 private int mCommand = 0; 196 197 // Assign a dummy listener in order to avoid null checks. 198 private DisplayListener mListener = new DisplayListener() { 199 @Override 200 public void emitEvent(CaptionEvent event) { 201 // do nothing 202 } 203 }; 204 205 /** 206 * {@link Cea708Parser} emits caption event of three different types. 207 * {@link DisplayListener#emitEvent} is invoked with the parameter 208 * {@link CaptionEvent} to pass all the results to an observer of the decoding process . 209 * 210 * <p>{@link CaptionEvent#type} determines the type of the result and 211 * {@link CaptionEvent#obj} contains the output value of a caption event. 212 * The observer must do the casting to the corresponding type. 213 * 214 * <ul><li>{@code CAPTION_EMIT_TYPE_BUFFER}: Passes a caption text buffer to a observer. 215 * {@code obj} must be of {@link String}.</li> 216 * 217 * <li>{@code CAPTION_EMIT_TYPE_CONTROL}: Passes a caption character control code to a observer. 218 * {@code obj} must be of {@link Character}.</li> 219 * 220 * <li>{@code CAPTION_EMIT_TYPE_CLEAR_COMMAND}: Passes a clear command to a observer. 221 * {@code obj} must be {@code NULL}.</li></ul> 222 */ 223 public static final int CAPTION_EMIT_TYPE_BUFFER = 1; 224 public static final int CAPTION_EMIT_TYPE_CONTROL = 2; 225 public static final int CAPTION_EMIT_TYPE_COMMAND_CWX = 3; 226 public static final int CAPTION_EMIT_TYPE_COMMAND_CLW = 4; 227 public static final int CAPTION_EMIT_TYPE_COMMAND_DSW = 5; 228 public static final int CAPTION_EMIT_TYPE_COMMAND_HDW = 6; 229 public static final int CAPTION_EMIT_TYPE_COMMAND_TGW = 7; 230 public static final int CAPTION_EMIT_TYPE_COMMAND_DLW = 8; 231 public static final int CAPTION_EMIT_TYPE_COMMAND_DLY = 9; 232 public static final int CAPTION_EMIT_TYPE_COMMAND_DLC = 10; 233 public static final int CAPTION_EMIT_TYPE_COMMAND_RST = 11; 234 public static final int CAPTION_EMIT_TYPE_COMMAND_SPA = 12; 235 public static final int CAPTION_EMIT_TYPE_COMMAND_SPC = 13; 236 public static final int CAPTION_EMIT_TYPE_COMMAND_SPL = 14; 237 public static final int CAPTION_EMIT_TYPE_COMMAND_SWA = 15; 238 public static final int CAPTION_EMIT_TYPE_COMMAND_DFX = 16; 239 Cea708CCParser(DisplayListener listener)240 Cea708CCParser(DisplayListener listener) { 241 if (listener != null) { 242 mListener = listener; 243 } 244 } 245 246 interface DisplayListener { emitEvent(CaptionEvent event)247 void emitEvent(CaptionEvent event); 248 } 249 emitCaptionEvent(CaptionEvent captionEvent)250 private void emitCaptionEvent(CaptionEvent captionEvent) { 251 // Emit the existing string buffer before a new event is arrived. 252 emitCaptionBuffer(); 253 mListener.emitEvent(captionEvent); 254 } 255 emitCaptionBuffer()256 private void emitCaptionBuffer() { 257 if (mBuffer.length() > 0) { 258 mListener.emitEvent(new CaptionEvent(CAPTION_EMIT_TYPE_BUFFER, mBuffer.toString())); 259 mBuffer.setLength(0); 260 } 261 } 262 263 // Step 3. DTVCC packet -> Service Blocks (parseDtvCcPacket method) parse(byte[] data)264 public void parse(byte[] data) { 265 // From this point, starts to read DTVCC coding layer. 266 // First, identify code groups, which is defined in CEA-708B Section 7.1. 267 int pos = 0; 268 while (pos < data.length) { 269 pos = parseServiceBlockData(data, pos); 270 } 271 272 // Emit the buffer after reading codes. 273 emitCaptionBuffer(); 274 } 275 276 // Step 4. Main code groups parseServiceBlockData(byte[] data, int pos)277 private int parseServiceBlockData(byte[] data, int pos) { 278 // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6. 279 mCommand = data[pos] & 0xff; 280 ++pos; 281 if (mCommand == Const.CODE_C0_EXT1) { 282 if (DEBUG) { 283 Log.d(TAG, String.format("parseServiceBlockData EXT1 %x", mCommand)); 284 } 285 pos = parseExt1(data, pos); 286 } else if (mCommand >= Const.CODE_C0_RANGE_START 287 && mCommand <= Const.CODE_C0_RANGE_END) { 288 if (DEBUG) { 289 Log.d(TAG, String.format("parseServiceBlockData C0 %x", mCommand)); 290 } 291 pos = parseC0(data, pos); 292 } else if (mCommand >= Const.CODE_C1_RANGE_START 293 && mCommand <= Const.CODE_C1_RANGE_END) { 294 if (DEBUG) { 295 Log.d(TAG, String.format("parseServiceBlockData C1 %x", mCommand)); 296 } 297 pos = parseC1(data, pos); 298 } else if (mCommand >= Const.CODE_G0_RANGE_START 299 && mCommand <= Const.CODE_G0_RANGE_END) { 300 if (DEBUG) { 301 Log.d(TAG, String.format("parseServiceBlockData G0 %x", mCommand)); 302 } 303 pos = parseG0(data, pos); 304 } else if (mCommand >= Const.CODE_G1_RANGE_START 305 && mCommand <= Const.CODE_G1_RANGE_END) { 306 if (DEBUG) { 307 Log.d(TAG, String.format("parseServiceBlockData G1 %x", mCommand)); 308 } 309 pos = parseG1(data, pos); 310 } 311 return pos; 312 } 313 parseC0(byte[] data, int pos)314 private int parseC0(byte[] data, int pos) { 315 // For the details of C0 code group, see CEA-708B Section 7.4.1. 316 // CL Group: C0 Subset of ASCII Control codes 317 if (mCommand >= Const.CODE_C0_SKIP2_RANGE_START 318 && mCommand <= Const.CODE_C0_SKIP2_RANGE_END) { 319 if (mCommand == Const.CODE_C0_P16) { 320 // P16 escapes next two bytes for the large character maps.(no standard rule) 321 // For Korea broadcasting, express whole letters by using this. 322 try { 323 if (data[pos] == 0) { 324 mBuffer.append((char) data[pos + 1]); 325 } else { 326 String value = new String(Arrays.copyOfRange(data, pos, pos + 2), "EUC-KR"); 327 mBuffer.append(value); 328 } 329 } catch (UnsupportedEncodingException e) { 330 Log.e(TAG, "P16 Code - Could not find supported encoding", e); 331 } 332 } 333 pos += 2; 334 } else if (mCommand >= Const.CODE_C0_SKIP1_RANGE_START 335 && mCommand <= Const.CODE_C0_SKIP1_RANGE_END) { 336 ++pos; 337 } else { 338 // NUL, BS, FF, CR interpreted as they are in ASCII control codes. 339 // HCR moves the pen location to th beginning of the current line and deletes contents. 340 // FF clears the screen and moves the pen location to (0,0). 341 // ETX is the NULL command which is used to flush text to the current window when no 342 // other command is pending. 343 switch (mCommand) { 344 case Const.CODE_C0_NUL: 345 break; 346 case Const.CODE_C0_ETX: 347 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); 348 break; 349 case Const.CODE_C0_BS: 350 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); 351 break; 352 case Const.CODE_C0_FF: 353 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); 354 break; 355 case Const.CODE_C0_CR: 356 mBuffer.append('\n'); 357 break; 358 case Const.CODE_C0_HCR: 359 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); 360 break; 361 default: 362 break; 363 } 364 } 365 return pos; 366 } 367 parseC1(byte[] data, int pos)368 private int parseC1(byte[] data, int pos) { 369 // For the details of C1 code group, see CEA-708B Section 8.10. 370 // CR Group: C1 Caption Control Codes 371 switch (mCommand) { 372 case Const.CODE_C1_CW0: 373 case Const.CODE_C1_CW1: 374 case Const.CODE_C1_CW2: 375 case Const.CODE_C1_CW3: 376 case Const.CODE_C1_CW4: 377 case Const.CODE_C1_CW5: 378 case Const.CODE_C1_CW6: 379 case Const.CODE_C1_CW7: { 380 // SetCurrentWindow0-7 381 int windowId = mCommand - Const.CODE_C1_CW0; 382 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CWX, windowId)); 383 if (DEBUG) { 384 Log.d(TAG, String.format("CaptionCommand CWX windowId: %d", windowId)); 385 } 386 break; 387 } 388 389 case Const.CODE_C1_CLW: { 390 // ClearWindows 391 int windowBitmap = data[pos] & 0xff; 392 ++pos; 393 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CLW, windowBitmap)); 394 if (DEBUG) { 395 Log.d(TAG, String.format("CaptionCommand CLW windowBitmap: %d", windowBitmap)); 396 } 397 break; 398 } 399 400 case Const.CODE_C1_DSW: { 401 // DisplayWindows 402 int windowBitmap = data[pos] & 0xff; 403 ++pos; 404 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DSW, windowBitmap)); 405 if (DEBUG) { 406 Log.d(TAG, String.format("CaptionCommand DSW windowBitmap: %d", windowBitmap)); 407 } 408 break; 409 } 410 411 case Const.CODE_C1_HDW: { 412 // HideWindows 413 int windowBitmap = data[pos] & 0xff; 414 ++pos; 415 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_HDW, windowBitmap)); 416 if (DEBUG) { 417 Log.d(TAG, String.format("CaptionCommand HDW windowBitmap: %d", windowBitmap)); 418 } 419 break; 420 } 421 422 case Const.CODE_C1_TGW: { 423 // ToggleWindows 424 int windowBitmap = data[pos] & 0xff; 425 ++pos; 426 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_TGW, windowBitmap)); 427 if (DEBUG) { 428 Log.d(TAG, String.format("CaptionCommand TGW windowBitmap: %d", windowBitmap)); 429 } 430 break; 431 } 432 433 case Const.CODE_C1_DLW: { 434 // DeleteWindows 435 int windowBitmap = data[pos] & 0xff; 436 ++pos; 437 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLW, windowBitmap)); 438 if (DEBUG) { 439 Log.d(TAG, String.format("CaptionCommand DLW windowBitmap: %d", windowBitmap)); 440 } 441 break; 442 } 443 444 case Const.CODE_C1_DLY: { 445 // Delay 446 int tenthsOfSeconds = data[pos] & 0xff; 447 ++pos; 448 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLY, tenthsOfSeconds)); 449 if (DEBUG) { 450 Log.d(TAG, String.format("CaptionCommand DLY %d tenths of seconds", 451 tenthsOfSeconds)); 452 } 453 break; 454 } 455 case Const.CODE_C1_DLC: { 456 // DelayCancel 457 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLC, null)); 458 if (DEBUG) { 459 Log.d(TAG, "CaptionCommand DLC"); 460 } 461 break; 462 } 463 464 case Const.CODE_C1_RST: { 465 // Reset 466 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_RST, null)); 467 if (DEBUG) { 468 Log.d(TAG, "CaptionCommand RST"); 469 } 470 break; 471 } 472 473 case Const.CODE_C1_SPA: { 474 // SetPenAttributes 475 int textTag = (data[pos] & 0xf0) >> 4; 476 int penSize = data[pos] & 0x03; 477 int penOffset = (data[pos] & 0x0c) >> 2; 478 boolean italic = (data[pos + 1] & 0x80) != 0; 479 boolean underline = (data[pos + 1] & 0x40) != 0; 480 int edgeType = (data[pos + 1] & 0x38) >> 3; 481 int fontTag = data[pos + 1] & 0x7; 482 pos += 2; 483 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPA, 484 new CaptionPenAttr(penSize, penOffset, textTag, fontTag, edgeType, 485 underline, italic))); 486 if (DEBUG) { 487 Log.d(TAG, String.format( 488 "CaptionCommand SPA penSize: %d, penOffset: %d, textTag: %d, " 489 + "fontTag: %d, edgeType: %d, underline: %s, italic: %s", 490 penSize, penOffset, textTag, fontTag, edgeType, underline, italic)); 491 } 492 break; 493 } 494 495 case Const.CODE_C1_SPC: { 496 // SetPenColor 497 int opacity = (data[pos] & 0xc0) >> 6; 498 int red = (data[pos] & 0x30) >> 4; 499 int green = (data[pos] & 0x0c) >> 2; 500 int blue = data[pos] & 0x03; 501 CaptionColor foregroundColor = new CaptionColor(opacity, red, green, blue); 502 ++pos; 503 opacity = (data[pos] & 0xc0) >> 6; 504 red = (data[pos] & 0x30) >> 4; 505 green = (data[pos] & 0x0c) >> 2; 506 blue = data[pos] & 0x03; 507 CaptionColor backgroundColor = new CaptionColor(opacity, red, green, blue); 508 ++pos; 509 red = (data[pos] & 0x30) >> 4; 510 green = (data[pos] & 0x0c) >> 2; 511 blue = data[pos] & 0x03; 512 CaptionColor edgeColor = new CaptionColor( 513 CaptionColor.OPACITY_SOLID, red, green, blue); 514 ++pos; 515 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPC, 516 new CaptionPenColor(foregroundColor, backgroundColor, edgeColor))); 517 if (DEBUG) { 518 Log.d(TAG, String.format( 519 "CaptionCommand SPC foregroundColor %s backgroundColor %s edgeColor %s", 520 foregroundColor, backgroundColor, edgeColor)); 521 } 522 break; 523 } 524 525 case Const.CODE_C1_SPL: { 526 // SetPenLocation 527 // column is normally 0-31 for 4:3 formats, and 0-41 for 16:9 formats 528 int row = data[pos] & 0x0f; 529 int column = data[pos + 1] & 0x3f; 530 pos += 2; 531 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPL, 532 new CaptionPenLocation(row, column))); 533 if (DEBUG) { 534 Log.d(TAG, String.format("CaptionCommand SPL row: %d, column: %d", 535 row, column)); 536 } 537 break; 538 } 539 540 case Const.CODE_C1_SWA: { 541 // SetWindowAttributes 542 int opacity = (data[pos] & 0xc0) >> 6; 543 int red = (data[pos] & 0x30) >> 4; 544 int green = (data[pos] & 0x0c) >> 2; 545 int blue = data[pos] & 0x03; 546 CaptionColor fillColor = new CaptionColor(opacity, red, green, blue); 547 int borderType = (data[pos + 1] & 0xc0) >> 6 | (data[pos + 2] & 0x80) >> 5; 548 red = (data[pos + 1] & 0x30) >> 4; 549 green = (data[pos + 1] & 0x0c) >> 2; 550 blue = data[pos + 1] & 0x03; 551 CaptionColor borderColor = new CaptionColor( 552 CaptionColor.OPACITY_SOLID, red, green, blue); 553 boolean wordWrap = (data[pos + 2] & 0x40) != 0; 554 int printDirection = (data[pos + 2] & 0x30) >> 4; 555 int scrollDirection = (data[pos + 2] & 0x0c) >> 2; 556 int justify = (data[pos + 2] & 0x03); 557 int effectSpeed = (data[pos + 3] & 0xf0) >> 4; 558 int effectDirection = (data[pos + 3] & 0x0c) >> 2; 559 int displayEffect = data[pos + 3] & 0x3; 560 pos += 4; 561 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SWA, 562 new CaptionWindowAttr(fillColor, borderColor, borderType, wordWrap, 563 printDirection, scrollDirection, justify, 564 effectDirection, effectSpeed, displayEffect))); 565 if (DEBUG) { 566 Log.d(TAG, String.format( 567 "CaptionCommand SWA fillColor: %s, borderColor: %s, borderType: %d" 568 + "wordWrap: %s, printDirection: %d, scrollDirection: %d, " 569 + "justify: %s, effectDirection: %d, effectSpeed: %d, " 570 + "displayEffect: %d", 571 fillColor, borderColor, borderType, wordWrap, printDirection, 572 scrollDirection, justify, effectDirection, effectSpeed, displayEffect)); 573 } 574 break; 575 } 576 577 case Const.CODE_C1_DF0: 578 case Const.CODE_C1_DF1: 579 case Const.CODE_C1_DF2: 580 case Const.CODE_C1_DF3: 581 case Const.CODE_C1_DF4: 582 case Const.CODE_C1_DF5: 583 case Const.CODE_C1_DF6: 584 case Const.CODE_C1_DF7: { 585 // DefineWindow0-7 586 int windowId = mCommand - Const.CODE_C1_DF0; 587 boolean visible = (data[pos] & 0x20) != 0; 588 boolean rowLock = (data[pos] & 0x10) != 0; 589 boolean columnLock = (data[pos] & 0x08) != 0; 590 int priority = data[pos] & 0x07; 591 boolean relativePositioning = (data[pos + 1] & 0x80) != 0; 592 int anchorVertical = data[pos + 1] & 0x7f; 593 int anchorHorizontal = data[pos + 2] & 0xff; 594 int anchorId = (data[pos + 3] & 0xf0) >> 4; 595 int rowCount = data[pos + 3] & 0x0f; 596 int columnCount = data[pos + 4] & 0x3f; 597 int windowStyle = (data[pos + 5] & 0x38) >> 3; 598 int penStyle = data[pos + 5] & 0x07; 599 pos += 6; 600 emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DFX, 601 new CaptionWindow(windowId, visible, rowLock, columnLock, priority, 602 relativePositioning, anchorVertical, anchorHorizontal, anchorId, 603 rowCount, columnCount, penStyle, windowStyle))); 604 if (DEBUG) { 605 Log.d(TAG, String.format( 606 "CaptionCommand DFx windowId: %d, priority: %d, columnLock: %s, " 607 + "rowLock: %s, visible: %s, anchorVertical: %d, " 608 + "relativePositioning: %s, anchorHorizontal: %d, " 609 + "rowCount: %d, anchorId: %d, columnCount: %d, penStyle: %d, " 610 + "windowStyle: %d", 611 windowId, priority, columnLock, rowLock, visible, anchorVertical, 612 relativePositioning, anchorHorizontal, rowCount, anchorId, columnCount, 613 penStyle, windowStyle)); 614 } 615 break; 616 } 617 618 default: 619 break; 620 } 621 return pos; 622 } 623 parseG0(byte[] data, int pos)624 private int parseG0(byte[] data, int pos) { 625 // For the details of G0 code group, see CEA-708B Section 7.4.3. 626 // GL Group: G0 Modified version of ANSI X3.4 Printable Character Set (ASCII) 627 if (mCommand == Const.CODE_G0_MUSICNOTE) { 628 // Music note. 629 mBuffer.append(MUSIC_NOTE_CHAR); 630 } else { 631 // Put ASCII code into buffer. 632 mBuffer.append((char) mCommand); 633 } 634 return pos; 635 } 636 parseG1(byte[] data, int pos)637 private int parseG1(byte[] data, int pos) { 638 // For the details of G0 code group, see CEA-708B Section 7.4.4. 639 // GR Group: G1 ISO 8859-1 Latin 1 Characters 640 // Put ASCII Extended character set into buffer. 641 mBuffer.append((char) mCommand); 642 return pos; 643 } 644 645 // Step 4. Extended code groups parseExt1(byte[] data, int pos)646 private int parseExt1(byte[] data, int pos) { 647 // For the details of EXT1 code group, see CEA-708B Section 7.2. 648 mCommand = data[pos] & 0xff; 649 ++pos; 650 if (mCommand >= Const.CODE_C2_RANGE_START 651 && mCommand <= Const.CODE_C2_RANGE_END) { 652 pos = parseC2(data, pos); 653 } else if (mCommand >= Const.CODE_C3_RANGE_START 654 && mCommand <= Const.CODE_C3_RANGE_END) { 655 pos = parseC3(data, pos); 656 } else if (mCommand >= Const.CODE_G2_RANGE_START 657 && mCommand <= Const.CODE_G2_RANGE_END) { 658 pos = parseG2(data, pos); 659 } else if (mCommand >= Const.CODE_G3_RANGE_START 660 && mCommand <= Const.CODE_G3_RANGE_END) { 661 pos = parseG3(data ,pos); 662 } 663 return pos; 664 } 665 parseC2(byte[] data, int pos)666 private int parseC2(byte[] data, int pos) { 667 // For the details of C2 code group, see CEA-708B Section 7.4.7. 668 // Extended Miscellaneous Control Codes 669 // C2 Table : No commands as of CEA-708B. A decoder must skip. 670 if (mCommand >= Const.CODE_C2_SKIP0_RANGE_START 671 && mCommand <= Const.CODE_C2_SKIP0_RANGE_END) { 672 // Do nothing. 673 } else if (mCommand >= Const.CODE_C2_SKIP1_RANGE_START 674 && mCommand <= Const.CODE_C2_SKIP1_RANGE_END) { 675 ++pos; 676 } else if (mCommand >= Const.CODE_C2_SKIP2_RANGE_START 677 && mCommand <= Const.CODE_C2_SKIP2_RANGE_END) { 678 pos += 2; 679 } else if (mCommand >= Const.CODE_C2_SKIP3_RANGE_START 680 && mCommand <= Const.CODE_C2_SKIP3_RANGE_END) { 681 pos += 3; 682 } 683 return pos; 684 } 685 parseC3(byte[] data, int pos)686 private int parseC3(byte[] data, int pos) { 687 // For the details of C3 code group, see CEA-708B Section 7.4.8. 688 // Extended Control Code Set 2 689 // C3 Table : No commands as of CEA-708B. A decoder must skip. 690 if (mCommand >= Const.CODE_C3_SKIP4_RANGE_START 691 && mCommand <= Const.CODE_C3_SKIP4_RANGE_END) { 692 pos += 4; 693 } else if (mCommand >= Const.CODE_C3_SKIP5_RANGE_START 694 && mCommand <= Const.CODE_C3_SKIP5_RANGE_END) { 695 pos += 5; 696 } 697 return pos; 698 } 699 parseG2(byte[] data, int pos)700 private int parseG2(byte[] data, int pos) { 701 // For the details of C3 code group, see CEA-708B Section 7.4.5. 702 // Extended Control Code Set 1(G2 Table) 703 switch (mCommand) { 704 case Const.CODE_G2_TSP: 705 // TODO : TSP is the Transparent space 706 break; 707 case Const.CODE_G2_NBTSP: 708 // TODO : NBTSP is Non-Breaking Transparent Space. 709 break; 710 case Const.CODE_G2_BLK: 711 // TODO : BLK indicates a solid block which fills the entire character block 712 // TODO : with a solid foreground color. 713 break; 714 default: 715 break; 716 } 717 return pos; 718 } 719 parseG3(byte[] data, int pos)720 private int parseG3(byte[] data, int pos) { 721 // For the details of C3 code group, see CEA-708B Section 7.4.6. 722 // Future characters and icons(G3 Table) 723 if (mCommand == Const.CODE_G3_CC) { 724 // TODO : [CC] icon with square corners 725 } 726 727 // Do nothing 728 return pos; 729 } 730 731 /** 732 * @hide 733 * 734 * Collection of CEA-708 structures. 735 */ 736 private static class Const { 737 Const()738 private Const() { 739 } 740 741 // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6. 742 public static final int CODE_C0_RANGE_START = 0x00; 743 public static final int CODE_C0_RANGE_END = 0x1f; 744 public static final int CODE_C1_RANGE_START = 0x80; 745 public static final int CODE_C1_RANGE_END = 0x9f; 746 public static final int CODE_G0_RANGE_START = 0x20; 747 public static final int CODE_G0_RANGE_END = 0x7f; 748 public static final int CODE_G1_RANGE_START = 0xa0; 749 public static final int CODE_G1_RANGE_END = 0xff; 750 public static final int CODE_C2_RANGE_START = 0x00; 751 public static final int CODE_C2_RANGE_END = 0x1f; 752 public static final int CODE_C3_RANGE_START = 0x80; 753 public static final int CODE_C3_RANGE_END = 0x9f; 754 public static final int CODE_G2_RANGE_START = 0x20; 755 public static final int CODE_G2_RANGE_END = 0x7f; 756 public static final int CODE_G3_RANGE_START = 0xa0; 757 public static final int CODE_G3_RANGE_END = 0xff; 758 759 // The following ranges are defined in CEA-708B Section 7.4.1. 760 public static final int CODE_C0_SKIP2_RANGE_START = 0x18; 761 public static final int CODE_C0_SKIP2_RANGE_END = 0x1f; 762 public static final int CODE_C0_SKIP1_RANGE_START = 0x10; 763 public static final int CODE_C0_SKIP1_RANGE_END = 0x17; 764 765 // The following ranges are defined in CEA-708B Section 7.4.7. 766 public static final int CODE_C2_SKIP0_RANGE_START = 0x00; 767 public static final int CODE_C2_SKIP0_RANGE_END = 0x07; 768 public static final int CODE_C2_SKIP1_RANGE_START = 0x08; 769 public static final int CODE_C2_SKIP1_RANGE_END = 0x0f; 770 public static final int CODE_C2_SKIP2_RANGE_START = 0x10; 771 public static final int CODE_C2_SKIP2_RANGE_END = 0x17; 772 public static final int CODE_C2_SKIP3_RANGE_START = 0x18; 773 public static final int CODE_C2_SKIP3_RANGE_END = 0x1f; 774 775 // The following ranges are defined in CEA-708B Section 7.4.8. 776 public static final int CODE_C3_SKIP4_RANGE_START = 0x80; 777 public static final int CODE_C3_SKIP4_RANGE_END = 0x87; 778 public static final int CODE_C3_SKIP5_RANGE_START = 0x88; 779 public static final int CODE_C3_SKIP5_RANGE_END = 0x8f; 780 781 // The following values are the special characters of CEA-708 spec. 782 public static final int CODE_C0_NUL = 0x00; 783 public static final int CODE_C0_ETX = 0x03; 784 public static final int CODE_C0_BS = 0x08; 785 public static final int CODE_C0_FF = 0x0c; 786 public static final int CODE_C0_CR = 0x0d; 787 public static final int CODE_C0_HCR = 0x0e; 788 public static final int CODE_C0_EXT1 = 0x10; 789 public static final int CODE_C0_P16 = 0x18; 790 public static final int CODE_G0_MUSICNOTE = 0x7f; 791 public static final int CODE_G2_TSP = 0x20; 792 public static final int CODE_G2_NBTSP = 0x21; 793 public static final int CODE_G2_BLK = 0x30; 794 public static final int CODE_G3_CC = 0xa0; 795 796 // The following values are the command bits of CEA-708 spec. 797 public static final int CODE_C1_CW0 = 0x80; 798 public static final int CODE_C1_CW1 = 0x81; 799 public static final int CODE_C1_CW2 = 0x82; 800 public static final int CODE_C1_CW3 = 0x83; 801 public static final int CODE_C1_CW4 = 0x84; 802 public static final int CODE_C1_CW5 = 0x85; 803 public static final int CODE_C1_CW6 = 0x86; 804 public static final int CODE_C1_CW7 = 0x87; 805 public static final int CODE_C1_CLW = 0x88; 806 public static final int CODE_C1_DSW = 0x89; 807 public static final int CODE_C1_HDW = 0x8a; 808 public static final int CODE_C1_TGW = 0x8b; 809 public static final int CODE_C1_DLW = 0x8c; 810 public static final int CODE_C1_DLY = 0x8d; 811 public static final int CODE_C1_DLC = 0x8e; 812 public static final int CODE_C1_RST = 0x8f; 813 public static final int CODE_C1_SPA = 0x90; 814 public static final int CODE_C1_SPC = 0x91; 815 public static final int CODE_C1_SPL = 0x92; 816 public static final int CODE_C1_SWA = 0x97; 817 public static final int CODE_C1_DF0 = 0x98; 818 public static final int CODE_C1_DF1 = 0x99; 819 public static final int CODE_C1_DF2 = 0x9a; 820 public static final int CODE_C1_DF3 = 0x9b; 821 public static final int CODE_C1_DF4 = 0x9c; 822 public static final int CODE_C1_DF5 = 0x9d; 823 public static final int CODE_C1_DF6 = 0x9e; 824 public static final int CODE_C1_DF7 = 0x9f; 825 } 826 827 /** 828 * @hide 829 * 830 * CEA-708B-specific color. 831 */ 832 public static class CaptionColor { 833 public static final int OPACITY_SOLID = 0; 834 public static final int OPACITY_FLASH = 1; 835 public static final int OPACITY_TRANSLUCENT = 2; 836 public static final int OPACITY_TRANSPARENT = 3; 837 838 private static final int[] COLOR_MAP = new int[] { 0x00, 0x0f, 0xf0, 0xff }; 839 private static final int[] OPACITY_MAP = new int[] { 0xff, 0xfe, 0x80, 0x00 }; 840 841 public final int opacity; 842 public final int red; 843 public final int green; 844 public final int blue; 845 CaptionColor(int opacity, int red, int green, int blue)846 public CaptionColor(int opacity, int red, int green, int blue) { 847 this.opacity = opacity; 848 this.red = red; 849 this.green = green; 850 this.blue = blue; 851 } 852 getArgbValue()853 public int getArgbValue() { 854 return Color.argb( 855 OPACITY_MAP[opacity], COLOR_MAP[red], COLOR_MAP[green], COLOR_MAP[blue]); 856 } 857 } 858 859 /** 860 * @hide 861 * 862 * Caption event generated by {@link Cea708CCParser}. 863 */ 864 public static class CaptionEvent { 865 public final int type; 866 public final Object obj; 867 CaptionEvent(int type, Object obj)868 public CaptionEvent(int type, Object obj) { 869 this.type = type; 870 this.obj = obj; 871 } 872 } 873 874 /** 875 * @hide 876 * 877 * Pen style information. 878 */ 879 public static class CaptionPenAttr { 880 // Pen sizes 881 public static final int PEN_SIZE_SMALL = 0; 882 public static final int PEN_SIZE_STANDARD = 1; 883 public static final int PEN_SIZE_LARGE = 2; 884 885 // Offsets 886 public static final int OFFSET_SUBSCRIPT = 0; 887 public static final int OFFSET_NORMAL = 1; 888 public static final int OFFSET_SUPERSCRIPT = 2; 889 890 public final int penSize; 891 public final int penOffset; 892 public final int textTag; 893 public final int fontTag; 894 public final int edgeType; 895 public final boolean underline; 896 public final boolean italic; 897 CaptionPenAttr(int penSize, int penOffset, int textTag, int fontTag, int edgeType, boolean underline, boolean italic)898 public CaptionPenAttr(int penSize, int penOffset, int textTag, int fontTag, int edgeType, 899 boolean underline, boolean italic) { 900 this.penSize = penSize; 901 this.penOffset = penOffset; 902 this.textTag = textTag; 903 this.fontTag = fontTag; 904 this.edgeType = edgeType; 905 this.underline = underline; 906 this.italic = italic; 907 } 908 } 909 910 /** 911 * @hide 912 * 913 * {@link CaptionColor} objects that indicate the foreground, background, and edge color of a 914 * pen. 915 */ 916 public static class CaptionPenColor { 917 public final CaptionColor foregroundColor; 918 public final CaptionColor backgroundColor; 919 public final CaptionColor edgeColor; 920 CaptionPenColor(CaptionColor foregroundColor, CaptionColor backgroundColor, CaptionColor edgeColor)921 public CaptionPenColor(CaptionColor foregroundColor, CaptionColor backgroundColor, 922 CaptionColor edgeColor) { 923 this.foregroundColor = foregroundColor; 924 this.backgroundColor = backgroundColor; 925 this.edgeColor = edgeColor; 926 } 927 } 928 929 /** 930 * @hide 931 * 932 * Location information of a pen. 933 */ 934 public static class CaptionPenLocation { 935 public final int row; 936 public final int column; 937 CaptionPenLocation(int row, int column)938 public CaptionPenLocation(int row, int column) { 939 this.row = row; 940 this.column = column; 941 } 942 } 943 944 /** 945 * @hide 946 * 947 * Attributes of a caption window, which is defined in CEA-708B. 948 */ 949 public static class CaptionWindowAttr { 950 public final CaptionColor fillColor; 951 public final CaptionColor borderColor; 952 public final int borderType; 953 public final boolean wordWrap; 954 public final int printDirection; 955 public final int scrollDirection; 956 public final int justify; 957 public final int effectDirection; 958 public final int effectSpeed; 959 public final int displayEffect; 960 CaptionWindowAttr(CaptionColor fillColor, CaptionColor borderColor, int borderType, boolean wordWrap, int printDirection, int scrollDirection, int justify, int effectDirection, int effectSpeed, int displayEffect)961 public CaptionWindowAttr(CaptionColor fillColor, CaptionColor borderColor, int borderType, 962 boolean wordWrap, int printDirection, int scrollDirection, int justify, 963 int effectDirection, 964 int effectSpeed, int displayEffect) { 965 this.fillColor = fillColor; 966 this.borderColor = borderColor; 967 this.borderType = borderType; 968 this.wordWrap = wordWrap; 969 this.printDirection = printDirection; 970 this.scrollDirection = scrollDirection; 971 this.justify = justify; 972 this.effectDirection = effectDirection; 973 this.effectSpeed = effectSpeed; 974 this.displayEffect = displayEffect; 975 } 976 } 977 978 /** 979 * @hide 980 * 981 * Construction information of the caption window of CEA-708B. 982 */ 983 public static class CaptionWindow { 984 public final int id; 985 public final boolean visible; 986 public final boolean rowLock; 987 public final boolean columnLock; 988 public final int priority; 989 public final boolean relativePositioning; 990 public final int anchorVertical; 991 public final int anchorHorizontal; 992 public final int anchorId; 993 public final int rowCount; 994 public final int columnCount; 995 public final int penStyle; 996 public final int windowStyle; 997 CaptionWindow(int id, boolean visible, boolean rowLock, boolean columnLock, int priority, boolean relativePositioning, int anchorVertical, int anchorHorizontal, int anchorId, int rowCount, int columnCount, int penStyle, int windowStyle)998 public CaptionWindow(int id, boolean visible, 999 boolean rowLock, boolean columnLock, int priority, boolean relativePositioning, 1000 int anchorVertical, int anchorHorizontal, int anchorId, 1001 int rowCount, int columnCount, int penStyle, int windowStyle) { 1002 this.id = id; 1003 this.visible = visible; 1004 this.rowLock = rowLock; 1005 this.columnLock = columnLock; 1006 this.priority = priority; 1007 this.relativePositioning = relativePositioning; 1008 this.anchorVertical = anchorVertical; 1009 this.anchorHorizontal = anchorHorizontal; 1010 this.anchorId = anchorId; 1011 this.rowCount = rowCount; 1012 this.columnCount = columnCount; 1013 this.penStyle = penStyle; 1014 this.windowStyle = windowStyle; 1015 } 1016 } 1017 } 1018 1019 /** 1020 * Widget capable of rendering CEA-708 closed captions. 1021 * 1022 * @hide 1023 */ 1024 class Cea708CCWidget extends ClosedCaptionWidget implements Cea708CCParser.DisplayListener { 1025 private final CCHandler mCCHandler; 1026 Cea708CCWidget(Context context)1027 public Cea708CCWidget(Context context) { 1028 this(context, null); 1029 } 1030 Cea708CCWidget(Context context, AttributeSet attrs)1031 public Cea708CCWidget(Context context, AttributeSet attrs) { 1032 this(context, attrs, 0); 1033 } 1034 Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr)1035 public Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr) { 1036 this(context, attrs, defStyleAttr, 0); 1037 } 1038 Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)1039 public Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr, 1040 int defStyleRes) { 1041 super(context, attrs, defStyleAttr, defStyleRes); 1042 1043 mCCHandler = new CCHandler((CCLayout) mClosedCaptionLayout); 1044 } 1045 1046 @Override createCaptionLayout(Context context)1047 public ClosedCaptionLayout createCaptionLayout(Context context) { 1048 return new CCLayout(context); 1049 } 1050 1051 @Override emitEvent(Cea708CCParser.CaptionEvent event)1052 public void emitEvent(Cea708CCParser.CaptionEvent event) { 1053 mCCHandler.processCaptionEvent(event); 1054 1055 setSize(getWidth(), getHeight()); 1056 1057 if (mListener != null) { 1058 mListener.onChanged(this); 1059 } 1060 } 1061 1062 @Override onDraw(Canvas canvas)1063 public void onDraw(Canvas canvas) { 1064 super.onDraw(canvas); 1065 ((ViewGroup) mClosedCaptionLayout).draw(canvas); 1066 } 1067 1068 /** 1069 * @hide 1070 * 1071 * A layout that scales its children using the given percentage value. 1072 */ 1073 static class ScaledLayout extends ViewGroup { 1074 private static final String TAG = "ScaledLayout"; 1075 private static final boolean DEBUG = false; 1076 private static final Comparator<Rect> mRectTopLeftSorter = new Comparator<Rect>() { 1077 @Override 1078 public int compare(Rect lhs, Rect rhs) { 1079 if (lhs.top != rhs.top) { 1080 return lhs.top - rhs.top; 1081 } else { 1082 return lhs.left - rhs.left; 1083 } 1084 } 1085 }; 1086 1087 private Rect[] mRectArray; 1088 ScaledLayout(Context context)1089 public ScaledLayout(Context context) { 1090 super(context); 1091 } 1092 1093 /** 1094 * @hide 1095 * 1096 * ScaledLayoutParams stores the four scale factors. 1097 * <br> 1098 * Vertical coordinate system: (scaleStartRow * 100) % ~ (scaleEndRow * 100) % 1099 * Horizontal coordinate system: (scaleStartCol * 100) % ~ (scaleEndCol * 100) % 1100 * <br> 1101 * In XML, for example, 1102 * <pre> 1103 * {@code 1104 * <View 1105 * app:layout_scaleStartRow="0.1" 1106 * app:layout_scaleEndRow="0.5" 1107 * app:layout_scaleStartCol="0.4" 1108 * app:layout_scaleEndCol="1" /> 1109 * } 1110 * </pre> 1111 */ 1112 static class ScaledLayoutParams extends ViewGroup.LayoutParams { 1113 public static final float SCALE_UNSPECIFIED = -1; 1114 public float scaleStartRow; 1115 public float scaleEndRow; 1116 public float scaleStartCol; 1117 public float scaleEndCol; 1118 ScaledLayoutParams(float scaleStartRow, float scaleEndRow, float scaleStartCol, float scaleEndCol)1119 public ScaledLayoutParams(float scaleStartRow, float scaleEndRow, 1120 float scaleStartCol, float scaleEndCol) { 1121 super(MATCH_PARENT, MATCH_PARENT); 1122 this.scaleStartRow = scaleStartRow; 1123 this.scaleEndRow = scaleEndRow; 1124 this.scaleStartCol = scaleStartCol; 1125 this.scaleEndCol = scaleEndCol; 1126 } 1127 ScaledLayoutParams(Context context, AttributeSet attrs)1128 public ScaledLayoutParams(Context context, AttributeSet attrs) { 1129 super(MATCH_PARENT, MATCH_PARENT); 1130 } 1131 } 1132 1133 @Override generateLayoutParams(AttributeSet attrs)1134 public LayoutParams generateLayoutParams(AttributeSet attrs) { 1135 return new ScaledLayoutParams(getContext(), attrs); 1136 } 1137 1138 @Override checkLayoutParams(LayoutParams p)1139 protected boolean checkLayoutParams(LayoutParams p) { 1140 return (p instanceof ScaledLayoutParams); 1141 } 1142 1143 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)1144 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1145 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 1146 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 1147 int width = widthSpecSize - getPaddingLeft() - getPaddingRight(); 1148 int height = heightSpecSize - getPaddingTop() - getPaddingBottom(); 1149 if (DEBUG) { 1150 Log.d(TAG, String.format("onMeasure width: %d, height: %d", width, height)); 1151 } 1152 int count = getChildCount(); 1153 mRectArray = new Rect[count]; 1154 for (int i = 0; i < count; ++i) { 1155 View child = getChildAt(i); 1156 ViewGroup.LayoutParams params = child.getLayoutParams(); 1157 float scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol; 1158 if (!(params instanceof ScaledLayoutParams)) { 1159 throw new RuntimeException( 1160 "A child of ScaledLayout cannot have the UNSPECIFIED scale factors"); 1161 } 1162 scaleStartRow = ((ScaledLayoutParams) params).scaleStartRow; 1163 scaleEndRow = ((ScaledLayoutParams) params).scaleEndRow; 1164 scaleStartCol = ((ScaledLayoutParams) params).scaleStartCol; 1165 scaleEndCol = ((ScaledLayoutParams) params).scaleEndCol; 1166 if (scaleStartRow < 0 || scaleStartRow > 1) { 1167 throw new RuntimeException("A child of ScaledLayout should have a range of " 1168 + "scaleStartRow between 0 and 1"); 1169 } 1170 if (scaleEndRow < scaleStartRow || scaleStartRow > 1) { 1171 throw new RuntimeException("A child of ScaledLayout should have a range of " 1172 + "scaleEndRow between scaleStartRow and 1"); 1173 } 1174 if (scaleEndCol < 0 || scaleEndCol > 1) { 1175 throw new RuntimeException("A child of ScaledLayout should have a range of " 1176 + "scaleStartCol between 0 and 1"); 1177 } 1178 if (scaleEndCol < scaleStartCol || scaleEndCol > 1) { 1179 throw new RuntimeException("A child of ScaledLayout should have a range of " 1180 + "scaleEndCol between scaleStartCol and 1"); 1181 } 1182 if (DEBUG) { 1183 Log.d(TAG, String.format("onMeasure child scaleStartRow: %f scaleEndRow: %f " 1184 + "scaleStartCol: %f scaleEndCol: %f", 1185 scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol)); 1186 } 1187 mRectArray[i] = new Rect((int) (scaleStartCol * width), (int) (scaleStartRow 1188 * height), (int) (scaleEndCol * width), (int) (scaleEndRow * height)); 1189 int childWidthSpec = MeasureSpec.makeMeasureSpec( 1190 (int) (width * (scaleEndCol - scaleStartCol)), MeasureSpec.EXACTLY); 1191 int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1192 child.measure(childWidthSpec, childHeightSpec); 1193 1194 // If the height of the measured child view is bigger than the height of the 1195 // calculated region by the given ScaleLayoutParams, the height of the region should 1196 // be increased to fit the size of the child view. 1197 if (child.getMeasuredHeight() > mRectArray[i].height()) { 1198 int overflowedHeight = child.getMeasuredHeight() - mRectArray[i].height(); 1199 overflowedHeight = (overflowedHeight + 1) / 2; 1200 mRectArray[i].bottom += overflowedHeight; 1201 mRectArray[i].top -= overflowedHeight; 1202 if (mRectArray[i].top < 0) { 1203 mRectArray[i].bottom -= mRectArray[i].top; 1204 mRectArray[i].top = 0; 1205 } 1206 if (mRectArray[i].bottom > height) { 1207 mRectArray[i].top -= mRectArray[i].bottom - height; 1208 mRectArray[i].bottom = height; 1209 } 1210 } 1211 childHeightSpec = MeasureSpec.makeMeasureSpec( 1212 (int) (height * (scaleEndRow - scaleStartRow)), MeasureSpec.EXACTLY); 1213 child.measure(childWidthSpec, childHeightSpec); 1214 } 1215 1216 // Avoid overlapping rectangles. 1217 // Step 1. Sort rectangles by position (top-left). 1218 int visibleRectCount = 0; 1219 int[] visibleRectGroup = new int[count]; 1220 Rect[] visibleRectArray = new Rect[count]; 1221 for (int i = 0; i < count; ++i) { 1222 if (getChildAt(i).getVisibility() == View.VISIBLE) { 1223 visibleRectGroup[visibleRectCount] = visibleRectCount; 1224 visibleRectArray[visibleRectCount] = mRectArray[i]; 1225 ++visibleRectCount; 1226 } 1227 } 1228 Arrays.sort(visibleRectArray, 0, visibleRectCount, mRectTopLeftSorter); 1229 1230 // Step 2. Move down if there are overlapping rectangles. 1231 for (int i = 0; i < visibleRectCount - 1; ++i) { 1232 for (int j = i + 1; j < visibleRectCount; ++j) { 1233 if (Rect.intersects(visibleRectArray[i], visibleRectArray[j])) { 1234 visibleRectGroup[j] = visibleRectGroup[i]; 1235 visibleRectArray[j].set(visibleRectArray[j].left, 1236 visibleRectArray[i].bottom, 1237 visibleRectArray[j].right, 1238 visibleRectArray[i].bottom + visibleRectArray[j].height()); 1239 } 1240 } 1241 } 1242 1243 // Step 3. Move up if there is any overflowed rectangle. 1244 for (int i = visibleRectCount - 1; i >= 0; --i) { 1245 if (visibleRectArray[i].bottom > height) { 1246 int overflowedHeight = visibleRectArray[i].bottom - height; 1247 for (int j = 0; j <= i; ++j) { 1248 if (visibleRectGroup[i] == visibleRectGroup[j]) { 1249 visibleRectArray[j].set(visibleRectArray[j].left, 1250 visibleRectArray[j].top - overflowedHeight, 1251 visibleRectArray[j].right, 1252 visibleRectArray[j].bottom - overflowedHeight); 1253 } 1254 } 1255 } 1256 } 1257 setMeasuredDimension(widthSpecSize, heightSpecSize); 1258 } 1259 1260 @Override onLayout(boolean changed, int l, int t, int r, int b)1261 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1262 int paddingLeft = getPaddingLeft(); 1263 int paddingTop = getPaddingTop(); 1264 int count = getChildCount(); 1265 for (int i = 0; i < count; ++i) { 1266 View child = getChildAt(i); 1267 if (child.getVisibility() != GONE) { 1268 int childLeft = paddingLeft + mRectArray[i].left; 1269 int childTop = paddingTop + mRectArray[i].top; 1270 int childBottom = paddingLeft + mRectArray[i].bottom; 1271 int childRight = paddingTop + mRectArray[i].right; 1272 if (DEBUG) { 1273 Log.d(TAG, String.format( 1274 "child layout bottom: %d left: %d right: %d top: %d", 1275 childBottom, childLeft, childRight, childTop)); 1276 } 1277 child.layout(childLeft, childTop, childRight, childBottom); 1278 } 1279 } 1280 } 1281 1282 @Override dispatchDraw(Canvas canvas)1283 public void dispatchDraw(Canvas canvas) { 1284 int paddingLeft = getPaddingLeft(); 1285 int paddingTop = getPaddingTop(); 1286 int count = getChildCount(); 1287 for (int i = 0; i < count; ++i) { 1288 View child = getChildAt(i); 1289 if (child.getVisibility() != GONE) { 1290 if (i >= mRectArray.length) { 1291 break; 1292 } 1293 int childLeft = paddingLeft + mRectArray[i].left; 1294 int childTop = paddingTop + mRectArray[i].top; 1295 final int saveCount = canvas.save(); 1296 canvas.translate(childLeft, childTop); 1297 child.draw(canvas); 1298 canvas.restoreToCount(saveCount); 1299 } 1300 } 1301 } 1302 } 1303 1304 /** 1305 * @hide 1306 * 1307 * Layout containing the safe title area that helps the closed captions look more prominent. 1308 * 1309 * <p>This is required by CEA-708B. 1310 */ 1311 static class CCLayout extends ScaledLayout implements ClosedCaptionLayout { 1312 private static final float SAFE_TITLE_AREA_SCALE_START_X = 0.1f; 1313 private static final float SAFE_TITLE_AREA_SCALE_END_X = 0.9f; 1314 private static final float SAFE_TITLE_AREA_SCALE_START_Y = 0.1f; 1315 private static final float SAFE_TITLE_AREA_SCALE_END_Y = 0.9f; 1316 1317 private final ScaledLayout mSafeTitleAreaLayout; 1318 CCLayout(Context context)1319 public CCLayout(Context context) { 1320 super(context); 1321 1322 mSafeTitleAreaLayout = new ScaledLayout(context); 1323 addView(mSafeTitleAreaLayout, new ScaledLayout.ScaledLayoutParams( 1324 SAFE_TITLE_AREA_SCALE_START_X, SAFE_TITLE_AREA_SCALE_END_X, 1325 SAFE_TITLE_AREA_SCALE_START_Y, SAFE_TITLE_AREA_SCALE_END_Y)); 1326 } 1327 addOrUpdateViewToSafeTitleArea(CCWindowLayout captionWindowLayout, ScaledLayoutParams scaledLayoutParams)1328 public void addOrUpdateViewToSafeTitleArea(CCWindowLayout captionWindowLayout, 1329 ScaledLayoutParams scaledLayoutParams) { 1330 int index = mSafeTitleAreaLayout.indexOfChild(captionWindowLayout); 1331 if (index < 0) { 1332 mSafeTitleAreaLayout.addView(captionWindowLayout, scaledLayoutParams); 1333 return; 1334 } 1335 mSafeTitleAreaLayout.updateViewLayout(captionWindowLayout, scaledLayoutParams); 1336 } 1337 removeViewFromSafeTitleArea(CCWindowLayout captionWindowLayout)1338 public void removeViewFromSafeTitleArea(CCWindowLayout captionWindowLayout) { 1339 mSafeTitleAreaLayout.removeView(captionWindowLayout); 1340 } 1341 setCaptionStyle(CaptionStyle style)1342 public void setCaptionStyle(CaptionStyle style) { 1343 final int count = mSafeTitleAreaLayout.getChildCount(); 1344 for (int i = 0; i < count; ++i) { 1345 final CCWindowLayout windowLayout = 1346 (CCWindowLayout) mSafeTitleAreaLayout.getChildAt(i); 1347 windowLayout.setCaptionStyle(style); 1348 } 1349 } 1350 setFontScale(float fontScale)1351 public void setFontScale(float fontScale) { 1352 final int count = mSafeTitleAreaLayout.getChildCount(); 1353 for (int i = 0; i < count; ++i) { 1354 final CCWindowLayout windowLayout = 1355 (CCWindowLayout) mSafeTitleAreaLayout.getChildAt(i); 1356 windowLayout.setFontScale(fontScale); 1357 } 1358 } 1359 } 1360 1361 /** 1362 * @hide 1363 * 1364 * Renders the selected CC track. 1365 */ 1366 static class CCHandler implements Handler.Callback { 1367 // TODO: Remaining works 1368 // CaptionTrackRenderer does not support the full spec of CEA-708. The remaining works are 1369 // described in the follows. 1370 // C0 Table: Backspace, FF, and HCR are not supported. The rule for P16 is not standardized 1371 // but it is handled as EUC-KR charset for Korea broadcasting. 1372 // C1 Table: All the styles of windows and pens except underline, italic, pen size, and pen 1373 // offset specified in CEA-708 are ignored and this follows system wide CC 1374 // preferences for look and feel. SetPenLocation is not implemented. 1375 // G2 Table: TSP, NBTSP and BLK are not supported. 1376 // Text/commands: Word wrapping, fonts, row and column locking are not supported. 1377 1378 private static final String TAG = "CCHandler"; 1379 private static final boolean DEBUG = false; 1380 1381 private static final int TENTHS_OF_SECOND_IN_MILLIS = 100; 1382 1383 // According to CEA-708B, there can exist up to 8 caption windows. 1384 private static final int CAPTION_WINDOWS_MAX = 8; 1385 private static final int CAPTION_ALL_WINDOWS_BITMAP = 255; 1386 1387 private static final int MSG_DELAY_CANCEL = 1; 1388 private static final int MSG_CAPTION_CLEAR = 2; 1389 1390 private static final long CAPTION_CLEAR_INTERVAL_MS = 60000; 1391 1392 private final CCLayout mCCLayout; 1393 private boolean mIsDelayed = false; 1394 private CCWindowLayout mCurrentWindowLayout; 1395 private final CCWindowLayout[] mCaptionWindowLayouts = 1396 new CCWindowLayout[CAPTION_WINDOWS_MAX]; 1397 private final ArrayList<Cea708CCParser.CaptionEvent> mPendingCaptionEvents 1398 = new ArrayList<>(); 1399 private final Handler mHandler; 1400 CCHandler(CCLayout ccLayout)1401 public CCHandler(CCLayout ccLayout) { 1402 mCCLayout = ccLayout; 1403 mHandler = new Handler(this); 1404 } 1405 1406 @Override handleMessage(Message msg)1407 public boolean handleMessage(Message msg) { 1408 switch (msg.what) { 1409 case MSG_DELAY_CANCEL: 1410 delayCancel(); 1411 return true; 1412 case MSG_CAPTION_CLEAR: 1413 clearWindows(CAPTION_ALL_WINDOWS_BITMAP); 1414 return true; 1415 } 1416 return false; 1417 } 1418 processCaptionEvent(Cea708CCParser.CaptionEvent event)1419 public void processCaptionEvent(Cea708CCParser.CaptionEvent event) { 1420 if (mIsDelayed) { 1421 mPendingCaptionEvents.add(event); 1422 return; 1423 } 1424 switch (event.type) { 1425 case Cea708CCParser.CAPTION_EMIT_TYPE_BUFFER: 1426 sendBufferToCurrentWindow((String) event.obj); 1427 break; 1428 case Cea708CCParser.CAPTION_EMIT_TYPE_CONTROL: 1429 sendControlToCurrentWindow((char) event.obj); 1430 break; 1431 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_CWX: 1432 setCurrentWindowLayout((int) event.obj); 1433 break; 1434 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_CLW: 1435 clearWindows((int) event.obj); 1436 break; 1437 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DSW: 1438 displayWindows((int) event.obj); 1439 break; 1440 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_HDW: 1441 hideWindows((int) event.obj); 1442 break; 1443 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_TGW: 1444 toggleWindows((int) event.obj); 1445 break; 1446 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLW: 1447 deleteWindows((int) event.obj); 1448 break; 1449 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLY: 1450 delay((int) event.obj); 1451 break; 1452 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLC: 1453 delayCancel(); 1454 break; 1455 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_RST: 1456 reset(); 1457 break; 1458 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPA: 1459 setPenAttr((Cea708CCParser.CaptionPenAttr) event.obj); 1460 break; 1461 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPC: 1462 setPenColor((Cea708CCParser.CaptionPenColor) event.obj); 1463 break; 1464 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPL: 1465 setPenLocation((Cea708CCParser.CaptionPenLocation) event.obj); 1466 break; 1467 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SWA: 1468 setWindowAttr((Cea708CCParser.CaptionWindowAttr) event.obj); 1469 break; 1470 case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DFX: 1471 defineWindow((Cea708CCParser.CaptionWindow) event.obj); 1472 break; 1473 } 1474 } 1475 1476 // The window related caption commands setCurrentWindowLayout(int windowId)1477 private void setCurrentWindowLayout(int windowId) { 1478 if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { 1479 return; 1480 } 1481 CCWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; 1482 if (windowLayout == null) { 1483 return; 1484 } 1485 if (DEBUG) { 1486 Log.d(TAG, "setCurrentWindowLayout to " + windowId); 1487 } 1488 mCurrentWindowLayout = windowLayout; 1489 } 1490 1491 // Each bit of windowBitmap indicates a window. 1492 // If a bit is set, the window id is the same as the number of the trailing zeros of the 1493 // bit. getWindowsFromBitmap(int windowBitmap)1494 private ArrayList<CCWindowLayout> getWindowsFromBitmap(int windowBitmap) { 1495 ArrayList<CCWindowLayout> windows = new ArrayList<>(); 1496 for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { 1497 if ((windowBitmap & (1 << i)) != 0) { 1498 CCWindowLayout windowLayout = mCaptionWindowLayouts[i]; 1499 if (windowLayout != null) { 1500 windows.add(windowLayout); 1501 } 1502 } 1503 } 1504 return windows; 1505 } 1506 clearWindows(int windowBitmap)1507 private void clearWindows(int windowBitmap) { 1508 if (windowBitmap == 0) { 1509 return; 1510 } 1511 for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 1512 windowLayout.clear(); 1513 } 1514 } 1515 displayWindows(int windowBitmap)1516 private void displayWindows(int windowBitmap) { 1517 if (windowBitmap == 0) { 1518 return; 1519 } 1520 for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 1521 windowLayout.show(); 1522 } 1523 } 1524 hideWindows(int windowBitmap)1525 private void hideWindows(int windowBitmap) { 1526 if (windowBitmap == 0) { 1527 return; 1528 } 1529 for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 1530 windowLayout.hide(); 1531 } 1532 } 1533 toggleWindows(int windowBitmap)1534 private void toggleWindows(int windowBitmap) { 1535 if (windowBitmap == 0) { 1536 return; 1537 } 1538 for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 1539 if (windowLayout.isShown()) { 1540 windowLayout.hide(); 1541 } else { 1542 windowLayout.show(); 1543 } 1544 } 1545 } 1546 deleteWindows(int windowBitmap)1547 private void deleteWindows(int windowBitmap) { 1548 if (windowBitmap == 0) { 1549 return; 1550 } 1551 for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 1552 windowLayout.removeFromCaptionView(); 1553 mCaptionWindowLayouts[windowLayout.getCaptionWindowId()] = null; 1554 } 1555 } 1556 reset()1557 public void reset() { 1558 mCurrentWindowLayout = null; 1559 mIsDelayed = false; 1560 mPendingCaptionEvents.clear(); 1561 for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { 1562 if (mCaptionWindowLayouts[i] != null) { 1563 mCaptionWindowLayouts[i].removeFromCaptionView(); 1564 } 1565 mCaptionWindowLayouts[i] = null; 1566 } 1567 mCCLayout.setVisibility(View.INVISIBLE); 1568 mHandler.removeMessages(MSG_CAPTION_CLEAR); 1569 } 1570 setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr)1571 private void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) { 1572 if (mCurrentWindowLayout != null) { 1573 mCurrentWindowLayout.setWindowAttr(windowAttr); 1574 } 1575 } 1576 defineWindow(Cea708CCParser.CaptionWindow window)1577 private void defineWindow(Cea708CCParser.CaptionWindow window) { 1578 if (window == null) { 1579 return; 1580 } 1581 int windowId = window.id; 1582 if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { 1583 return; 1584 } 1585 CCWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; 1586 if (windowLayout == null) { 1587 windowLayout = new CCWindowLayout(mCCLayout.getContext()); 1588 } 1589 windowLayout.initWindow(mCCLayout, window); 1590 mCurrentWindowLayout = mCaptionWindowLayouts[windowId] = windowLayout; 1591 } 1592 1593 // The job related caption commands delay(int tenthsOfSeconds)1594 private void delay(int tenthsOfSeconds) { 1595 if (tenthsOfSeconds < 0 || tenthsOfSeconds > 255) { 1596 return; 1597 } 1598 mIsDelayed = true; 1599 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DELAY_CANCEL), 1600 tenthsOfSeconds * TENTHS_OF_SECOND_IN_MILLIS); 1601 } 1602 delayCancel()1603 private void delayCancel() { 1604 mIsDelayed = false; 1605 processPendingBuffer(); 1606 } 1607 processPendingBuffer()1608 private void processPendingBuffer() { 1609 for (Cea708CCParser.CaptionEvent event : mPendingCaptionEvents) { 1610 processCaptionEvent(event); 1611 } 1612 mPendingCaptionEvents.clear(); 1613 } 1614 1615 // The implicit write caption commands sendControlToCurrentWindow(char control)1616 private void sendControlToCurrentWindow(char control) { 1617 if (mCurrentWindowLayout != null) { 1618 mCurrentWindowLayout.sendControl(control); 1619 } 1620 } 1621 sendBufferToCurrentWindow(String buffer)1622 private void sendBufferToCurrentWindow(String buffer) { 1623 if (mCurrentWindowLayout != null) { 1624 mCurrentWindowLayout.sendBuffer(buffer); 1625 mHandler.removeMessages(MSG_CAPTION_CLEAR); 1626 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CAPTION_CLEAR), 1627 CAPTION_CLEAR_INTERVAL_MS); 1628 } 1629 } 1630 1631 // The pen related caption commands setPenAttr(Cea708CCParser.CaptionPenAttr attr)1632 private void setPenAttr(Cea708CCParser.CaptionPenAttr attr) { 1633 if (mCurrentWindowLayout != null) { 1634 mCurrentWindowLayout.setPenAttr(attr); 1635 } 1636 } 1637 setPenColor(Cea708CCParser.CaptionPenColor color)1638 private void setPenColor(Cea708CCParser.CaptionPenColor color) { 1639 if (mCurrentWindowLayout != null) { 1640 mCurrentWindowLayout.setPenColor(color); 1641 } 1642 } 1643 setPenLocation(Cea708CCParser.CaptionPenLocation location)1644 private void setPenLocation(Cea708CCParser.CaptionPenLocation location) { 1645 if (mCurrentWindowLayout != null) { 1646 mCurrentWindowLayout.setPenLocation(location.row, location.column); 1647 } 1648 } 1649 } 1650 1651 /** 1652 * @hide 1653 * 1654 * Layout which renders a caption window of CEA-708B. It contains a {@link TextView} that takes 1655 * care of displaying the actual CC text. 1656 */ 1657 static class CCWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener { 1658 private static final String TAG = "CCWindowLayout"; 1659 1660 private static final float PROPORTION_PEN_SIZE_SMALL = .75f; 1661 private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f; 1662 1663 // The following values indicates the maximum cell number of a window. 1664 private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99; 1665 private static final int ANCHOR_VERTICAL_MAX = 74; 1666 private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209; 1667 private static final int MAX_COLUMN_COUNT_16_9 = 42; 1668 1669 // The following values indicates a gravity of a window. 1670 private static final int ANCHOR_MODE_DIVIDER = 3; 1671 private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0; 1672 private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1; 1673 private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2; 1674 private static final int ANCHOR_VERTICAL_MODE_TOP = 0; 1675 private static final int ANCHOR_VERTICAL_MODE_CENTER = 1; 1676 private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2; 1677 1678 private CCLayout mCCLayout; 1679 1680 private CCView mCCView; 1681 private CaptionStyle mCaptionStyle; 1682 private int mRowLimit = 0; 1683 private final SpannableStringBuilder mBuilder = new SpannableStringBuilder(); 1684 private final List<CharacterStyle> mCharacterStyles = new ArrayList<>(); 1685 private int mCaptionWindowId; 1686 private int mRow = -1; 1687 private float mFontScale; 1688 private float mTextSize; 1689 private String mWidestChar; 1690 private int mLastCaptionLayoutWidth; 1691 private int mLastCaptionLayoutHeight; 1692 CCWindowLayout(Context context)1693 public CCWindowLayout(Context context) { 1694 this(context, null); 1695 } 1696 CCWindowLayout(Context context, AttributeSet attrs)1697 public CCWindowLayout(Context context, AttributeSet attrs) { 1698 this(context, attrs, 0); 1699 } 1700 CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr)1701 public CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) { 1702 this(context, attrs, defStyleAttr, 0); 1703 } 1704 CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)1705 public CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr, 1706 int defStyleRes) { 1707 super(context, attrs, defStyleAttr, defStyleRes); 1708 1709 // Add a subtitle view to the layout. 1710 mCCView = new CCView(context); 1711 LayoutParams params = new RelativeLayout.LayoutParams( 1712 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1713 addView(mCCView, params); 1714 1715 // Set the system wide CC preferences to the subtitle view. 1716 CaptioningManager captioningManager = 1717 (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); 1718 mFontScale = captioningManager.getFontScale(); 1719 setCaptionStyle(captioningManager.getUserStyle()); 1720 mCCView.setText(""); 1721 updateWidestChar(); 1722 } 1723 setCaptionStyle(CaptionStyle style)1724 public void setCaptionStyle(CaptionStyle style) { 1725 mCaptionStyle = style; 1726 mCCView.setCaptionStyle(style); 1727 } 1728 setFontScale(float fontScale)1729 public void setFontScale(float fontScale) { 1730 mFontScale = fontScale; 1731 updateTextSize(); 1732 } 1733 getCaptionWindowId()1734 public int getCaptionWindowId() { 1735 return mCaptionWindowId; 1736 } 1737 setCaptionWindowId(int captionWindowId)1738 public void setCaptionWindowId(int captionWindowId) { 1739 mCaptionWindowId = captionWindowId; 1740 } 1741 clear()1742 public void clear() { 1743 clearText(); 1744 hide(); 1745 } 1746 show()1747 public void show() { 1748 setVisibility(View.VISIBLE); 1749 requestLayout(); 1750 } 1751 hide()1752 public void hide() { 1753 setVisibility(View.INVISIBLE); 1754 requestLayout(); 1755 } 1756 setPenAttr(Cea708CCParser.CaptionPenAttr penAttr)1757 public void setPenAttr(Cea708CCParser.CaptionPenAttr penAttr) { 1758 mCharacterStyles.clear(); 1759 if (penAttr.italic) { 1760 mCharacterStyles.add(new StyleSpan(Typeface.ITALIC)); 1761 } 1762 if (penAttr.underline) { 1763 mCharacterStyles.add(new UnderlineSpan()); 1764 } 1765 switch (penAttr.penSize) { 1766 case Cea708CCParser.CaptionPenAttr.PEN_SIZE_SMALL: 1767 mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL)); 1768 break; 1769 case Cea708CCParser.CaptionPenAttr.PEN_SIZE_LARGE: 1770 mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE)); 1771 break; 1772 } 1773 switch (penAttr.penOffset) { 1774 case Cea708CCParser.CaptionPenAttr.OFFSET_SUBSCRIPT: 1775 mCharacterStyles.add(new SubscriptSpan()); 1776 break; 1777 case Cea708CCParser.CaptionPenAttr.OFFSET_SUPERSCRIPT: 1778 mCharacterStyles.add(new SuperscriptSpan()); 1779 break; 1780 } 1781 } 1782 setPenColor(Cea708CCParser.CaptionPenColor penColor)1783 public void setPenColor(Cea708CCParser.CaptionPenColor penColor) { 1784 // TODO: apply pen colors or skip this and use the style of system wide CC style as is. 1785 } 1786 setPenLocation(int row, int column)1787 public void setPenLocation(int row, int column) { 1788 // TODO: change the location of pen based on row and column both. 1789 if (mRow >= 0) { 1790 for (int r = mRow; r < row; ++r) { 1791 appendText("\n"); 1792 } 1793 } 1794 mRow = row; 1795 } 1796 setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr)1797 public void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) { 1798 // TODO: apply window attrs or skip this and use the style of system wide CC style as 1799 // is. 1800 } 1801 sendBuffer(String buffer)1802 public void sendBuffer(String buffer) { 1803 appendText(buffer); 1804 } 1805 sendControl(char control)1806 public void sendControl(char control) { 1807 // TODO: there are a bunch of ASCII-style control codes. 1808 } 1809 1810 /** 1811 * This method places the window on a given CaptionLayout along with the anchor of the 1812 * window. 1813 * <p> 1814 * According to CEA-708B, the anchor id indicates the gravity of the window as the follows. 1815 * For example, A value 7 of a anchor id says that a window is align with its parent bottom 1816 * and is located at the center horizontally of its parent. 1817 * </p> 1818 * <h4>Anchor id and the gravity of a window</h4> 1819 * <table> 1820 * <tr> 1821 * <th>GRAVITY</th> 1822 * <th>LEFT</th> 1823 * <th>CENTER_HORIZONTAL</th> 1824 * <th>RIGHT</th> 1825 * </tr> 1826 * <tr> 1827 * <th>TOP</th> 1828 * <td>0</td> 1829 * <td>1</td> 1830 * <td>2</td> 1831 * </tr> 1832 * <tr> 1833 * <th>CENTER_VERTICAL</th> 1834 * <td>3</td> 1835 * <td>4</td> 1836 * <td>5</td> 1837 * </tr> 1838 * <tr> 1839 * <th>BOTTOM</th> 1840 * <td>6</td> 1841 * <td>7</td> 1842 * <td>8</td> 1843 * </tr> 1844 * </table> 1845 * <p> 1846 * In order to handle the gravity of a window, there are two steps. First, set the size of 1847 * the window. Since the window will be positioned at ScaledLayout, the size factors are 1848 * determined in a ratio. Second, set the gravity of the window. CaptionWindowLayout is 1849 * inherited from RelativeLayout. Hence, we could set the gravity of its child view, 1850 * SubtitleView. 1851 * </p> 1852 * <p> 1853 * The gravity of the window is also related to its size. When it should be pushed to a one 1854 * of the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a 1855 * boundary of the window. When it should be pushed in the horizontal/vertical center of its 1856 * container, the horizontal/vertical center point of the window should be the same as the 1857 * anchor point. 1858 * </p> 1859 * 1860 * @param ccLayout a given CaptionLayout, which contains a safe title area. 1861 * @param captionWindow a given CaptionWindow, which stores the construction info of the 1862 * window. 1863 */ initWindow(CCLayout ccLayout, Cea708CCParser.CaptionWindow captionWindow)1864 public void initWindow(CCLayout ccLayout, Cea708CCParser.CaptionWindow captionWindow) { 1865 if (mCCLayout != ccLayout) { 1866 if (mCCLayout != null) { 1867 mCCLayout.removeOnLayoutChangeListener(this); 1868 } 1869 mCCLayout = ccLayout; 1870 mCCLayout.addOnLayoutChangeListener(this); 1871 updateWidestChar(); 1872 } 1873 1874 // Both anchor vertical and horizontal indicates the position cell number of the window. 1875 float scaleRow = (float) captionWindow.anchorVertical / 1876 (captionWindow.relativePositioning 1877 ? ANCHOR_RELATIVE_POSITIONING_MAX : ANCHOR_VERTICAL_MAX); 1878 1879 // Assumes it has a wide aspect ratio track. 1880 float scaleCol = (float) captionWindow.anchorHorizontal / 1881 (captionWindow.relativePositioning ? ANCHOR_RELATIVE_POSITIONING_MAX 1882 : ANCHOR_HORIZONTAL_16_9_MAX); 1883 1884 // The range of scaleRow/Col need to be verified to be in [0, 1]. 1885 // Otherwise a RuntimeException will be raised in ScaledLayout. 1886 if (scaleRow < 0 || scaleRow > 1) { 1887 Log.i(TAG, "The vertical position of the anchor point should be at the range of 0 " 1888 + "and 1 but " + scaleRow); 1889 scaleRow = Math.max(0, Math.min(scaleRow, 1)); 1890 } 1891 if (scaleCol < 0 || scaleCol > 1) { 1892 Log.i(TAG, "The horizontal position of the anchor point should be at the range of 0" 1893 + " and 1 but " + scaleCol); 1894 scaleCol = Math.max(0, Math.min(scaleCol, 1)); 1895 } 1896 int gravity = Gravity.CENTER; 1897 int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER; 1898 int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER; 1899 float scaleStartRow = 0; 1900 float scaleEndRow = 1; 1901 float scaleStartCol = 0; 1902 float scaleEndCol = 1; 1903 switch (horizontalMode) { 1904 case ANCHOR_HORIZONTAL_MODE_LEFT: 1905 gravity = Gravity.LEFT; 1906 mCCView.setAlignment(Alignment.ALIGN_NORMAL); 1907 scaleStartCol = scaleCol; 1908 break; 1909 case ANCHOR_HORIZONTAL_MODE_CENTER: 1910 float gap = Math.min(1 - scaleCol, scaleCol); 1911 1912 // Since all TV sets use left text alignment instead of center text alignment 1913 // for this case, we follow the industry convention if possible. 1914 int columnCount = captionWindow.columnCount + 1; 1915 columnCount = Math.min(getScreenColumnCount(), columnCount); 1916 StringBuilder widestTextBuilder = new StringBuilder(); 1917 for (int i = 0; i < columnCount; ++i) { 1918 widestTextBuilder.append(mWidestChar); 1919 } 1920 Paint paint = new Paint(); 1921 paint.setTypeface(mCaptionStyle.getTypeface()); 1922 paint.setTextSize(mTextSize); 1923 float maxWindowWidth = paint.measureText(widestTextBuilder.toString()); 1924 float halfMaxWidthScale = mCCLayout.getWidth() > 0 1925 ? maxWindowWidth / 2.0f / (mCCLayout.getWidth() * 0.8f) : 0.0f; 1926 if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) { 1927 // Calculate the expected max window size based on the column count of the 1928 // caption window multiplied by average alphabets char width, then align the 1929 // left side of the window with the left side of the expected max window. 1930 gravity = Gravity.LEFT; 1931 mCCView.setAlignment(Alignment.ALIGN_NORMAL); 1932 scaleStartCol = scaleCol - halfMaxWidthScale; 1933 scaleEndCol = 1.0f; 1934 } else { 1935 // The gap will be the minimum distance value of the distances from both 1936 // horizontal end points to the anchor point. 1937 // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2]. 1938 // If scaleCol > 0.5, the range of scaleCol is 1939 // [(1 - the anchor point) * 2, 1]. 1940 // The anchor point is located at the horizontal center of the window in 1941 // both cases. 1942 gravity = Gravity.CENTER_HORIZONTAL; 1943 mCCView.setAlignment(Alignment.ALIGN_CENTER); 1944 scaleStartCol = scaleCol - gap; 1945 scaleEndCol = scaleCol + gap; 1946 } 1947 break; 1948 case ANCHOR_HORIZONTAL_MODE_RIGHT: 1949 gravity = Gravity.RIGHT; 1950 mCCView.setAlignment(Alignment.ALIGN_RIGHT); 1951 scaleEndCol = scaleCol; 1952 break; 1953 } 1954 switch (verticalMode) { 1955 case ANCHOR_VERTICAL_MODE_TOP: 1956 gravity |= Gravity.TOP; 1957 scaleStartRow = scaleRow; 1958 break; 1959 case ANCHOR_VERTICAL_MODE_CENTER: 1960 gravity |= Gravity.CENTER_VERTICAL; 1961 1962 // See the above comment. 1963 float gap = Math.min(1 - scaleRow, scaleRow); 1964 scaleStartRow = scaleRow - gap; 1965 scaleEndRow = scaleRow + gap; 1966 break; 1967 case ANCHOR_VERTICAL_MODE_BOTTOM: 1968 gravity |= Gravity.BOTTOM; 1969 scaleEndRow = scaleRow; 1970 break; 1971 } 1972 mCCLayout.addOrUpdateViewToSafeTitleArea(this, new ScaledLayout 1973 .ScaledLayoutParams(scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol)); 1974 setCaptionWindowId(captionWindow.id); 1975 setRowLimit(captionWindow.rowCount); 1976 setGravity(gravity); 1977 if (captionWindow.visible) { 1978 show(); 1979 } else { 1980 hide(); 1981 } 1982 } 1983 1984 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)1985 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 1986 int oldTop, int oldRight, int oldBottom) { 1987 int width = right - left; 1988 int height = bottom - top; 1989 if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) { 1990 mLastCaptionLayoutWidth = width; 1991 mLastCaptionLayoutHeight = height; 1992 updateTextSize(); 1993 } 1994 } 1995 updateWidestChar()1996 private void updateWidestChar() { 1997 Paint paint = new Paint(); 1998 paint.setTypeface(mCaptionStyle.getTypeface()); 1999 Charset latin1 = Charset.forName("ISO-8859-1"); 2000 float widestCharWidth = 0f; 2001 for (int i = 0; i < 256; ++i) { 2002 String ch = new String(new byte[]{(byte) i}, latin1); 2003 float charWidth = paint.measureText(ch); 2004 if (widestCharWidth < charWidth) { 2005 widestCharWidth = charWidth; 2006 mWidestChar = ch; 2007 } 2008 } 2009 updateTextSize(); 2010 } 2011 updateTextSize()2012 private void updateTextSize() { 2013 if (mCCLayout == null) return; 2014 2015 // Calculate text size based on the max window size. 2016 StringBuilder widestTextBuilder = new StringBuilder(); 2017 int screenColumnCount = getScreenColumnCount(); 2018 for (int i = 0; i < screenColumnCount; ++i) { 2019 widestTextBuilder.append(mWidestChar); 2020 } 2021 String widestText = widestTextBuilder.toString(); 2022 Paint paint = new Paint(); 2023 paint.setTypeface(mCaptionStyle.getTypeface()); 2024 float startFontSize = 0f; 2025 float endFontSize = 255f; 2026 while (startFontSize < endFontSize) { 2027 float testTextSize = (startFontSize + endFontSize) / 2f; 2028 paint.setTextSize(testTextSize); 2029 float width = paint.measureText(widestText); 2030 if (mCCLayout.getWidth() * 0.8f > width) { 2031 startFontSize = testTextSize + 0.01f; 2032 } else { 2033 endFontSize = testTextSize - 0.01f; 2034 } 2035 } 2036 mTextSize = endFontSize * mFontScale; 2037 mCCView.setTextSize(mTextSize); 2038 } 2039 getScreenColumnCount()2040 private int getScreenColumnCount() { 2041 // Assume it has a wide aspect ratio track. 2042 return MAX_COLUMN_COUNT_16_9; 2043 } 2044 removeFromCaptionView()2045 public void removeFromCaptionView() { 2046 if (mCCLayout != null) { 2047 mCCLayout.removeViewFromSafeTitleArea(this); 2048 mCCLayout.removeOnLayoutChangeListener(this); 2049 mCCLayout = null; 2050 } 2051 } 2052 setText(String text)2053 public void setText(String text) { 2054 updateText(text, false); 2055 } 2056 appendText(String text)2057 public void appendText(String text) { 2058 updateText(text, true); 2059 } 2060 clearText()2061 public void clearText() { 2062 mBuilder.clear(); 2063 mCCView.setText(""); 2064 } 2065 updateText(String text, boolean appended)2066 private void updateText(String text, boolean appended) { 2067 if (!appended) { 2068 mBuilder.clear(); 2069 } 2070 if (text != null && text.length() > 0) { 2071 int length = mBuilder.length(); 2072 mBuilder.append(text); 2073 for (CharacterStyle characterStyle : mCharacterStyles) { 2074 mBuilder.setSpan(characterStyle, length, mBuilder.length(), 2075 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 2076 } 2077 } 2078 String[] lines = TextUtils.split(mBuilder.toString(), "\n"); 2079 2080 // Truncate text not to exceed the row limit. 2081 // Plus one here since the range of the rows is [0, mRowLimit]. 2082 String truncatedText = TextUtils.join("\n", Arrays.copyOfRange( 2083 lines, Math.max(0, lines.length - (mRowLimit + 1)), lines.length)); 2084 mBuilder.delete(0, mBuilder.length() - truncatedText.length()); 2085 2086 // Trim the buffer first then set text to CCView. 2087 int start = 0, last = mBuilder.length() - 1; 2088 int end = last; 2089 while ((start <= end) && (mBuilder.charAt(start) <= ' ')) { 2090 ++start; 2091 } 2092 while ((end >= start) && (mBuilder.charAt(end) <= ' ')) { 2093 --end; 2094 } 2095 if (start == 0 && end == last) { 2096 mCCView.setText(mBuilder); 2097 } else { 2098 SpannableStringBuilder trim = new SpannableStringBuilder(); 2099 trim.append(mBuilder); 2100 if (end < last) { 2101 trim.delete(end + 1, last + 1); 2102 } 2103 if (start > 0) { 2104 trim.delete(0, start); 2105 } 2106 mCCView.setText(trim); 2107 } 2108 } 2109 setRowLimit(int rowLimit)2110 public void setRowLimit(int rowLimit) { 2111 if (rowLimit < 0) { 2112 throw new IllegalArgumentException("A rowLimit should have a positive number"); 2113 } 2114 mRowLimit = rowLimit; 2115 } 2116 } 2117 2118 /** @hide */ 2119 static class CCView extends SubtitleView { 2120 private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT; 2121 CCView(Context context)2122 public CCView(Context context) { 2123 this(context, null); 2124 } 2125 CCView(Context context, AttributeSet attrs)2126 public CCView(Context context, AttributeSet attrs) { 2127 this(context, attrs, 0); 2128 } 2129 CCView(Context context, AttributeSet attrs, int defStyleAttr)2130 public CCView(Context context, AttributeSet attrs, int defStyleAttr) { 2131 this(context, attrs, defStyleAttr, 0); 2132 } 2133 CCView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)2134 public CCView(Context context, AttributeSet attrs, int defStyleAttr, 2135 int defStyleRes) { 2136 super(context, attrs, defStyleAttr, defStyleRes); 2137 } 2138 setCaptionStyle(CaptionStyle style)2139 public void setCaptionStyle(CaptionStyle style) { 2140 setForegroundColor(style.hasForegroundColor() 2141 ? style.foregroundColor : DEFAULT_CAPTION_STYLE.foregroundColor); 2142 setBackgroundColor(style.hasBackgroundColor() 2143 ? style.backgroundColor : DEFAULT_CAPTION_STYLE.backgroundColor); 2144 setEdgeType(style.hasEdgeType() 2145 ? style.edgeType : DEFAULT_CAPTION_STYLE.edgeType); 2146 setEdgeColor(style.hasEdgeColor() 2147 ? style.edgeColor : DEFAULT_CAPTION_STYLE.edgeColor); 2148 setTypeface(style.getTypeface()); 2149 } 2150 } 2151 } 2152