com.battlelancer.seriesguide.ui.EpisodeDetailsFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.battlelancer.seriesguide.ui.EpisodeDetailsFragment.java

Source

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

package com.battlelancer.seriesguide.ui;

import android.content.Intent;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.TextAppearanceSpan;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import butterknife.ButterKnife;
import butterknife.InjectView;
import com.battlelancer.seriesguide.R;
import com.battlelancer.seriesguide.api.Action;
import com.battlelancer.seriesguide.enums.EpisodeFlags;
import com.battlelancer.seriesguide.enums.TraktAction;
import com.battlelancer.seriesguide.extensions.ActionsFragmentContract;
import com.battlelancer.seriesguide.extensions.EpisodeActionsHelper;
import com.battlelancer.seriesguide.extensions.ExtensionManager;
import com.battlelancer.seriesguide.loaders.EpisodeActionsLoader;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.Episodes;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.ListItemTypes;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.Seasons;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.Shows;
import com.battlelancer.seriesguide.provider.SeriesGuideDatabase.Tables;
import com.battlelancer.seriesguide.thetvdbapi.TheTVDB;
import com.battlelancer.seriesguide.ui.dialogs.CheckInDialogFragment;
import com.battlelancer.seriesguide.ui.dialogs.ManageListsDialogFragment;
import com.battlelancer.seriesguide.util.EpisodeTools;
import com.battlelancer.seriesguide.util.ServiceUtils;
import com.battlelancer.seriesguide.util.ShareUtils;
import com.battlelancer.seriesguide.util.TimeTools;
import com.battlelancer.seriesguide.util.TraktSummaryTask;
import com.battlelancer.seriesguide.util.TraktTask.TraktActionCompleteEvent;
import com.battlelancer.seriesguide.util.TraktTools;
import com.battlelancer.seriesguide.util.Utils;
import com.squareup.picasso.Callback;
import com.uwetrottmann.androidutils.AndroidUtils;
import com.uwetrottmann.androidutils.CheatSheet;
import de.greenrobot.event.EventBus;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import timber.log.Timber;

/**
 * Displays details about a single episode like summary, ratings and episode image if available.
 */
public class EpisodeDetailsFragment extends Fragment implements ActionsFragmentContract {

    private static final String TAG = "Episode Details";

    private static final String KEY_EPISODE_TVDB_ID = "episodeTvdbId";

    private Handler mHandler = new Handler();
    private TraktSummaryTask mTraktTask;

    protected int mEpisodeFlag;
    protected boolean mCollected;
    protected int mShowTvdbId;
    protected int mSeasonNumber;
    protected int mEpisodeNumber;
    private String mEpisodeTitle;
    private String mShowTitle;
    private int mShowRunTime;
    private long mEpisodeReleaseTime;

    @InjectView(R.id.scrollViewEpisode)
    ScrollView mEpisodeScrollView;
    @InjectView(R.id.containerEpisode)
    View mEpisodeContainer;
    @InjectView(R.id.ratingbar)
    View mRatingsContainer;
    @InjectView(R.id.containerEpisodeActions)
    LinearLayout mActionsContainer;

    @InjectView(R.id.containerEpisodeImage)
    View mImageContainer;
    @InjectView(R.id.imageViewEpisode)
    ImageView mEpisodeImage;

    @InjectView(R.id.textViewEpisodeTitle)
    TextView mTitle;
    @InjectView(R.id.textViewEpisodeDescription)
    TextView mDescription;
    @InjectView(R.id.textViewEpisodeReleaseTime)
    TextView mReleaseTime;
    @InjectView(R.id.textViewEpisodeReleaseDay)
    TextView mReleaseDay;
    @InjectView(R.id.textViewEpisodeLastEdit)
    TextView mLastEdit;
    @InjectView(R.id.labelEpisodeGuestStars)
    View mLabelGuestStars;
    @InjectView(R.id.textViewEpisodeGuestStars)
    TextView mGuestStars;
    @InjectView(R.id.textViewEpisodeDirectors)
    TextView mDirectors;
    @InjectView(R.id.textViewEpisodeWriters)
    TextView mWriters;
    @InjectView(R.id.labelEpisodeDvd)
    View mLabelDvd;
    @InjectView(R.id.textViewEpisodeDvd)
    TextView mDvd;
    @InjectView(R.id.textViewRatingsTvdbValue)
    TextView mTvdbRating;

