public class UserController extends BaseObservable
        implements TubiPlaybackControlInterface, ExoPlayer.EventListener, SeekBar.OnSeekBarChangeListener {

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

    public static final int NORMAL_CONTROL_STATE = 1;
    public static final int CUSTOM_SEEK_CONTROL_STATE = 2; // Every time long press left/right will enter this state
    public static final int EDIT_CUSTOM_SEEK_CONTROL_STATE = 3; // After long press left/right will enter this state
    public static final int OPTIONS_CONTROL_STATE = 4; // Every time focus on caption button will enter this state

    private static final long DEFAULT_FREQUENCY = 1000;

    /**
     * video action states
     */
    public final ObservableInt playerPlaybackState = new ObservableInt(ExoPlayer.STATE_IDLE);

    public final ObservableBoolean isVideoPlayWhenReady = new ObservableBoolean(false);

    /**
     * video information states
     */
    public final ObservableField<String> videoName = new ObservableField("");

    public final ObservableField<Uri> videoPoster = new ObservableField(null);

    public final ObservableField<String> videoMetaData = new ObservableField("");

    public final ObservableField<Long> videoDuration = new ObservableField<>(0l);

    public final ObservableField<Long> videoCurrentTime = new ObservableField<>(0l);

    public final ObservableField<Long> videoBufferedPosition = new ObservableField<>(0l);

    public final ObservableField<String> videoRemainInString = new ObservableField<>("");

    public final ObservableField<String> videoPositionInString = new ObservableField<>("");

    public final ObservableField<Boolean> videoHasSubtitle = new ObservableField<>(false);

    /**
     * Ad information
     */
    public final ObservableField<String> adClickUrl = new ObservableField("");

    public final ObservableField<String> adMetaData = new ObservableField("");

    public final ObservableInt numberOfAdsLeft = new ObservableInt(0);

    public final ObservableField<Boolean> isCurrentAd = new ObservableField<>(false);

    /**
     * user interaction attribute
     */
    public final ObservableField<Boolean> isSubtitleEnabled = new ObservableField<>(false);

    public final ObservableField<Boolean> isDraggingSeekBar = new ObservableField<>(false);

    private Handler mProgressUpdateHandler = new Handler();

    private Runnable updateProgressAction = () -> updateProgress();
    private Runnable mOnControlStateChange;

    /**
     * the Exoplayer instance which this {@link UserController} is controlling.
     */
    private SimpleExoPlayer mPlayer;

    /**
     * this is the current mediaModel being played, it could be a ad or actually video
     */
    private MediaModel mMediaModel;

    private PlaybackActionCallback mPlaybackActionCallback;

    private TubiExoPlayerView mTubiExoPlayerView;

    private int mControlState = NORMAL_CONTROL_STATE;

    /**
     * Every time the {@link com.tubitv.media.fsm.state_machine.FsmPlayer} change states between
     * {@link com.tubitv.media.fsm.concrete.AdPlayingState} and {@link com.tubitv.media.fsm.concrete.MoviePlayingState},
     * current controller instance need to update the video instance.
     *
     * @param mediaModel the current video that will be played by the {@link UserController#mPlayer} instance.
     */
    public void setMediaModel(@NonNull MediaModel mediaModel, Context context) {
        if (mediaModel == null) {
            ExoPlayerLogger.e(TAG, "setMediaModel is null");
            return;
        }

        this.mMediaModel = mediaModel;

        //mark flag for ads to movie
        isCurrentAd.set(mMediaModel.isAd());

        if (mMediaModel.isAd()) {

            if (!PlayerDeviceUtils.isTVDevice(context)
                    && !TextUtils.isEmpty(mMediaModel.getClickThroughUrl())) {
                adClickUrl.set(mMediaModel.getClickThroughUrl());
            }

            videoName.set(context.getString(R.string.commercial));

            videoHasSubtitle.set(false);

        } else {

            if (!TextUtils.isEmpty(mMediaModel.getMediaName())) {
                videoName.set(mMediaModel.getMediaName());
            }

            if (!PlayerDeviceUtils.isTVDevice(context) // Disable artwork for TV
                    && mMediaModel.getArtworkUrl() != null) {
                videoPoster.set(mMediaModel.getArtworkUrl());
            }

            if (mMediaModel.getSubtitlesUrl() != null) {
                videoHasSubtitle.set(true);
            }

        }

    }

    /**
     * Every time the {@link com.tubitv.media.fsm.state_machine.FsmPlayer} change states between
     * {@link com.tubitv.media.fsm.concrete.AdPlayingState} and {@link com.tubitv.media.fsm.concrete.MoviePlayingState},
     * {@link UserController#mPlayer} instance need to update .
     *
     * @param player the current player that is playing the video
     */
    public void setPlayer(@NonNull SimpleExoPlayer player, @NonNull PlaybackActionCallback playbackActionCallback,
            @NonNull TubiExoPlayerView tubiExoPlayerView) {
        if (player == null || tubiExoPlayerView == null) {
            ExoPlayerLogger.e(TAG, "setPlayer is null");
            return;
        }

        if (this.mPlayer == player) {
            return;
        }

        mTubiExoPlayerView = tubiExoPlayerView;

        //remove the old listener
        if (mPlayer != null) {
            this.mPlayer.removeListener(this);
        }

        this.mPlayer = player;

        mPlayer.addListener(this);
        playerPlaybackState.set(mPlayer.getPlaybackState());
        mPlaybackActionCallback = playbackActionCallback;
        updateProgress();

    }

    public void setAvailableAdLeft(int count) {
        numberOfAdsLeft.set(count);
    }

    public void updateTimeTextViews(long position, long duration) {
        //translate the movie remaining time number into display string, and update the UI
        videoRemainInString.set(Utils.getProgressTime((duration - position), true));
        videoPositionInString.set(Utils.getProgressTime(position, false));
    }

    public void togglePlayPause() {
        triggerPlayOrPause(!isVideoPlayWhenReady.get());
    }

    public void fastForward() {
        seekBy(SeekCalculator.FAST_SEEK_INTERVAL);
    }

    public void fastRewind() {
        seekBy(SeekCalculator.REWIND_DIRECTION * SeekCalculator.FAST_SEEK_INTERVAL);
    }

    /**
     * Get current player control state
     *
     * @return Current control state
     */
    public int getState() {
        return mControlState;
    }

    /**
     * Set current player state
     */
    public void setState(final int state) {
        mControlState = state;

        if (mOnControlStateChange != null) {
            mOnControlStateChange.run();
        }
    }

    /**
     * Check if it is during custom seek
     *
     * @return True if custom seek is performing
     */
    public boolean isDuringCustomSeek() {
        return mControlState == CUSTOM_SEEK_CONTROL_STATE || mControlState == EDIT_CUSTOM_SEEK_CONTROL_STATE;
    }

    /**
     * Set callback for control state change
     *
     * @param onControlStateChange Callback to run when control state change
     */
    public void setOnControlStateChange(final Runnable onControlStateChange) {
        mOnControlStateChange = onControlStateChange;
    }

    @Override
    public void triggerSubtitlesToggle(final boolean enabled) {

        if (mTubiExoPlayerView == null) {
            ExoPlayerLogger.e(TAG, "triggerSubtitlesToggle() --> tubiExoPlayerView is null");
            return;
        }

        //trigger the hide or show subtitles.
        View subtitles = mTubiExoPlayerView.getSubtitleView();
        if (subtitles != null) {
            subtitles.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
        }

        if (mPlaybackActionCallback != null && mPlaybackActionCallback.isActive()) {
            mPlaybackActionCallback.onSubtitles(mMediaModel, enabled);
        }

        isSubtitleEnabled.set(enabled);
    }

    @Override
    public void seekBy(final long millisecond) {
        if (mPlayer == null) {
            ExoPlayerLogger.e(TAG, "seekBy() ---> player is empty");
            return;
        }

        long currentPosition = mPlayer.getCurrentPosition();
        long seekPosition = currentPosition + millisecond;

        //lower bound
        seekPosition = seekPosition < 0 ? 0 : seekPosition;
        //upper bound
        seekPosition = seekPosition > mPlayer.getDuration() ? mPlayer.getDuration() : seekPosition;

        if (mPlaybackActionCallback != null && mPlaybackActionCallback.isActive()) {

            mPlaybackActionCallback.onSeek(mMediaModel, currentPosition, seekPosition);
        }

        seekToPosition(seekPosition);
    }

    @Override
    public void seekTo(final long millisecond) {
        if (mPlaybackActionCallback != null && mPlaybackActionCallback.isActive()) {
            long currentProgress = mPlayer != null ? mPlayer.getCurrentPosition() : 0;
            mPlaybackActionCallback.onSeek(mMediaModel, currentProgress, millisecond);
        }

        seekToPosition(millisecond);
    }

    @Override
    public void triggerPlayOrPause(final boolean setPlay) {

        if (mPlayer != null) {
            mPlayer.setPlayWhenReady(setPlay);
            isVideoPlayWhenReady.set(setPlay);
        }

        if (mPlaybackActionCallback != null && mPlaybackActionCallback.isActive()) {
            mPlaybackActionCallback.onPlayToggle(mMediaModel, setPlay);
        }
    }

    @Override
    public void clickCurrentAd() {
        if (mPlaybackActionCallback == null) {
            ExoPlayerLogger.w(TAG, "clickCurrentAd params is null");
            return;
        }

        mPlaybackActionCallback.onLearnMoreClick(mMediaModel);
    }

    @Override
    public String getCurrentVideoName() {
        return videoName.get();
    }

    @Override
    public boolean isPlayWhenReady() {
        return isVideoPlayWhenReady.get();
    }

    @Override
    public boolean isCurrentVideoAd() {
        return isCurrentAd.get();
    }

    @Override
    public long currentDuration() {
        return videoDuration.get();
    }

    @Override
    public long currentProgressPosition() {
        return videoCurrentTime.get();
    }

    @Override
    public long currentBufferPosition() {
        return videoBufferedPosition.get();
    }

    //------------------------------player playback listener-------------------------------------------//

    @Override
    public void onTimelineChanged(final Timeline timeline, final Object manifest, final int reason) {
        setPlaybackState();
        updateProgress();
    }

    @Override
    public void onPositionDiscontinuity(final int reason) {
        setPlaybackState();
        updateProgress();
    }

    @Override
    public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
        playerPlaybackState.set(playbackState);
        isVideoPlayWhenReady.set(playWhenReady);
        updateProgress();
    }

    @Override
    public void onRepeatModeChanged(final int repeatMode) {

    }

    @Override
    public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {

    }

    @Override
    public void onTracksChanged(final TrackGroupArray trackGroups, final TrackSelectionArray trackSelections) {
        ExoPlayerLogger.i(TAG, "onTracksChanged");
    }

    @Override
    public void onLoadingChanged(final boolean isLoading) {
        ExoPlayerLogger.i(TAG, "onLoadingChanged");
    }

    @Override
    public void onPlayerError(final ExoPlaybackException error) {
        ExoPlayerLogger.i(TAG, "onPlayerError");
    }

    @Override
    public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) {
    }

    @Override
    public void onSeekProcessed() {

    }

    //-----------------------------------------SeekBar listener--------------------------------------------------------------//
    @Override
    public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) {

        if (fromUser) {
            long position = Utils.progressToMilli(mPlayer.getDuration(), seekBar);
            long duration = mPlayer == null ? 0 : mPlayer.getDuration();
            updateTimeTextViews(position, duration);
        }
    }

    @Override
    public void onStartTrackingTouch(final SeekBar seekBar) {
        isDraggingSeekBar.set(true);
        ExoPlayerLogger.i(TAG, "onStartTrackingTouch");
    }

    @Override
    public void onStopTrackingTouch(final SeekBar seekBar) {

        if (mPlayer != null) {
            seekTo(Utils.progressToMilli(mPlayer.getDuration(), seekBar));
        }

        isDraggingSeekBar.set(false);
        ExoPlayerLogger.i(TAG, "onStopTrackingTouch");
    }

    //---------------------------------------private method---------------------------------------------------------------------------//

    private void setPlaybackState() {
        int playBackState = mPlayer == null ? ExoPlayer.STATE_IDLE : mPlayer.getPlaybackState();
        playerPlaybackState.set(playBackState);
    }

    private void seekToPosition(long positionMs) {
        if (mPlayer != null) {
            mPlayer.seekTo(mPlayer.getCurrentWindowIndex(), positionMs);
        }
    }

    private void updateProgress() {

        long position = mPlayer == null ? 0 : mPlayer.getCurrentPosition();
        long duration = mPlayer == null ? 0 : mPlayer.getDuration();
        long bufferedPosition = mPlayer == null ? 0 : mPlayer.getBufferedPosition();

        //only update the seekBar UI when user are not interacting, to prevent UI interference
        if (!isDraggingSeekBar.get() && !isDuringCustomSeek()) {
            updateSeekBar(position, duration, bufferedPosition);
            updateTimeTextViews(position, duration);
        }

        ExoPlayerLogger.i(TAG, "updateProgress:----->" + videoCurrentTime.get());

        if (mPlaybackActionCallback != null && mPlaybackActionCallback.isActive()) {
            mPlaybackActionCallback.onProgress(mMediaModel, position, duration);
        }

        mProgressUpdateHandler.removeCallbacks(updateProgressAction);

        // Schedule an update if necessary.
        if (playerPlaybackState.get() != ExoPlayer.STATE_IDLE && playerPlaybackState.get() != STATE_ENDED
                && mPlaybackActionCallback
                .isActive()) {

            //don't post the updateProgress event when user pause the video
            if (mPlayer != null && !mPlayer.getPlayWhenReady()) {
                return;
            }

            long delayMs = DEFAULT_FREQUENCY;
            mProgressUpdateHandler.postDelayed(updateProgressAction, delayMs);
        }
    }

    private void updateSeekBar(long position, long duration, long bufferedPosition) {
        //update progressBar.
        videoCurrentTime.set(position);
        videoDuration.set(duration);
        videoBufferedPosition.set(bufferedPosition);
    }

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

} @Module
public class PlayerModuleDefault {

