lib/src/main/java/com/tubitv/media/fsm/state_machine/FsmPlayerImperial.java
public abstract class FsmPlayerImperial extends FsmPlayer implements CuePointCallBack {

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

    public FsmPlayerImperial(StateFactory factory) {
        super(factory);
    }

    @Override
    public void onCuePointReceived(long[] cuePoints) {

        ExoPlayerLogger.i(Constants.FSMPLAYER_TESTING, "CuePoint received");

        //if it has pre-roll in the list of cue points, remove the pre-roll cue point,
        // because the pre-roll should not managed by the CuePointMonitor
        if (hasPrerollAd(cuePoints)) {

            updateCuePointsWithRemoveFirstCue(cuePoints, true);
            transit(Input.HAS_PREROLL_AD);
        } else {

            updateCuePointsWithRemoveFirstCue(cuePoints, false);
            transit(Input.NO_PREROLL_AD);
        }
        /**
         *  update the cuepoint receive event to Activity {@link com.tubitv.media.activities.DoubleViewTubiPlayerActivity}
         *
         */

        playerComponentController.getTubiPlaybackInterface().onCuePointReceived(cuePoints);
    }

    @Override
    public void onCuePointError() {

        ExoPlayerLogger.e(Constants.FSMPLAYER_TESTING, "CuePoint fetch fail");
        //TODO: need to handle situation when cuepoint not able to retrieve.
        transit(Input.ERROR);
    }

    private void updateCuePointsWithRemoveFirstCue(long[] array, boolean yes) {

        if (playerComponentController == null || playerComponentController.getCuePointMonitor() == null) {

            ExoPlayerLogger.e(TAG, " playerComponentController || playerComponentController is empty");
            return;
        }

        if (yes) {
            // update the cuePointMonitor with the first cue remove, because it is a pre-roll ad
            playerComponentController.getCuePointMonitor().setQuePoints(removePreroll(array));

        } else {
            // update the cuePointMonitor
            playerComponentController.getCuePointMonitor().setQuePoints(array);
        }
    }

    private boolean hasPrerollAd(long[] cuePoints) {
        if (cuePoints != null && cuePoints.length > 0 && cuePoints[0] == 0l) {
            //TODO: need to remove the time 0 cuePoint indicator.
            return true;
        }
        return false;
    }

    private long[] removePreroll(long[] array) {
        if (array.length <= 1) {
            return array;
        }

        return Arrays.copyOfRange(array, 1, array.length);
    }
}
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/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);
        }
    }
}
lib/src/main/java/com/tubitv/media/fsm/concrete/FinishState.java
public class FinishState extends BaseState {

    @Override
    public State transformToState(Input input, StateFactory factory) {
        return null;
    }

    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        super.performWorkAndUpdatePlayerUI(fsmPlayer);

        if (isNull(fsmPlayer)) {
            return;
        }

    }
}
lib/src/main/java/com/tubitv/media/fsm/concrete/MakingAdCallState.java
public class MakingAdCallState extends BaseState {

    @Override
    public State transformToState(Input input, StateFactory factory) {
        switch (input) {
            case AD_RECEIVED:
                return factory.createState(ReceiveAdState.class);

            case EMPTY_AD:
                return null;

            case MAKE_AD_CALL:
                return factory.createState(MakingAdCallState.class);

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

        return null;
    }

    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        super.performWorkAndUpdatePlayerUI(fsmPlayer);

        if (isNull(fsmPlayer)) {
            return;
        }

        fetchAd(fsmPlayer.getAdServerInterface(), fsmPlayer.getAdRetriever(), fsmPlayer);

        //in the MadingAdCallState, no UI need to be updated.

    }

    private void fetchAd(AdInterface adInterface, AdRetriever retriever, RetrieveAdCallback callback) {
        if (adInterface != null && retriever != null && callback != null) {
            adInterface.fetchAd(retriever, callback);
        } else {
            ExoPlayerLogger.e("TAG", "fetchAd fail, adInterface or AdRetriever is empty");
        }
    }
}
lib/src/main/java/com/tubitv/media/fsm/concrete/MakingPrerollAdCallState.java
public class MakingPrerollAdCallState extends BaseState {

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

