lib/src/main/java/com/tubitv/media/controller/PlayerUIController.java
public class PlayerUIController {

    public boolean isPlayingAds = false;

    private SimpleExoPlayer contentPlayer;

    private SimpleExoPlayer adPlayer;

    private WebView vpaidWebView;

    private View exoPlayerView;

    private int adResumeWindow = C.INDEX_UNSET;

    private long adResumePosition = C.TIME_UNSET;

    private int movieResumeWindow = C.INDEX_UNSET;

    private long movieResumePosition = C.TIME_UNSET;

    private boolean hasHistory = false;

    private long historyPosition = C.TIME_UNSET;

    public PlayerUIController() {
    }

    public PlayerUIController(@Nullable SimpleExoPlayer contentPlayer, @Nullable SimpleExoPlayer adPlayer,
            @Nullable WebView vpaidWebView, @Nullable View exoPlayerView) {
        this.contentPlayer = contentPlayer;
        this.adPlayer = adPlayer;
        this.vpaidWebView = vpaidWebView;
        this.exoPlayerView = exoPlayerView;
    }

    public SimpleExoPlayer getContentPlayer() {
        return contentPlayer;
    }

    public void setContentPlayer(SimpleExoPlayer contentPlayer) {
        this.contentPlayer = contentPlayer;
    }

    public SimpleExoPlayer getAdPlayer() {
        // We'll reuse content player to play ads for single player instance case
        if (PlayerDeviceUtils.useSinglePlayer()) {
            return contentPlayer;
        }
        return adPlayer;
    }

    public void setAdPlayer(SimpleExoPlayer adPlayer) {
        this.adPlayer = adPlayer;
    }

    public WebView getVpaidWebView() {
        return vpaidWebView;
    }

    public void setVpaidWebView(WebView vpaidWebView) {
        this.vpaidWebView = vpaidWebView;
    }

    public View getExoPlayerView() {
        return exoPlayerView;
    }

    public void setExoPlayerView(View exoPlayerView) {
        this.exoPlayerView = exoPlayerView;
    }

    /**
     * This is set when user want to begin the movie from current position
     *
     * @param pos
     */
    public void setPlayFromHistory(long pos) {
        hasHistory = true;
        historyPosition = pos;
    }

    public boolean hasHistory() {
        return hasHistory;
    }

    public long getHistoryPosition() {
        return historyPosition;
    }

    public void clearHistoryRecord() {
        hasHistory = false;
        historyPosition = C.TIME_UNSET;
    }

    public void setAdResumeInfo(int window, long position) {
        adResumeWindow = window;
        adResumePosition = position;
    }

    public void clearAdResumeInfo() {
        setAdResumeInfo(C.INDEX_UNSET, C.TIME_UNSET);
    }

    public void setMovieResumeInfo(int window, long position) {
        movieResumeWindow = window;
        movieResumePosition = position;
    }

    public void clearMovieResumeInfo() {
        setMovieResumeInfo(C.INDEX_UNSET, C.TIME_UNSET);
    }

    public int getAdResumeWindow() {
        return adResumeWindow;
    }

    public long getAdResumePosition() {
        return adResumePosition;
    }

    public int getMovieResumeWindow() {
        return movieResumeWindow;
    }

    public long getMovieResumePosition() {
        return movieResumePosition;
    }

    public static class Builder {

        private SimpleExoPlayer contentPlayer = null;

        private SimpleExoPlayer adPlayer = null;

        private WebView vpaidWebView = null;

        private View exoPlayerView = null;

        public Builder() {
        }

        public Builder setContentPlayer(SimpleExoPlayer contentPlayer) {
            this.contentPlayer = contentPlayer;
            return this;
        }

        public Builder setAdPlayer(SimpleExoPlayer adPlayer) {
            this.adPlayer = adPlayer;
            return this;
        }

        public Builder setVpaidWebView(WebView vpaidWebView) {
            this.vpaidWebView = vpaidWebView;
            return this;
        }

        public Builder setExoPlayerView(View exoPlayerView) {
            this.exoPlayerView = exoPlayerView;
            return this;
        }

        public PlayerUIController build() {

            return new PlayerUIController(contentPlayer, adPlayer, vpaidWebView, exoPlayerView);
        }
    }

}
lib/src/main/java/com/tubitv/media/models/MediaModel.java
public class MediaModel implements Serializable {

    /**
     * The url of the media
     */
    @NonNull
    private final String videoUrl;

    /**
     * The title of the media to display
     */
    @Nullable
    private final String mediaName;

    /**
     * The url of the artwork to display while loading
     */
    @Nullable
    private final String artworkUrl;

    /**
     * The nullable subtitles that we sideload for the main media source
     */
    @Nullable
    private final String subtitlesUrl;

