Home | History | Annotate | Download | only in media
      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 
     63     public Cea708CaptionRenderer(Context context) {
     64         mContext = context;
     65     }
     66 
     67     @Override
     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
     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 
     94     Cea708CaptionTrack(Cea708CCWidget renderingWidget, MediaFormat format) {
     95         super(format);
     96 
     97         mRenderingWidget = renderingWidget;
     98         mCCParser = new Cea708CCParser(mRenderingWidget);
     99     }
    100 
    101     @Override
    102     public void onData(byte[] data, boolean eos, long runID) {
    103         mCCParser.parse(data);
    104     }
    105 
    106     @Override
    107     public RenderingWidget getRenderingWidget() {
    108         return mRenderingWidget;
    109     }
    110 
    111     @Override
    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 -&gt; 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 -&gt; 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 -&gt; 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 
    240     Cea708CCParser(DisplayListener listener) {
    241         if (listener != null) {
    242             mListener = listener;
    243         }
    244     }
    245 
    246     interface DisplayListener {
    247         void emitEvent(CaptionEvent event);
    248     }
    249 
    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 
    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)
    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
    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 
    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 
    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 
    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 
    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
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
   1027     public Cea708CCWidget(Context context) {
   1028         this(context, null);
   1029     }
   1030 
   1031     public Cea708CCWidget(Context context, AttributeSet attrs) {
   1032         this(context, attrs, 0);
   1033     }
   1034 
   1035     public Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr) {
   1036         this(context, attrs, defStyleAttr, 0);
   1037     }
   1038 
   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
   1047     public ClosedCaptionLayout createCaptionLayout(Context context) {
   1048         return new CCLayout(context);
   1049     }
   1050 
   1051     @Override
   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
   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 
   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 
   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 
   1128             public ScaledLayoutParams(Context context, AttributeSet attrs) {
   1129                 super(MATCH_PARENT, MATCH_PARENT);
   1130             }
   1131         }
   1132 
   1133         @Override
   1134         public LayoutParams generateLayoutParams(AttributeSet attrs) {
   1135             return new ScaledLayoutParams(getContext(), attrs);
   1136         }
   1137 
   1138         @Override
   1139         protected boolean checkLayoutParams(LayoutParams p) {
   1140             return (p instanceof ScaledLayoutParams);
   1141         }
   1142 
   1143         @Override
   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
   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
   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 
   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 
   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 
   1338         public void removeViewFromSafeTitleArea(CCWindowLayout captionWindowLayout) {
   1339             mSafeTitleAreaLayout.removeView(captionWindowLayout);
   1340         }
   1341 
   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 
   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 
   1401         public CCHandler(CCLayout ccLayout) {
   1402             mCCLayout = ccLayout;
   1403             mHandler = new Handler(this);
   1404         }
   1405 
   1406         @Override
   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 
   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
   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.
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   1571         private void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) {
   1572             if (mCurrentWindowLayout != null) {
   1573                 mCurrentWindowLayout.setWindowAttr(windowAttr);
   1574             }
   1575         }
   1576 
   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
   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 
   1603         private void delayCancel() {
   1604             mIsDelayed = false;
   1605             processPendingBuffer();
   1606         }
   1607 
   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
   1616         private void sendControlToCurrentWindow(char control) {
   1617             if (mCurrentWindowLayout != null) {
   1618                 mCurrentWindowLayout.sendControl(control);
   1619             }
   1620         }
   1621 
   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
   1632         private void setPenAttr(Cea708CCParser.CaptionPenAttr attr) {
   1633             if (mCurrentWindowLayout != null) {
   1634                 mCurrentWindowLayout.setPenAttr(attr);
   1635             }
   1636         }
   1637 
   1638         private void setPenColor(Cea708CCParser.CaptionPenColor color) {
   1639             if (mCurrentWindowLayout != null) {
   1640                 mCurrentWindowLayout.setPenColor(color);
   1641             }
   1642         }
   1643 
   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 
   1693         public CCWindowLayout(Context context) {
   1694             this(context, null);
   1695         }
   1696 
   1697         public CCWindowLayout(Context context, AttributeSet attrs) {
   1698             this(context, attrs, 0);
   1699         }
   1700 
   1701         public CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
   1702             this(context, attrs, defStyleAttr, 0);
   1703         }
   1704 
   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 
   1724         public void setCaptionStyle(CaptionStyle style) {
   1725             mCaptionStyle = style;
   1726             mCCView.setCaptionStyle(style);
   1727         }
   1728 
   1729         public void setFontScale(float fontScale) {
   1730             mFontScale = fontScale;
   1731             updateTextSize();
   1732         }
   1733 
   1734         public int getCaptionWindowId() {
   1735             return mCaptionWindowId;
   1736         }
   1737 
   1738         public void setCaptionWindowId(int captionWindowId) {
   1739             mCaptionWindowId = captionWindowId;
   1740         }
   1741 
   1742         public void clear() {
   1743             clearText();
   1744             hide();
   1745         }
   1746 
   1747         public void show() {
   1748             setVisibility(View.VISIBLE);
   1749             requestLayout();
   1750         }
   1751 
   1752         public void hide() {
   1753             setVisibility(View.INVISIBLE);
   1754             requestLayout();
   1755         }
   1756 
   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 
   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 
   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 
   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 
   1802         public void sendBuffer(String buffer) {
   1803             appendText(buffer);
   1804         }
   1805 
   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          */
   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
   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 
   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 
   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 
   2040         private int getScreenColumnCount() {
   2041             // Assume it has a wide aspect ratio track.
   2042             return MAX_COLUMN_COUNT_16_9;
   2043         }
   2044 
   2045         public void removeFromCaptionView() {
   2046             if (mCCLayout != null) {
   2047                 mCCLayout.removeViewFromSafeTitleArea(this);
   2048                 mCCLayout.removeOnLayoutChangeListener(this);
   2049                 mCCLayout = null;
   2050             }
   2051         }
   2052 
   2053         public void setText(String text) {
   2054             updateText(text, false);
   2055         }
   2056 
   2057         public void appendText(String text) {
   2058             updateText(text, true);
   2059         }
   2060 
   2061         public void clearText() {
   2062             mBuilder.clear();
   2063             mCCView.setText("");
   2064         }
   2065 
   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 
   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 
   2122         public CCView(Context context) {
   2123             this(context, null);
   2124         }
   2125 
   2126         public CCView(Context context, AttributeSet attrs) {
   2127             this(context, attrs, 0);
   2128         }
   2129 
   2130         public CCView(Context context, AttributeSet attrs, int defStyleAttr) {
   2131             this(context, attrs, defStyleAttr, 0);
   2132         }
   2133 
   2134         public CCView(Context context, AttributeSet attrs, int defStyleAttr,
   2135                 int defStyleRes) {
   2136             super(context, attrs, defStyleAttr, defStyleRes);
   2137         }
   2138 
   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