        switch (input) {
            case PRE_ROLL_AD_RECEIVED:
                return factory.createState(AdPlayingState.class);
        }

        return null;
    }

    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        super.performWorkAndUpdatePlayerUI(fsmPlayer);

        // don't do any UI work.
        if (isNull(fsmPlayer)) {
            return;
        }

        //update the AdRetriever for pre_roll cue point, which is 0.
        if (controller.hasHistory()) {
            fsmPlayer.updateCuePointForRetriever(controller.getHistoryPosition());
        } else {
            fsmPlayer.updateCuePointForRetriever(0);
        }
        fetchAd(fsmPlayer.getAdServerInterface(), fsmPlayer.getAdRetriever(), fsmPlayer);
    }

    private void fetchAd(AdInterface adInterface, AdRetriever retriever, RetrieveAdCallback callback) {
        if (adInterface != null && retriever != null && callback != null) {
            adInterface.fetchAd(retriever, callback);
        } else {
            ExoPlayerLogger.e(FSMPLAYER_TESTING, "fetchAd fail, adInterface or AdRetriever is empty");
        }
    }
}
lib/src/main/java/com/tubitv/media/fsm/concrete/MoviePlayingState.java
public class MoviePlayingState extends BaseState {

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

        switch (input) {
            case MAKE_AD_CALL:
                return factory.createState(MakingAdCallState.class);

            case MOVIE_FINISH:
                return factory.createState(FinishState.class);
        }

        return null;
    }

    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        super.performWorkAndUpdatePlayerUI(fsmPlayer);

        if (isNull(fsmPlayer)) {
            return;
        }

        stopAdandPlayerMovie(controller, componentController, movieMedia);
    }

    private void stopAdandPlayerMovie(PlayerUIController controller, PlayerAdLogicController componentController,
            MediaModel movieMedia) {

        SimpleExoPlayer adPlayer = controller.getAdPlayer();
        SimpleExoPlayer moviePlayer = controller.getContentPlayer();

        boolean shouldReprepareForSinglePlayer = PlayerDeviceUtils.useSinglePlayer() && controller.isPlayingAds;

        //first remove the AdPlayer's listener and pause the player
        adPlayer.removeAnalyticsListener(componentController.getAdPlayingMonitor());

        if (shouldReprepareForSinglePlayer) {
            adPlayer.setPlayWhenReady(false);
        }

        //then update the playerView with SimpleExoPlayer and Movie MediaModel
        TubiExoPlayerView tubiExoPlayerView = (TubiExoPlayerView) controller.getExoPlayerView();
        tubiExoPlayerView.setPlayer(moviePlayer, componentController.getTubiPlaybackInterface());
        tubiExoPlayerView.setMediaModel(movieMedia);

        //prepare the moviePlayer with data source and set it play

        boolean haveResumePosition = controller.getMovieResumePosition() != C.TIME_UNSET;

        boolean isPlayerIdle = moviePlayer.getPlaybackState() == Player.STATE_IDLE;

        if (shouldReprepareForSinglePlayer || isPlayerIdle) {
            moviePlayer.prepare(movieMedia.getMediaSource(), !haveResumePosition, false);
            updatePlayerPosition(moviePlayer, controller);
        }

        moviePlayer.setPlayWhenReady(true);

        controller.isPlayingAds = false;

        hideVpaidNShowPlayer(controller);

        //when return to the movie playing state, show the subtitle if necessary
        if (shouldShowSubtitle()) {
            ((TubiExoPlayerView) controller.getExoPlayerView()).getSubtitleView().setVisibility(View.VISIBLE);
        }
    }

    private void updatePlayerPosition(SimpleExoPlayer moviePlayer, PlayerUIController controller) {

        // if want to play movie from certain position when first open the movie
        if (controller.hasHistory()) {
            moviePlayer.seekTo(moviePlayer.getCurrentWindowIndex(), controller.getHistoryPosition());
            controller.clearHistoryRecord();
            return;
        }

        boolean haveResumePosition = controller.getMovieResumePosition() != C.TIME_UNSET;
        if (haveResumePosition) {
            moviePlayer.seekTo(moviePlayer.getCurrentWindowIndex(), controller.getMovieResumePosition());
        }
    }

    private void hideVpaidNShowPlayer(final PlayerUIController controller) {

        controller.getExoPlayerView().setVisibility(View.VISIBLE);

        WebView vpaidEWebView = controller.getVpaidWebView();
        if (vpaidEWebView != null) {
            vpaidEWebView.setVisibility(View.GONE);
            vpaidEWebView.loadUrl(VpaidClient.EMPTY_URL);
            vpaidEWebView.clearHistory();
        }
    }

    private boolean shouldShowSubtitle() {

        TubiExoPlayerView view = (TubiExoPlayerView) controller.getExoPlayerView();

        UserController controller = (UserController) view.getPlayerController();

        if (controller.videoHasSubtitle.get() && controller.isSubtitleEnabled.get()) {
            return true;
        }

        return false;
    }

}
lib/src/main/java/com/tubitv/media/fsm/concrete/ReceiveAdState.java
public class ReceiveAdState extends BaseState {

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