    /**
     * The nullable click through url for this media if its an ad
     *
     * @see #isAd
     */
    @Nullable
    private final String clickThroughUrl;

    /**
     * The media source representation of this model
     */
    private MediaSource mediaSource;

    /**
     * Whether this media is an ad or not
     */
    private boolean isAd;

    /**
     * Whether this media is an ad or not
     */
    private boolean isVpaid;

    public MediaModel(@Nullable String mediaName, @NonNull String videoUrl, @Nullable String artworkUrl,
            @Nullable String subtitlesUrl, @Nullable String clickThroughUrl, boolean isAd, boolean isVpaid) {
        this.mediaName = mediaName;
        this.videoUrl = videoUrl;
        this.artworkUrl = artworkUrl;
        this.subtitlesUrl = subtitlesUrl;
        this.clickThroughUrl = clickThroughUrl;
        this.isAd = isAd;
        this.isVpaid = isVpaid;
    }

    public static MediaModel video(@NonNull String mediaName, @NonNull String videoUrl, @NonNull String artworkUrl,
            @Nullable String subtitlesUrl) {
        return new MediaModel(mediaName, videoUrl, artworkUrl, subtitlesUrl, null, false, false);
    }

    public static MediaModel ad(@NonNull String videoUrl, @Nullable String clickThroughUrl, boolean isVpaid) {
        return new MediaModel(null, videoUrl, null, null, clickThroughUrl, true, isVpaid);
    }

    @Nullable
    public String getMediaName() {
        return mediaName;
    }

    @NonNull
    public Uri getVideoUrl() {
        return Uri.parse(videoUrl);
    }

    @Nullable
    public Uri getArtworkUrl() {
        return Uri.parse(artworkUrl);
    }

    @Nullable
    public Uri getSubtitlesUrl() {
        return subtitlesUrl != null ? Uri.parse(subtitlesUrl) : null;
    }

    @Nullable
    public String getClickThroughUrl() {
        return clickThroughUrl;
    }

    public boolean isAd() {
        return isAd;
    }

    public String getMediaExtension() {
        return "m3u8";
    }

    public MediaSource getMediaSource() {
        return mediaSource;
    }

    public void setMediaSource(MediaSource mediaSource) {
        this.mediaSource = mediaSource;
    }

    public boolean isVpaid() {
        return isVpaid;
    }
}
lib/src/main/java/com/tubitv/media/controller/PlayerAdLogicController.java
public class PlayerAdLogicController {

    private AdPlayingMonitor adPlayingMonitor;

    private PlaybackActionCallback playbackActionCallback;

    private DoublePlayerInterface doublePlayerInterface;

    private CuePointMonitor cuePointMonitor;

    private VpaidClient vpaidClient;

    public PlayerAdLogicController() {
    }

    public PlayerAdLogicController(@Nullable AdPlayingMonitor adPlayingMonitor,
            @Nullable PlaybackActionCallback playbackActionCallback,
            @Nullable DoublePlayerInterface doublePlayerInterface, @Nullable CuePointMonitor cuePointMonitor) {
        this.adPlayingMonitor = adPlayingMonitor;
        this.playbackActionCallback = playbackActionCallback;
        this.doublePlayerInterface = doublePlayerInterface;
        this.cuePointMonitor = cuePointMonitor;
        this.vpaidClient = null;
    }

    public PlayerAdLogicController(@Nullable AdPlayingMonitor adPlayingMonitor,
            @Nullable PlaybackActionCallback playbackActionCallback,
            @Nullable DoublePlayerInterface doublePlayerInterface, @Nullable CuePointMonitor cuePointMonitor,
            @Nullable VpaidClient vpaidClient) {
        this.adPlayingMonitor = adPlayingMonitor;
        this.playbackActionCallback = playbackActionCallback;
        this.doublePlayerInterface = doublePlayerInterface;
        this.cuePointMonitor = cuePointMonitor;
        this.vpaidClient = vpaidClient;
    }

    public DoublePlayerInterface getDoublePlayerInterface() {
        return doublePlayerInterface;
    }

    public void setDoublePlayerInterface(DoublePlayerInterface doublePlayerInterface) {
        this.doublePlayerInterface = doublePlayerInterface;
    }

    public AdPlayingMonitor getAdPlayingMonitor() {
        return adPlayingMonitor;
    }

    public void setAdPlayingMonitor(AdPlayingMonitor adPlayingMonitor) {
        this.adPlayingMonitor = adPlayingMonitor;
    }

    public PlaybackActionCallback getTubiPlaybackInterface() {
        return playbackActionCallback;
    }

    public void setTubiPlaybackInterface(PlaybackActionCallback playbackActionCallback) {
        this.playbackActionCallback = playbackActionCallback;
    }

    public CuePointMonitor getCuePointMonitor() {
        return cuePointMonitor;
    }