    public PlayerModuleDefault() {
    }

    @ActicityScope
    @Provides
    StateFactory provideStateFactory() {
        return new StateFactory();
    }

    @ActicityScope
    @Provides
    FsmPlayer provideFsmPlayer(StateFactory factory) {
        return new FsmPlayerImperial(factory) {
            @Override
            public Class initializeState() {
                return FetchCuePointState.class;
            }
        };
    }

    @ActicityScope
    @Provides
    PlayerUIController provideController() {
        return new PlayerUIController();
    }

    @ActicityScope
    @Provides
    PlayerAdLogicController provideComponentController() {
        return new PlayerAdLogicController();
    }

    @ActicityScope
    @Provides
    AdRetriever provideAdRetriever() {
        return new AdRetriever();
    }

    @ActicityScope
    @Provides
    CuePointsRetriever provideCuePointsRetriever() {
        return new CuePointsRetriever();
    }

    @ActicityScope
    @Provides
    AdPlayingMonitor provideAdPlayingMonitor(FsmPlayer player) {
        return new AdPlayingMonitor(player);
    }

    @ActicityScope
    @Provides
    CuePointMonitor provideCuePointMonitor(FsmPlayer fsmPlayer) {

        CuePointMonitor cuePointMonitor = new CuePointMonitor(fsmPlayer) {
            @Override
            public int networkingAhead() {
                return 5000;
            }
        };
        return cuePointMonitor;
    }