        switch (input) {
            case SHOW_ADS:
                return factory.createState(AdPlayingState.class);
        }

        return null;
    }

    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        super.performWorkAndUpdatePlayerUI(fsmPlayer);

        // doesn't need to do any UI work.
        if (isNull(fsmPlayer)) {
            return;
        }

        SimpleExoPlayer moviePlayer = controller.getContentPlayer();

        // this mean, user jump out of the activity lifecycle in ReceivedAdState.
        if (moviePlayer != null && moviePlayer.getPlaybackState() == ExoPlayer.STATE_IDLE) {
            fsmPlayer.transit(Input.ERROR);
        }

    }
}
lib/src/main/java/com/tubitv/media/fsm/concrete/VastAdInteractionSandBoxState.java
public class VastAdInteractionSandBoxState extends BaseState {

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

        switch (input) {
            case BACK_TO_PLAYER_FROM_VAST_AD:
                return factory.createState(AdPlayingState.class);

        }
        return null;
    }

    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        super.performWorkAndUpdatePlayerUI(fsmPlayer);

        if (isNull(fsmPlayer)) {
            return;
        }
    }
}
lib/src/main/java/com/tubitv/media/fsm/concrete/FetchCuePointState.java
public class FetchCuePointState extends BaseState {

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

        switch (input) {
            case HAS_PREROLL_AD:
                return factory.createState(MakingPrerollAdCallState.class);

            case NO_PREROLL_AD:
                return factory.createState(MoviePlayingState.class);
        }