    public void setCuePointMonitor(CuePointMonitor cuePointMonitor) {
        this.cuePointMonitor = cuePointMonitor;
    }

    @Nullable
    public VpaidClient getVpaidClient() {
        return vpaidClient;
    }

    public void setVpaidClient(@Nullable VpaidClient vpaidClient) {
        this.vpaidClient = vpaidClient;
    }
}
lib/src/main/java/com/tubitv/media/models/AdMediaModel.java
public class AdMediaModel {

    private List<MediaModel> listOfAds;

    public AdMediaModel(@NonNull List<MediaModel> listOfAds) {
        this.listOfAds = listOfAds;
    }

    @Nullable
    public MediaModel nextAD() {
        return listOfAds != null && listOfAds.size() > 0 ? listOfAds.get(0) : null;
    }

    public List<MediaModel> getListOfAds() {
        return listOfAds;
    }

    public void popFirstAd() {
        if (listOfAds != null && listOfAds.size() > 0) {
            listOfAds.remove(0);
        }
    }

    public int nubmerOfAd() {
        if (listOfAds == null) {
            return 0;
        } else {
            return listOfAds.size();
        }
    }

}
lib/src/main/java/com/tubitv/media/helpers/Constants.java
public class Constants {
    public static final String FSMPLAYER_TESTING = "FSM_LOGGING";
}
lib/src/main/java/com/tubitv/media/views/TubiExoPlayerView.java
@TargetApi(16)
public class TubiExoPlayerView extends FrameLayout {

    private static final String TAG = TubiExoPlayerView.class.getSimpleName();

    private static final int SURFACE_TYPE_NONE = 0;
    private static final int SURFACE_TYPE_SURFACE_VIEW = 1;
    private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;
    private static final float TV_SUBTITLES_TEXT_SIZE = 24f; // In dp

    private final AspectRatioFrameLayout contentFrame;
    private final View shutterView;
    private final View surfaceView;
    private final SubtitleView subtitleView;
    private View mUserInteractionView;
    private final ComponentListener componentListener;

    private SimpleExoPlayer player;

    private UserController userController;

    public TubiExoPlayerView(Context context) {
        this(context, null);
    }

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

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

        if (isInEditMode()) {
            contentFrame = null;
            shutterView = null;
            surfaceView = null;
            subtitleView = null;
            mUserInteractionView = null;
            componentListener = null;
            ImageView logo = new ImageView(context, attrs);
            if (Util.SDK_INT >= 23) {
                configureEditModeLogoV23(getResources(), logo);
            } else {
                configureEditModeLogo(getResources(), logo);
            }
            addView(logo);
            return;
        }

