public class

Cea708CaptionRenderer

extends SubtitleController.Renderer

 java.lang.Object

androidx.media2.player.subtitle.SubtitleController.Renderer

↳androidx.media2.player.subtitle.Cea708CaptionRenderer

Summary

Constructors
publicCea708CaptionRenderer(Context context)

Methods
public abstract SubtitleTrackcreateTrack(MediaFormat format)

Called by MediaPlayer's SubtitleController for each subtitle track that was detected and is supported by this object to create a SubtitleTrack object.

public abstract booleansupports(MediaFormat format)

Called by MediaPlayer's SubtitleController when a new subtitle track is detected, to see if it should use this object to parse and display this subtitle track.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public Cea708CaptionRenderer(Context context)

Methods

public abstract boolean supports(MediaFormat format)

Called by MediaPlayer's SubtitleController when a new subtitle track is detected, to see if it should use this object to parse and display this subtitle track.

Parameters:

format: the format of the track that will include at least the MIME type .

Returns:

true if and only if the track format is supported by this renderer

public abstract SubtitleTrack createTrack(MediaFormat format)

Called by MediaPlayer's SubtitleController for each subtitle track that was detected and is supported by this object to create a SubtitleTrack object. This object will be created for each track that was found. If the track is selected for display, this object will be used to parse and display the track data.

Parameters:

format: the format of the track that will include at least the MIME type .

Returns:

a SubtitleTrack object that will be used to parse and render the subtitle track.

Source

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.media2.player.subtitle;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.CaptioningManager;
import android.view.accessibility.CaptioningManager.CaptionStyle;
import android.widget.RelativeLayout;
import android.widget.TextView;

import androidx.annotation.RestrictTo;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