    @ActicityScope
    @Provides
    AdMediaModel provideAdMediaModel() {
        MediaModel ad_1 = MediaModel
                .ad("http://devimages.apple.com/samplecode/adDemo/ad.m3u8",
                        "https://tubitv.com/", false);

        final List<MediaModel> list = new ArrayList<>();
        list.add(ad_1);

        AdMediaModel adMediaModel = new AdMediaModel(list) {
            @Nullable
            @Override
            public MediaModel nextAD() {
                return list != null && list.size() > 0 ? list.get(0) : null;
            }
        };

        return adMediaModel;
    }

    @ActicityScope
    @Provides
    AdInterface provideAdInterfaceNoPreroll() {

        // using the fake generated AdMediaModel to do has the returned data.
        return new AdInterface() {
            @Override
            public void fetchAd(AdRetriever retriever, RetrieveAdCallback callback) {
                callback.onReceiveAd(provideAdMediaModel());
            }

            @Override
            public void fetchQuePoint(CuePointsRetriever retriever, CuePointCallBack callBack) {

                callBack.onCuePointReceived(null);
                //"AdBreak point at 0s, 1min, 15min, 30min, 60min. With each adbreak showing one ads"
                //new long[] { 60000, 900000, 1800000, 3600000 }
            }
        };
    }