    @InjectView(R.id.buttonEpisodeCheckin)
    Button mCheckinButton;
    @InjectView(R.id.buttonEpisodeWatched)
    Button mWatchedButton;
    @InjectView(R.id.buttonEpisodeCollected)
    Button mCollectedButton;
    @InjectView(R.id.buttonEpisodeSkip)
    Button mSkipButton;

    @InjectView(R.id.buttonShowInfoIMDB)
    View mImdbButton;
    @InjectView(R.id.buttonTVDB)
    View mTvdbButton;
    @InjectView(R.id.buttonTrakt)
    View mTraktButton;
    @InjectView(R.id.buttonWebSearch)
    View mWebSearchButton;
    @InjectView(R.id.buttonShouts)
    View mCommentsButton;

    /**
     * Data which has to be passed when creating this fragment.
     */
    public interface InitBundle {

        /**
         * Integer extra.
         */
        String EPISODE_TVDBID = "episode_tvdbid";

        /**
         * Boolean extra.
         */
        String IS_IN_MULTIPANE_LAYOUT = "multipane";
    }

    public static EpisodeDetailsFragment newInstance(int episodeId, boolean isInMultiPaneLayout) {
        EpisodeDetailsFragment f = new EpisodeDetailsFragment();

        // Supply index input as an argument.
        Bundle args = new Bundle();
        args.putInt(InitBundle.EPISODE_TVDBID, episodeId);
        args.putBoolean(InitBundle.IS_IN_MULTIPANE_LAYOUT, isInMultiPaneLayout);
        f.setArguments(args);

        return f;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragment_episode, container, false);
        ButterKnife.inject(this, v);

        mEpisodeContainer.setVisibility(View.GONE);

        // web search button unused, is available as extension
        mWebSearchButton.setVisibility(View.GONE);

        return v;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        getLoaderManager().initLoader(EpisodesActivity.EPISODE_LOADER_ID, null, mEpisodeDataLoaderCallbacks);

