public class Utils {

    /*
     * Making sure public utility methods remain static
     */
    private Utils() {
    }

    /**
     * Returns the screen/display size
     */
    public static Point getDisplaySize(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        return size;
    }

    /**
     * Shows a (long) toast
     */
    public static void showToast(Context context, String msg) {
        Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
    }

    /**
     * Shows a (long) toast.
     */
    public static void showToast(Context context, int resourceId) {
        Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG).show();
    }

    public static int convertDpToPixel(Context ctx, int dp) {
        float density = ctx.getResources().getDisplayMetrics().density;
        return Math.round((float) dp * density);
    }

    /**
     * Formats time in milliseconds to hh:mm:ss string format.
     */
    public static String formatMillis(int millis) {
        String result = "";
        int hr = millis / 3600000;
        millis %= 3600000;
        int min = millis / 60000;
        millis %= 60000;
        int sec = millis / 1000;
        if (hr > 0) {
            result += hr + ":";
        }
        if (min >= 0) {
            if (min > 9) {
                result += min + ":";
            } else {
                result += "0" + min + ":";
            }
        }
        if (sec > 9) {
            result += sec;
        } else {
            result += "0" + sec;
        }
        return result;
    }

    public static String stripCategory(String str) {
        if (str != null) {
            String[] parts = str.split("/");
            if (parts.length > 1) {
                return parts[1];
            } else {
                return str;
            }
        }
        return "";
    }
} @Table(databaseName = ContentDatabase.NAME)
public class EpisodeBaseModel extends BaseModel implements Serializable {

    private static final String TAG = "EpisodeBaseModel";

    @Column
    @PrimaryKey(autoincrement = true)
    public long DATA_ID;

    @Column
    public String DATA_TYPE;

    @Column
    public String DATA_COLLECTION_KEY;

    @Column
    public int DATA_COLLECTION_INDEX;

    @Column
    private String href;

    @Column
    private String seriesTitle;

    @Column
    private String channel;

    @Column
    private String thumbnail;

    @Column
    private String episodeHouseNumber;

    private Set<String> categories = new HashSet<>();

    @Column
    public String categoriesSerialized;

    @Column
    private String title;

    @Column
    private int duration;

    @Column
    private String rating;

    @Column
    private int episodeCount;

    @Column
    private String description;

    @Column
    private String cover;

    @Column
    private boolean isFilm = false;

    @Column
    private String related;

    @Column
    private long expiry;

    @Column
    private long pubDate;

    private long resumePosition;
    private boolean recent;

    private Map<String, List<EpisodeBaseModel>> others = new HashMap<>();

    public List<String> getCategories() {
        return new ArrayList<>(categories);
    }

    public void save() {
        updateCategoriesSerialized();
        super.save();
    }

    public void insert() {
        updateCategoriesSerialized();
        super.insert();
    }

    public void update() {
        updateCategoriesSerialized();
        super.update();
    }