// Note: This is forked from android.media.Cea708CaptionRenderer since P
/** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class Cea708CaptionRenderer extends SubtitleController.Renderer {
    private final Context mContext;
    private Cea708CCWidget mCCWidget;

    public Cea708CaptionRenderer(Context context) {
        mContext = context;
    }

    @Override
    public boolean supports(MediaFormat format) {
        if (format.containsKey(MediaFormat.KEY_MIME)) {
            String mimeType = format.getString(MediaFormat.KEY_MIME);
            return MediaFormat.MIMETYPE_TEXT_CEA_708.equals(mimeType);
        }
        return false;
    }

    @Override
    public SubtitleTrack createTrack(MediaFormat format) {
        String mimeType = format.getString(MediaFormat.KEY_MIME);
        if (MediaFormat.MIMETYPE_TEXT_CEA_708.equals(mimeType)) {
            if (mCCWidget == null) {
                mCCWidget = new Cea708CCWidget(mContext);
            }
            return new Cea708CaptionTrack(mCCWidget, format);
        }
        throw new RuntimeException("No matching format: " + format.toString());
    }

    static class Cea708CaptionTrack extends SubtitleTrack {
        private final Cea708CCParser mCCParser;
        private final Cea708CCWidget mRenderingWidget;

        Cea708CaptionTrack(Cea708CCWidget renderingWidget, MediaFormat format) {
            super(format);

            mRenderingWidget = renderingWidget;
            mCCParser = new Cea708CCParser(mRenderingWidget);
        }

        @Override
        public void onData(byte[] data, boolean eos, long runID) {
            mCCParser.parse(data);
        }

        @Override
        public RenderingWidget getRenderingWidget() {
            return mRenderingWidget;
        }

        @Override
        public void updateView(ArrayList<Cue> activeCues) {
            // Overriding with NO-OP, CC rendering by-passes this
        }
    }

    /**
     * Widget capable of rendering CEA-708 closed captions.
     */
    class Cea708CCWidget extends ClosedCaptionWidget implements Cea708CCParser.DisplayListener {
        private final CCHandler mCCHandler;

        Cea708CCWidget(Context context) {
            this(context, null);
        }

        Cea708CCWidget(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }

        Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);

            mCCHandler = new CCHandler((CCLayout) mClosedCaptionLayout);
        }

        @Override
        public ClosedCaptionLayout createCaptionLayout(Context context) {
            return new CCLayout(context);
        }

        @Override
        public void emitEvent(Cea708CCParser.CaptionEvent event) {
            mCCHandler.processCaptionEvent(event);

            setSize(getWidth(), getHeight());

            if (mListener != null) {
                mListener.onChanged(this);
            }
        }

        @Override
        public void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            ((ViewGroup) mClosedCaptionLayout).draw(canvas);
        }

        /**
         * A layout that scales its children using the given percentage value.
         */
        class ScaledLayout extends ViewGroup {
            private static final String TAG = "ScaledLayout";
            private static final boolean DEBUG = false;
            private final Comparator<Rect> mRectTopLeftSorter = new Comparator<Rect>() {
                @Override
                public int compare(Rect lhs, Rect rhs) {
                    if (lhs.top != rhs.top) {
                        return lhs.top - rhs.top;
                    } else {
                        return lhs.left - rhs.left;
                    }
                }
            };

            private Rect[] mRectArray;

            ScaledLayout(Context context) {
                super(context);
            }

            /**
             * ScaledLayoutParams stores the four scale factors.
             * <br>
             * Vertical coordinate system:   (scaleStartRow * 100) % ~ (scaleEndRow * 100) %
             * Horizontal coordinate system: (scaleStartCol * 100) % ~ (scaleEndCol * 100) %
             * <br>
             * In XML, for example,
             * <pre>
             * {@code
             * <View
             *     app:layout_scaleStartRow="0.1"
             *     app:layout_scaleEndRow="0.5"
             *     app:layout_scaleStartCol="0.4"
             *     app:layout_scaleEndCol="1" />
             * }
             * </pre>
             */
            class ScaledLayoutParams extends ViewGroup.LayoutParams {
                public static final float SCALE_UNSPECIFIED = -1;
                public float scaleStartRow;
                public float scaleEndRow;
                public float scaleStartCol;
                public float scaleEndCol;

                ScaledLayoutParams(float scaleStartRow, float scaleEndRow,
                        float scaleStartCol, float scaleEndCol) {
                    super(MATCH_PARENT, MATCH_PARENT);
                    this.scaleStartRow = scaleStartRow;
                    this.scaleEndRow = scaleEndRow;
                    this.scaleStartCol = scaleStartCol;
                    this.scaleEndCol = scaleEndCol;
                }

                ScaledLayoutParams(Context context, AttributeSet attrs) {
                    super(MATCH_PARENT, MATCH_PARENT);
                }
            }

            @Override
            public LayoutParams generateLayoutParams(AttributeSet attrs) {
                return new ScaledLayoutParams(getContext(), attrs);
            }

            @Override
            protected boolean checkLayoutParams(LayoutParams p) {
                return (p instanceof ScaledLayoutParams);
            }

            @Override
            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
                int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
                int width = widthSpecSize - getPaddingLeft() - getPaddingRight();
                int height = heightSpecSize - getPaddingTop() - getPaddingBottom();
                if (DEBUG) {
                    Log.d(TAG, String.format("onMeasure width: %d, height: %d", width, height));
                }
                int count = getChildCount();
                mRectArray = new Rect[count];
                for (int i = 0; i < count; ++i) {
                    View child = getChildAt(i);
                    ViewGroup.LayoutParams params = child.getLayoutParams();
                    float scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol;
                    if (!(params instanceof ScaledLayoutParams)) {
                        throw new RuntimeException("A child of ScaledLayout cannot have the "
                                + "UNSPECIFIED scale factors");
                    }
                    scaleStartRow = ((ScaledLayoutParams) params).scaleStartRow;
                    scaleEndRow = ((ScaledLayoutParams) params).scaleEndRow;
                    scaleStartCol = ((ScaledLayoutParams) params).scaleStartCol;
                    scaleEndCol = ((ScaledLayoutParams) params).scaleEndCol;
                    if (scaleStartRow < 0 || scaleStartRow > 1) {
                        throw new RuntimeException("A child of ScaledLayout should have a range of "
                                + "scaleStartRow between 0 and 1");
                    }
                    if (scaleEndRow < scaleStartRow || scaleStartRow > 1) {
                        throw new RuntimeException("A child of ScaledLayout should have a range of "
                                + "scaleEndRow between scaleStartRow and 1");
                    }
                    if (scaleEndCol < 0 || scaleEndCol > 1) {
                        throw new RuntimeException("A child of ScaledLayout should have a range of "
                                + "scaleStartCol between 0 and 1");
                    }
                    if (scaleEndCol < scaleStartCol || scaleEndCol > 1) {
                        throw new RuntimeException("A child of ScaledLayout should have a range of "
                                + "scaleEndCol between scaleStartCol and 1");
                    }
                    if (DEBUG) {
                        Log.d(TAG, String.format("onMeasure child scaleStartRow: %f scaleEndRow: "
                                        + "%f scaleStartCol: %f scaleEndCol: %f",
                                scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
                    }
                    mRectArray[i] = new Rect((int) (scaleStartCol * width), (int) (scaleStartRow
                            * height), (int) (scaleEndCol * width), (int) (scaleEndRow * height));
                    int childWidthSpec = MeasureSpec.makeMeasureSpec(
                            (int) (width * (scaleEndCol - scaleStartCol)), MeasureSpec.EXACTLY);
                    int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                    child.measure(childWidthSpec, childHeightSpec);

                    // If the height of the measured child view is bigger than the height of the
                    // calculated region by the given ScaleLayoutParams, the height of the region
                    // should be increased to fit the size of the child view.
                    if (child.getMeasuredHeight() > mRectArray[i].height()) {
                        int overflowedHeight = child.getMeasuredHeight() - mRectArray[i].height();
                        overflowedHeight = (overflowedHeight + 1) / 2;
                        mRectArray[i].bottom += overflowedHeight;
                        mRectArray[i].top -= overflowedHeight;
                        if (mRectArray[i].top < 0) {
                            mRectArray[i].bottom -= mRectArray[i].top;
                            mRectArray[i].top = 0;
                        }
                        if (mRectArray[i].bottom > height) {
                            mRectArray[i].top -= mRectArray[i].bottom - height;
                            mRectArray[i].bottom = height;
                        }
                    }
                    childHeightSpec = MeasureSpec.makeMeasureSpec(
                            (int) (height * (scaleEndRow - scaleStartRow)), MeasureSpec.EXACTLY);
                    child.measure(childWidthSpec, childHeightSpec);
                }

                // Avoid overlapping rectangles.
                // Step 1. Sort rectangles by position (top-left).
                int visibleRectCount = 0;
                int[] visibleRectGroup = new int[count];
                Rect[] visibleRectArray = new Rect[count];
                for (int i = 0; i < count; ++i) {
                    if (getChildAt(i).getVisibility() == View.VISIBLE) {
                        visibleRectGroup[visibleRectCount] = visibleRectCount;
                        visibleRectArray[visibleRectCount] = mRectArray[i];
                        ++visibleRectCount;
                    }
                }
                Arrays.sort(visibleRectArray, 0, visibleRectCount, mRectTopLeftSorter);

                // Step 2. Move down if there are overlapping rectangles.
                for (int i = 0; i < visibleRectCount - 1; ++i) {
                    for (int j = i + 1; j < visibleRectCount; ++j) {
                        if (Rect.intersects(visibleRectArray[i], visibleRectArray[j])) {
                            visibleRectGroup[j] = visibleRectGroup[i];
                            visibleRectArray[j].set(visibleRectArray[j].left,
                                    visibleRectArray[i].bottom,
                                    visibleRectArray[j].right,
                                    visibleRectArray[i].bottom + visibleRectArray[j].height());
                        }
                    }
                }

                // Step 3. Move up if there is any overflowed rectangle.
                for (int i = visibleRectCount - 1; i >= 0; --i) {
                    if (visibleRectArray[i].bottom > height) {
                        int overflowedHeight = visibleRectArray[i].bottom - height;
                        for (int j = 0; j <= i; ++j) {
                            if (visibleRectGroup[i] == visibleRectGroup[j]) {
                                visibleRectArray[j].set(visibleRectArray[j].left,
                                        visibleRectArray[j].top - overflowedHeight,
                                        visibleRectArray[j].right,
                                        visibleRectArray[j].bottom - overflowedHeight);
                            }
                        }
                    }
                }
                setMeasuredDimension(widthSpecSize, heightSpecSize);
            }

            @Override
            protected void onLayout(boolean changed, int l, int t, int r, int b) {
                int paddingLeft = getPaddingLeft();
                int paddingTop = getPaddingTop();
                int count = getChildCount();
                for (int i = 0; i < count; ++i) {
                    View child = getChildAt(i);
                    if (child.getVisibility() != GONE) {
                        int childLeft = paddingLeft + mRectArray[i].left;
                        int childTop = paddingTop + mRectArray[i].top;
                        int childBottom = paddingLeft + mRectArray[i].bottom;
                        int childRight = paddingTop + mRectArray[i].right;
                        if (DEBUG) {
                            Log.d(TAG, String.format(
                                    "child layout bottom: %d left: %d right: %d top: %d",
                                    childBottom, childLeft, childRight, childTop));
                        }
                        child.layout(childLeft, childTop, childRight, childBottom);
                    }
                }
            }

            @Override
            public void dispatchDraw(Canvas canvas) {
                int paddingLeft = getPaddingLeft();
                int paddingTop = getPaddingTop();
                int count = getChildCount();
                for (int i = 0; i < count; ++i) {
                    View child = getChildAt(i);
                    if (child.getVisibility() != GONE) {
                        if (i >= mRectArray.length) {
                            break;
                        }
                        int childLeft = paddingLeft + mRectArray[i].left;
                        int childTop = paddingTop + mRectArray[i].top;
                        final int saveCount = canvas.save();
                        canvas.translate(childLeft, childTop);
                        child.draw(canvas);
                        canvas.restoreToCount(saveCount);
                    }
                }
            }
        }

        /**
         * Layout containing the safe title area that helps the closed captions look more prominent.
         *
         * <p>This is required by CEA-708B.
         */
        class CCLayout extends ScaledLayout implements ClosedCaptionLayout {
            private static final float SAFE_TITLE_AREA_SCALE_START_X = 0.1f;
            private static final float SAFE_TITLE_AREA_SCALE_END_X = 0.9f;
            private static final float SAFE_TITLE_AREA_SCALE_START_Y = 0.1f;
            private static final float SAFE_TITLE_AREA_SCALE_END_Y = 0.9f;

            private final ScaledLayout mSafeTitleAreaLayout;

            CCLayout(Context context) {
                super(context);

                mSafeTitleAreaLayout = new ScaledLayout(context);
                addView(mSafeTitleAreaLayout, new ScaledLayout.ScaledLayoutParams(
                        SAFE_TITLE_AREA_SCALE_START_X, SAFE_TITLE_AREA_SCALE_END_X,
                        SAFE_TITLE_AREA_SCALE_START_Y, SAFE_TITLE_AREA_SCALE_END_Y));
            }

            public void addOrUpdateViewToSafeTitleArea(CCWindowLayout captionWindowLayout,
                    ScaledLayoutParams scaledLayoutParams) {
                int index = mSafeTitleAreaLayout.indexOfChild(captionWindowLayout);
                if (index < 0) {
                    mSafeTitleAreaLayout.addView(captionWindowLayout, scaledLayoutParams);
                    return;
                }
                mSafeTitleAreaLayout.updateViewLayout(captionWindowLayout, scaledLayoutParams);
            }

            public void removeViewFromSafeTitleArea(CCWindowLayout captionWindowLayout) {
                mSafeTitleAreaLayout.removeView(captionWindowLayout);
            }

            @Override
            public void setCaptionStyle(CaptionStyle style) {
                final int count = mSafeTitleAreaLayout.getChildCount();
                for (int i = 0; i < count; ++i) {
                    final CCWindowLayout windowLayout =
                            (CCWindowLayout) mSafeTitleAreaLayout.getChildAt(i);
                    windowLayout.setCaptionStyle(style);
                }
            }

            @Override
            public void setFontScale(float fontScale) {
                final int count = mSafeTitleAreaLayout.getChildCount();
                for (int i = 0; i < count; ++i) {
                    final CCWindowLayout windowLayout =
                            (CCWindowLayout) mSafeTitleAreaLayout.getChildAt(i);
                    windowLayout.setFontScale(fontScale);
                }
            }
        }

        /**
         * Renders the selected CC track.
         */
        class CCHandler implements Handler.Callback {
            // TODO: Remaining works
            // CaptionTrackRenderer does not support the full spec of CEA-708.
            // The remaining works are described in the follows.
            // C0 Table: Backspace, FF, and HCR are not supported. The rule for P16 is not
            //           standardized but it is handled as EUC-KR charset for Korea broadcasting.
            // C1 Table: All the styles of windows and pens except underline, italic, pen size,
            //           and pen offset specified in CEA-708 are ignored and this follows system
            //           wide CC preferences for look and feel. SetPenLocation is not implemented.
            // G2 Table: TSP, NBTSP and BLK are not supported.
            // Text/commands: Word wrapping, fonts, row and column locking are not supported.

            private static final String TAG = "CCHandler";
            private static final boolean DEBUG = false;

            private static final int TENTHS_OF_SECOND_IN_MILLIS = 100;

            // According to CEA-708B, there can exist up to 8 caption windows.
            private static final int CAPTION_WINDOWS_MAX = 8;
            private static final int CAPTION_ALL_WINDOWS_BITMAP = 255;

            private static final int MSG_DELAY_CANCEL = 1;
            private static final int MSG_CAPTION_CLEAR = 2;

            private static final long CAPTION_CLEAR_INTERVAL_MS = 60000;

            private final CCLayout mCCLayout;
            private boolean mIsDelayed = false;
            private CCWindowLayout mCurrentWindowLayout;
            private final CCWindowLayout[] mCaptionWindowLayouts =
                    new CCWindowLayout[CAPTION_WINDOWS_MAX];
            private final ArrayList<Cea708CCParser.CaptionEvent> mPendingCaptionEvents =
                    new ArrayList<>();
            private final Handler mHandler;

            CCHandler(CCLayout ccLayout) {
                mCCLayout = ccLayout;
                mHandler = new Handler(this);
            }

            @Override
            public boolean handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_DELAY_CANCEL:
                        delayCancel();
                        return true;
                    case MSG_CAPTION_CLEAR:
                        clearWindows(CAPTION_ALL_WINDOWS_BITMAP);
                        return true;
                }
                return false;
            }

            public void processCaptionEvent(Cea708CCParser.CaptionEvent event) {
                if (mIsDelayed) {
                    mPendingCaptionEvents.add(event);
                    return;
                }
                switch (event.type) {
                    case Cea708CCParser.CAPTION_EMIT_TYPE_BUFFER:
                        sendBufferToCurrentWindow((String) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_CONTROL:
                        sendControlToCurrentWindow((char) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_CWX:
                        setCurrentWindowLayout((int) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_CLW:
                        clearWindows((int) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DSW:
                        displayWindows((int) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_HDW:
                        hideWindows((int) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_TGW:
                        toggleWindows((int) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLW:
                        deleteWindows((int) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLY:
                        delay((int) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLC:
                        delayCancel();
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_RST:
                        reset();
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPA:
                        setPenAttr((Cea708CCParser.CaptionPenAttr) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPC:
                        setPenColor((Cea708CCParser.CaptionPenColor) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPL:
                        setPenLocation((Cea708CCParser.CaptionPenLocation) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SWA:
                        setWindowAttr((Cea708CCParser.CaptionWindowAttr) event.obj);
                        break;
                    case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DFX:
                        defineWindow((Cea708CCParser.CaptionWindow) event.obj);
                        break;
                }
            }

            // The window related caption commands
            private void setCurrentWindowLayout(int windowId) {
                if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) {
                    return;
                }
                CCWindowLayout windowLayout = mCaptionWindowLayouts[windowId];
                if (windowLayout == null) {
                    return;
                }
                if (DEBUG) {
                    Log.d(TAG, "setCurrentWindowLayout to " + windowId);
                }
                mCurrentWindowLayout = windowLayout;
            }

            // Each bit of windowBitmap indicates a window.
            // If a bit is set, the window id is the same as the number of the trailing zeros of the
            // bit.
            private ArrayList<CCWindowLayout> getWindowsFromBitmap(int windowBitmap) {
                ArrayList<CCWindowLayout> windows = new ArrayList<>();
                for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) {
                    if ((windowBitmap & (1 << i)) != 0) {
                        CCWindowLayout windowLayout = mCaptionWindowLayouts[i];
                        if (windowLayout != null) {
                            windows.add(windowLayout);
                        }
                    }
                }
                return windows;
            }

            private void clearWindows(int windowBitmap) {
                if (windowBitmap == 0) {
                    return;
                }
                for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
                    windowLayout.clear();
                }
            }

            private void displayWindows(int windowBitmap) {
                if (windowBitmap == 0) {
                    return;
                }
                for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
                    windowLayout.show();
                }
            }

            private void hideWindows(int windowBitmap) {
                if (windowBitmap == 0) {
                    return;
                }
                for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
                    windowLayout.hide();
                }
            }

            private void toggleWindows(int windowBitmap) {
                if (windowBitmap == 0) {
                    return;
                }
                for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
                    if (windowLayout.isShown()) {
                        windowLayout.hide();
                    } else {
                        windowLayout.show();
                    }
                }
            }

            private void deleteWindows(int windowBitmap) {
                if (windowBitmap == 0) {
                    return;
                }
                for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
                    windowLayout.removeFromCaptionView();
                    mCaptionWindowLayouts[windowLayout.getCaptionWindowId()] = null;
                }
            }

            public void reset() {
                mCurrentWindowLayout = null;
                mIsDelayed = false;
                mPendingCaptionEvents.clear();
                for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) {
                    if (mCaptionWindowLayouts[i] != null) {
                        mCaptionWindowLayouts[i].removeFromCaptionView();
                    }
                    mCaptionWindowLayouts[i] = null;
                }
                mCCLayout.setVisibility(View.INVISIBLE);
                mHandler.removeMessages(MSG_CAPTION_CLEAR);
            }

            private void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) {
                if (mCurrentWindowLayout != null) {
                    mCurrentWindowLayout.setWindowAttr(windowAttr);
                }
            }

            private void defineWindow(Cea708CCParser.CaptionWindow window) {
                if (window == null) {
                    return;
                }
                int windowId = window.id;
                if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) {
                    return;
                }
                CCWindowLayout windowLayout = mCaptionWindowLayouts[windowId];
                if (windowLayout == null) {
                    windowLayout = new CCWindowLayout(mCCLayout.getContext());
                }
                windowLayout.initWindow(mCCLayout, window);
                mCurrentWindowLayout = mCaptionWindowLayouts[windowId] = windowLayout;
            }

            // The job related caption commands
            private void delay(int tenthsOfSeconds) {
                if (tenthsOfSeconds < 0 || tenthsOfSeconds > 255) {
                    return;
                }
                mIsDelayed = true;
                mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DELAY_CANCEL),
                        tenthsOfSeconds * TENTHS_OF_SECOND_IN_MILLIS);
            }

            private void delayCancel() {
                mIsDelayed = false;
                processPendingBuffer();
            }

            private void processPendingBuffer() {
                for (Cea708CCParser.CaptionEvent event : mPendingCaptionEvents) {
                    processCaptionEvent(event);
                }
                mPendingCaptionEvents.clear();
            }

            // The implicit write caption commands
            private void sendControlToCurrentWindow(char control) {
                if (mCurrentWindowLayout != null) {
                    mCurrentWindowLayout.sendControl(control);
                }
            }

            private void sendBufferToCurrentWindow(String buffer) {
                if (mCurrentWindowLayout != null) {
                    mCurrentWindowLayout.sendBuffer(buffer);
                    mHandler.removeMessages(MSG_CAPTION_CLEAR);
                    mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CAPTION_CLEAR),
                            CAPTION_CLEAR_INTERVAL_MS);
                }
            }

            // The pen related caption commands
            private void setPenAttr(Cea708CCParser.CaptionPenAttr attr) {
                if (mCurrentWindowLayout != null) {
                    mCurrentWindowLayout.setPenAttr(attr);
                }
            }

            private void setPenColor(Cea708CCParser.CaptionPenColor color) {
                if (mCurrentWindowLayout != null) {
                    mCurrentWindowLayout.setPenColor(color);
                }
            }

            private void setPenLocation(Cea708CCParser.CaptionPenLocation location) {
                if (mCurrentWindowLayout != null) {
                    mCurrentWindowLayout.setPenLocation(location.row, location.column);
                }
            }
        }

        /**
         * Layout which renders a caption window of CEA-708B. It contains a {@link TextView} that
         * takes care of displaying the actual CC text.
         */
        private class CCWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener {
            private static final String TAG = "CCWindowLayout";

            private static final float PROPORTION_PEN_SIZE_SMALL = .75f;
            private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f;

            // The following values indicates the maximum cell number of a window.
            private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99;
            private static final int ANCHOR_VERTICAL_MAX = 74;
            private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209;
            private static final int MAX_COLUMN_COUNT_16_9 = 42;

            // The following values indicates a gravity of a window.
            private static final int ANCHOR_MODE_DIVIDER = 3;
            private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0;
            private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1;
            private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2;
            private static final int ANCHOR_VERTICAL_MODE_TOP = 0;
            private static final int ANCHOR_VERTICAL_MODE_CENTER = 1;
            private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2;

            private CCLayout mCCLayout;

            private CCView mCCView;
            private CaptionStyle mCaptionStyle;
            private int mRowLimit = 0;
            private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
            private final List<CharacterStyle> mCharacterStyles = new ArrayList<>();
            private int mCaptionWindowId;
            private int mRow = -1;
            private float mFontScale;
            private float mTextSize;
            private String mWidestChar;
            private int mLastCaptionLayoutWidth;
            private int mLastCaptionLayoutHeight;

            CCWindowLayout(Context context) {
                this(context, null);
            }

            CCWindowLayout(Context context, AttributeSet attrs) {
                this(context, attrs, 0);
            }

            CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
                super(context, attrs, defStyleAttr);

                // Add a subtitle view to the layout.
                mCCView = new CCView(context);
                LayoutParams params = new RelativeLayout.LayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                addView(mCCView, params);

                // Set the system wide CC preferences to the subtitle view.
                CaptioningManager captioningManager =
                        (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
                mFontScale = captioningManager.getFontScale();
                setCaptionStyle(captioningManager.getUserStyle());
                mCCView.setText("");
                updateWidestChar();
            }

            public void setCaptionStyle(CaptionStyle style) {
                mCaptionStyle = style;
                mCCView.setCaptionStyle(style);
            }

            public void setFontScale(float fontScale) {
                mFontScale = fontScale;
                updateTextSize();
            }

            public int getCaptionWindowId() {
                return mCaptionWindowId;
            }

            public void setCaptionWindowId(int captionWindowId) {
                mCaptionWindowId = captionWindowId;
            }

            public void clear() {
                clearText();
                hide();
            }

            public void show() {
                setVisibility(View.VISIBLE);
                requestLayout();
            }

            public void hide() {
                setVisibility(View.INVISIBLE);
                requestLayout();
            }

            public void setPenAttr(Cea708CCParser.CaptionPenAttr penAttr) {
                mCharacterStyles.clear();
                if (penAttr.italic) {
                    mCharacterStyles.add(new StyleSpan(Typeface.ITALIC));
                }
                if (penAttr.underline) {
                    mCharacterStyles.add(new UnderlineSpan());
                }
                switch (penAttr.penSize) {
                    case Cea708CCParser.CaptionPenAttr.PEN_SIZE_SMALL:
                        mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL));
                        break;
                    case Cea708CCParser.CaptionPenAttr.PEN_SIZE_LARGE:
                        mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE));
                        break;
                }
                switch (penAttr.penOffset) {
                    case Cea708CCParser.CaptionPenAttr.OFFSET_SUBSCRIPT:
                        mCharacterStyles.add(new SubscriptSpan());
                        break;
                    case Cea708CCParser.CaptionPenAttr.OFFSET_SUPERSCRIPT:
                        mCharacterStyles.add(new SuperscriptSpan());
                        break;
                }
            }

            public void setPenColor(Cea708CCParser.CaptionPenColor penColor) {
                // TODO: apply pen colors or skip this and use the style of system wide CC style
                // as is.
            }

            public void setPenLocation(int row, int column) {
                // TODO: change the location of pen based on row and column both.
                if (mRow >= 0) {
                    for (int r = mRow; r < row; ++r) {
                        appendText("\n");
                    }
                }
                mRow = row;
            }

            public void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) {
                // TODO: apply window attrs or skip this and use the style of system wide CC style
                // as is.
            }

            public void sendBuffer(String buffer) {
                appendText(buffer);
            }

            public void sendControl(char control) {
                // TODO: there are a bunch of ASCII-style control codes.
            }

            /**
             * This method places the window on a given CaptionLayout along with the anchor of the
             * window.
             * <p>
             * According to CEA-708B, the anchor id indicates the gravity of the window as the
             * follows.
             * For example, A value 7 of a anchor id says that a window is align with its parent
             * bottom and is located at the center horizontally of its parent.
             * </p>
             * <h4>Anchor id and the gravity of a window</h4>
             * <table>
             *     <tr>
             *         <th>GRAVITY</th>
             *         <th>LEFT</th>
             *         <th>CENTER_HORIZONTAL</th>
             *         <th>RIGHT</th>
             *     </tr>
             *     <tr>
             *         <th>TOP</th>
             *         <td>0</td>
             *         <td>1</td>
             *         <td>2</td>
             *     </tr>
             *     <tr>
             *         <th>CENTER_VERTICAL</th>
             *         <td>3</td>
             *         <td>4</td>
             *         <td>5</td>
             *     </tr>
             *     <tr>
             *         <th>BOTTOM</th>
             *         <td>6</td>
             *         <td>7</td>
             *         <td>8</td>
             *     </tr>
             * </table>
             * <p>
             * In order to handle the gravity of a window, there are two steps.
             * First, set the size of the window. Since the window will be positioned at
             * ScaledLayout, the size factors are determined in a ratio.
             * Second, set the gravity of the window. CaptionWindowLayout is inherited from
             * RelativeLayout. Hence, we could set the gravity of its child view, SubtitleView.
             * </p>
             * <p>
             * The gravity of the window is also related to its size. When it should be pushed to
             * one of the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point
             * should be a boundary of the window. When it should be pushed
             * in the horizontal/vertical center of its container, the horizontal/vertical center
             * point of the window should be the same as the anchor point.
             * </p>
             *
             * @param ccLayout a given CaptionLayout, which contains a safe title area.
             * @param captionWindow a given CaptionWindow, which stores the construction info of the
             *                      window.
             */
            public void initWindow(CCLayout ccLayout, Cea708CCParser.CaptionWindow captionWindow) {
                if (mCCLayout != ccLayout) {
                    if (mCCLayout != null) {
                        mCCLayout.removeOnLayoutChangeListener(this);
                    }
                    mCCLayout = ccLayout;
                    mCCLayout.addOnLayoutChangeListener(this);
                    updateWidestChar();
                }

                // Both anchor vertical and horizontal indicates the position cell number of
                // the window.
                float scaleRow = (float) captionWindow.anchorVertical
                        / (captionWindow.relativePositioning
                                ? ANCHOR_RELATIVE_POSITIONING_MAX : ANCHOR_VERTICAL_MAX);

                // Assumes it has a wide aspect ratio track.
                float scaleCol = (float) captionWindow.anchorHorizontal
                        / (captionWindow.relativePositioning ? ANCHOR_RELATIVE_POSITIONING_MAX
                                : ANCHOR_HORIZONTAL_16_9_MAX);

                // The range of scaleRow/Col need to be verified to be in [0, 1].
                // Otherwise a RuntimeException will be raised in ScaledLayout.
                if (scaleRow < 0 || scaleRow > 1) {
                    Log.i(TAG, "The vertical position of the anchor point should be at the "
                            + "range of 0 and 1 but " + scaleRow);
                    scaleRow = Math.max(0, Math.min(scaleRow, 1));
                }
                if (scaleCol < 0 || scaleCol > 1) {
                    Log.i(TAG, "The horizontal position of the anchor point should be at the "
                            + "range of 0 and 1 but " + scaleCol);
                    scaleCol = Math.max(0, Math.min(scaleCol, 1));
                }
                int gravity = Gravity.CENTER;
                int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER;
                int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER;
                float scaleStartRow = 0;
                float scaleEndRow = 1;
                float scaleStartCol = 0;
                float scaleEndCol = 1;
                switch (horizontalMode) {
                    case ANCHOR_HORIZONTAL_MODE_LEFT:
                        gravity = Gravity.LEFT;
                        mCCView.setAlignment(Alignment.ALIGN_NORMAL);
                        scaleStartCol = scaleCol;
                        break;
                    case ANCHOR_HORIZONTAL_MODE_CENTER:
                        float gap = Math.min(1 - scaleCol, scaleCol);

                        // Since all TV sets use left text alignment instead of center text
                        // alignment for this case, we follow the industry convention if possible.
                        int columnCount = captionWindow.columnCount + 1;
                        columnCount = Math.min(getScreenColumnCount(), columnCount);
                        StringBuilder widestTextBuilder = new StringBuilder();
                        for (int i = 0; i < columnCount; ++i) {
                            widestTextBuilder.append(mWidestChar);
                        }
                        Paint paint = new Paint();
                        paint.setTypeface(mCaptionStyle.getTypeface());
                        paint.setTextSize(mTextSize);
                        float maxWindowWidth = paint.measureText(widestTextBuilder.toString());
                        float halfMaxWidthScale = mCCLayout.getWidth() > 0
                                ? maxWindowWidth / 2.0f / (mCCLayout.getWidth() * 0.8f) : 0.0f;
                        if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) {
                            // Calculate the expected max window size based on the column count of
                            // the caption window multiplied by average alphabets char width,
                            // then align the left side of the window with the left side of
                            // the expected max window.
                            gravity = Gravity.LEFT;
                            mCCView.setAlignment(Alignment.ALIGN_NORMAL);
                            scaleStartCol = scaleCol - halfMaxWidthScale;
                            scaleEndCol = 1.0f;
                        } else {
                            // The gap will be the minimum distance value of the distances from both
                            // horizontal end points to the anchor point.
                            // If scaleCol <= 0.5, the range of scaleCol is
                            // [0, the anchor point * 2].
                            // If scaleCol > 0.5, the range of scaleCol is
                            // [(1 - the anchor point) * 2, 1].
                            // The anchor point is located at the horizontal center of the window in
                            // both cases.
                            gravity = Gravity.CENTER_HORIZONTAL;
                            mCCView.setAlignment(Alignment.ALIGN_CENTER);
                            scaleStartCol = scaleCol - gap;
                            scaleEndCol = scaleCol + gap;
                        }
                        break;
                    case ANCHOR_HORIZONTAL_MODE_RIGHT:
                        gravity = Gravity.RIGHT;
                        // TODO: Alignment.ALIGN_RIGHT is hidden. Implement setAlignment()
                        // in a different way.
                        // mCCView.setAlignment(Alignment.ALIGN_RIGHT);
                        scaleEndCol = scaleCol;
                        break;
                }
                switch (verticalMode) {
                    case ANCHOR_VERTICAL_MODE_TOP:
                        gravity |= Gravity.TOP;
                        scaleStartRow = scaleRow;
                        break;
                    case ANCHOR_VERTICAL_MODE_CENTER:
                        gravity |= Gravity.CENTER_VERTICAL;

                        // See the above comment.
                        float gap = Math.min(1 - scaleRow, scaleRow);
                        scaleStartRow = scaleRow - gap;
                        scaleEndRow = scaleRow + gap;
                        break;
                    case ANCHOR_VERTICAL_MODE_BOTTOM:
                        gravity |= Gravity.BOTTOM;
                        scaleEndRow = scaleRow;
                        break;
                }
                mCCLayout.addOrUpdateViewToSafeTitleArea(this,
                        mCCLayout.new ScaledLayoutParams(
                                scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
                setCaptionWindowId(captionWindow.id);
                setRowLimit(captionWindow.rowCount);
                setGravity(gravity);
                if (captionWindow.visible) {
                    show();
                } else {
                    hide();
                }
            }

            @Override
            public void onLayoutChange(
                    View v, int left, int top, int right, int bottom, int oldLeft,
                    int oldTop, int oldRight, int oldBottom) {
                int width = right - left;
                int height = bottom - top;
                if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) {
                    mLastCaptionLayoutWidth = width;
                    mLastCaptionLayoutHeight = height;
                    updateTextSize();
                }
            }

            private void updateWidestChar() {
                Paint paint = new Paint();
                paint.setTypeface(mCaptionStyle.getTypeface());
                Charset latin1 = Charset.forName("ISO-8859-1");
                float widestCharWidth = 0f;
                for (int i = 0; i < 256; ++i) {
                    String ch = new String(new byte[]{(byte) i}, latin1);
                    float charWidth = paint.measureText(ch);
                    if (widestCharWidth < charWidth) {
                        widestCharWidth = charWidth;
                        mWidestChar = ch;
                    }
                }
                updateTextSize();
            }

            private void updateTextSize() {
                if (mCCLayout == null) return;

                // Calculate text size based on the max window size.
                StringBuilder widestTextBuilder = new StringBuilder();
                int screenColumnCount = getScreenColumnCount();
                for (int i = 0; i < screenColumnCount; ++i) {
                    widestTextBuilder.append(mWidestChar);
                }
                String widestText = widestTextBuilder.toString();
                Paint paint = new Paint();
                paint.setTypeface(mCaptionStyle.getTypeface());
                float startFontSize = 0f;
                float endFontSize = 255f;
                while (startFontSize < endFontSize) {
                    float testTextSize = (startFontSize + endFontSize) / 2f;
                    paint.setTextSize(testTextSize);
                    float width = paint.measureText(widestText);
                    if (mCCLayout.getWidth() * 0.8f > width) {
                        startFontSize = testTextSize + 0.01f;
                    } else {
                        endFontSize = testTextSize - 0.01f;
                    }
                }
                mTextSize = endFontSize * mFontScale;
                mCCView.setTextSize(mTextSize);
            }

            private int getScreenColumnCount() {
                // Assume it has a wide aspect ratio track.
                return MAX_COLUMN_COUNT_16_9;
            }

            public void removeFromCaptionView() {
                if (mCCLayout != null) {
                    mCCLayout.removeViewFromSafeTitleArea(this);
                    mCCLayout.removeOnLayoutChangeListener(this);
                    mCCLayout = null;
                }
            }

            public void setText(String text) {
                updateText(text, false);
            }

            public void appendText(String text) {
                updateText(text, true);
            }

            public void clearText() {
                mBuilder.clear();
                mCCView.setText("");
            }

            private void updateText(String text, boolean appended) {
                if (!appended) {
                    mBuilder.clear();
                }
                if (text != null && text.length() > 0) {
                    int length = mBuilder.length();
                    mBuilder.append(text);
                    for (CharacterStyle characterStyle : mCharacterStyles) {
                        mBuilder.setSpan(characterStyle, length, mBuilder.length(),
                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                }
                String[] lines = TextUtils.split(mBuilder.toString(), "\n");

                // Truncate text not to exceed the row limit.
                // Plus one here since the range of the rows is [0, mRowLimit].
                String truncatedText = TextUtils.join("\n", Arrays.copyOfRange(
                        lines, Math.max(0, lines.length - (mRowLimit + 1)), lines.length));
                mBuilder.delete(0, mBuilder.length() - truncatedText.length());

                // Trim the buffer first then set text to CCView.
                int start = 0, last = mBuilder.length() - 1;
                int end = last;
                while ((start <= end) && (mBuilder.charAt(start) <= ' ')) {
                    ++start;
                }
                while ((end >= start) && (mBuilder.charAt(end) <= ' ')) {
                    --end;
                }
                if (start == 0 && end == last) {
                    mCCView.setText(mBuilder);
                } else {
                    SpannableStringBuilder trim = new SpannableStringBuilder();
                    trim.append(mBuilder);
                    if (end < last) {
                        trim.delete(end + 1, last + 1);
                    }
                    if (start > 0) {
                        trim.delete(0, start);
                    }
                    mCCView.setText(trim);
                }
            }

            public void setRowLimit(int rowLimit) {
                if (rowLimit < 0) {
                    throw new IllegalArgumentException("A rowLimit should have a positive number");
                }
                mRowLimit = rowLimit;
            }
        }

        class CCView extends SubtitleView {
            CCView(Context context) {
                this(context, null);
            }

            CCView(Context context, AttributeSet attrs) {
                this(context, attrs, 0);
            }

            CCView(Context context, AttributeSet attrs, int defStyleAttr) {
                super(context, attrs, defStyleAttr);
            }

            void setCaptionStyle(CaptionStyle style) {
                if (Build.VERSION.SDK_INT >= 21) {
                    if (style.hasForegroundColor()) {
                        setForegroundColor(style.foregroundColor);
                    }
                    if (style.hasBackgroundColor()) {
                        setBackgroundColor(style.backgroundColor);
                    }
                    if (style.hasEdgeType()) {
                        setEdgeType(style.edgeType);
                    }
                    if (style.hasEdgeColor()) {
                        setEdgeColor(style.edgeColor);
                    }
                }
                setTypeface(style.getTypeface());
            }
        }
    }
}