        setHasOptionsMenu(true);
    }

    @Override
    public void onResume() {
        super.onResume();

        EventBus.getDefault().register(this);
        loadEpisodeActionsDelayed();
    }

    @Override
    public void onPause() {
        super.onPause();

        if (mHandler != null) {
            mHandler.removeCallbacks(mEpisodeActionsRunnable);
        }
        EventBus.getDefault().unregister(this);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();

        // Always cancel the request here, this is safe to call even if the image has been loaded.
        // This ensures that the anonymous callback we have does not prevent the fragment from
        // being garbage collected. It also prevents our callback from getting invoked even after the
        // fragment is destroyed.
        ServiceUtils.getPicasso(getActivity()).cancelRequest(mEpisodeImage);
        ButterKnife.reset(this);
    }

    @Override
    public void onDestroy() {
        if (mTraktTask != null) {
            mTraktTask.cancel(true);
            mTraktTask = null;
        }
        super.onDestroy();
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);

        boolean isLightTheme = SeriesGuidePreferences.THEME == R.style.Theme_SeriesGuide_Light;
        // multi-pane layout has non-transparent action bar, adjust icon color
        boolean isInMultipane = getArguments().getBoolean(InitBundle.IS_IN_MULTIPANE_LAYOUT);
        inflater.inflate(
                isLightTheme && !isInMultipane ? R.menu.episodedetails_menu_light : R.menu.episodedetails_menu,
                menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int itemId = item.getItemId();
        if (itemId == R.id.menu_share) {
            shareEpisode();
            return true;
        } else if (itemId == R.id.menu_manage_lists) {
            ManageListsDialogFragment.showListsDialog(getEpisodeTvdbId(), ListItemTypes.EPISODE,
                    getFragmentManager());
            fireTrackerEvent("Manage lists");
            return true;
        } else if (itemId == R.id.menu_action_episode_calendar) {
            ShareUtils.suggestCalendarEvent(getActivity(), mShowTitle,
                    Utils.getNextEpisodeString(getActivity(), mSeasonNumber, mEpisodeNumber, mEpisodeTitle),
                    mEpisodeReleaseTime, mShowRunTime);
            fireTrackerEvent("Add to calendar");
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private int getEpisodeTvdbId() {
        return getArguments().getInt(InitBundle.EPISODE_TVDBID);
    }

    /**
     * If episode was watched, flags as unwatched. Otherwise, flags as watched.
     */
    private void onToggleWatched() {
        changeEpisodeFlag(EpisodeTools.isWatched(mEpisodeFlag) ? EpisodeFlags.UNWATCHED : EpisodeFlags.WATCHED);
    }

    /**
     * If episode was skipped, flags as unwatched. Otherwise, flags as skipped.
     */
    private void onToggleSkipped() {
        changeEpisodeFlag(EpisodeTools.isSkipped(mEpisodeFlag) ? EpisodeFlags.UNWATCHED : EpisodeFlags.SKIPPED);
    }

    private void changeEpisodeFlag(int episodeFlag) {
        mEpisodeFlag = episodeFlag;
        EpisodeTools.episodeWatched(getActivity(), mShowTvdbId, getEpisodeTvdbId(), mSeasonNumber, mEpisodeNumber,
                episodeFlag);
    }

    private void onToggleCollected() {
        mCollected = !mCollected;
        EpisodeTools.episodeCollected(getActivity(), mShowTvdbId, getEpisodeTvdbId(), mSeasonNumber, mEpisodeNumber,
                mCollected);
    }

    @Override
    public void onEventMainThread(ExtensionManager.EpisodeActionReceivedEvent event) {
        if (getEpisodeTvdbId() == event.episodeTvdbId) {
            loadEpisodeActionsDelayed();
        }
    }

    public void onEvent(TraktActionCompleteEvent event) {
        if (event.mTraktAction == TraktAction.RATE_EPISODE) {
            loadTraktRatings(false);
        }
    }

    private LoaderManager.LoaderCallbacks<Cursor> mEpisodeDataLoaderCallbacks = new LoaderManager.LoaderCallbacks<Cursor>() {
        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            return new CursorLoader(getActivity(),
                    Episodes.buildEpisodeWithShowUri(String.valueOf(getEpisodeTvdbId())), DetailsQuery.PROJECTION,
                    null, null, null);
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            populateEpisodeData(data);
        }

        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
            // do nothing (we are never holding onto the cursor
        }
    };

    private void populateEpisodeData(Cursor cursor) {
        if (cursor == null || !cursor.moveToFirst()) {
            // no data to display
            if (mEpisodeContainer != null) {
                mEpisodeContainer.setVisibility(View.GONE);
            }
            return;
        }

        mShowTvdbId = cursor.getInt(DetailsQuery.REF_SHOW_ID);
        mSeasonNumber = cursor.getInt(DetailsQuery.SEASON);
        mEpisodeNumber = cursor.getInt(DetailsQuery.NUMBER);
        mShowRunTime = cursor.getInt(DetailsQuery.SHOW_RUNTIME);
        mEpisodeReleaseTime = cursor.getLong(DetailsQuery.FIRST_RELEASE_MS);

        // title and description
        mEpisodeTitle = cursor.getString(DetailsQuery.TITLE);
        mTitle.setText(mEpisodeTitle);
        mDescription.setText(cursor.getString(DetailsQuery.OVERVIEW));

        // show title
        mShowTitle = cursor.getString(DetailsQuery.SHOW_TITLE);

        // release time and day
        SpannableStringBuilder timeAndNumbersText = new SpannableStringBuilder();
        if (mEpisodeReleaseTime != -1) {
            Date actualRelease = TimeTools.getEpisodeReleaseTime(getActivity(), mEpisodeReleaseTime);
            mReleaseDay.setText(TimeTools.formatToDate(getActivity(), actualRelease));
            // "in 15 mins (Fri)"
            timeAndNumbersText.append(getString(R.string.release_date_and_day,
                    TimeTools.formatToRelativeLocalReleaseTime(getActivity(), actualRelease),
                    TimeTools.formatToLocalReleaseDay(actualRelease)).toUpperCase(Locale.getDefault()));
            timeAndNumbersText.append("  ");
        } else {
            mReleaseDay.setText(R.string.unknown);
        }
        // absolute number (e.g. relevant for Anime): "ABSOLUTE 142"
        int numberStartIndex = timeAndNumbersText.length();
        int absoluteNumber = cursor.getInt(DetailsQuery.ABSOLUTE_NUMBER);
        if (absoluteNumber > 0) {
            timeAndNumbersText.append(getString(R.string.episode_number_absolute)).append(" ")
                    .append(String.valueOf(absoluteNumber));
            // de-emphasize number
            timeAndNumbersText.setSpan(new TextAppearanceSpan(getActivity(), R.style.TextAppearance_Caption_Dim),
                    numberStartIndex, timeAndNumbersText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        mReleaseTime.setText(timeAndNumbersText);

        // guest stars
        Utils.setLabelValueOrHide(mLabelGuestStars, mGuestStars,
                Utils.splitAndKitTVDBStrings(cursor.getString(DetailsQuery.GUESTSTARS)));
        // DVD episode number
        Utils.setLabelValueOrHide(mLabelDvd, mDvd, cursor.getDouble(DetailsQuery.DVDNUMBER));
        // directors
        Utils.setValueOrPlaceholder(mDirectors,
                Utils.splitAndKitTVDBStrings(cursor.getString(DetailsQuery.DIRECTORS)));
        // writers
        Utils.setValueOrPlaceholder(mWriters, Utils.splitAndKitTVDBStrings(cursor.getString(DetailsQuery.WRITERS)));

        // last TVDb edit date
        long lastEditSeconds = cursor.getLong(DetailsQuery.LASTEDIT);
        if (lastEditSeconds > 0) {
            mLastEdit.setText(DateUtils.formatDateTime(getActivity(), lastEditSeconds * 1000,
                    DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME));
        } else {
            mLastEdit.setText(R.string.unknown);
        }

        // ratings
        mRatingsContainer.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                rateOnTrakt();
            }
        });
        mRatingsContainer.setFocusable(true);
        CheatSheet.setup(mRatingsContainer, R.string.action_rate);
        // TVDb rating
        String tvdbRating = cursor.getString(DetailsQuery.RATING);
        if (!TextUtils.isEmpty(tvdbRating)) {
            mTvdbRating.setText(tvdbRating);
        }
        // trakt ratings
        loadTraktRatings(true);

        // episode image
        final String imagePath = cursor.getString(DetailsQuery.IMAGE);
        mImageContainer.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent fullscreen = new Intent(getActivity(), FullscreenImageActivity.class);
                fullscreen.putExtra(FullscreenImageActivity.InitBundle.IMAGE_PATH, imagePath);
                ActivityCompat.startActivity(getActivity(), fullscreen, ActivityOptionsCompat
                        .makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight()).toBundle());
            }
        });
        loadImage(imagePath);

        // check in button
        final int episodeTvdbId = cursor.getInt(DetailsQuery._ID);
        mCheckinButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // display a check-in dialog
                CheckInDialogFragment f = CheckInDialogFragment.newInstance(getActivity(), episodeTvdbId);
                f.show(getFragmentManager(), "checkin-dialog");
                fireTrackerEvent("Check-In");
            }
        });
        CheatSheet.setup(mCheckinButton);

        // watched button
        mEpisodeFlag = cursor.getInt(DetailsQuery.WATCHED);
        boolean isWatched = EpisodeTools.isWatched(mEpisodeFlag);
        Utils.setCompoundDrawablesRelativeWithIntrinsicBounds(mWatchedButton, 0,
                isWatched ? Utils.resolveAttributeToResourceId(getActivity().getTheme(), R.attr.drawableWatched)
                        : Utils.resolveAttributeToResourceId(getActivity().getTheme(), R.attr.drawableWatch),
                0, 0);
        mWatchedButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // disable button, will be re-enabled on data reload once action completes
                v.setEnabled(false);
                onToggleWatched();
                fireTrackerEvent("Toggle watched");
            }
        });
        mWatchedButton.setEnabled(true);
        mWatchedButton.setText(isWatched ? R.string.action_unwatched : R.string.action_watched);
        CheatSheet.setup(mWatchedButton, isWatched ? R.string.action_unwatched : R.string.action_watched);

        // collected button
        mCollected = cursor.getInt(DetailsQuery.COLLECTED) == 1;
        Utils.setCompoundDrawablesRelativeWithIntrinsicBounds(mCollectedButton, 0,
                mCollected ? R.drawable.ic_collected
                        : Utils.resolveAttributeToResourceId(getActivity().getTheme(), R.attr.drawableCollect),
                0, 0);
        mCollectedButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // disable button, will be re-enabled on data reload once action completes
                v.setEnabled(false);
                onToggleCollected();
                fireTrackerEvent("Toggle collected");
            }
        });
        mCollectedButton.setEnabled(true);
        mCollectedButton.setText(mCollected ? R.string.action_collection_remove : R.string.action_collection_add);
        CheatSheet.setup(mCollectedButton,
                mCollected ? R.string.action_collection_remove : R.string.action_collection_add);

        // skip button
        boolean isSkipped = EpisodeTools.isSkipped(mEpisodeFlag);
        if (isWatched) {
            // if watched do not allow skipping
            mSkipButton.setVisibility(View.INVISIBLE);
        } else {
            mSkipButton.setVisibility(View.VISIBLE);
            Utils.setCompoundDrawablesRelativeWithIntrinsicBounds(mSkipButton, 0,
                    isSkipped ? R.drawable.ic_skipped
                            : Utils.resolveAttributeToResourceId(getActivity().getTheme(), R.attr.drawableSkip),
                    0, 0);
            mSkipButton.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    // disable button, will be re-enabled on data reload once action completes
                    v.setEnabled(false);
                    onToggleSkipped();
                    fireTrackerEvent("Toggle skipped");
                }
            });
            mSkipButton.setText(isSkipped ? R.string.action_dont_skip : R.string.action_skip);
            CheatSheet.setup(mSkipButton, isSkipped ? R.string.action_dont_skip : R.string.action_skip);
        }
        mSkipButton.setEnabled(true);

        // service buttons
        ServiceUtils.setUpTraktButton(mShowTvdbId, mSeasonNumber, mEpisodeNumber, mTraktButton, TAG);
        // IMDb
        String imdbId = cursor.getString(DetailsQuery.IMDBID);
        if (TextUtils.isEmpty(imdbId)) {
            // fall back to show IMDb id
            imdbId = cursor.getString(DetailsQuery.SHOW_IMDBID);
        }
        ServiceUtils.setUpImdbButton(imdbId, mImdbButton, TAG, getActivity());
        // TVDb
        final int seasonTvdbId = cursor.getInt(DetailsQuery.REF_SEASON_ID);
        ServiceUtils.setUpTvdbButton(mShowTvdbId, seasonTvdbId, getEpisodeTvdbId(), mTvdbButton, TAG);
        // trakt comments
        mCommentsButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getActivity(), TraktShoutsActivity.class);
                intent.putExtras(TraktShoutsActivity.createInitBundleEpisode(mShowTvdbId, mSeasonNumber,
                        mEpisodeNumber, mEpisodeTitle));
                ActivityCompat.startActivity(getActivity(), intent, ActivityOptionsCompat
                        .makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight()).toBundle());
                fireTrackerEvent("Comments");
            }
        });

        mEpisodeContainer.setVisibility(View.VISIBLE);
    }

    private void loadTraktRatings(boolean isUseCachedValues) {
        if (mTraktTask == null || mTraktTask.getStatus() == AsyncTask.Status.FINISHED) {
            mTraktTask = new TraktSummaryTask(getActivity(), mRatingsContainer, isUseCachedValues)
                    .episode(mShowTvdbId, getEpisodeTvdbId(), mSeasonNumber, mEpisodeNumber);
            AndroidUtils.executeOnPool(mTraktTask);
        }
    }

    private void rateOnTrakt() {
        TraktTools.rateEpisode(getActivity(), getFragmentManager(), mShowTvdbId, mSeasonNumber, mEpisodeNumber);
        fireTrackerEvent("Rate (trakt)");
    }

    private void shareEpisode() {
        if (mEpisodeTitle == null || mShowTitle == null) {
            return;
        }
        ShareUtils.shareEpisode(getActivity(), mShowTvdbId, mSeasonNumber, mEpisodeNumber, mShowTitle,
                mEpisodeTitle);

        fireTrackerEvent("Share");
    }

    private void loadImage(String imagePath) {
        // immediately hide container if there is no image
        if (TextUtils.isEmpty(imagePath)) {
            mImageContainer.setVisibility(View.GONE);
            return;
        }

        // try loading image
        mImageContainer.setVisibility(View.VISIBLE);
        ServiceUtils.getPicasso(getActivity()).load(TheTVDB.buildScreenshotUrl(imagePath))
                .error(R.drawable.ic_image_missing).into(mEpisodeImage, new Callback() {
                    @Override
                    public void onSuccess() {
                        mEpisodeImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
                    }

                    @Override
                    public void onError() {
                        mEpisodeImage.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
                    }
                });
    }

    private LoaderManager.LoaderCallbacks<List<Action>> mEpisodeActionsLoaderCallbacks = new LoaderManager.LoaderCallbacks<List<Action>>() {
        @Override
        public Loader<List<Action>> onCreateLoader(int id, Bundle args) {
            int episodeTvdbId = args.getInt(KEY_EPISODE_TVDB_ID);
            return new EpisodeActionsLoader(getActivity(), episodeTvdbId);
        }

        @Override
        public void onLoadFinished(Loader<List<Action>> loader, List<Action> data) {
            if (data == null) {
                Timber.e("onLoadFinished: did not receive valid actions for " + getEpisodeTvdbId());
            } else {
                Timber.d("onLoadFinished: received " + data.size() + " actions for " + getEpisodeTvdbId());
            }
            EpisodeActionsHelper.populateEpisodeActions(getActivity().getLayoutInflater(), mActionsContainer, data);
        }

        @Override
        public void onLoaderReset(Loader<List<Action>> loader) {
            // do nothing, we are not holding onto the actions list
        }
    };

    public void loadEpisodeActions() {
        Bundle args = new Bundle();
        args.putInt(KEY_EPISODE_TVDB_ID, getEpisodeTvdbId());
        getLoaderManager().restartLoader(EpisodesActivity.ACTIONS_LOADER_ID, args, mEpisodeActionsLoaderCallbacks);
    }

    Runnable mEpisodeActionsRunnable = new Runnable() {
        @Override
        public void run() {
            loadEpisodeActions();
        }
    };

    public void loadEpisodeActionsDelayed() {
        mHandler.removeCallbacks(mEpisodeActionsRunnable);
        mHandler.postDelayed(mEpisodeActionsRunnable, ActionsFragmentContract.ACTION_LOADER_DELAY_MILLIS);
    }

    private void fireTrackerEvent(String label) {
        Utils.trackAction(getActivity(), TAG, label);
    }

    interface DetailsQuery {

        String[] PROJECTION = new String[] { Tables.EPISODES + "." + Episodes._ID, Shows.REF_SHOW_ID,
                Episodes.OVERVIEW, Episodes.NUMBER, Episodes.SEASON, Episodes.WATCHED, Episodes.FIRSTAIREDMS,
                Episodes.DIRECTORS, Episodes.GUESTSTARS, Episodes.WRITERS, Tables.EPISODES + "." + Episodes.RATING,
                Episodes.IMAGE, Episodes.DVDNUMBER, Episodes.TITLE, Shows.TITLE, Shows.IMDBID, Shows.RUNTIME,
                Shows.POSTER, Seasons.REF_SEASON_ID, Episodes.COLLECTED, Episodes.IMDBID, Episodes.LAST_EDITED,
                Episodes.ABSOLUTE_NUMBER };

        int _ID = 0;

        int REF_SHOW_ID = 1;

        int OVERVIEW = 2;

        int NUMBER = 3;

        int SEASON = 4;

        int WATCHED = 5;

        int FIRST_RELEASE_MS = 6;

        int DIRECTORS = 7;

        int GUESTSTARS = 8;

        int WRITERS = 9;

        int RATING = 10;

        int IMAGE = 11;

        int DVDNUMBER = 12;

        int TITLE = 13;

        int SHOW_TITLE = 14;

        int SHOW_IMDBID = 15;

        int SHOW_RUNTIME = 16;

        int SHOW_POSTER = 17;

        int REF_SEASON_ID = 18;

        int COLLECTED = 19;

        int IMDBID = 20;

        int LASTEDIT = 21;

        int ABSOLUTE_NUMBER = 22;
    }
}