    void unserialize() {
        if (categoriesSerialized != null) {
            JSONArray arr = parseArray(categoriesSerialized);
            if (arr != null) {
                for (int i = 0; i < arr.length(); i++) {
                    try {
                        addCategory(arr.getString(i));
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private JSONArray parseArray(String content) {
        if (content != null && content.contains("[") && content.contains("]")) {
            try {
                return new JSONArray(content);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    public void addCategory(String cat) {
        categories.add(cat);
    }

    public void setCategories(List<String> cats) {
        categories = new HashSet<>(cats);
    }

    private void updateCategoriesSerialized() {
        Gson gson = new GsonBuilder().create();
        categoriesSerialized = gson.toJson(categories);
    }

    public String getDurationText() {
        if (getRating() != null) {
            return getRating() + ", " + Utils.formatMillis(getDuration() * 1000);
        } else {
            return Utils.formatMillis(getDuration() * 1000);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof EpisodeBaseModel))
            return false;
        EpisodeBaseModel other = (EpisodeBaseModel) o;
        return other.getHref().equals(this.getHref());
    }

    public void merge(EpisodeBaseModel ep) {
        this.href = ep.href == null ? this.href : ep.href;
        this.seriesTitle = ep.seriesTitle == null ? this.seriesTitle : ep.seriesTitle;
        this.channel = ep.channel == null ? this.channel : ep.channel;
        this.thumbnail = ep.thumbnail == null ? this.thumbnail : ep.thumbnail;
        this.episodeHouseNumber = ep.episodeHouseNumber == null ? this.episodeHouseNumber : ep.episodeHouseNumber;
        this.categories.addAll(ep.categories);
        this.title = ep.title == null ? this.title : ep.title;
        this.duration = ep.duration;
        this.rating = ep.rating == null ? this.rating : ep.rating;
        this.episodeCount = ep.episodeCount;
        this.description = ep.description == null ? this.description : ep.description;
        this.cover = ep.cover == null ? this.cover : ep.cover;
        this.isFilm = ep.isFilm;
        this.others = ep.others.size() == 0 ? this.others : new LinkedHashMap<>(ep.others);
        this.related = ep.related == null ? this.related : ep.related;
        this.expiry = ep.expiry == 0 ? this.expiry : ep.expiry;
        this.pubDate = ep.pubDate == 0 ? this.pubDate : ep.pubDate;
    }

    protected boolean hasOther(String key) {
        return others.containsKey(key);
    }

    public void setOtherEpisodes(Map<String, List<EpisodeBaseModel>> more) {
        others = more;
    }

    public void setOtherEpisodes(String key, List<EpisodeBaseModel> more) {
        others.put(key, more);
    }

    public Map<String, List<EpisodeBaseModel>> getOtherEpisodes() {
        LinkedHashMap<String, List<EpisodeBaseModel>> all = new LinkedHashMap<>();
        for (Map.Entry<String, List<EpisodeBaseModel>> more : others().entrySet()) {
            List<EpisodeBaseModel> episodes = new ArrayList<>(more.getValue());
            if (ContentManagerBase.OTHER_EPISODES.equals(more.getKey())) {
                Collections.sort(episodes, comparePubDate());
            }
            all.put(more.getKey(), episodes);
        }
        return all;
    }

    protected Map<String, List<EpisodeBaseModel>> others() {
        return others;
    }

    private List<EpisodeBaseModel> getOtherEpisodes(String cat) {
        Map<String, List<EpisodeBaseModel>> all = getOtherEpisodes();
        if (all.containsKey(cat)) {
            return all.get(cat);
        }
        return new ArrayList<>();
    }

    protected Comparator<EpisodeBaseModel> comparePubDate() {
        return new Comparator<EpisodeBaseModel>() {
            @Override
            public int compare(EpisodeBaseModel lhs, EpisodeBaseModel rhs) {
                return (int) (lhs.pubDate - rhs.pubDate);
            }
        };
    }

    protected Comparator<EpisodeBaseModel> compareTitle() {
        return new Comparator<EpisodeBaseModel>() {
            @Override
            public int compare(EpisodeBaseModel lhs, EpisodeBaseModel rhs) {
                return lhs.getTitle().compareTo(rhs.getTitle());
            }
        };
    }

    public List<String> getOtherEpisodeUrls(String cat) {
        List<String> urls = new ArrayList<>();
        for (EpisodeBaseModel ep : getOtherEpisodes(cat)) {
            urls.add(ep.getHref());
        }
        return urls;
    }

    public boolean hasOtherEpisodes() {
        return others.size() > 0;
    }

    public boolean matches(String query) {
        boolean found = false;
        if (getSeriesTitle() != null) {
            found = found || getSeriesTitle().toLowerCase().contains(query);
        }
        if (getTitle() != null) {
            found = found || getTitle().toLowerCase().contains(query);
        }
        return found;
    }

    public boolean hasCover() {
        return getIsFilm() && getCover() != null && getCover().length() > 0;
    }

    public String getSeriesTitle() {
        return seriesTitle;
    }

    public void setSeriesTitle(String seriesTitle) {
        this.seriesTitle = seriesTitle;
    }

    public String getHref() {
        return href;
    }

    public void setHref(String href) {
        this.href = href;
    }

    public String getChannel() {
        return channel;
    }

    public void setChannel(String channel) {
        this.channel = channel;
    }

    public String getThumbnail() {
        return thumbnail;
    }

    public void setThumbnail(String thumbnail) {
        this.thumbnail = thumbnail;
    }

    public String getEpisodeHouseNumber() {
        return episodeHouseNumber;
    }

    public void setEpisodeHouseNumber(String episodeHouseNumber) {
        this.episodeHouseNumber = episodeHouseNumber;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getDuration() {
        return duration;
    }

    public void setDuration(int duration) {
        this.duration = duration;
    }

    public String getRating() {
        return rating;
    }

    public void setRating(String rating) {
        this.rating = rating;
    }

    public int getEpisodeCount() {
        return episodeCount;
    }

    public void setEpisodeCount(int episodeCount) {
        this.episodeCount = episodeCount;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getCover() {
        return cover;
    }

    public void setCover(String cover) {
        this.cover = cover;
    }

    public boolean getIsFilm() {
        return isFilm;
    }

    public void setIsFilm(boolean film) {
        this.isFilm = film;
    }

    public String getRelated() {
        return related;
    }

    public void setRelated(String related) {
        this.related = related;
    }

    public String toString() {
        return "{" + getHref() + ":'" + getSeriesTitle() + "', '" + getTitle() + "', " + pubDate + "}";
    }

    public long getExpiry() {
        return expiry;
    }

    public void setExpiry(long expiry) {
        this.expiry = expiry;
    }

    public long getResumePosition() {
        return resumePosition;
    }

    public void setResumePosition(long resumePosition) {
        this.resumePosition = resumePosition;
    }

    public int getProgress() {
        return Math.round(100 * (float) getResumePosition() / (getDuration() * 1000));
    }

    public boolean isRecent() {
        return recent;
    }

    public void setRecent(boolean recent) {
        this.recent = recent;
    }

    public long getPubDate() {
        return pubDate;
    }

    public void setPubDate(long pubDate) {
        this.pubDate = pubDate;
    }
} @Table(databaseName = ContentDatabase.NAME)
public class PlayHistory extends BaseModel {

    private static final String TAG = "PlayHistory";

    @Column
    @PrimaryKey
    public String href;

    @Column
    public String action;

    @Column
    public long duration;

    @Column
    public long position;

    @Column
    public long progress;

    @Column
    public long timestamp;

    public String toString() {
        return "PlayHistory:" + href + ", duration=" + duration + ", position=" + position + ", progress=" + progress;
    }

    public static void updateProgress(EpisodeBaseModel ep, long position) {
        PlayHistory history = new PlayHistory();
        history.href = ep.getHref();
        history.timestamp = (new Date()).getTime();
        history.duration = ep.getDuration() * 1000;
        history.position = position;
        if (history.duration > 0) {
            history.progress = Math.round(100 * (float) position / history.duration);
        } else {
            history.progress = 0;
        }
        history.save();
        Log.d(TAG, "Saving " + history);
    }
} public abstract class ContentManagerBase {

    public static final String CONTENT_ID = "io.github.xwz.base.CONTENT_ID";
    public static final String CONTENT_TAG = "io.github.xwz.base.CONTENT_TAG";

    public static final String CONTENT_SHOW_LIST_FETCHING = "io.github.xwz.base.CONTENT_SHOW_LIST_FETCHING";
    public static final String CONTENT_SHOW_LIST_START = "io.github.xwz.base.CONTENT_SHOW_LIST_START";
    public static final String CONTENT_SHOW_LIST_DONE = "io.github.xwz.base.CONTENT_SHOW_LIST_DONE";
    public static final String CONTENT_SHOW_LIST_ERROR = "io.github.xwz.base.CONTENT_SHOW_LIST_ERROR";
    public static final String CONTENT_SHOW_LIST_PROGRESS = "io.github.xwz.base.CONTENT_SHOW_LIST_PROGRESS";

    public static final String CONTENT_EPISODE_FETCHING = "io.github.xwz.base.CONTENT_EPISODE_FETCHING";
    public static final String CONTENT_EPISODE_START = "io.github.xwz.base.CONTENT_EPISODE_START";
    public static final String CONTENT_EPISODE_DONE = "io.github.xwz.base.CONTENT_EPISODE_DONE";
    public static final String CONTENT_EPISODE_ERROR = "io.github.xwz.base.CONTENT_EPISODE_ERROR";

    public static final String CONTENT_AUTH_FETCHING = "io.github.xwz.base.CONTENT_AUTH_FETCHING";
    public static final String CONTENT_AUTH_START = "io.github.xwz.base.CONTENT_AUTH_START";
    public static final String CONTENT_AUTH_DONE = "io.github.xwz.base.CONTENT_AUTH_DONE";
    public static final String CONTENT_AUTH_ERROR = "io.github.xwz.base.CONTENT_AUTH_ERROR";

    public static final String AUTH_FAILED_NETWORK = "AUTH_FAILED_NETWORK";
    public static final String AUTH_FAILED_TOKEN = "AUTH_FAILED_TOKEN";
    public static final String AUTH_FAILED_URL = "AUTH_FAILED_URL";

    public static final String OTHER_EPISODES = "OTHER_EPISODES";
    public static final String MORE_LIKE_THIS = "More Like This";
    public static final String GLOBAL_SEARCH_INTENT = "GLOBAL_SEARCH_INTENT";
    public static final String RECENTLY_PLAYED = "Recently played";

    //The columns we'll include in the video database table
    public static final String KEY_SERIES_TITLE = SearchManager.SUGGEST_COLUMN_TEXT_1;
    public static final String KEY_TITLE = SearchManager.SUGGEST_COLUMN_TEXT_2;

    public static final String KEY_IMAGE = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE;
    public static final String KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE;
    public static final String KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH;
    public static final String KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT;
    public static final String KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR;
    public static final String KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION;
    public static final String KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION;
    public static final String KEY_EXTRA_DATA = SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA;
    public static final String KEY_EXTRA_NAME = SearchManager.EXTRA_DATA_KEY;

    private static final String TAG = "ContentManagerBase";

    private static ContentManagerBase instance = null;

    private Context mContext = null;
    private ContentCacheManager mCache = null;
    private ContentDatabaseCache mDb = null;

    public enum RecommendationPosition {
        FIRST(0), SECOND(1);
        private final int id;

        private RecommendationPosition(int id) {
            this.id = id;
        }

        public int getId() {
            return id;
        }
    }

    public ContentManagerBase(Context context) {
        instance = this;
        mContext = context;
        mCache = new ContentCacheManager(context);
        mDb = new ContentDatabaseCache();
    }

    public static ContentManagerBase getInstance() {
        return instance;
    }

    public static ContentCacheManager cache() {
        return getInstance().mCache;
    }

    public static ContentDatabaseCache db() {
        return getInstance().mDb;
    }

    protected Context getContext() {
        return mContext;
    }

    public void broadcastChange(String change, String tag, String id) {
        mCache.broadcastChange(change, tag, id);
    }

    public void broadcastChange(String change, String tag) {
        mCache.broadcastChange(change, tag);
    }

    public void broadcastChange(String change) {
        mCache.broadcastChange(change);
    }

    public List<EpisodeBaseModel> searchShows(String query) {
        List<EpisodeBaseModel> results = new ArrayList<>();
        query = query.toLowerCase();
        for (EpisodeBaseModel episode : mCache.getAllShows()) {
            if (episode.matches(query)) {
                results.add(episode);
            }
        }
        Log.d(TAG, "Search: '" + query + "' found : " + results.size());
        return results;
    }

    public Cursor searchShowsCursor(String query) {
        String[] columns = new String[]{
                BaseColumns._ID,
                KEY_SERIES_TITLE,
                KEY_TITLE,
                KEY_IMAGE,
                KEY_DATA_TYPE,
                KEY_VIDEO_WIDTH,
                KEY_VIDEO_HEIGHT,
                KEY_PRODUCTION_YEAR,
                KEY_COLUMN_DURATION,
                KEY_ACTION,
                KEY_EXTRA_DATA,
                KEY_EXTRA_NAME
        };
        MatrixCursor cursor = new MatrixCursor(columns);
        for (EpisodeBaseModel ep : searchShows(query)) {
            LinkedHashMap row = new LinkedHashMap();
            row.put(BaseColumns._ID, ep.getHref());
            row.put(KEY_SERIES_TITLE, ep.getSeriesTitle());
            row.put(KEY_TITLE, ep.getTitle());
            row.put(KEY_IMAGE, ep.getThumbnail());
            row.put(KEY_DATA_TYPE, "video/mp4");
            row.put(KEY_VIDEO_WIDTH, 1280);
            row.put(KEY_VIDEO_HEIGHT, 720);
            row.put(KEY_PRODUCTION_YEAR, 2015);
            row.put(KEY_COLUMN_DURATION, ep.getDuration() * 1000);
            row.put(KEY_ACTION, GLOBAL_SEARCH_INTENT);
            row.put(KEY_EXTRA_DATA, ep.getHref());
            row.put(KEY_EXTRA_NAME, KEY_EXTRA_DATA);
            cursor.addRow(row.values());
        }
        return cursor;
    }

    public List<String> suggestions(String query) {
        return mCache.getSuggestions(query);
    }

    public List<EpisodeBaseModel> getAllShows() {
        return mCache.getAllShows();
    }

    public List<EpisodeBaseModel> getAllShowsByCategory(String cat) {
        List<EpisodeBaseModel> all = getAllShowsByCategories().get(cat);
        if (all == null) {
            all = new ArrayList<>();
        }
        return all;
    }

    public EpisodeBaseModel getEpisode(String href) {
        return mCache.getEpisode(href);
    }

    public Uri getEpisodeStreamUrl(EpisodeBaseModel episode) {
        return mCache.getEpisodeStreamUrl(episode.getHref());
    }

    public LinkedHashMap<String, List<EpisodeBaseModel>> getAllShowsByCategories() {
        return cache().getCollections();
    }

    public EpisodeBaseModel findNextEpisode(List<String> urls, String current) {
        String next = null;
        boolean found = false;
        for (String href : urls) {
            if (found) {
                next = href;
                break;
            }
            found = href.equals(current);
        }
        if (!found && next == null && urls.size() > 0) {
            next = urls.get(0);
        }
        if (next != null) {
            return getEpisode(next);
        }
        return null;
    }

    public List<EpisodeBaseModel> getRecentlyPlayed() {

        FlowCursorList<PlayHistory> cursor = new FlowCursorList<>(false,
                (new Select()).from(PlayHistory.class)
                        .orderBy("CASE WHEN "+PlayHistory$Table.PROGRESS+" >= 75 THEN 1 ELSE 0 END ASC, "+PlayHistory$Table.TIMESTAMP+" DESC")
                        .limit(30));

        List<EpisodeBaseModel> recent = new ArrayList<>();
        for (int i = 0, k = cursor.getCount(); i < k; i++) {
            PlayHistory history = cursor.getItem(i);
            EpisodeBaseModel ep = getEpisode(history.href);
            if (ep != null) {
                if (history.progress < 75) {
                    ep.setResumePosition(history.position);
                }
                ep.setRecent(true);
                recent.add(ep);
            }
        }
        return recent;
    }

    public void recommendEpisode(Context context, EpisodeBaseModel ep, RecommendationPosition position) {
        Intent intent = new Intent(context, getRecommendationServiceClass());
        intent.putExtra(CONTENT_ID, ep);
        intent.putExtra(CONTENT_TAG, position.getId());
        context.startService(intent);
    }

    public void updateRecommendations(Context context) {
        List<EpisodeBaseModel> shows = getRecommendations();
        int i = 0;
        for (EpisodeBaseModel show : shows) {
            Log.d(TAG, "Recommendation: " + i + ", " + show);
            if (i < 2) {
                recommendEpisode(context, show, RecommendationPosition.values()[i++]);
            }
        }
    }

    public abstract void fetchShowList(boolean force);
    public abstract void fetchAuthToken(EpisodeBaseModel episode);
    public abstract void fetchEpisode(EpisodeBaseModel episode);

    public abstract List<EpisodeBaseModel> getRecommendations();
    public abstract Class getRecommendationServiceClass();
} public class DurationLogger implements Runnable, VideoPlayer.Listener {

    private static final String TAG = "VideoTimeLogger";
    private static final int REFRESH_INTERVAL_MS = 1000;
    private final Handler handler = new Handler();
    private VideoPlayer player;
    private int state;

    private List<Pair<Long, OnTimeReached>> listeners = new ArrayList<>();

    public void start(VideoPlayer player) {
        this.stop();
        this.player = player;
        this.run();
    }

    private void stop() {
        handler.removeCallbacks(this);
        player = null;
    }

    public void endSession() {
        stop();
        listeners = new ArrayList<>();
    }

    /**
     * Listen for time remaining, in seconds, during play back.
     *
     * @param remaining seconds remaining
     * @param listener listener
     */
    public void addListener(long remaining, OnTimeReached listener) {
        listeners.add(new Pair<>(remaining, listener));
    }

    @Override
    public void run() {
        if (player != null) {
            //Log.d(TAG, "duration: " + player.getDuration() / 1000 + ", position:" + player.getCurrentPosition() / 1000);
            executeListeners();
        }
        handler.postDelayed(this, REFRESH_INTERVAL_MS);
    }

    private void executeListeners() {
        long duration = player.getDuration() / 1000;
        long position = player.getCurrentPosition() / 1000;
        List<Pair<Long, OnTimeReached>> executed = new ArrayList<>();
        if (duration > 0 && (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_ENDED)) {
            long remain = duration - position;
            for (Pair<Long, OnTimeReached> listener : listeners) {
                long condition = listener.first;
                if (remain <= condition) {
                    Log.d(TAG, "Executing listener, duration=" + duration + ", position=" + position + ", remaining=" + remain);
                    listener.second.onPositionRemainingReached(duration, position);
                    executed.add(listener);
                }
            }
        }
        listeners.removeAll(executed);
    }

    @Override
    public void onStateChanged(boolean playWhenReady, int playbackState) {
        state = playbackState;
        String text = "playWhenReady=" + playWhenReady + ", playbackState=";
        switch (playbackState) {
            case ExoPlayer.STATE_BUFFERING:
                text += "buffering";
                break;
            case ExoPlayer.STATE_ENDED:
                text += "ended";
                break;
            case ExoPlayer.STATE_IDLE:
                text += "idle";
                break;
            case ExoPlayer.STATE_PREPARING:
                text += "preparing";
                break;
            case ExoPlayer.STATE_READY:
                text += "ready";
                break;
            default:
                text += "unknown";
                break;
        }
        Log.d(TAG, text);
    }

    @Override
    public void onError(Exception e) {

    }

    @Override
    public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {

    }

    public interface OnTimeReached {
        void onPositionRemainingReached(long duration, long position);
    }
} public class EventLogger implements VideoPlayer.Listener, VideoPlayer.InfoListener, VideoPlayer.InternalErrorListener {

    private static final String TAG = "EventLogger";
    private static final NumberFormat TIME_FORMAT;

    static {
        TIME_FORMAT = NumberFormat.getInstance(Locale.US);
        TIME_FORMAT.setMinimumFractionDigits(2);
        TIME_FORMAT.setMaximumFractionDigits(2);
    }

    private long sessionStartTimeMs;
    private long[] loadStartTimeMs;
    private long[] seekRangeValuesUs;

    public EventLogger() {
        loadStartTimeMs = new long[VideoPlayer.RENDERER_COUNT];
    }

    public void startSession() {
        sessionStartTimeMs = SystemClock.elapsedRealtime();
        Log.d(TAG, "start [0]");
    }

    public void endSession() {
        Log.d(TAG, "end [" + getSessionTimeString() + "]");
    }

    // VideoPlayer.Listener

    @Override
    public void onStateChanged(boolean playWhenReady, int state) {
        Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", "
                + getStateString(state) + "]");
    }

    @Override
    public void onError(Exception e) {
        Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e);
    }

    @Override
    public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
        Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + pixelWidthHeightRatio + "]");
    }

    // VideoPlayer.InfoListener

    @Override
    public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
        Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes + ", "
                + getTimeString(elapsedMs) + ", " + bitrateEstimate + "]");
    }

    @Override
    public void onDroppedFrames(int count, long elapsed) {
        Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]");
    }

    @Override
    public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
                              int mediaStartTimeMs, int mediaEndTimeMs) {
        loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
        if (VerboseLogUtil.isTagEnabled(TAG)) {
            Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type
                    + ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]");
        }
    }