        int playerLayoutId = R.layout.tubi_player_view;
        int surfaceType = SURFACE_TYPE_SURFACE_VIEW;
        int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
        if (attrs != null) {
            TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
                    R.styleable.PlayerView, 0, 0);
            try {
                playerLayoutId = a.getResourceId(R.styleable.PlayerView_player_layout_id,
                        playerLayoutId);
                surfaceType = a.getInt(R.styleable.PlayerView_surface_type, surfaceType);
                resizeMode = a.getInt(R.styleable.PlayerView_resize_mode, resizeMode);
            } finally {
                a.recycle();
            }
        }

        LayoutInflater.from(context).inflate(playerLayoutId, this);
        componentListener = new ComponentListener();
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);

        // Content frame.
        contentFrame = findViewById(R.id.exo_content_frame);
        if (contentFrame != null) {
            setResizeModeRaw(contentFrame, resizeMode);
        }

        // Shutter view.
        shutterView = findViewById(R.id.exo_shutter);

        // Create a surface view and insert it into the content frame, if there is one.
        if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) {
            ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            surfaceView = surfaceType == SURFACE_TYPE_TEXTURE_VIEW ? new TextureView(context)
                    : new SurfaceView(context);
            surfaceView.setLayoutParams(params);
            contentFrame.addView(surfaceView, 0);
        } else {
            surfaceView = null;
        }

        // Subtitle view.
        subtitleView = (SubtitleView) findViewById(R.id.exo_subtitles);
        if (subtitleView != null) {
            subtitleView.setStyle(new CaptionStyleCompat(
                    Color.WHITE,
                    getResources().getColor(R.color.tubi_tv_player_controls_subtitles_background),
                    Color.TRANSPARENT,
                    CaptionStyleCompat.EDGE_TYPE_NONE,
                    Color.WHITE,
                    VaudTextView.getFont(context, VaudType.VAUD_REGULAR.getAssetFileName())));

            float subtitleTextSize = PlayerDeviceUtils.isTVDevice(this.getContext()) ?
                    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, TV_SUBTITLES_TEXT_SIZE,
                            getResources().getDisplayMetrics())
                    : getResources().getDimension(R.dimen.view_tubi_exo_player_subtitle_text_size);

            subtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, subtitleTextSize);

            subtitleView.setApplyEmbeddedStyles(false);
            subtitleView.setVisibility(View.INVISIBLE);
        }

        userController = new UserController();
    }

    public void addUserInteractionView(@Nullable View controlView) {
        if (controlView == null) {
            ExoPlayerLogger.e(TAG, "addUserInteractionView()----> adding empty view");
            return;
        }
        // Playback control view.
        View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder);
        if (controllerPlaceholder != null) {
            // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit
            // calls to set them.
            mUserInteractionView = controlView;

            ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());
            int controllerIndex = parent.indexOfChild(controllerPlaceholder);
            parent.removeView(controllerPlaceholder);
            parent.addView(mUserInteractionView, controllerIndex);
        } else {
            this.mUserInteractionView = null;
        }
    }

    @TargetApi(23)
    private static void configureEditModeLogoV23(Resources resources, ImageView logo) {
        logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null));
        logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null));
    }

    @SuppressWarnings("deprecation")
    private static void configureEditModeLogo(Resources resources, ImageView logo) {
        logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo));
        logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color));
    }

    @SuppressWarnings("ResourceType")
    private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) {
        aspectRatioFrame.setResizeMode(resizeMode);
    }

    public View getControlView() {
        return mUserInteractionView;
    }

    public TubiPlaybackControlInterface getPlayerController() {
        return userController;
    }

    /**
     * Returns the player currently set on this view, or null if no player is set.
     */
    public SimpleExoPlayer getPlayer() {
        return player;
    }

    /**
     * Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and
     * {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous
     * assignments are overridden.
     *
     * @param player The {@link SimpleExoPlayer} to use.
     */
    public void setPlayer(SimpleExoPlayer player, @NonNull PlaybackActionCallback playbackActionCallback) {
        if (this.player == player) {
            return;
        }
        if (this.player != null) {
            this.player.removeListener(componentListener);
            this.player.clearTextOutput(componentListener);
            this.player.clearVideoListener(componentListener);
            if (surfaceView instanceof TextureView) {
                this.player.clearVideoTextureView((TextureView) surfaceView);
            } else if (surfaceView instanceof SurfaceView) {
                this.player.clearVideoSurfaceView((SurfaceView) surfaceView);
            }
        }
        this.player = player;

        if (userController != null) {
            userController.setPlayer(player, playbackActionCallback, this);
        }

        if (shutterView != null) {
            shutterView.setVisibility(VISIBLE);
        }
        if (player != null) {
            if (surfaceView instanceof TextureView) {
                player.setVideoTextureView((TextureView) surfaceView);
            } else if (surfaceView instanceof SurfaceView) {
                player.setVideoSurfaceView((SurfaceView) surfaceView);
            }
            player.setVideoListener(componentListener);
            player.setTextOutput(componentListener);
            player.addListener(componentListener);
        }
    }

    /**
     * Sets the resize mode.
     *
     * @param resizeMode The resize mode.
     */
    public void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
        Assertions.checkState(contentFrame != null);
        contentFrame.setResizeMode(resizeMode);
    }

    /**
     * Gets the {@link SubtitleView}.
     *
     * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the
     * subtitle view is not present.
     */
    public SubtitleView getSubtitleView() {
        return subtitleView;
    }

    public void setMediaModel(@NonNull MediaModel mediaModel) {
        if (userController != null) {
            userController.setMediaModel(mediaModel, getContext());
        }
    }

    public void setAvailableAdLeft(int count) {
        if (userController != null) {
            userController.setAvailableAdLeft(count);
        }
    }

    private final class ComponentListener implements SimpleExoPlayer.VideoListener,
            TextRenderer.Output, ExoPlayer.EventListener {

        // TextRenderer.Output implementation

        @Override
        public void onCues(List<Cue> cues) {
            if (subtitleView != null) {
                subtitleView.onCues(cues);
            }
        }

        // SimpleExoPlayer.VideoListener implementation

        @Override
        public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
                float pixelWidthHeightRatio) {
            if (contentFrame != null) {
                float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height;
                contentFrame.setAspectRatio(aspectRatio);
            }
        }

        @Override
        public void onRenderedFirstFrame() {
            if (shutterView != null) {
                shutterView.setVisibility(INVISIBLE);
            }
        }

        @Override
        public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {

        }

        // ExoPlayer.EventListener implementation

        @Override
        public void onLoadingChanged(boolean isLoading) {
            // Do nothing.
        }

        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            //            maybeShowController(false);
        }

        @Override
        public void onRepeatModeChanged(final int repeatMode) {

        }

        @Override
        public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {

        }

        @Override
        public void onPlayerError(ExoPlaybackException e) {
            // Do nothing.
        }

        @Override
        public void onPositionDiscontinuity(final int reason) {
            // Do nothing.
        }

        @Override
        public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
            // Do nothing.
        }

        @Override
        public void onSeekProcessed() {

        }

        @Override
        public void onTimelineChanged(final Timeline timeline, final Object manifest, final int reason) {
            // Do nothing.
        }

    }

}
lib/src/main/java/com/tubitv/media/fsm/State.java
public interface State {