    @ActicityScope
    @Provides
    VpaidClient provideVpaidClient() {
        return new VpaidClient() {
            @Override
            public void init(MediaModel ad) {

            }

            @Override
            public void notifyAdError(int code, String error) {

            }

            @Override
            public void notifyVideoEnd() {

            }

            @Override
            public String getVastXml() {
                return null;
            }
        };
    }
} 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,
} public interface AdInterface {

    /**
     * make call to network server to call ad
     *
     * @param retriever information to provide when retrieving ad
     * @param callback  the callback to be notify
     */
    void fetchAd(AdRetriever retriever, RetrieveAdCallback callback);

    void fetchQuePoint(CuePointsRetriever retriever, CuePointCallBack callBack);

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

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

    }

} public class AdPlayingMonitor extends EventLogger {

    public FsmAdController fsmPlayer;

    public AdPlayingMonitor(@NonNull FsmPlayer fsmPlayer) {
        super(null);
        this.fsmPlayer = fsmPlayer;
    }

    @Override
    public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) {
        super.onPlayerStateChanged(eventTime, playWhenReady, playbackState);

        //the last ad has finish playing.
        if (playbackState == Player.STATE_ENDED && playWhenReady == true) {
            fsmPlayer.removePlayedAdAndTransitToNextState();
        }
    }

    @Override
    public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
        super.onPlayerError(eventTime, error);
        fsmPlayer.removePlayedAdAndTransitToNextState();
    }

    @Override
    public void onDroppedVideoFrames(final EventTime eventTime, final int droppedFrames, final long elapsedM) {
        super.onDroppedVideoFrames(eventTime, droppedFrames, elapsedM);
        seekOrSkip();
    }

    // this is a hack to handle when played corrupted video file, it stuck in the buffering state forever.
    private void seekOrSkip() {
        if (fsmPlayer == null) {
            return;
        }

        if (fsmPlayer instanceof FsmPlayer) {

            if (((FsmPlayer) fsmPlayer).getController() != null) {
                SimpleExoPlayer adPlayer = ((FsmPlayer) fsmPlayer).getController().getAdPlayer();

                if (adPlayer != null && adPlayer.getPlaybackState() == ExoPlayer.STATE_BUFFERING) {

                    long position = adPlayer.getCurrentPosition() + 1000 < adPlayer.getDuration() ?
                            adPlayer.getCurrentPosition() + 1000 :
                            adPlayer.getDuration();
                    adPlayer.seekTo(position);
                    adPlayer.setPlayWhenReady(true);
                    return;
                }
            }
        }
    }
} public abstract class CuePointMonitor {

    private static final String TAG = CuePointMonitor.class.getSimpleName();
    private static final long RANGE_FACTOR = 1500;
    public FsmPlayer fsmPlayer;
    /**
     * this is the safe check to only call ad work one time given that the range check can make multiple add call for one cuepoint.
     */
    private boolean safeCheckForAdcall = true;
    private boolean safeCheckForCue = true;
    private long[] cuePoints;
    private long[] adCallPoints;
    /**
     * keep track of current relevant cue point this time,
     */
    private int currentQueuePointPos = -1;

    public CuePointMonitor(FsmPlayer fsmPlayer) {
        this.fsmPlayer = fsmPlayer;
    }

    public static int binarySerchWithRange(long[] a, long key) {
        return binarySearchWithRange(a, 0, a.length, key, RANGE_FACTOR);
    }

    public static int binarySerchExactly(long[] a, long key) {
        return binarySearchWithRange(a, 0, a.length, key, 0);
    }

    // Like public version, but without range checks.
    private static int binarySearchWithRange(long[] a, int fromIndex, int toIndex,
            long key, long range_factor) {
        int low = fromIndex;
        int high = toIndex - 1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            long midVal = a[mid];

            if (midVal + range_factor < key)
                low = mid + 1;
            else if (midVal - range_factor > key)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found.
    }

    public abstract int networkingAhead();

    public void setQuePoints(@Nullable long[] cuePoints) {

        currentQueuePointPos = -1;

        if (cuePoints == null) {
            this.cuePoints = null;
            adCallPoints = null;
            return;
        }

        this.cuePoints = cuePoints;
        adCallPoints = getAddCallPoints(cuePoints);
    }

    /**
     * remove the cuepoint that already shown, only when a add call is finished.
     * Not used
     */
    public void remoteShowedCuePoints() {

        if (cuePoints == null || cuePoints.length <= 0) {
            return;
        }

        //when only remain the last one.
        if (cuePoints.length == 1) {
            setQuePoints(null);
            return;
        }

        long[] newcuePoints = removeElementFromArray(cuePoints, currentQueuePointPos);

        if (newcuePoints != null) {
            setQuePoints(newcuePoints);
        }
    }

    @Nullable
    private long[] removeElementFromArray(long[] array, int keyPos) {

        if (array == null || keyPos < 0 || array.length <= 1) {
            return null;
        }

        int length = array.length - 1;

        long[] result = new long[length];

        int tempPos = 0;
        for (int i = 0; i < length; i++) {

            if (i == keyPos) {
                tempPos++;
            }

            result[i] = array[tempPos];

            tempPos++;
        }

        return result;
    }

    /**
     * this method will update frame by frame on movie millisecond to check if any action can be triggered
     *
     * @param milliseconds
     * @param durationMillis
     */
    public void onMovieProgress(long milliseconds, long durationMillis) {

        if (fsmPlayer.getCurrentState() instanceof AdPlayingState || fsmPlayer
                .getCurrentState() instanceof VpaidState) {
            // if ad playing, do nothing
            return;
        }
        //check if need to request ad call, if does, update the fsmPlayer, Request_AD.
        preformAdCallIfNecessary(milliseconds);

        //check if need to show ad, id does, update the fsmPlayer to Show_AD
        preformShowAdIfNecessary(milliseconds);
    }

    private void preformShowAdIfNecessary(long milliseconds) {
        if (isProgressActionable(cuePoints, milliseconds) && safeCheckForCue) {
            safeCheckForCue = false;
            ExoPlayerLogger.i(Constants.FSMPLAYER_TESTING, "Show ads at : " + milliseconds);
            fsmPlayer.transit(Input.SHOW_ADS);
            return;
        } else if (!isProgressActionable(cuePoints, milliseconds)) {
            safeCheckForCue = true;
        }
    }

    private void preformAdCallIfNecessary(long milliseconds) {
        if (isProgressActionable(adCallPoints, milliseconds) && safeCheckForAdcall) {
            safeCheckForAdcall = false;

            //update the cuepoint;
            if (currentQueuePointPos >= 0) {
                long currentQueuePoint = cuePoints[currentQueuePointPos];

                // update the cue point infor to AdRetriever and FsmPlayer status.
                fsmPlayer.updateCuePointForRetriever(currentQueuePoint);
                ExoPlayerLogger.i(Constants.FSMPLAYER_TESTING, "make network call at: " + milliseconds);
                fsmPlayer.transit(Input.MAKE_AD_CALL);
                return;
            }

        } else if (!isProgressActionable(adCallPoints, milliseconds)) {
            safeCheckForAdcall = true;
        }
    }

    /**
     * check if current progress millisecond is capable of trigger action
     *
     * @param array           array of relevant check points.
     * @param currentProgress current milliSecond of movie
     * @return is current millisecond need to trigger action.
     */
    private boolean isProgressActionable(long[] array, long currentProgress) {
        if (array == null || array.length <= 0) {
            currentQueuePointPos = -1;
            return false;
        }

        int resultPos = binarySerchWithRange(array, currentProgress);

        if (resultPos < 0) {
            currentQueuePointPos = -1;
            return false;
        }

        currentQueuePointPos = resultPos;
        return true;
    }

    /**
     * create add call points array base on queuePoints
     *
     * @param cuePoints the cuePoints of the movie
     * @return ad call points
     */
    private long[] getAddCallPoints(long[] cuePoints) {
        long[] array = new long[cuePoints.length];
        for (int i = 0; i < cuePoints.length; i++) {
            long temp = cuePoints[i] - networkingAhead();

            //minimum networking call is 0 millisecond.
            array[i] = temp > 0 ? temp : 0;
        }//end for
        return array;
    }

} 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);
        }
    }
} public class Constants {
    public static final String FSMPLAYER_TESTING = "FSM_LOGGING";
} public interface AutoPlay {
    void playNext(MediaModel nextVideo);
} public interface DoublePlayerInterface {

    /**
     * prepare the {@link AdMediaModel} to have the {@link com.google.android.exoplayer2.source.MediaSource} insert into it.
     *
     * @param ads the adMediaModel.
     */
    void onPrepareAds(AdMediaModel ads);

    void prepareFSM();
} 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();
        }
    }

} public class AdRetriever {

    private String videoId;

    private String publisherId;

    private long cubPoint;

    public AdRetriever() {
    }

    public AdRetriever(String videoId, String publisherId, long cubPoint) {
        this.videoId = videoId;
        this.publisherId = publisherId;
        this.cubPoint = cubPoint;
    }

    public String getVideoId() {
        return videoId;
    }

    public void setVideoId(String videoId) {
        this.videoId = videoId;
    }

    public String getPublisherId() {
        return publisherId;
    }

    public void setPublisherId(String publisherId) {
        this.publisherId = publisherId;
    }

    public long getCubPoint() {
        return cubPoint;
    }

    public void setCubPoint(long cubPoint) {
        this.cubPoint = cubPoint;
    }
} public class CuePointsRetriever {

    private String videoId;

    private String publisherId;

    public CuePointsRetriever() {
    }

    public CuePointsRetriever(String videoId, String publisherId) {
        this.videoId = videoId;
        this.publisherId = publisherId;
    }

    public String getVideoId() {
        return videoId;
    }

    public void setVideoId(String videoId) {
        this.videoId = videoId;
    }

    public String getPublisherId() {
        return publisherId;
    }

    public void setPublisherId(String publisherId) {
        this.publisherId = publisherId;
    }
} 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;
    }
} 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();
} 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);
        }
    }
} public class PlayerDeviceUtils {
    private static final String TAG = PlayerDeviceUtils.class.getSimpleName();
    private static final String XIAOMI_MANUFACTURER = "Xiaomi";
    private static final String MI_BOX_DEVICE = "once";
    private static final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv";
    private static Boolean sIsTVDevice = null;

    public static boolean isTVDevice(final Context context) {
        if (sIsTVDevice == null) {
            UiModeManager uiModeManager = (UiModeManager) context.getSystemService(UI_MODE_SERVICE);
            sIsTVDevice = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;

            if (!sIsTVDevice) { // We also check fire tv
                sIsTVDevice = context.getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
            }
        }
        return sIsTVDevice;
    }

    /**
     * Check if we should use one player instance instead of two to handle video and ads playback
     * Single player instance will only use content player without initializing ads player
     * Single player instance will re-buffer every time when we switch between MediaSource
     */
    public static boolean useSinglePlayer() {
        // Turn it on for all TV devices
        if (sIsTVDevice) {
            return true;
        }

        // Use single player instance for Mi Box, since for 2.8.3 exoplayer it doesn't support more than one player instance
        // When we start second player instance and prepare, we got insufficiant resource error and player stuck
        if (XIAOMI_MANUFACTURER.equals(Util.MANUFACTURER) && MI_BOX_DEVICE.equals(Util.DEVICE)) {
            return true;
        }
        return false;
    }
} public class Utils {
    private static String FORMAT_HOURS = "%d:%02d:%02d";
    private static String FORMAT_MINUTES = "%02d:%02d";
    private static StringBuilder formatBuilder = new StringBuilder();
    private static Formatter formatter = new Formatter(formatBuilder, Locale.getDefault());

    public static String getProgressTime(long timeMs, boolean remaining) {
        if (timeMs == C.TIME_UNSET) {
            timeMs = 0;
        }
        long totalSeconds = (timeMs + 500) / 1000;
        long seconds = totalSeconds % 60;
        long minutes = (totalSeconds / 60) % 60;
        long hours = totalSeconds / 3600;
        formatBuilder.setLength(0);
        String time = hours > 0 ? formatter.format(FORMAT_HOURS, hours, minutes, seconds).toString()
                : formatter.format(FORMAT_MINUTES, minutes, seconds).toString();
        return remaining && timeMs != 0 ? "-" + time : time;
    }

    public static long progressToMilli(long playerDurationMs, SeekBar seekBar) {
        long duration = playerDurationMs < 1 ? C.TIME_UNSET : playerDurationMs;
        return duration == C.TIME_UNSET ? 0 : ((duration * seekBar.getProgress()) / seekBar.getMax());
    }

    // This snippet hides the system bars.
    public static void hideSystemUI(@NonNull final Activity activity, final boolean immediate) {
        hideSystemUI(activity, immediate, 5000);
    }

    // This snippet hides the system bars.
    public static void hideSystemUI(@NonNull final Activity activity, final boolean immediate, final int delayMs) {
        View decorView = activity.getWindow().getDecorView();
        // Set the IMMERSIVE flag.
        // Set the content to appear under the system bars so that the content
        // doesn't resize when the system bars hide and show.
        int uiState = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
                | View.SYSTEM_UI_FLAG_FULLSCREEN; // hide status bar
        if (Util.SDK_INT > 18) {
            uiState |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                    | View.SYSTEM_UI_FLAG_IMMERSIVE;
        } else {
            final Handler handler = new Handler();
            decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
                @Override
                public void onSystemUiVisibilityChange(int visibility) {
                    if (visibility == View.VISIBLE) {
                        Runnable runnable = new Runnable() {
                            @Override
                            public void run() {
                                hideSystemUI(activity, false);
                            }
                        };
                        if (immediate) {
                            handler.post(runnable);
                        } else {
                            handler.postDelayed(runnable, delayMs);
                        }
                    }
                }
            });
        }
        decorView.setSystemUiVisibility(uiState);
    }

    /**
     * Checks if a string is empty
     *
     * @param text String to check
     * @return True if string is null or ""
     */
    public static boolean isEmpty(@Nullable String text) {
        return text == null || text.equalsIgnoreCase("");
    }

    public static float dpFromPx(final Context context, final float px) {
        return px / context.getResources().getDisplayMetrics().density;
    }

    public static float pxFromDp(final Context context, final float dp) {
        return dp * context.getResources().getDisplayMetrics().density;
    }
} public class UIControllerView extends FrameLayout {

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

    private static final int TIME_TO_HIDE_CONTROL = 3000;
    private UserController userController;

    private UiControllerViewBinding binding;

    private Handler countdownHandler;

    private Runnable hideUIAction = new Runnable() {
        @Override
        public void run() {
            binding.controllerPanel.setVisibility(GONE);
        }
    };

    public UIControllerView(final Context context) {
        this(context, null);
    }

    public UIControllerView(final Context context, @Nullable final AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public UIControllerView(final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initLayout(context);
    }

    public UIControllerView setUserController(UserController userController) {
        if (userController == null) {
            ExoPlayerLogger.w(TAG, "setUserController()--> param passed in null");
            return null;
        }

        this.userController = userController;

        if (binding != null) {
            binding.setController(userController);
        }
        return this;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        ExoPlayerLogger.w(TAG, "onDetachedFromWindow");
        countdownHandler.removeCallbacks(hideUIAction);
    }

    @Override
    public boolean onTouchEvent(final MotionEvent event) {
        countdownHandler.removeCallbacks(hideUIAction);

        if (binding.controllerPanel.getVisibility() == VISIBLE) {
            binding.controllerPanel.setVisibility(GONE);
        } else {
            if (userController.playerPlaybackState.get() != Player.STATE_IDLE) {
                binding.controllerPanel.setVisibility(VISIBLE);
                hideUiTimeout();
            }
        }

        return super.onTouchEvent(event);
    }

    private void initLayout(Context context) {
        binding = DataBindingUtil
                .inflate(LayoutInflater.from(context), R.layout.ui_controller_view, this, true);
        countdownHandler = new Handler();
    }

    private void hideUiTimeout() {
        countdownHandler.postDelayed(hideUIAction, TIME_TO_HIDE_CONTROL);
    }

}
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.View;
import android.webkit.WebBackForwardList;
import android.webkit.WebHistoryItem;
import android.webkit.WebView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.tubitv.media.bindings.UserController;
import com.tubitv.media.controller.PlayerAdLogicController;
import com.tubitv.media.controller.PlayerUIController;
import com.tubitv.media.di.PlayerModuleDefault;
import com.tubitv.media.di.component.DaggerFsmComonent;
import com.tubitv.media.fsm.Input;
import com.tubitv.media.fsm.callback.AdInterface;
import com.tubitv.media.fsm.concrete.AdPlayingState;
import com.tubitv.media.fsm.concrete.VpaidState;
import com.tubitv.media.fsm.listener.AdPlayingMonitor;
import com.tubitv.media.fsm.listener.CuePointMonitor;
import com.tubitv.media.fsm.state_machine.FsmPlayer;
import com.tubitv.media.helpers.Constants;
import com.tubitv.media.interfaces.AutoPlay;
import com.tubitv.media.interfaces.DoublePlayerInterface;
import com.tubitv.media.models.AdMediaModel;
import com.tubitv.media.models.AdRetriever;
import com.tubitv.media.models.CuePointsRetriever;
import com.tubitv.media.models.MediaModel;
import com.tubitv.media.models.VpaidClient;
import com.tubitv.media.utilities.ExoPlayerLogger;
import com.tubitv.media.utilities.PlayerDeviceUtils;
import com.tubitv.media.utilities.Utils;
import com.tubitv.media.views.UIControllerView;
import javax.inject.Inject;
    @Override
    protected void releaseMoviePlayer() {
        super.releaseMoviePlayer();
        if (!PlayerDeviceUtils.useSinglePlayer()) {
            releaseAdPlayer();
        }
    }

    private void setupAdPlayer() {
        TrackSelection.Factory adaptiveTrackSelectionFactory =
                new AdaptiveTrackSelection.Factory(BANDWIDTH_METER_AD);
        trackSelector_ad = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
        adPlayer = ExoPlayerFactory.newSimpleInstance(this, trackSelector_ad);
    }

    private void releaseAdPlayer() {
        if (adPlayer != null) {
            updateAdResumePosition();
            adPlayer.release();
            adPlayer = null;
            trackSelector_ad = null;
        }

        if (vpaidWebView != null) {
            vpaidWebView.loadUrl(VpaidClient.EMPTY_URL);
            vpaidWebView.clearHistory();
        }
    }

    private void updateAdResumePosition() {
        if (adPlayer != null && playerUIController != null) {
            int adResumeWindow = adPlayer.getCurrentWindowIndex();
            long adResumePosition = adPlayer.isCurrentWindowSeekable() ? Math.max(0, adPlayer.getCurrentPosition())
                    : C.TIME_UNSET;
            playerUIController.setAdResumeInfo(adResumeWindow, adResumePosition);
        }
    }

    /**
     * update the movie and ad playing position when players are released
     */
    @Override
    protected void updateResumePosition() {
        //keep track of movie player's position when activity resume back
        if (mMoviePlayer != null && playerUIController != null
                && mMoviePlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) {
            int resumeWindow = mMoviePlayer.getCurrentWindowIndex();
            long resumePosition = mMoviePlayer.isCurrentWindowSeekable() ?
                    Math.max(0, mMoviePlayer.getCurrentPosition())
                    :
                    C.TIME_UNSET;
            playerUIController.setMovieResumeInfo(resumeWindow, resumePosition);
            ExoPlayerLogger.i(Constants.FSMPLAYER_TESTING, resumePosition + "");
        }

        //keep track of ad player's position when activity resume back, only keep track when current state is in AdPlayingState.
        if (fsmPlayer.getCurrentState() instanceof AdPlayingState && adPlayer != null && playerUIController != null
                && adPlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) {
            int ad_resumeWindow = adPlayer.getCurrentWindowIndex();
            long ad_resumePosition = adPlayer.isCurrentWindowSeekable() ? Math.max(0, adPlayer.getCurrentPosition())
                    : C.TIME_UNSET;
            playerUIController.setAdResumeInfo(ad_resumeWindow, ad_resumePosition);
        }
    }

    @Override
    protected boolean isCaptionPreferenceEnable() {
        return true;
    }

    /**
     * prepare / set up FSM and inject all the elements into the FSM
     */
    @Override
    public void prepareFSM() {
        //update the playerUIController view, need to update the view everything when two ExoPlayer being recreated in activity lifecycle.
        playerUIController.setContentPlayer(mMoviePlayer);

        if (!PlayerDeviceUtils.useSinglePlayer()) {
            playerUIController.setAdPlayer(adPlayer);
        }

        playerUIController.setExoPlayerView(mTubiPlayerView);
        playerUIController.setVpaidWebView(vpaidWebView);

        //update the MediaModel
        fsmPlayer.setController(playerUIController);
        fsmPlayer.setMovieMedia(mediaModel);
        fsmPlayer.setAdRetriever(adRetriever);
        fsmPlayer.setCuePointsRetriever(cuePointsRetriever);
        fsmPlayer.setAdServerInterface(adInterface);

        //set the PlayerComponentController.
        playerComponentController.setAdPlayingMonitor(adPlayingMonitor);
        playerComponentController.setTubiPlaybackInterface(this);
        playerComponentController.setDoublePlayerInterface(this);
        playerComponentController.setCuePointMonitor(cuePointMonitor);
        playerComponentController.setVpaidClient(vpaidClient);
        fsmPlayer.setPlayerComponentController(playerComponentController);
        fsmPlayer.setLifecycle(getLifecycle());

        if (fsmPlayer.isInitialized()) {
            fsmPlayer.updateSelf();
            Utils.hideSystemUI(this, true);
        } else {
            fsmPlayer.transit(Input.INITIALIZE);
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        if (playerUIController != null) {
            playerUIController.clearMovieResumeInfo();
        }
    }

    @Override
    public void onBackPressed() {

        if (fsmPlayer != null && fsmPlayer.getCurrentState() instanceof VpaidState && vpaidWebView != null