        return null;
    }

    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        super.performWorkAndUpdatePlayerUI(fsmPlayer);

        if (isNull(fsmPlayer)) {
            return;
        }
        //does nothing with the UI.
        fetchCuePointCall(fsmPlayer.getAdServerInterface(), fsmPlayer.getCuePointsRetriever(),
                (FsmPlayerImperial) fsmPlayer);
    }

    private void fetchCuePointCall(AdInterface adInterface, CuePointsRetriever retriever, CuePointCallBack callBack) {
        if (adInterface != null && retriever != null && callBack != null) {
            adInterface.fetchQuePoint(retriever, callBack);
        } else {
            ExoPlayerLogger.e(FSMPLAYER_TESTING, "fetch Ad fail, adInterface or CuePointsRetriever is empty");
        }
    }

}
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/fsm/concrete/VpaidState.java
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);
    }

    private void pausePlayerAndSHowVpaid(PlayerUIController controller, PlayerAdLogicController componentController,
            FsmPlayer fsmPlayer, AdMediaModel adMedia) {

        ExoPlayer moviePlayer = controller.getContentPlayer();

        if (moviePlayer != null && moviePlayer.getPlayWhenReady()) {
            moviePlayer.setPlayWhenReady(false);
        }

        ExoPlayer adPlayer = controller.getAdPlayer();
        if (adPlayer != null && adPlayer.getPlayWhenReady()) {
            adPlayer.setPlayWhenReady(false);
        }

        VpaidClient client = componentController.getVpaidClient();

        if (client != null) {

            MediaModel ad = adMedia.nextAD();

            if (ad == null) {
                ExoPlayerLogger.w(Constants.FSMPLAYER_TESTING, "Vpaid ad is null");
                return;
            }

            client.init(ad);

            controller.getExoPlayerView().setVisibility(View.INVISIBLE);

            WebView vpaidWebView = controller.getVpaidWebView();

            vpaidWebView.setVisibility(View.VISIBLE);
            vpaidWebView.bringToFront();
            vpaidWebView.invalidate();

            vpaidWebView.addJavascriptInterface(client, "TubiNativeJSInterface");
            vpaidWebView.loadUrl(fsmPlayer.getVPAID_END_POINT());

            //hide the subtitle view when vpaid is playing
            ((TubiExoPlayerView) controller.getExoPlayerView()).getSubtitleView().setVisibility(View.INVISIBLE);
        } else {
            ExoPlayerLogger.w(Constants.FSMPLAYER_TESTING, "VpaidClient is null");
        }

    }

}
lib/src/main/java/com/tubitv/media/fsm/concrete/AdPlayingState.java
public class AdPlayingState extends BaseState {

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

        switch (input) {

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

            case AD_CLICK:
                return factory.createState(VastAdInteractionSandBoxState.class);

            case AD_FINISH:
                return factory.createState(MoviePlayingState.class);

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

        }
        return null;
    }

    @Override
    public void performWorkAndUpdatePlayerUI(@NonNull FsmPlayer fsmPlayer) {
        super.performWorkAndUpdatePlayerUI(fsmPlayer);

        if (isNull(fsmPlayer)) {
            return;
        }

        //reset the ad player position everytime when a transition to AdPlaying occur
        controller.clearAdResumeInfo();

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

    private void playingAdAndPauseMovie(PlayerUIController controller, AdMediaModel adMediaModel,
            PlayerAdLogicController componentController, FsmPlayer fsmPlayer) {

        SimpleExoPlayer adPlayer = controller.getAdPlayer();
        SimpleExoPlayer moviePlayer = controller.getContentPlayer();

        // then setup the player for ad to playe
        MediaModel adMedia = adMediaModel.nextAD();

        //TODO: Handle situation when ad medaia is empty, or invalid urls.
        if (adMedia != null) {

            if (adMedia.isVpaid()) {
                fsmPlayer.transit(Input.VPAID_MANIFEST);
                return;
            }

            hideVpaidNShowPlayer(controller);

            moviePlayer.setPlayWhenReady(false);

            // We need save movie play position before play ads for single player instance case
            if (PlayerDeviceUtils.useSinglePlayer() && !controller.isPlayingAds) {
                long resumePosition = Math.max(0, moviePlayer.getCurrentPosition());
                controller.setMovieResumeInfo(moviePlayer.getCurrentWindowIndex(), resumePosition);
            }

            //prepare the moviePlayer with data source and set it play

            boolean haveResumePosition = controller.getAdResumePosition() != C.TIME_UNSET;

            //prepare the mediaSource to AdPlayer
            adPlayer.prepare(adMedia.getMediaSource(), !haveResumePosition, true);
            controller.isPlayingAds = true;

            if (haveResumePosition) {
                adPlayer.seekTo(adPlayer.getCurrentWindowIndex(), controller.getAdResumePosition());
            }

            //update the ExoPlayerView with AdPlayer and AdMedia
            TubiExoPlayerView tubiExoPlayerView = (TubiExoPlayerView) controller.getExoPlayerView();
            tubiExoPlayerView.setPlayer(adPlayer, componentController.getTubiPlaybackInterface());
            tubiExoPlayerView.setMediaModel(adMedia);
            //update the numbers of ad left to give user indicator
            tubiExoPlayerView.setAvailableAdLeft(adMediaModel.nubmerOfAd());

            //Player the Ad.
            adPlayer.setPlayWhenReady(true);
            adPlayer.addAnalyticsListener(componentController.getAdPlayingMonitor());
            adPlayer.setMetadataOutput(componentController.getAdPlayingMonitor());

            //hide the subtitle view when ad is playing
            ((TubiExoPlayerView) controller.getExoPlayerView()).getSubtitleView().setVisibility(View.INVISIBLE);
        }
    }

    private void hideVpaidNShowPlayer(final PlayerUIController imcontroller) {

        imcontroller.getExoPlayerView().setVisibility(View.VISIBLE);

        WebView vpaidEWebView = imcontroller.getVpaidWebView();
        if (vpaidEWebView != null) {
            vpaidEWebView.setVisibility(View.GONE);
            vpaidEWebView.loadUrl(VpaidClient.EMPTY_URL);
            vpaidEWebView.clearHistory();
        }
    }

}
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);

}