    /**
     * let the state to examine itself in a constant time line to detects any input that can change the state.
     */
    @Nullable
    State transformToState(@NonNull Input input, @NonNull StateFactory factory);

    /**
     * once the fsm changes states, update player's UI components.
     *
     * @param fsmPlayer the state machine itself that contains the UI and Business logic parts.
     */
    void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer);

}
lib/src/main/java/com/tubitv/media/fsm/concrete/factory/StateFactory.java
public final class StateFactory {

    /**
     * map store singleton instance of every types of {@link State},
     */
    private final Map<Class, State> stateInstance = new HashMap<>();

    /**
     * the key is the default state, and value is the custom state.
     */
    private final Map<Class, Class> customStateType = new HashMap<>();

    @Nullable
    private synchronized State getCacheInstance(@NonNull Class type) {
        return stateInstance.get(type);
    }

    private synchronized void setCacheInstance(@NonNull Class type, @NonNull State instance) {
        stateInstance.put(type, instance);
    }

    /**
     * convert the default {@link State} into custom State
     *
     * @param cla default state type
     * @return the custom state type
     */
    @Nullable
    private Class convertToCustomClass(@NonNull Class cla) {
        return customStateType.get(cla);
    }

    /**
     * Method should only be called right after initialization, before {@link #createState} ever been called for max state predictability.
     *
     * @param subClass must be the subclass of {@link com.tubitv.media.fsm.BaseState} to swap original to subclass
     *                 Must be called before any createState method being called.
     */
    public void overrideStateCreation(@NonNull Class subClass) {

        if (MakingAdCallState.class.isAssignableFrom(subClass)) {
            customStateType.put(MakingAdCallState.class, subClass);

        } else if (MoviePlayingState.class.isAssignableFrom(subClass)) {
            customStateType.put(MoviePlayingState.class, subClass);

        } else if (FinishState.class.isAssignableFrom(subClass)) {
            customStateType.put(FinishState.class, subClass);

        } else if (ReceiveAdState.class.isAssignableFrom(subClass)) {
            customStateType.put(ReceiveAdState.class, subClass);

        } else if (AdPlayingState.class.isAssignableFrom(subClass)) {
            customStateType.put(AdPlayingState.class, subClass);

        } else if (VpaidState.class.isAssignableFrom(subClass)) {
            customStateType.put(VpaidState.class, subClass);

        } else if (VastAdInteractionSandBoxState.class.isAssignableFrom(subClass)) {
            customStateType.put(VastAdInteractionSandBoxState.class, subClass);

        } else if (FetchCuePointState.class.isAssignableFrom(subClass)) {
            customStateType.put(FetchCuePointState.class, subClass);

        } else if (MakingPrerollAdCallState.class.isAssignableFrom(subClass)) {
            customStateType.put(MakingPrerollAdCallState.class, subClass);

        } else {
            throw new IllegalStateException(
                    String.valueOf(subClass.getName() + "is not a base class of default State class "));
        }
    }