    @Override
    public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
                                int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
        if (VerboseLogUtil.isTagEnabled(TAG)) {
            long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId];
            Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + downloadTime
                    + "]");
        }
    }

    @Override
    public void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs) {
        Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + format.id + ", "
                + Integer.toString(trigger) + "]");
    }

    @Override
    public void onAudioFormatEnabled(Format format, int trigger, int mediaTimeMs) {
        Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + format.id + ", "
                + Integer.toString(trigger) + "]");
    }

    // VideoPlayer.InternalErrorListener

    @Override
    public void onLoadError(int sourceId, IOException e) {
        printInternalError("loadError", e);
    }

    @Override
    public void onRendererInitializationError(Exception e) {
        printInternalError("rendererInitError", e);
    }

    @Override
    public void onDrmSessionManagerError(Exception e) {
        printInternalError("drmSessionManagerError", e);
    }

    @Override
    public void onDecoderInitializationError(DecoderInitializationException e) {
        printInternalError("decoderInitializationError", e);
    }

    @Override
    public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
        printInternalError("audioTrackInitializationError", e);
    }

    @Override
    public void onAudioTrackWriteError(AudioTrack.WriteException e) {
        printInternalError("audioTrackWriteError", e);
    }

    @Override
    public void onCryptoError(CryptoException e) {
        printInternalError("cryptoError", e);
    }

    @Override
    public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
                                     long initializationDurationMs) {
        Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
    }

    @Override
    public void onSeekRangeChanged(TimeRange seekRange) {
        seekRangeValuesUs = seekRange.getCurrentBoundsUs(seekRangeValuesUs);
        Log.d(TAG, "seekRange [ " + seekRange.type + ", " + seekRangeValuesUs[0] + ", "
                + seekRangeValuesUs[1] + "]");
    }

    private void printInternalError(String type, Exception e) {
        Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
    }

    private String getStateString(int state) {
        switch (state) {
            case ExoPlayer.STATE_BUFFERING:
                return "B";
            case ExoPlayer.STATE_ENDED:
                return "E";
            case ExoPlayer.STATE_IDLE:
                return "I";
            case ExoPlayer.STATE_PREPARING:
                return "P";
            case ExoPlayer.STATE_READY:
                return "R";
            default:
                return "?";
        }
    }

    private String getSessionTimeString() {
        return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs);
    }

    private String getTimeString(long timeMs) {
        return TIME_FORMAT.format((timeMs) / 1000f);
    }

} public class HlsRendererBuilder implements VideoPlayer.RendererBuilder {

    private static final int BUFFER_SEGMENT_SIZE = 256 * 1024;
    private static final int BUFFER_SEGMENTS = 64;

    private final Context context;
    private final String userAgent;
    private final String url;
    private final AudioCapabilities audioCapabilities;

    private AsyncRendererBuilder currentAsyncBuilder;

    public HlsRendererBuilder(Context context, String userAgent, String url,
                              AudioCapabilities audioCapabilities) {
        this.context = context;
        this.userAgent = userAgent;
        this.url = url;
        this.audioCapabilities = audioCapabilities;
    }

    @Override
    public void buildRenderers(VideoPlayer player) {
        currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, audioCapabilities,
                player);
        currentAsyncBuilder.init();
    }

    @Override
    public void cancel() {
        if (currentAsyncBuilder != null) {
            currentAsyncBuilder.cancel();
            currentAsyncBuilder = null;
        }
    }

    private static final class AsyncRendererBuilder implements ManifestCallback<HlsPlaylist> {

        private final Context context;
        private final String userAgent;
        private final String url;
        private final AudioCapabilities audioCapabilities;
        private final VideoPlayer player;
        private final ManifestFetcher<HlsPlaylist> playlistFetcher;

        private boolean canceled;

        public AsyncRendererBuilder(Context context, String userAgent, String url,
                                    AudioCapabilities audioCapabilities, VideoPlayer player) {
            this.context = context;
            this.userAgent = userAgent;
            this.url = url;
            this.audioCapabilities = audioCapabilities;
            this.player = player;
            HlsPlaylistParser parser = new HlsPlaylistParser();
            playlistFetcher = new ManifestFetcher<>(url, new DefaultUriDataSource(context, userAgent),
                    parser);
        }

        public void init() {
            playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this);
        }

        public void cancel() {
            canceled = true;
        }

        @Override
        public void onSingleManifestError(IOException e) {
            if (canceled) {
                return;
            }

            player.onRenderersError(e);
        }

        @Override
        public void onSingleManifest(HlsPlaylist manifest) {
            if (canceled) {
                return;
            }

            Handler mainHandler = player.getMainHandler();
            LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
            DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();

            int[] variantIndices = null;
            if (manifest instanceof HlsMasterPlaylist) {
                HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) manifest;
                try {
                    variantIndices = VideoFormatSelectorUtil.selectVideoFormatsForDefaultDisplay(
                            context, masterPlaylist.variants, null, false);
                } catch (DecoderQueryException e) {
                    player.onRenderersError(e);
                    return;
                }
                if (variantIndices.length == 0) {
                    player.onRenderersError(new IllegalStateException("No variants selected."));
                    return;
                }
            }

            DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
            HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter,
                    variantIndices, HlsChunkSource.ADAPTIVE_MODE_SPLICE, audioCapabilities);
            HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
                    BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, VideoPlayer.TYPE_VIDEO);
            MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
                    MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, mainHandler, player, 50);
            MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
            MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
                    sampleSource, new Id3Parser(), player, mainHandler.getLooper());
            Eia608TrackRenderer closedCaptionRenderer = new Eia608TrackRenderer(sampleSource, player,
                    mainHandler.getLooper());

            TrackRenderer[] renderers = new TrackRenderer[VideoPlayer.RENDERER_COUNT];
            renderers[VideoPlayer.TYPE_VIDEO] = videoRenderer;
            renderers[VideoPlayer.TYPE_AUDIO] = audioRenderer;
            renderers[VideoPlayer.TYPE_METADATA] = id3Renderer;
            renderers[VideoPlayer.TYPE_TEXT] = closedCaptionRenderer;
            player.onRenderers(null, null, renderers, bandwidthMeter);
        }

    }

} public class VideoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
        HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
        MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
        StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer,
        MetadataRenderer<Map<String, Object>>, DebugTextViewHelper.Provider {

    /**
     * Builds renderers for the player.
     */
    public interface RendererBuilder {
        /**
         * Builds renderers for playback.
         *
         * @param player The player for which renderers are being built. {@link VideoPlayer#onRenderers}
         *               should be invoked once the renderers have been built. If building fails,
         *               {@link VideoPlayer#onRenderersError} should be invoked.
         */
        void buildRenderers(VideoPlayer player);

        /**
         * Cancels the current build operation, if there is one. Else does nothing.
         * <p/>
         * A canceled build operation must not invoke {@link VideoPlayer#onRenderers} or
         * {@link VideoPlayer#onRenderersError} on the player, which may have been released.
         */
        void cancel();
    }

    /**
     * A listener for core events.
     */
    public interface Listener {
        void onStateChanged(boolean playWhenReady, int playbackState);

        void onError(Exception e);

        void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio);
    }

    /**
     * A listener for internal errors.
     * <p/>
     * These errors are not visible to the user, and hence this listener is provided for
     * informational purposes only. Note however that an internal error may cause a fatal
     * error if the player fails to recover. If this happens, {@link Listener#onError(Exception)}
     * will be invoked.
     */
    public interface InternalErrorListener {
        void onRendererInitializationError(Exception e);

        void onAudioTrackInitializationError(AudioTrack.InitializationException e);

        void onAudioTrackWriteError(AudioTrack.WriteException e);

        void onDecoderInitializationError(DecoderInitializationException e);

        void onCryptoError(CryptoException e);

        void onLoadError(int sourceId, IOException e);

        void onDrmSessionManagerError(Exception e);
    }

    /**
     * A listener for debugging information.
     */
    public interface InfoListener {
        void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs);

        void onAudioFormatEnabled(Format format, int trigger, int mediaTimeMs);

        void onDroppedFrames(int count, long elapsed);

        void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);

        void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
                           int mediaStartTimeMs, int mediaEndTimeMs);

        void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
                             int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);

        void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
                                  long initializationDurationMs);

        void onSeekRangeChanged(TimeRange seekRange);
    }

    /**
     * A listener for receiving notifications of timed text.
     */
    public interface CaptionListener {
        void onCues(List<Cue> cues);
    }

    /**
     * A listener for receiving ID3 metadata parsed from the media stream.
     */
    public interface Id3MetadataListener {
        void onId3Metadata(Map<String, Object> metadata);
    }

    // Constants pulled into this class for convenience.
    public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
    public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
    public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
    public static final int STATE_READY = ExoPlayer.STATE_READY;
    public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;

    public static final int DISABLED_TRACK = -1;
    public static final int PRIMARY_TRACK = 0;

    public static final int RENDERER_COUNT = 4;
    public static final int TYPE_VIDEO = 0;
    public static final int TYPE_AUDIO = 1;
    public static final int TYPE_TEXT = 2;
    public static final int TYPE_METADATA = 3;

    private static final int RENDERER_BUILDING_STATE_IDLE = 1;
    private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
    private static final int RENDERER_BUILDING_STATE_BUILT = 3;

    private final RendererBuilder rendererBuilder;
    private final ExoPlayer player;
    private final PlayerControl playerControl;
    private final Handler mainHandler;
    private final CopyOnWriteArrayList<Listener> listeners;

    private int rendererBuildingState;
    private int lastReportedPlaybackState;
    private boolean lastReportedPlayWhenReady;

    private Surface surface;
    private TrackRenderer videoRenderer;
    private CodecCounters codecCounters;
    private Format videoFormat;
    private int videoTrackToRestore;

    private BandwidthMeter bandwidthMeter;
    private MultiTrackChunkSource[] multiTrackSources;
    private String[][] trackNames;
    private int[] selectedTracks;
    private boolean backgrounded;

    private CaptionListener captionListener;
    private Id3MetadataListener id3MetadataListener;
    private InternalErrorListener internalErrorListener;
    private InfoListener infoListener;

    public VideoPlayer(RendererBuilder rendererBuilder) {
        this.rendererBuilder = rendererBuilder;
        player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
        player.addListener(this);
        playerControl = new PlayerControl(player);
        mainHandler = new Handler();
        listeners = new CopyOnWriteArrayList<>();
        lastReportedPlaybackState = STATE_IDLE;
        rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
        selectedTracks = new int[RENDERER_COUNT];
        // Disable text initially.
        selectedTracks[TYPE_TEXT] = DISABLED_TRACK;
    }

    public PlayerControl getPlayerControl() {
        return playerControl;
    }

    public void addListener(Listener listener) {
        listeners.add(listener);
    }

    public void removeListener(Listener listener) {
        listeners.remove(listener);
    }

    public void setInternalErrorListener(InternalErrorListener listener) {
        internalErrorListener = listener;
    }

    public void setInfoListener(InfoListener listener) {
        infoListener = listener;
    }

    public void setCaptionListener(CaptionListener listener) {
        captionListener = listener;
    }

    public void setMetadataListener(Id3MetadataListener listener) {
        id3MetadataListener = listener;
    }

    public void setSurface(Surface surface) {
        this.surface = surface;
        pushSurface(false);
    }

    public Surface getSurface() {
        return surface;
    }

    public void blockingClearSurface() {
        surface = null;
        pushSurface(true);
    }

    public int getTrackCount(int type) {
        return !player.getRendererHasMedia(type) ? 0 : trackNames[type].length;
    }

    public String getTrackName(int type, int index) {
        return trackNames[type][index];
    }

    public int getSelectedTrackIndex(int type) {
        return selectedTracks[type];
    }

    public void selectTrack(int type, int index) {
        if (selectedTracks[type] == index) {
            return;
        }
        selectedTracks[type] = index;
        pushTrackSelection(type, true);
        if (type == TYPE_TEXT && index == DISABLED_TRACK && captionListener != null) {
            captionListener.onCues(Collections.<Cue>emptyList());
        }
    }

    public void setBackgrounded(boolean backgrounded) {
        if (this.backgrounded == backgrounded) {
            return;
        }
        this.backgrounded = backgrounded;
        if (backgrounded) {
            videoTrackToRestore = getSelectedTrackIndex(TYPE_VIDEO);
            selectTrack(TYPE_VIDEO, DISABLED_TRACK);
            blockingClearSurface();
        } else {
            selectTrack(TYPE_VIDEO, videoTrackToRestore);
        }
    }

    public void prepare() {
        if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
            player.stop();
        }
        rendererBuilder.cancel();
        videoFormat = null;
        videoRenderer = null;
        multiTrackSources = null;
        rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
        maybeReportPlayerState();
        rendererBuilder.buildRenderers(this);
    }

    /**
     * Invoked with the results from a {@link RendererBuilder}.
     *
     * @param trackNames        The names of the available tracks, indexed by {@link VideoPlayer} TYPE_*
     *                          constants. May be null if the track names are unknown. An individual element may be null
     *                          if the track names are unknown for the corresponding type.
     * @param multiTrackSources Sources capable of switching between multiple available tracks,
     *                          indexed by {@link VideoPlayer} TYPE_* constants. May be null if there are no types with
     *                          multiple tracks. An individual element may be null if it does not have multiple tracks.
     * @param renderers         Renderers indexed by {@link VideoPlayer} TYPE_* constants. An individual
     *                          element may be null if there do not exist tracks of the corresponding type.
     * @param bandwidthMeter    Provides an estimate of the currently available bandwidth. May be null.
     */
  /* package */ void onRenderers(String[][] trackNames,
                                 MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers,
                                 BandwidthMeter bandwidthMeter) {
        // Normalize the results.
        if (trackNames == null) {
            trackNames = new String[RENDERER_COUNT][];
        }
        if (multiTrackSources == null) {
            multiTrackSources = new MultiTrackChunkSource[RENDERER_COUNT];
        }
        for (int rendererIndex = 0; rendererIndex < RENDERER_COUNT; rendererIndex++) {
            if (renderers[rendererIndex] == null) {
                // Convert a null renderer to a dummy renderer.
                renderers[rendererIndex] = new DummyTrackRenderer();
            }
            if (trackNames[rendererIndex] == null) {
                // Convert a null trackNames to an array of suitable length.
                int trackCount = multiTrackSources[rendererIndex] != null
                        ? multiTrackSources[rendererIndex].getTrackCount() : 1;
                trackNames[rendererIndex] = new String[trackCount];
            }
        }
        // Complete preparation.
        this.trackNames = trackNames;
        this.videoRenderer = renderers[TYPE_VIDEO];
        this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer
                ? ((MediaCodecTrackRenderer) videoRenderer).codecCounters
                : renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer
                ? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null;
        this.multiTrackSources = multiTrackSources;
        this.bandwidthMeter = bandwidthMeter;
        pushSurface(false);
        pushTrackSelection(TYPE_VIDEO, true);
        pushTrackSelection(TYPE_AUDIO, true);
        pushTrackSelection(TYPE_TEXT, true);
        player.prepare(renderers);
        rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
    }

    /**
     * Invoked if a {@link RendererBuilder} encounters an error.
     *
     * @param e Describes the error.
     */
  /* package */ void onRenderersError(Exception e) {
        if (internalErrorListener != null) {
            internalErrorListener.onRendererInitializationError(e);
        }
        for (Listener listener : listeners) {
            listener.onError(e);
        }
        rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
        maybeReportPlayerState();
    }

    public void setPlayWhenReady(boolean playWhenReady) {
        player.setPlayWhenReady(playWhenReady);
    }

    public void seekTo(long positionMs) {
        player.seekTo(positionMs);
    }

    public void release() {
        rendererBuilder.cancel();
        rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
        surface = null;
        player.release();
    }


    public int getPlaybackState() {
        if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
            return STATE_PREPARING;
        }
        int playerState = player.getPlaybackState();
        if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) {
            // This is an edge case where the renderers are built, but are still being passed to the
            // player's playback thread.
            return STATE_PREPARING;
        }
        return playerState;
    }

    @Override
    public Format getFormat() {
        return videoFormat;
    }

    @Override
    public BandwidthMeter getBandwidthMeter() {
        return bandwidthMeter;
    }

    @Override
    public CodecCounters getCodecCounters() {
        return codecCounters;
    }

    @Override
    public long getCurrentPosition() {
        return player.getCurrentPosition();
    }

    public long getDuration() {
        return player.getDuration();
    }

    public int getBufferedPercentage() {
        return player.getBufferedPercentage();
    }

    public boolean getPlayWhenReady() {
        return player.getPlayWhenReady();
    }

    /* package */ Looper getPlaybackLooper() {
        return player.getPlaybackLooper();
    }

    /* package */ Handler getMainHandler() {
        return mainHandler;
    }

    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int state) {
        maybeReportPlayerState();
    }

    @Override
    public void onPlayerError(ExoPlaybackException exception) {
        rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
        for (Listener listener : listeners) {
            listener.onError(exception);
        }
    }

    @Override
    public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
        for (Listener listener : listeners) {
            listener.onVideoSizeChanged(width, height, pixelWidthHeightRatio);
        }
    }

    @Override
    public void onDroppedFrames(int count, long elapsed) {
        if (infoListener != null) {
            infoListener.onDroppedFrames(count, elapsed);
        }
    }

    @Override
    public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
        if (infoListener != null) {
            infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
        }
    }

    @Override
    public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, int mediaTimeMs) {
        if (infoListener == null) {
            return;
        }
        if (sourceId == TYPE_VIDEO) {
            videoFormat = format;
            infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
        } else if (sourceId == TYPE_AUDIO) {
            infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
        }
    }

    @Override
    public void onDrmSessionManagerError(Exception e) {
        if (internalErrorListener != null) {
            internalErrorListener.onDrmSessionManagerError(e);
        }
    }

    @Override
    public void onDecoderInitializationError(DecoderInitializationException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onDecoderInitializationError(e);
        }
    }

    @Override
    public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onAudioTrackInitializationError(e);
        }
    }

    @Override
    public void onAudioTrackWriteError(AudioTrack.WriteException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onAudioTrackWriteError(e);
        }
    }

    @Override
    public void onCryptoError(CryptoException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onCryptoError(e);
        }
    }

    @Override
    public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
                                     long initializationDurationMs) {
        if (infoListener != null) {
            infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
        }
    }

    @Override
    public void onLoadError(int sourceId, IOException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onLoadError(sourceId, e);
        }
    }

    @Override
    public void onCues(List<Cue> cues) {
        if (captionListener != null && selectedTracks[TYPE_TEXT] != DISABLED_TRACK) {
            captionListener.onCues(cues);
        }
    }

    @Override
    public void onMetadata(Map<String, Object> metadata) {
        if (id3MetadataListener != null && selectedTracks[TYPE_METADATA] != DISABLED_TRACK) {
            id3MetadataListener.onId3Metadata(metadata);
        }
    }

    @Override
    public void onSeekRangeChanged(TimeRange seekRange) {
        if (infoListener != null) {
            infoListener.onSeekRangeChanged(seekRange);
        }
    }

    @Override
    public void onPlayWhenReadyCommitted() {
        // Do nothing.
    }

    @Override
    public void onDrawnToSurface(Surface surface) {
        // Do nothing.
    }

    @Override
    public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
                              int mediaStartTimeMs, int mediaEndTimeMs) {
        if (infoListener != null) {
            infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs,
                    mediaEndTimeMs);
        }
    }

    @Override
    public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
                                int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
        if (infoListener != null) {
            infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs,
                    mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs);
        }
    }

    @Override
    public void onLoadCanceled(int sourceId, long bytesLoaded) {
        // Do nothing.
    }

    @Override
    public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs) {
        // Do nothing.
    }

    private void maybeReportPlayerState() {
        boolean playWhenReady = player.getPlayWhenReady();
        int playbackState = getPlaybackState();
        if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) {
            for (Listener listener : listeners) {
                listener.onStateChanged(playWhenReady, playbackState);
            }
            lastReportedPlayWhenReady = playWhenReady;
            lastReportedPlaybackState = playbackState;
        }
    }

    private void pushSurface(boolean blockForSurfacePush) {
        if (videoRenderer == null) {
            return;
        }

        if (blockForSurfacePush) {
            player.blockingSendMessage(
                    videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
        } else {
            player.sendMessage(
                    videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
        }
    }

    private void pushTrackSelection(int type, boolean allowRendererEnable) {
        if (multiTrackSources == null) {
            return;
        }

        int trackIndex = selectedTracks[type];
        if (trackIndex == DISABLED_TRACK) {
            player.setRendererEnabled(type, false);
        } else if (multiTrackSources[type] == null) {
            player.setRendererEnabled(type, allowRendererEnable);
        } else {
            boolean playWhenReady = player.getPlayWhenReady();
            player.setPlayWhenReady(false);
            player.setRendererEnabled(type, false);
            player.sendMessage(multiTrackSources[type], MultiTrackChunkSource.MSG_SELECT_TRACK,
                    trackIndex);
            player.setRendererEnabled(type, allowRendererEnable);
            player.setPlayWhenReady(playWhenReady);
        }
    }

} public class PlaybackControls extends MediaController {
    private MediaPlayerControl mPlayer;
    private View mRoot;
    private ImageButton mPauseButton;


    public PlaybackControls(Context context) {
        super(context);
    }

    @Override
    public void setMediaPlayer(MediaPlayerControl player) {
        mPlayer = player;
        super.setMediaPlayer(player);
    }

    @Override
    public void setAnchorView(View view) {
        super.setAnchorView(view);
        mRoot = this.getChildAt(0);
        //mPauseButton = (ImageButton) mRoot.findViewById();
    }

    public boolean dispatchKeyEvent(KeyEvent event, int timeout) {
        int keyCode = event.getKeyCode();
        final boolean uniqueDown = event.getRepeatCount() == 0
                && event.getAction() == KeyEvent.ACTION_DOWN;
        if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
                || keyCode == KeyEvent.KEYCODE_SPACE) {
            if (uniqueDown) {
                doPauseResume();
                show(timeout);
                if (mPauseButton != null) {
                    mPauseButton.requestFocus();
                }
            }
            return true;
        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
            if (uniqueDown && !mPlayer.isPlaying()) {
                mPlayer.start();
                updatePausePlay();
                show(timeout);
            }
            return true;
        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
                || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
            if (uniqueDown && mPlayer.isPlaying()) {
                mPlayer.pause();
                updatePausePlay();
                show(timeout);
            }
            return true;
        } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
                || keyCode == KeyEvent.KEYCODE_VOLUME_UP
                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
                || keyCode == KeyEvent.KEYCODE_CAMERA) {
            // don't show the controls for volume adjustment
            return super.dispatchKeyEvent(event);
        } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
            if (uniqueDown) {
                hide();
            }
            return true;
        }

        show(timeout);
        return false;
    }

    private void updatePausePlay() {
        super.setMediaPlayer(mPlayer);
    }

    private void doPauseResume() {
        if (mPlayer.isPlaying()) {
            mPlayer.pause();
        } else {
            mPlayer.start();
        }
        updatePausePlay();
    }
} public class VideoPlayerView {
    private static final String TAG = "VideoPlayerView";

    private final View shutterView;
    private final AspectRatioFrameLayout videoFrame;
    private final SurfaceView surfaceView;
    private final TextView debugTextView;
    private final TextView playerStateTextView;
    private final View debugView;
    private final TextView statusTextView;
    private final SubtitleLayout subtitleLayout;
    private final EpisodeCardView nextEpisode;
    private final View nextEpisodeCard;
    private final View episodeDetails;
    private final TextView episodeTitle;
    private final TextView seriesTitle;
    private final TextView duration;

    private DebugTextViewHelper debugViewHelper;

    private final Context mContext;

    private final PlaybackControls mediaController;
    private MediaController.MediaPlayerControl mPlayer;

    private static final List<Integer> PLAY_PAUSE_EVENTS = Arrays.asList(
            KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
            KeyEvent.KEYCODE_MEDIA_PLAY,
            KeyEvent.KEYCODE_MEDIA_STOP,
            KeyEvent.KEYCODE_DPAD_CENTER
    );

    private static final List<Integer> PLAYER_EVENTS = Arrays.asList(
            KeyEvent.KEYCODE_VOLUME_DOWN,
            KeyEvent.KEYCODE_VOLUME_UP,
            KeyEvent.KEYCODE_VOLUME_MUTE,
            KeyEvent.KEYCODE_CAMERA
    );

    public VideoPlayerView(Context context, PlaybackControls controller, View root) {
        mContext = context;
        mediaController = controller;
        shutterView = root.findViewById(R.id.shutter);
        videoFrame = (AspectRatioFrameLayout) root.findViewById(R.id.video_frame);
        surfaceView = (SurfaceView) root.findViewById(R.id.surface_view);
        debugTextView = (TextView) root.findViewById(R.id.debug_text_view);
        debugView = root.findViewById(R.id.debug_view);
        statusTextView = (TextView) root.findViewById(R.id.status);
        playerStateTextView = (TextView) root.findViewById(R.id.player_state_view);
        subtitleLayout = (SubtitleLayout) root.findViewById(R.id.subtitles);
        nextEpisodeCard = root.findViewById(R.id.next_episode_card);
        episodeDetails = root.findViewById(R.id.episode_details);
        episodeTitle = (TextView) root.findViewById(R.id.episode_title);
        seriesTitle = (TextView) root.findViewById(R.id.series_title);
        duration = (TextView) root.findViewById(R.id.duration);

        ImageCardView card = (ImageCardView) root.findViewById(R.id.next_episode);
        card.setFocusable(true);
        card.setFocusableInTouchMode(true);
        card.setInfoVisibility(View.VISIBLE);
        card.setExtraVisibility(View.VISIBLE);
        card.setInfoAreaBackgroundColor(context.getResources().getColor(R.color.black_900));
        Point size = new Point(context.getResources().getDimensionPixelSize(R.dimen.card_width),
                context.getResources().getDimensionPixelSize(R.dimen.card_height));
        nextEpisode = new EpisodeCardView(context, card, size, false);
        nextEpisode.getImageCardView().setCardType(BaseCardView.CARD_TYPE_INFO_OVER);

        debugView.setVisibility(View.GONE);

        root.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                return handleTouchEvents(view, motionEvent);
            }
        });
        root.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                return handleKeyEvents(v, keyCode, event);
            }
        });
    }

    private boolean handleKeyEvents(View v, int keyCode, KeyEvent event) {
        final boolean uniqueDown = event.getRepeatCount() == 0 && event.getAction() == KeyEvent.ACTION_DOWN;
        if (uniqueDown) {
            Log.d(TAG, "keyCode:" + keyCode + ", event:" + event);
        }
        if (PLAY_PAUSE_EVENTS.contains(keyCode) && uniqueDown) {
            doPauseResume();
            return true;
        } else if (PLAYER_EVENTS.contains(keyCode)) {
            return mediaController.dispatchKeyEvent(event, 0);
        }
        return false;
    }

    private boolean handleTouchEvents(View view, MotionEvent motionEvent) {
        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
            toggleControlsVisibility();
        } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
            view.performClick();
        }
        return true;
    }

    private void doPauseResume() {
        Log.d(TAG, "doPauseResume:" + mPlayer);
        if (mPlayer != null) {
            if (mPlayer.isPlaying()) {
                mPlayer.pause();
                showControls();
            } else {
                mPlayer.start();
                hideControlsDelayed();
            }
        }
    }

    public void setEpisode(EpisodeBaseModel episode) {
        episodeTitle.setText(episode.getTitle());
        seriesTitle.setText(episode.getSeriesTitle());
        duration.setText(episode.getDurationText());
        showShutter(true);
        showEpisodeDetails();
    }

    public void setMediaPlayer(MediaController.MediaPlayerControl player) {
        mPlayer = player;
    }

    public void resetView() {
        nextEpisodeCard.setVisibility(View.GONE);
    }

    public void setVideoFrameAspectRatio(float ratio) {
        videoFrame.setAspectRatio(ratio);
    }

    public void showShutter(boolean show) {
        shutterView.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    public void setCues(List<Cue> cues) {
        subtitleLayout.setCues(cues);
    }

    public void startDebugView(VideoPlayer player) {
        debugViewHelper = new DebugTextViewHelper(player, debugTextView);
        debugViewHelper.start();
    }

    public void stopDebugView() {
        debugViewHelper.stop();
        debugViewHelper = null;
    }

    private void toggleControlsVisibility() {
        if (mediaController.isShowing()) {
            hideControls();
        } else {
            showControls();
        }
    }

    public void showControls() {
        Log.d(TAG, "Show controls");
        mediaController.show(0);
        showEpisodeDetails();
    }

    private void showEpisodeDetails() {
        episodeDetails.setVisibility(View.VISIBLE);
    }

    private void hideControls() {
        Log.d(TAG, "Hide controls");
        mediaController.hide();
        episodeDetails.setVisibility(View.GONE);
    }

    public void configureSubtitleView() {
        CaptionStyleCompat captionStyle;
        float captionFontScale;
        if (Util.SDK_INT >= 19) {
            captionStyle = getUserCaptionStyleV19();
            captionFontScale = getUserCaptionFontScaleV19();
        } else {
            captionStyle = CaptionStyleCompat.DEFAULT;
            captionFontScale = 1.0f;
        }
        subtitleLayout.setStyle(captionStyle);
        subtitleLayout.setFontScale(captionFontScale);
    }

    public Surface getVideoSurface() {
        return surfaceView.getHolder().getSurface();
    }

    public void onStateChanged(boolean playWhenReady, int playbackState) {
        if (playbackState == ExoPlayer.STATE_ENDED) {
            showControls();
        }
        String text = "playWhenReady=" + playWhenReady + ", playbackState=";
        switch (playbackState) {
            case ExoPlayer.STATE_BUFFERING:
                text += "buffering";
                showStatusText(R.string.buffering);
                break;
            case ExoPlayer.STATE_ENDED:
                text += "ended";
                break;
            case ExoPlayer.STATE_IDLE:
                text += "idle";
                break;
            case ExoPlayer.STATE_PREPARING:
                text += "preparing";
                showStatusText(R.string.loading);
                break;
            case ExoPlayer.STATE_READY:
                text += "ready";
                hideStatusText();
                showShutter(false);
                if (mPlayer != null && mPlayer.isPlaying()) {
                    hideControlsDelayed();
                }
                break;
            default:
                text += "unknown";
                break;
        }
        playerStateTextView.setText(text);
    }

    private void hideControlsDelayed() {
        final Handler handler = new Handler();
        Log.d(TAG, "hideControlsDelayed");
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                hideControls();
            }
        }, 3000);
    }

    public void suggestNextEpisode(EpisodeBaseModel episode) {
        nextEpisode.setEpisode(episode);
        nextEpisodeCard.setVisibility(View.VISIBLE);
    }

    private void showStatusText(int resId) {
        showStatusText(mContext.getResources().getString(resId));
    }

    private void hideStatusText() {
        statusTextView.setVisibility(View.GONE);
    }

    public void showStatusText(String text) {
        showShutter(false);
        statusTextView.setVisibility(View.VISIBLE);
        statusTextView.setText(text);
    }

    @TargetApi(19)
    private float getUserCaptionFontScaleV19() {
        CaptioningManager captioningManager =
                (CaptioningManager) mContext.getSystemService(Context.CAPTIONING_SERVICE);
        return captioningManager.getFontScale();
    }

    @TargetApi(19)
    private CaptionStyleCompat getUserCaptionStyleV19() {
        CaptioningManager captioningManager =
                (CaptioningManager) mContext.getSystemService(Context.CAPTIONING_SERVICE);
        return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
    }
}
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.View;
import android.widget.Toast;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
import com.google.android.exoplayer.drm.UnsupportedDrmException;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.util.Util;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.github.xwz.base.R;
import io.github.xwz.base.Utils;
import io.github.xwz.base.api.EpisodeBaseModel;
import io.github.xwz.base.api.PlayHistory;
import io.github.xwz.base.content.ContentManagerBase;
import io.github.xwz.base.player.DurationLogger;
import io.github.xwz.base.player.EventLogger;
import io.github.xwz.base.player.HlsRendererBuilder;
import io.github.xwz.base.player.VideoPlayer;
import io.github.xwz.base.views.PlaybackControls;
import io.github.xwz.base.views.VideoPlayerView;
package io.github.xwz.base.activities;





/**
 * An activity that plays media using {@link VideoPlayer}.
 */
public abstract class VideoPlayerActivity extends BaseActivity implements SurfaceHolder.Callback, VideoPlayer.Listener, VideoPlayer.CaptionListener,
        AudioCapabilitiesReceiver.Listener {

    private static final String TAG = "PlayerActivity";
    private static final String MEDIA_SESSION_TAG = "io.github.xwz.base.MEDIA_SESSION_TAG";
    private static final String RESUME_POSITION = "io.github.xwz.base.RESUME_POSITION";

    private static final CookieManager defaultCookieManager;

    static {
        defaultCookieManager = new CookieManager();
        defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
    }

    private EventLogger eventLogger;
    private MediaSession mediaSession;
    private PlaybackControls mediaController;
    private DurationLogger timeLogger;

    private VideoPlayerView videoPlayerView;