Question: package com.tubitv.demo;
import com.tubitv.media.fsm.Input;
import com.tubitv.media.fsm.State;
import com.tubitv.media.fsm.concrete.AdPlayingState;
import com.tubitv.media.fsm.concrete.FetchCuePointState;
import com.tubitv.media.fsm.concrete.FinishState;
import com.tubitv.media.fsm.concrete.MakingAdCallState;
import com.tubitv.media.fsm.concrete.MakingPrerollAdCallState;
import com.tubitv.media.fsm.concrete.MoviePlayingState;
import com.tubitv.media.fsm.concrete.ReceiveAdState;
import com.tubitv.media.fsm.concrete.VastAdInteractionSandBoxState;
import com.tubitv.media.fsm.concrete.VpaidState;
import com.tubitv.media.fsm.concrete.factory.StateFactory;
import com.tubitv.media.fsm.state_machine.FsmPlayer;
import com.tubitv.media.fsm.state_machine.FsmPlayerImperial;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;



/**
 * Created by allensun on 8/15/17.
 * on Tubitv.com, allengotstuff@gmail.com
 */

@RunWith(JUnit4.class)
public class ExoPlayerFactoryTest {

    StateFactory stateFactory;

    FsmPlayer playerFsm;

    @Before
    public void setup() {

        stateFactory = new StateFactory();
        stateFactory.overrideStateCreation(TestMoviePlayingState.class);
        stateFactory.overrideStateCreation(TestMakingAdCallState.class);
        stateFactory.overrideStateCreation(TestFinishState.class);
        stateFactory.overrideStateCreation(TestReceivedState.class);
        stateFactory.overrideStateCreation(TestAdPlayingState.class);
        stateFactory.overrideStateCreation(TestVastAdSandBox.class);
        stateFactory.overrideStateCreation(TestVpadState.class);

        playerFsm = new FsmPlayerImperial(stateFactory) {
            @Override
            public Class initializeState() {
                return MoviePlayingState.class;
            }
        };
    }

    @Test
    public void testCustomClass() {

        State state = stateFactory.createState(MoviePlayingState.class);

        assertThat(state instanceof TestMoviePlayingState, is(true));

        // make ad call

        state = stateFactory.createState(MakingAdCallState.class);

        assertThat(state instanceof TestMakingAdCallState, is(true));

        //finish state

        state = stateFactory.createState(FinishState.class);

        assertThat(state instanceof TestFinishState, is(true));

        //receive ad state

        state = stateFactory.createState(ReceiveAdState.class);

        assertThat(state instanceof TestReceivedState, is(true));

        //ad playing state

        state = stateFactory.createState(AdPlayingState.class);

        assertThat(state instanceof TestAdPlayingState, is(true));

        //vast sandbox

Answer:         state = stateFactory.createState(VastAdInteractionSandBoxState.class);