    @NonNull
    public State createState(@NonNull Class classType) {

        if (!State.class.isAssignableFrom(classType)) {
            throw new IllegalStateException(
                    String.valueOf(classType.getName() + "is not a base class of default State class "));
        }

        // null check if there is any custom state class. if there is,
        Class finalClassType = convertToCustomClass(classType);

        if (finalClassType == null) {
            finalClassType = classType;
        }

        State buildState = getCacheInstance(finalClassType);

        if (buildState == null) {
            try {

                Constructor<?> ctor = finalClassType.getConstructor();
                buildState = (State) ctor.newInstance();

                setCacheInstance(finalClassType, buildState);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        return buildState;
    }
}
lib/src/main/java/com/tubitv/media/fsm/Input.java
public enum Input {

    /**
     * Only expect inputs of {@link com.tubitv.media.fsm.concrete.FetchCuePointState}
     */
    HAS_PREROLL_AD,
    NO_PREROLL_AD,

    /**
     * Only expect inputs of {@link com.tubitv.media.fsm.concrete.MakingPrerollAdCallState}
     */
    PRE_ROLL_AD_RECEIVED,

    /**
     * Only expect inputs of {@link com.tubitv.media.fsm.concrete.MakingAdCallState}
     */
    AD_RECEIVED,
    EMPTY_AD,

    /**
     * Only expect inputs of {@link com.tubitv.media.fsm.concrete.ReceiveAdState}
     */
    SHOW_ADS,

    /**
     * Only expect inputs of {@link com.tubitv.media.fsm.concrete.AdPlayingState}
     */
    NEXT_AD,
    AD_CLICK,
    AD_FINISH,
    VPAID_MANIFEST,

    /**
     * Only expect inputs of {@link com.tubitv.media.fsm.concrete.VpaidState}
     */
    VPAID_FINISH,

    /**
     * Only expect inputs of {@link VastAdInteractionSandBoxState}
     */
    BACK_TO_PLAYER_FROM_VAST_AD,

    /**
     * Only expect inputs of {@link com.tubitv.media.fsm.concrete.MoviePlayingState}
     */
    MAKE_AD_CALL,
    MOVIE_FINISH,

    /**
     * ERROR INPUT
     */
    ERROR,

    INITIALIZE,
}
lib/src/main/java/com/tubitv/media/models/VpaidClient.java
public interface VpaidClient {

    String EMPTY_URL = "about:blank";

    void init(MediaModel adMediaModel);

    @JavascriptInterface
    void notifyAdError(int code, String error);

    @JavascriptInterface
    void notifyVideoEnd();

    @JavascriptInterface
    String getVastXml();
}
lib/src/main/java/com/tubitv/media/utilities/ExoPlayerLogger.java
public class ExoPlayerLogger {

    public static Boolean SHOW_LOGGING = true;

    public static void d(String tag, String message) {
        if (SHOW_LOGGING) {
            // do something for a debug build
            Log.d(tag, message);
        }
    }

    public static void i(String tag, String message) {
        if (SHOW_LOGGING) {
            // do something for a debug build
            Log.i(tag, message);
        }
    }

    public static void w(String tag, String message) {
        if (SHOW_LOGGING) {
            // do something for a debug build
            Log.w(tag, message);
        }
    }

    public static void e(String tag, String message) {
        if (SHOW_LOGGING) {
            // do something for a debug build
            Log.e(tag, message);
        }
    }

    public static void v(String tag, String message) {
        if (SHOW_LOGGING) {
            // do something for a debug build
            Log.v(tag, message);
        }
    }
}
lib/src/main/java/com/tubitv/media/fsm/BaseState.java
public abstract class BaseState implements State {

    protected PlayerUIController controller;

    protected PlayerAdLogicController componentController;

    protected MediaModel movieMedia;

    protected AdMediaModel adMedia;

    /**
     * for testing purpose,
     *
     * @param fsmPlayer
     * @return
     */
    protected boolean isNull(@NonNull FsmPlayer fsmPlayer) {
        if (fsmPlayer == null) {
            throw new IllegalStateException("FsmPlayer can not be null");
        }

        if (controller == null || componentController == null || movieMedia == null) {
            ExoPlayerLogger.e(Constants.FSMPLAYER_TESTING, "components are null");
            return true;
        }

        return false;
    }

    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        /**
         * need to get the reference of the UI and Business logic components first.
         */
        controller = fsmPlayer.getController();
        componentController = fsmPlayer.getPlayerComponentController();
        movieMedia = fsmPlayer.getMovieMedia();
        adMedia = fsmPlayer.getAdMedia();
    }
}
lib/src/main/java/com/tubitv/media/fsm/state_machine/FsmPlayer.java
public abstract class FsmPlayer implements Fsm, RetrieveAdCallback, FsmAdController {

    /**
     * a wrapper class for player logic related component objects.
     */
    protected PlayerAdLogicController playerComponentController;
    /**
     * a wrapper class for player UI related objects
     */
    private PlayerUIController controller;
    /**
     * a generic call ad network class
     */
    private AdInterface adServerInterface;

    /**
     * information to use when retrieve ad from server
     */
    private AdRetriever adRetriever;

    /**
     * information to use when retrieve cuePoint from server.
     */
    private CuePointsRetriever cuePointsRetriever;

    /**
     * the main content media
     */
    private MediaModel movieMedia;

    /**
     * the content of ad being playing
     */
    private AdMediaModel adMedia;

    /**
     * the central state representing {@link com.google.android.exoplayer2.ExoPlayer} state at any given time.
     */
    private State currentState = null;

    /**
     * a factory class to create different state when fsm change to a different state.
     */
    private StateFactory factory;

    //this is a example url, you should use your own vpaid url instead.
    private String VPAID_END_POINT = "http://tubitv.com/";

    /**
     * only initialize the fsmPlay onc
     */
    private boolean isInitialized = false;

    private Lifecycle mLifecycle;

    public FsmPlayer(StateFactory factory) {
        this.factory = factory;
    }

