1 /* 2 * Copyright (C) 2011 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.compat.annotation.UnsupportedAppUsage; 20 import android.graphics.Rect; 21 import android.os.Build; 22 import android.os.Parcel; 23 import android.util.Log; 24 25 import java.util.ArrayList; 26 import java.util.HashMap; 27 import java.util.List; 28 import java.util.Set; 29 30 /** 31 * Class to hold the timed text's metadata, including: 32 * <ul> 33 * <li> The characters for rendering</li> 34 * <li> The rendering position for the timed text</li> 35 * </ul> 36 * 37 * <p> To render the timed text, applications need to do the following: 38 * 39 * <ul> 40 * <li> Implement the {@link MediaPlayer.OnTimedTextListener} interface</li> 41 * <li> Register the {@link MediaPlayer.OnTimedTextListener} callback on a MediaPlayer object that is used for playback</li> 42 * <li> When a onTimedText callback is received, do the following: 43 * <ul> 44 * <li> call {@link #getText} to get the characters for rendering</li> 45 * <li> call {@link #getBounds} to get the text rendering area/region</li> 46 * </ul> 47 * </li> 48 * </ul> 49 * 50 * @see android.media.MediaPlayer 51 */ 52 public final class TimedText 53 { 54 private static final int FIRST_PUBLIC_KEY = 1; 55 56 // These keys must be in sync with the keys in TextDescription.h 57 private static final int KEY_DISPLAY_FLAGS = 1; // int 58 private static final int KEY_STYLE_FLAGS = 2; // int 59 private static final int KEY_BACKGROUND_COLOR_RGBA = 3; // int 60 private static final int KEY_HIGHLIGHT_COLOR_RGBA = 4; // int 61 private static final int KEY_SCROLL_DELAY = 5; // int 62 private static final int KEY_WRAP_TEXT = 6; // int 63 private static final int KEY_START_TIME = 7; // int 64 private static final int KEY_STRUCT_BLINKING_TEXT_LIST = 8; // List<CharPos> 65 private static final int KEY_STRUCT_FONT_LIST = 9; // List<Font> 66 private static final int KEY_STRUCT_HIGHLIGHT_LIST = 10; // List<CharPos> 67 private static final int KEY_STRUCT_HYPER_TEXT_LIST = 11; // List<HyperText> 68 private static final int KEY_STRUCT_KARAOKE_LIST = 12; // List<Karaoke> 69 private static final int KEY_STRUCT_STYLE_LIST = 13; // List<Style> 70 private static final int KEY_STRUCT_TEXT_POS = 14; // TextPos 71 private static final int KEY_STRUCT_JUSTIFICATION = 15; // Justification 72 private static final int KEY_STRUCT_TEXT = 16; // Text 73 74 private static final int LAST_PUBLIC_KEY = 16; 75 76 private static final int FIRST_PRIVATE_KEY = 101; 77 78 // The following keys are used between TimedText.java and 79 // TextDescription.cpp in order to parce the Parcel. 80 private static final int KEY_GLOBAL_SETTING = 101; 81 private static final int KEY_LOCAL_SETTING = 102; 82 private static final int KEY_START_CHAR = 103; 83 private static final int KEY_END_CHAR = 104; 84 private static final int KEY_FONT_ID = 105; 85 private static final int KEY_FONT_SIZE = 106; 86 private static final int KEY_TEXT_COLOR_RGBA = 107; 87 88 private static final int LAST_PRIVATE_KEY = 107; 89 90 private static final String TAG = "TimedText"; 91 92 private final HashMap<Integer, Object> mKeyObjectMap = 93 new HashMap<Integer, Object>(); 94 95 private int mDisplayFlags = -1; 96 private int mBackgroundColorRGBA = -1; 97 private int mHighlightColorRGBA = -1; 98 private int mScrollDelay = -1; 99 private int mWrapText = -1; 100 101 private List<CharPos> mBlinkingPosList = null; 102 private List<CharPos> mHighlightPosList = null; 103 private List<Karaoke> mKaraokeList = null; 104 private List<Font> mFontList = null; 105 private List<Style> mStyleList = null; 106 private List<HyperText> mHyperTextList = null; 107 108 private Rect mTextBounds = null; 109 private String mTextChars = null; 110 111 private Justification mJustification; 112 113 /** 114 * Helper class to hold the start char offset and end char offset 115 * for Blinking Text or Highlight Text. endChar is the end offset 116 * of the text (startChar + number of characters to be highlighted 117 * or blinked). The member variables in this class are read-only. 118 * {@hide} 119 */ 120 public static final class CharPos { 121 /** 122 * The offset of the start character 123 */ 124 public final int startChar; 125 126 /** 127 * The offset of the end character 128 */ 129 public final int endChar; 130 131 /** 132 * Constuctor 133 * @param startChar the offset of the start character. 134 * @param endChar the offset of the end character. 135 */ CharPos(int startChar, int endChar)136 public CharPos(int startChar, int endChar) { 137 this.startChar = startChar; 138 this.endChar = endChar; 139 } 140 } 141 142 /** 143 * Helper class to hold the justification for text display in the text box. 144 * The member variables in this class are read-only. 145 * {@hide} 146 */ 147 public static final class Justification { 148 /** 149 * horizontal justification 0: left, 1: centered, -1: right 150 */ 151 public final int horizontalJustification; 152 153 /** 154 * vertical justification 0: top, 1: centered, -1: bottom 155 */ 156 public final int verticalJustification; 157 158 /** 159 * Constructor 160 * @param horizontal the horizontal justification of the text. 161 * @param vertical the vertical justification of the text. 162 */ Justification(int horizontal, int vertical)163 public Justification(int horizontal, int vertical) { 164 this.horizontalJustification = horizontal; 165 this.verticalJustification = vertical; 166 } 167 } 168 169 /** 170 * Helper class to hold the style information to display the text. 171 * The member variables in this class are read-only. 172 * {@hide} 173 */ 174 public static final class Style { 175 /** 176 * The offset of the start character which applys this style 177 */ 178 public final int startChar; 179 180 /** 181 * The offset of the end character which applys this style 182 */ 183 public final int endChar; 184 185 /** 186 * ID of the font. This ID will be used to choose the font 187 * to be used from the font list. 188 */ 189 public final int fontID; 190 191 /** 192 * True if the characters should be bold 193 */ 194 public final boolean isBold; 195 196 /** 197 * True if the characters should be italic 198 */ 199 public final boolean isItalic; 200 201 /** 202 * True if the characters should be underlined 203 */ 204 public final boolean isUnderlined; 205 206 /** 207 * The size of the font 208 */ 209 public final int fontSize; 210 211 /** 212 * To specify the RGBA color: 8 bits each of red, green, blue, 213 * and an alpha(transparency) value 214 */ 215 public final int colorRGBA; 216 217 /** 218 * Constructor 219 * @param startChar the offset of the start character which applys this style 220 * @param endChar the offset of the end character which applys this style 221 * @param fontId the ID of the font. 222 * @param isBold whether the characters should be bold. 223 * @param isItalic whether the characters should be italic. 224 * @param isUnderlined whether the characters should be underlined. 225 * @param fontSize the size of the font. 226 * @param colorRGBA red, green, blue, and alpha value for color. 227 */ Style(int startChar, int endChar, int fontId, boolean isBold, boolean isItalic, boolean isUnderlined, int fontSize, int colorRGBA)228 public Style(int startChar, int endChar, int fontId, 229 boolean isBold, boolean isItalic, boolean isUnderlined, 230 int fontSize, int colorRGBA) { 231 this.startChar = startChar; 232 this.endChar = endChar; 233 this.fontID = fontId; 234 this.isBold = isBold; 235 this.isItalic = isItalic; 236 this.isUnderlined = isUnderlined; 237 this.fontSize = fontSize; 238 this.colorRGBA = colorRGBA; 239 } 240 } 241 242 /** 243 * Helper class to hold the font ID and name. 244 * The member variables in this class are read-only. 245 * {@hide} 246 */ 247 public static final class Font { 248 /** 249 * The font ID 250 */ 251 public final int ID; 252 253 /** 254 * The font name 255 */ 256 public final String name; 257 258 /** 259 * Constructor 260 * @param id the font ID. 261 * @param name the font name. 262 */ Font(int id, String name)263 public Font(int id, String name) { 264 this.ID = id; 265 this.name = name; 266 } 267 } 268 269 /** 270 * Helper class to hold the karaoke information. 271 * The member variables in this class are read-only. 272 * {@hide} 273 */ 274 public static final class Karaoke { 275 /** 276 * The start time (in milliseconds) to highlight the characters 277 * specified by startChar and endChar. 278 */ 279 public final int startTimeMs; 280 281 /** 282 * The end time (in milliseconds) to highlight the characters 283 * specified by startChar and endChar. 284 */ 285 public final int endTimeMs; 286 287 /** 288 * The offset of the start character to be highlighted 289 */ 290 public final int startChar; 291 292 /** 293 * The offset of the end character to be highlighted 294 */ 295 public final int endChar; 296 297 /** 298 * Constructor 299 * @param startTimeMs the start time (in milliseconds) to highlight 300 * the characters between startChar and endChar. 301 * @param endTimeMs the end time (in milliseconds) to highlight 302 * the characters between startChar and endChar. 303 * @param startChar the offset of the start character to be highlighted. 304 * @param endChar the offset of the end character to be highlighted. 305 */ Karaoke(int startTimeMs, int endTimeMs, int startChar, int endChar)306 public Karaoke(int startTimeMs, int endTimeMs, int startChar, int endChar) { 307 this.startTimeMs = startTimeMs; 308 this.endTimeMs = endTimeMs; 309 this.startChar = startChar; 310 this.endChar = endChar; 311 } 312 } 313 314 /** 315 * Helper class to hold the hyper text information. 316 * The member variables in this class are read-only. 317 * {@hide} 318 */ 319 public static final class HyperText { 320 /** 321 * The offset of the start character 322 */ 323 public final int startChar; 324 325 /** 326 * The offset of the end character 327 */ 328 public final int endChar; 329 330 /** 331 * The linked-to URL 332 */ 333 public final String URL; 334 335 /** 336 * The "alt" string for user display 337 */ 338 public final String altString; 339 340 341 /** 342 * Constructor 343 * @param startChar the offset of the start character. 344 * @param endChar the offset of the end character. 345 * @param url the linked-to URL. 346 * @param alt the "alt" string for display. 347 */ HyperText(int startChar, int endChar, String url, String alt)348 public HyperText(int startChar, int endChar, String url, String alt) { 349 this.startChar = startChar; 350 this.endChar = endChar; 351 this.URL = url; 352 this.altString = alt; 353 } 354 } 355 356 /** 357 * @param obj the byte array which contains the timed text. 358 * @throws IllegalArgumentExcept if parseParcel() fails. 359 * {@hide} 360 */ TimedText(Parcel parcel)361 public TimedText(Parcel parcel) { 362 if (!parseParcel(parcel)) { 363 mKeyObjectMap.clear(); 364 throw new IllegalArgumentException("parseParcel() fails"); 365 } 366 } 367 368 /** 369 * @param text the characters in the timed text. 370 * @param bounds the rectangle area or region for rendering the timed text. 371 * {@hide} 372 */ TimedText(String text, Rect bounds)373 public TimedText(String text, Rect bounds) { 374 mTextChars = text; 375 mTextBounds = bounds; 376 } 377 378 /** 379 * Get the characters in the timed text. 380 * 381 * @return the characters as a String object in the TimedText. Applications 382 * should stop rendering previous timed text at the current rendering region if 383 * a null is returned, until the next non-null timed text is received. 384 */ getText()385 public String getText() { 386 return mTextChars; 387 } 388 389 /** 390 * Get the rectangle area or region for rendering the timed text as specified 391 * by a Rect object. 392 * 393 * @return the rectangle region to render the characters in the timed text. 394 * If no bounds information is available (a null is returned), render the 395 * timed text at the center bottom of the display. 396 */ getBounds()397 public Rect getBounds() { 398 return mTextBounds; 399 } 400 401 /* 402 * Go over all the records, collecting metadata keys and fields in the 403 * Parcel. These are stored in mKeyObjectMap for application to retrieve. 404 * @return false if an error occurred during parsing. Otherwise, true. 405 */ parseParcel(Parcel parcel)406 private boolean parseParcel(Parcel parcel) { 407 parcel.setDataPosition(0); 408 if (parcel.dataAvail() == 0) { 409 return false; 410 } 411 412 int type = parcel.readInt(); 413 if (type == KEY_LOCAL_SETTING) { 414 type = parcel.readInt(); 415 if (type != KEY_START_TIME) { 416 return false; 417 } 418 int mStartTimeMs = parcel.readInt(); 419 mKeyObjectMap.put(type, mStartTimeMs); 420 421 type = parcel.readInt(); 422 if (type != KEY_STRUCT_TEXT) { 423 return false; 424 } 425 426 int textLen = parcel.readInt(); 427 byte[] text = parcel.createByteArray(); 428 if (text == null || text.length == 0) { 429 mTextChars = null; 430 } else { 431 mTextChars = new String(text); 432 } 433 434 } else if (type != KEY_GLOBAL_SETTING) { 435 Log.w(TAG, "Invalid timed text key found: " + type); 436 return false; 437 } 438 439 while (parcel.dataAvail() > 0) { 440 int key = parcel.readInt(); 441 if (!isValidKey(key)) { 442 Log.w(TAG, "Invalid timed text key found: " + key); 443 return false; 444 } 445 446 Object object = null; 447 448 switch (key) { 449 case KEY_STRUCT_STYLE_LIST: { 450 readStyle(parcel); 451 object = mStyleList; 452 break; 453 } 454 case KEY_STRUCT_FONT_LIST: { 455 readFont(parcel); 456 object = mFontList; 457 break; 458 } 459 case KEY_STRUCT_HIGHLIGHT_LIST: { 460 readHighlight(parcel); 461 object = mHighlightPosList; 462 break; 463 } 464 case KEY_STRUCT_KARAOKE_LIST: { 465 readKaraoke(parcel); 466 object = mKaraokeList; 467 break; 468 } 469 case KEY_STRUCT_HYPER_TEXT_LIST: { 470 readHyperText(parcel); 471 object = mHyperTextList; 472 473 break; 474 } 475 case KEY_STRUCT_BLINKING_TEXT_LIST: { 476 readBlinkingText(parcel); 477 object = mBlinkingPosList; 478 479 break; 480 } 481 case KEY_WRAP_TEXT: { 482 mWrapText = parcel.readInt(); 483 object = mWrapText; 484 break; 485 } 486 case KEY_HIGHLIGHT_COLOR_RGBA: { 487 mHighlightColorRGBA = parcel.readInt(); 488 object = mHighlightColorRGBA; 489 break; 490 } 491 case KEY_DISPLAY_FLAGS: { 492 mDisplayFlags = parcel.readInt(); 493 object = mDisplayFlags; 494 break; 495 } 496 case KEY_STRUCT_JUSTIFICATION: { 497 498 int horizontal = parcel.readInt(); 499 int vertical = parcel.readInt(); 500 mJustification = new Justification(horizontal, vertical); 501 502 object = mJustification; 503 break; 504 } 505 case KEY_BACKGROUND_COLOR_RGBA: { 506 mBackgroundColorRGBA = parcel.readInt(); 507 object = mBackgroundColorRGBA; 508 break; 509 } 510 case KEY_STRUCT_TEXT_POS: { 511 int top = parcel.readInt(); 512 int left = parcel.readInt(); 513 int bottom = parcel.readInt(); 514 int right = parcel.readInt(); 515 mTextBounds = new Rect(left, top, right, bottom); 516 517 break; 518 } 519 case KEY_SCROLL_DELAY: { 520 mScrollDelay = parcel.readInt(); 521 object = mScrollDelay; 522 break; 523 } 524 default: { 525 break; 526 } 527 } 528 529 if (object != null) { 530 if (mKeyObjectMap.containsKey(key)) { 531 mKeyObjectMap.remove(key); 532 } 533 // Previous mapping will be replaced with the new object, if there was one. 534 mKeyObjectMap.put(key, object); 535 } 536 } 537 538 return true; 539 } 540 541 /* 542 * To parse and store the Style list. 543 */ readStyle(Parcel parcel)544 private void readStyle(Parcel parcel) { 545 boolean endOfStyle = false; 546 int startChar = -1; 547 int endChar = -1; 548 int fontId = -1; 549 boolean isBold = false; 550 boolean isItalic = false; 551 boolean isUnderlined = false; 552 int fontSize = -1; 553 int colorRGBA = -1; 554 while (!endOfStyle && (parcel.dataAvail() > 0)) { 555 int key = parcel.readInt(); 556 switch (key) { 557 case KEY_START_CHAR: { 558 startChar = parcel.readInt(); 559 break; 560 } 561 case KEY_END_CHAR: { 562 endChar = parcel.readInt(); 563 break; 564 } 565 case KEY_FONT_ID: { 566 fontId = parcel.readInt(); 567 break; 568 } 569 case KEY_STYLE_FLAGS: { 570 int flags = parcel.readInt(); 571 // In the absence of any bits set in flags, the text 572 // is plain. Otherwise, 1: bold, 2: italic, 4: underline 573 isBold = ((flags % 2) == 1); 574 isItalic = ((flags % 4) >= 2); 575 isUnderlined = ((flags / 4) == 1); 576 break; 577 } 578 case KEY_FONT_SIZE: { 579 fontSize = parcel.readInt(); 580 break; 581 } 582 case KEY_TEXT_COLOR_RGBA: { 583 colorRGBA = parcel.readInt(); 584 break; 585 } 586 default: { 587 // End of the Style parsing. Reset the data position back 588 // to the position before the last parcel.readInt() call. 589 parcel.setDataPosition(parcel.dataPosition() - 4); 590 endOfStyle = true; 591 break; 592 } 593 } 594 } 595 596 Style style = new Style(startChar, endChar, fontId, isBold, 597 isItalic, isUnderlined, fontSize, colorRGBA); 598 if (mStyleList == null) { 599 mStyleList = new ArrayList<Style>(); 600 } 601 mStyleList.add(style); 602 } 603 604 /* 605 * To parse and store the Font list 606 */ readFont(Parcel parcel)607 private void readFont(Parcel parcel) { 608 int entryCount = parcel.readInt(); 609 610 for (int i = 0; i < entryCount; i++) { 611 int id = parcel.readInt(); 612 int nameLen = parcel.readInt(); 613 614 byte[] text = parcel.createByteArray(); 615 final String name = new String(text, 0, nameLen); 616 617 Font font = new Font(id, name); 618 619 if (mFontList == null) { 620 mFontList = new ArrayList<Font>(); 621 } 622 mFontList.add(font); 623 } 624 } 625 626 /* 627 * To parse and store the Highlight list 628 */ readHighlight(Parcel parcel)629 private void readHighlight(Parcel parcel) { 630 int startChar = parcel.readInt(); 631 int endChar = parcel.readInt(); 632 CharPos pos = new CharPos(startChar, endChar); 633 634 if (mHighlightPosList == null) { 635 mHighlightPosList = new ArrayList<CharPos>(); 636 } 637 mHighlightPosList.add(pos); 638 } 639 640 /* 641 * To parse and store the Karaoke list 642 */ readKaraoke(Parcel parcel)643 private void readKaraoke(Parcel parcel) { 644 int entryCount = parcel.readInt(); 645 646 for (int i = 0; i < entryCount; i++) { 647 int startTimeMs = parcel.readInt(); 648 int endTimeMs = parcel.readInt(); 649 int startChar = parcel.readInt(); 650 int endChar = parcel.readInt(); 651 Karaoke kara = new Karaoke(startTimeMs, endTimeMs, 652 startChar, endChar); 653 654 if (mKaraokeList == null) { 655 mKaraokeList = new ArrayList<Karaoke>(); 656 } 657 mKaraokeList.add(kara); 658 } 659 } 660 661 /* 662 * To parse and store HyperText list 663 */ readHyperText(Parcel parcel)664 private void readHyperText(Parcel parcel) { 665 int startChar = parcel.readInt(); 666 int endChar = parcel.readInt(); 667 668 int len = parcel.readInt(); 669 byte[] url = parcel.createByteArray(); 670 final String urlString = new String(url, 0, len); 671 672 len = parcel.readInt(); 673 byte[] alt = parcel.createByteArray(); 674 final String altString = new String(alt, 0, len); 675 HyperText hyperText = new HyperText(startChar, endChar, urlString, altString); 676 677 678 if (mHyperTextList == null) { 679 mHyperTextList = new ArrayList<HyperText>(); 680 } 681 mHyperTextList.add(hyperText); 682 } 683 684 /* 685 * To parse and store blinking text list 686 */ readBlinkingText(Parcel parcel)687 private void readBlinkingText(Parcel parcel) { 688 int startChar = parcel.readInt(); 689 int endChar = parcel.readInt(); 690 CharPos blinkingPos = new CharPos(startChar, endChar); 691 692 if (mBlinkingPosList == null) { 693 mBlinkingPosList = new ArrayList<CharPos>(); 694 } 695 mBlinkingPosList.add(blinkingPos); 696 } 697 698 /* 699 * To check whether the given key is valid. 700 * @param key the key to be checked. 701 * @return true if the key is a valid one. Otherwise, false. 702 */ isValidKey(final int key)703 private boolean isValidKey(final int key) { 704 if (!((key >= FIRST_PUBLIC_KEY) && (key <= LAST_PUBLIC_KEY)) 705 && !((key >= FIRST_PRIVATE_KEY) && (key <= LAST_PRIVATE_KEY))) { 706 return false; 707 } 708 return true; 709 } 710 711 /* 712 * To check whether the given key is contained in this TimedText object. 713 * @param key the key to be checked. 714 * @return true if the key is contained in this TimedText object. 715 * Otherwise, false. 716 */ containsKey(final int key)717 private boolean containsKey(final int key) { 718 if (isValidKey(key) && mKeyObjectMap.containsKey(key)) { 719 return true; 720 } 721 return false; 722 } 723 724 /* 725 * @return a set of the keys contained in this TimedText object. 726 */ keySet()727 private Set keySet() { 728 return mKeyObjectMap.keySet(); 729 } 730 731 /* 732 * To retrieve the object associated with the key. Caller must make sure 733 * the key is present using the containsKey method otherwise a 734 * RuntimeException will occur. 735 * @param key the key used to retrieve the object. 736 * @return an object. The object could be 1) an instance of Integer; 2) a 737 * List of CharPos, Karaoke, Font, Style, and HyperText, or 3) an instance of 738 * Justification. 739 */ 740 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getObject(final int key)741 private Object getObject(final int key) { 742 if (containsKey(key)) { 743 return mKeyObjectMap.get(key); 744 } else { 745 throw new IllegalArgumentException("Invalid key: " + key); 746 } 747 } 748 } 749