    /**
     * update the resume position of the main video
     *
     * @param controller
     */
    public static void updateMovieResumePosition(PlayerUIController controller) {

        if (controller == null) {
            return;
        }

        SimpleExoPlayer moviePlayer = controller.getContentPlayer();

        if (moviePlayer != null && moviePlayer.getPlaybackState() != Player.STATE_IDLE) {
            int resumeWindow = moviePlayer.getCurrentWindowIndex();
            long resumePosition = moviePlayer.isCurrentWindowSeekable() ? Math.max(0, moviePlayer.getCurrentPosition())
                    : C.TIME_UNSET;
            controller.setMovieResumeInfo(resumeWindow, resumePosition);

            ExoPlayerLogger.i(Constants.FSMPLAYER_TESTING, resumePosition + "");
        }
    }

    public boolean isInitialized() {
        return isInitialized;
    }

    public MediaModel getMovieMedia() {
        return movieMedia;
    }

    public void setMovieMedia(MediaModel movieMedia) {
        this.movieMedia = movieMedia;
    }

    public AdMediaModel getAdMedia() {
        return adMedia;
    }

    public void setAdMedia(AdMediaModel adMedia) {
        this.adMedia = adMedia;
    }

    public AdInterface getAdServerInterface() {
        return adServerInterface;
    }

    public void setAdServerInterface(@NonNull AdInterface adServerInterface) {
        this.adServerInterface = adServerInterface;
    }

    public AdRetriever getAdRetriever() {
        return adRetriever;
    }

    public void setAdRetriever(@NonNull AdRetriever adRetriever) {
        this.adRetriever = adRetriever;
    }

    public Lifecycle getLifecycle() {
        return mLifecycle;
    }

    public void setLifecycle(Lifecycle mLifecycle) {
        this.mLifecycle = mLifecycle;
    }

    public boolean hasAdToPlay() {
        return adMedia != null && adMedia.getListOfAds() != null && adMedia.getListOfAds().size() > 0;
    }

    public String getVPAID_END_POINT() {
        return VPAID_END_POINT;
    }

    public void setVPAID_END_POINT(String VPAID_END_POINT) {
        this.VPAID_END_POINT = VPAID_END_POINT;
    }

    /**
     * delete the add at the first of the itme in list, which have been played already.
     */
    private void popPlayedAd() {
        if (adMedia != null) {
            adMedia.popFirstAd();
        }
    }

    public MediaModel getNextAdd() {
        return adMedia.nextAD();
    }

    public PlayerUIController getController() {
        return controller;
    }

    public void setController(@NonNull PlayerUIController controller) {
        this.controller = controller;
    }

    public PlayerAdLogicController getPlayerComponentController() {
        return playerComponentController;
    }

    public void setPlayerComponentController(PlayerAdLogicController playerComponentController) {
        this.playerComponentController = playerComponentController;
    }

    public CuePointsRetriever getCuePointsRetriever() {
        return cuePointsRetriever;
    }

    public void setCuePointsRetriever(CuePointsRetriever cuePointsRetriever) {
        this.cuePointsRetriever = cuePointsRetriever;
    }

    public void updateCuePointForRetriever(long cuepoint) {
        if (adRetriever != null) {
            adRetriever.setCubPoint(cuepoint);
        }
    }

    @Override
    public State getCurrentState() {
        return currentState;
    }

    @Override
    public void restart() {
        getController().getContentPlayer().stop();
        getController().getContentPlayer().setPlayWhenReady(false);
        currentState = null;
        getController().clearMovieResumeInfo();

        getController().getContentPlayer().prepare(movieMedia.getMediaSource(), true, true);
        transit(Input.INITIALIZE);
    }

    @Override
    public void transit(Input input) {

        // if the current lifecycle of activity is after on_stop, omit the transition
        if (getLifecycle() != null) {
            if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
                ExoPlayerLogger.e(Constants.FSMPLAYER_TESTING, "Activity out of lifecycle");
                return;
            }
        }

        State transitToState;

        /**
         * make state transition.
         */
        if (currentState != null) {
            transitToState = currentState.transformToState(input, factory);
        } else {

            isInitialized = true;
            transitToState = factory.createState(initializeState());

            ExoPlayerLogger.i(Constants.FSMPLAYER_TESTING, "initialize fsmPlayer");
        }

        /**
         * check if the transition flow is correct, if not then handle the error case.
         */
        if (transitToState != null) {
            /**
             * when transition is not null, state change is successful, and transit to a new state
             */
            currentState = transitToState;

        } else {

            /**
             * when transition is null, state change is error, transit to default {@link MoviePlayingState}
             */
            if (currentState instanceof MoviePlayingState) { // if player is current in moviePlayingstate when transition error happen, doesn't nothing.
                ExoPlayerLogger.e(Constants.FSMPLAYER_TESTING, "FSM flow error: remain in MoviePlayingState");
                return;
            }

            ExoPlayerLogger
                    .e(Constants.FSMPLAYER_TESTING, "FSM flow error:" + "prepare transition to MoviePlayingState");
            currentState = factory.createState(MoviePlayingState.class);
        }

        if (controller != null) {
            if (!PlayerDeviceUtils.useSinglePlayer() || !controller.isPlayingAds) {
                updateMovieResumePosition(controller);
            }
        }

        ExoPlayerLogger.d(Constants.FSMPLAYER_TESTING, "transit to: " + currentState.getClass().getSimpleName());

        currentState.performWorkAndUpdatePlayerUI(this);

    }

    @Override
    public void removePlayedAdAndTransitToNextState() {

        // need to remove the already played ad first.
        popPlayedAd();

        //then check if there are any ad need to be played.
        if (hasAdToPlay()) {

            if (getNextAdd().isVpaid()) {
                transit(Input.VPAID_MANIFEST);
            } else {
                transit(Input.NEXT_AD);
            }

        } else {
            //depends on current state, to transit to MoviePlayingState.
            if (currentState != null && currentState instanceof VpaidState) {
                transit(Input.VPAID_FINISH);
            } else {
                transit(Input.AD_FINISH);
            }
        }

    }

    @Override
    public void adPlayerError() {
        transit(Input.ERROR);
    }

    @Override
    public void updateSelf() {
        if (currentState != null) {
            ExoPlayerLogger
                    .i(Constants.FSMPLAYER_TESTING,
                            "Fsm updates self : " + currentState.getClass().getSimpleName());
            currentState.performWorkAndUpdatePlayerUI(this);
        }
    }

    @Override
    public void onReceiveAd(AdMediaModel mediaModels) {
        ExoPlayerLogger.i(Constants.FSMPLAYER_TESTING, "AdBreak received");

        adMedia = mediaModels;
        // prepare and build the adMediaModel
        playerComponentController.getDoublePlayerInterface().onPrepareAds(adMedia);

        transitToStateBaseOnCurrentState(currentState);
    }

    @Override
    public void onError() {
        ExoPlayerLogger.w(Constants.FSMPLAYER_TESTING, "Fetch Ad fail");
        transit(Input.ERROR);
    }

    @Override
    public void onEmptyAdReceived() {
        ExoPlayerLogger.w(Constants.FSMPLAYER_TESTING, "Fetch ad succeed, but empty ad");
        transit(Input.EMPTY_AD);
    }

    /**
     * transit to different state, when current state is {@link MakingAdCallState}, then when Ad comes back from server, transit to AD_RECEIVED,which transit current state to {@link com.tubitv.media.fsm.concrete.ReceiveAdState}
     * if current state is {@link MakingPrerollAdCallState}, then when Ad comes back, transit to PRE_ROLL_AD_RECEIVED, which then transit state to {@link com.tubitv.media.fsm.concrete.AdPlayingState}.
     *
     * @param currentState
     */
    private void transitToStateBaseOnCurrentState(State currentState) {

        if (currentState == null) {
            return;
        }

        if (currentState instanceof MakingPrerollAdCallState) {
            transit(Input.PRE_ROLL_AD_RECEIVED);
        } else if (currentState instanceof MakingAdCallState) {
            transit(Input.AD_RECEIVED);
        }
    }
}


Question: package com.tubitv.media.fsm.concrete;
import android.support.annotation.NonNull;
import android.view.View;
import android.webkit.WebView;
import com.google.android.exoplayer2.ExoPlayer;
import com.tubitv.media.controller.PlayerAdLogicController;
import com.tubitv.media.controller.PlayerUIController;
import com.tubitv.media.fsm.BaseState;
import com.tubitv.media.fsm.Input;
import com.tubitv.media.fsm.State;
import com.tubitv.media.fsm.concrete.factory.StateFactory;
import com.tubitv.media.fsm.state_machine.FsmPlayer;
import com.tubitv.media.helpers.Constants;
import com.tubitv.media.models.AdMediaModel;
import com.tubitv.media.models.MediaModel;
import com.tubitv.media.models.VpaidClient;
import com.tubitv.media.utilities.ExoPlayerLogger;
import com.tubitv.media.views.TubiExoPlayerView;


/**
 * Created by allensun on 8/1/17.
 */
public class VpaidState extends BaseState {

    @Override
    public State transformToState(Input input, StateFactory factory) {

        switch (input) {
            case VPAID_FINISH:
                return factory.createState(MoviePlayingState.class);

            case VPAID_MANIFEST:
                return factory.createState(VpaidState.class);

            case NEXT_AD:
                return factory.createState(AdPlayingState.class);
        }
        return null;
    }

    //TODO: API level lower that certain, will disable vpaid.
    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        super.performWorkAndUpdatePlayerUI(fsmPlayer);

        if (isNull(fsmPlayer)) {
            return;
        }

        pausePlayerAndSHowVpaid(controller, componentController, fsmPlayer, adMedia);
    }

Answer:     private void pausePlayerAndSHowVpaid(PlayerUIController controller, PlayerAdLogicController componentController,