<file_sep>package com.tlongdev.spicio.module;

import android.app.Application;
import android.content.ContentResolver;
import android.database.sqlite.SQLiteOpenHelper;

import com.tlongdev.spicio.storage.DatabaseHelper;

import javax.inject.Singleton;

import dagger.Module;
import dagger.Provides;

/**
 * @author Long
 * @since 2016. 03. 05.
 */
@Module
public class StorageModule {

    @Provides
    @Singleton
    ContentResolver provideContentResolver(Application application) {
        return application.getContentResolver();
    }

    @Provides
    @Singleton
    SQLiteOpenHelper provideSQLiteOpenHelper(Application application) {
        return new DatabaseHelper(application);
    }
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.component.DaggerInteractorComponent;
import com.tlongdev.spicio.component.InteractorComponent;
import com.tlongdev.spicio.domain.interactor.impl.LoadAllSeriesInteractorImpl;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.module.FakeAppModule;
import com.tlongdev.spicio.module.FakeDaoModule;
import com.tlongdev.spicio.module.FakeThreadingModule;
import com.tlongdev.spicio.module.NetworkRepositoryModule;
import com.tlongdev.spicio.storage.dao.SeriesDao;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.LinkedList;
import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * @author Long
 * @since 2016. 03. 07.
 */
@RunWith(MockitoJUnitRunner.class)
public class LoadAllSeriesInteractorTest {

    @Mock
    private SeriesDao mSeriesDao;

    @Mock
    private LoadAllSeriesInteractor.Callback mMockedCallback;

    @Mock
    private SpicioApplication mApp;

    @Before
    public void setUp() {
        FakeDaoModule storageModule = new FakeDaoModule();
        storageModule.setSeriesDao(mSeriesDao);

        InteractorComponent component = DaggerInteractorComponent.builder()
                .spicioAppModule(new FakeAppModule(mApp))
                .daoModule(storageModule)
                .networkRepositoryModule(mock(NetworkRepositoryModule.class))
                .threadingModule(new FakeThreadingModule())
                .build();

        when(mApp.getInteractorComponent()).thenReturn(component);
    }

    @Test
    public void testSuccess() {

        List<Series> series = new LinkedList<>();
        when(mSeriesDao.getAllSeries()).thenReturn(series);

        LoadAllSeriesInteractorImpl interactor = new LoadAllSeriesInteractorImpl(
                mApp, mMockedCallback
        );
        interactor.run();

        verify(mSeriesDao).getAllSeries();
        verifyNoMoreInteractions(mSeriesDao);
        verify(mMockedCallback).onLoadAllSeriesFinish(series);
    }

    @Test
    public void testFail() {
        when(mSeriesDao.getAllSeries()).thenReturn(null);

        LoadAllSeriesInteractorImpl interactor = new LoadAllSeriesInteractorImpl(
                mApp, mMockedCallback
        );
        interactor.run();

        verify(mSeriesDao).getAllSeries();
        verifyNoMoreInteractions(mSeriesDao);
        verify(mMockedCallback).onLoadAllSeriesFail();
    }
}
<file_sep>package com.tlongdev.spicio.presentation.presenter.activity;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.domain.interactor.LoadSeasonEpisodesInteractor;
import com.tlongdev.spicio.domain.interactor.impl.LoadSeasonEpisodesInteractorImpl;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.presentation.presenter.Presenter;
import com.tlongdev.spicio.presentation.ui.view.activity.SeasonEpisodesView;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 10.
 */
public class SeasonEpisodesPresenter implements Presenter<SeasonEpisodesView>,
        LoadSeasonEpisodesInteractor.Callback{

    private SeasonEpisodesView mView;

    private SpicioApplication mApplication;

    private int mSeriesId;
    private int mSeason;

    @Override
    public void attachView(SeasonEpisodesView view) {
        mView = view;
        mApplication = view.getSpicioApplication();
    }

    @Override
    public void detachView() {
        mView = null;
    }

    public void loadEpisodes() {
        LoadSeasonEpisodesInteractor interactor = new LoadSeasonEpisodesInteractorImpl(
                mApplication, mSeriesId, mSeason, this
        );
        interactor.execute();
    }

    public void setSeriesId(int seriesId) {
        mSeriesId = seriesId;
    }

    public void setSeason(int season) {
        mSeason = season;
    }

    @Override
    public void onLoadSeasonEpisodesFinish(List<Episode> episodes) {
        if (mView != null) {
            mView.showEpisodes(episodes);
        }
    }

    @Override
    public void onLoadSeasonEpisodesFail() {
        // TODO: 2016. 03. 11.
    }
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.component.DaggerInteractorComponent;
import com.tlongdev.spicio.component.InteractorComponent;
import com.tlongdev.spicio.domain.interactor.impl.LikeEpisodeInteractorImpl;
import com.tlongdev.spicio.module.FakeAppModule;
import com.tlongdev.spicio.module.FakeDaoModule;
import com.tlongdev.spicio.module.FakeNetworkRepositoryModule;
import com.tlongdev.spicio.module.FakeThreadingModule;
import com.tlongdev.spicio.storage.dao.EpisodeDao;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * @author Long
 * @since 2016. 03. 11.
 */
@RunWith(MockitoJUnitRunner.class)
public class LikeEpisodeInteractorTest {

    @Mock
    private EpisodeDao mEpisodeDao;

    @Mock
    private LikeEpisodeInteractor.Callback mMockedCallback;

    @Mock
    private SpicioApplication mApp;

    @Before
    public void setUp() {
        FakeDaoModule daoModule = new FakeDaoModule();
        daoModule.setEpisodeDao(mEpisodeDao);

        FakeNetworkRepositoryModule networkRepositoryModule = new FakeNetworkRepositoryModule();

        InteractorComponent component = DaggerInteractorComponent.builder()
                .spicioAppModule(new FakeAppModule(mApp))
                .daoModule(daoModule)
                .networkRepositoryModule(networkRepositoryModule)
                .threadingModule(new FakeThreadingModule())
                .build();

        when(mApp.getInteractorComponent()).thenReturn(component);
    }

    @Test
    public void testCheck(){

        when(mEpisodeDao.setLiked(0, true)).thenReturn(1);

        LikeEpisodeInteractorImpl interactor = new LikeEpisodeInteractorImpl(
                mApp, 0, true, mMockedCallback
        );
        interactor.run();

        verify(mEpisodeDao).setLiked(0, true);
        verifyNoMoreInteractions(mEpisodeDao);
        verify(mMockedCallback).onEpisodeLikeFinish();
    }

    @Test
    public void testFail(){

        when(mEpisodeDao.setLiked(0, true)).thenReturn(0);

        LikeEpisodeInteractorImpl interactor = new LikeEpisodeInteractorImpl(
                mApp, 0, true, mMockedCallback
        );
        interactor.run();

        verify(mEpisodeDao).setLiked(0, true);
        verifyNoMoreInteractions(mEpisodeDao);
        verify(mMockedCallback).onEpisodeLikeFail();
    }
}
<file_sep>package com.tlongdev.spicio.presentation.ui.view.activity;

import com.tlongdev.spicio.presentation.ui.view.BaseView;

/**
 * @author Long
 * @since 2016. 03. 12.
 */
public interface LoginView extends BaseView {
    void showLoadingAnim();

    void hideLoadingAnim();

    void onLogin();
}<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.domain.model.Season;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 09.
 */
public interface LoadSeasonsInteractor extends Interactor {
    interface Callback {
        void onLoadSeasonsFinish(List<Season> seasons);

        void onLoadSeasonsFail();
    }
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.domain.model.Series;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 04.
 */
public interface TraktSearchInteractor extends Interactor {
    interface Callback {
        void onTraktSearchFinish(List<Series> series);

        void onTraktSearchFailed(); // TODO: 2016. 02. 28. needs more descriptive error data
    }
}
<file_sep>package com.tlongdev.spicio.util;

import android.util.Log;

/**
 * @author Long
 * @since 2016. 03. 05.
 */
public class AndroidLogger implements Logger {

    @Override
    public void verbose(String tag, String msg) {
        Log.v(tag, msg);
    }

    @Override
    public void debug(String tag, String msg) {
        Log.d(tag, msg);
    }

    @Override
    public void info(String tag, String msg) {
        Log.i(tag, msg);
    }

    @Override
    public void warn(String tag, String msg) {
        Log.w(tag, msg);
    }

    @Override
    public void error(String tag, String msg) {
        Log.e(tag, msg);
    }

    @Override
    public void wtf(String tag, String msg) {
        Log.wtf(tag, msg);
    }

    @Override
    public void verbose(String tag, String msg, Throwable tr) {
        Log.v(tag, msg, tr);
    }

    @Override
    public void debug(String tag, String msg, Throwable tr) {
        Log.d(tag, msg, tr);
    }

    @Override
    public void info(String tag, String msg, Throwable tr) {
        Log.i(tag, msg, tr);
    }

    @Override
    public void warn(String tag, String msg, Throwable tr) {
        Log.w(tag, msg, tr);
    }

    @Override
    public void error(String tag, String msg, Throwable tr) {
        Log.e(tag, msg, tr);
    }

    @Override
    public void wtf(String tag, String msg, Throwable tr) {
        Log.wtf(tag, msg, tr);
    }
}
<file_sep>package com.tlongdev.spicio.component;

import com.tlongdev.spicio.domain.interactor.AbstractInteractor;
import com.tlongdev.spicio.domain.interactor.impl.CheckEpisodeInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.DeleteAllDataInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.LikeEpisodeInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.LoadAllSeriesInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.LoadEpisodeDetailsInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.LoadSeasonEpisodesInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.LoadSeasonsInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.LoadSeriesDetailsInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.SaveEpisodesInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.SaveSeriesInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.TraktFullSeriesInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.TraktSearchInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.TraktSeasonEpisodesInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.TraktSeriesDetailsInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.TvdbSearchInteractorImpl;
import com.tlongdev.spicio.module.DaoModule;
import com.tlongdev.spicio.module.NetworkRepositoryModule;
import com.tlongdev.spicio.module.SpicioAppModule;
import com.tlongdev.spicio.module.ThreadingModule;

import javax.inject.Singleton;

import dagger.Component;

/**
 * @author Long
 * @since 2016. 03. 07.
 */
@Singleton
@Component(modules = {SpicioAppModule.class, DaoModule.class, NetworkRepositoryModule.class,
        ThreadingModule.class})
public interface InteractorComponent {
    void inject(AbstractInteractor abstractInteractor);

    void inject(SaveSeriesInteractorImpl saveSeriesInteractor);

    void inject(TvdbSearchInteractorImpl tvdbSearchInteractor);

    void inject(TraktSearchInteractorImpl traktSearchInteractor);

    void inject(TraktSeriesDetailsInteractorImpl traktSeriesDetailsInteractor);

    void inject(LoadAllSeriesInteractorImpl loadAllSeriesInteractor);

    void inject(TraktFullSeriesInteractorImpl traktFullSeriesInteractor);

    void inject(LoadSeasonsInteractorImpl loadSeasonsInteractor);

    void inject(LoadSeriesDetailsInteractorImpl loadSeriesDetailsInteractor);

    void inject(TraktSeasonEpisodesInteractorImpl traktEpisodeImagesInteractor);

    void inject(LoadSeasonEpisodesInteractorImpl loadSeasonEpisodesInteractor);

    void inject(SaveEpisodesInteractorImpl saveEpisodesInteractor);

    void inject(LoadEpisodeDetailsInteractorImpl loadEpisodeDetailsInteractor);

    void inject(CheckEpisodeInteractorImpl checkEpisodeInteractor);

    void inject(LikeEpisodeInteractorImpl likeEpisodeInteractor);

    void inject(DeleteAllDataInteractorImpl deleteAllDataInteractor);
}
<file_sep>package com.tlongdev.spicio.util;

import android.content.SharedPreferences;

import com.tlongdev.spicio.SpicioApplication;

import javax.inject.Inject;

/**
 * @author Long
 * @since 2016. 03. 13.
 */
public class ProfileManager {

    private static String PREF_KEY_FACEBOOK_ID = "pref_facebook_id";
    private static String PREF_KEY_GOOGLE_ID = "pref_google_id";

    @Inject SharedPreferences mPrefs;
    @Inject SharedPreferences.Editor mEditor;

    public ProfileManager(SpicioApplication application) {
        application.getProfileManagerComponent().inject(this);
    }

    public boolean isLoggedIn() {
        return mPrefs.contains(PREF_KEY_FACEBOOK_ID) || mPrefs.contains(PREF_KEY_GOOGLE_ID);
    }

    public void logout() {
        mEditor.remove(PREF_KEY_FACEBOOK_ID);
        mEditor.remove(PREF_KEY_GOOGLE_ID);
        mEditor.apply();
    }

    public void loginWithFacebook(String id) {
        mEditor.putString(PREF_KEY_FACEBOOK_ID, id);
        mEditor.apply();
    }

    public void loginWithGoogle(String id) {
        mEditor.putString(PREF_KEY_GOOGLE_ID, id);
        mEditor.apply();
    }

    public String getFacebookId(){
        return mPrefs.getString(PREF_KEY_FACEBOOK_ID, null);
    }

    public String getGoogleId() {
        return mPrefs.getString(PREF_KEY_GOOGLE_ID, null);
    }
}
<file_sep>package com.tlongdev.spicio.presentation.presenter.fragment;

import com.tlongdev.spicio.domain.interactor.CheckEpisodeInteractor;
import com.tlongdev.spicio.domain.interactor.LikeEpisodeInteractor;
import com.tlongdev.spicio.domain.interactor.LoadEpisodeDetailsInteractor;
import com.tlongdev.spicio.domain.interactor.impl.CheckEpisodeInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.LikeEpisodeInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.LoadEpisodeDetailsInteractorImpl;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.domain.model.Watched;
import com.tlongdev.spicio.presentation.presenter.Presenter;
import com.tlongdev.spicio.presentation.ui.view.fragment.EpisodeView;

/**
 * @author Long
 * @since 2016. 03. 11.
 */
public class EpisodePresenter implements Presenter<EpisodeView>,
        LoadEpisodeDetailsInteractor.Callback, CheckEpisodeInteractor.Callback,
        LikeEpisodeInteractor.Callback {

    private EpisodeView mView;

    private Episode mEpisode;

    @Override
    public void attachView(EpisodeView view) {
        mView = view;
    }

    @Override
    public void detachView() {
        mView = null;
    }

    @Override
    public void onLoadEpisodeDetailsFinish(Episode episode) {
        mEpisode = episode;
        if (mView != null) {
            mView.showEpisodeDetails(episode);
        }
    }

    @Override
    public void onLoadEpisodeDetailsFail() {
        mEpisode = null;
        if (mView != null) {
            mView.showError();
        }
    }

    public void loadEpisode(int episodeId) {
        LoadEpisodeDetailsInteractor interactor = new LoadEpisodeDetailsInteractorImpl(
                mView.getSpicioApplication(), episodeId, this
        );
        interactor.execute();
    }

    public void checkEpisode() {
        int watched = mEpisode.isWatched() == Watched.WATCHED ? Watched.NONE : Watched.WATCHED;

        mEpisode.setWatched(watched);
        CheckEpisodeInteractor interactor = new CheckEpisodeInteractorImpl(
                mView.getSpicioApplication(), mEpisode.getTraktId(), watched, this
        );
        interactor.execute();
    }

    public void likeEpisode() {
        LikeEpisodeInteractor interactor = new LikeEpisodeInteractorImpl(
                mView.getSpicioApplication(), mEpisode.getTraktId(), !mEpisode.isLiked(), this
        );
        interactor.execute();
    }

    public void skipEpisode() {
        int watched = mEpisode.isWatched() == Watched.SKIPPED ? Watched.NONE : Watched.SKIPPED;

        mEpisode.setWatched(watched);
        CheckEpisodeInteractor interactor = new CheckEpisodeInteractorImpl(
                mView.getSpicioApplication(), mEpisode.getTraktId(), watched, this
        );
        interactor.execute();
    }

    @Override
    public void onEpisodeCheckFinish() {
        if (mView != null) {
            mView.updateCheckButton(mEpisode.isWatched());
        }
    }

    @Override
    public void onEpisodeCheckFail() {

    }

    @Override
    public void onEpisodeLikeFinish() {
        mEpisode.setLiked(!mEpisode.isLiked());
        if (mView != null) {
            mView.updateLikeButton(mEpisode.isLiked());
        }
    }

    @Override
    public void onEpisodeLikeFail() {

    }
}
<file_sep>package com.tlongdev.spicio.domain.model;

import org.joda.time.DateTime;

/**
 * Inner Layer, Model.
 *
 * @author Long
 * @since 2016. 02. 28.
 */
public class TvdbEpisodeOld {

    private int id;

    private int seasonId;

    private int episodeNumber;

    private String episodeName;

    private DateTime firstAired;

    private String[] guestStars;

    private String director;

    private String[] writers;

    private String overView;

    private int seasonNumber;

    private int absoluteNumber;

    private String fileName;

    private int seriesId;

    private String imdbId;

    private double tvdbRating;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getSeasonId() {
        return seasonId;
    }

    public void setSeasonId(int seasonId) {
        this.seasonId = seasonId;
    }

    public int getEpisodeNumber() {
        return episodeNumber;
    }

    public void setEpisodeNumber(int episodeNumber) {
        this.episodeNumber = episodeNumber;
    }

    public String getEpisodeName() {
        return episodeName;
    }

    public void setEpisodeName(String episodeName) {
        this.episodeName = episodeName;
    }

    public DateTime getFirstAired() {
        return firstAired;
    }

    public void setFirstAired(DateTime firstAired) {
        this.firstAired = firstAired;
    }

    public String[] getGuestStars() {
        return guestStars;
    }

    public void setGuestStars(String[] guestStars) {
        this.guestStars = guestStars;
    }

    public String getDirector() {
        return director;
    }

    public void setDirector(String director) {
        this.director = director;
    }

    public String[] getWriters() {
        return writers;
    }

    public void setWriters(String[] writers) {
        this.writers = writers;
    }

    public String getOverView() {
        return overView;
    }

    public void setOverView(String overView) {
        this.overView = overView;
    }

    public int getSeasonNumber() {
        return seasonNumber;
    }

    public void setSeasonNumber(int seasonNumber) {
        this.seasonNumber = seasonNumber;
    }

    public int getAbsoluteNumber() {
        return absoluteNumber;
    }

    public void setAbsoluteNumber(int absoluteNumber) {
        this.absoluteNumber = absoluteNumber;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public int getSeriesId() {
        return seriesId;
    }

    public void setSeriesId(int seriesId) {
        this.seriesId = seriesId;
    }

    public String getImdbId() {
        return imdbId;
    }

    public void setImdbId(String imdbId) {
        this.imdbId = imdbId;
    }

    public double getTvdbRating() {
        return tvdbRating;
    }

    public void setTvdbRating(double tvdbRating) {
        this.tvdbRating = tvdbRating;
    }
}
<file_sep>package com.tlongdev.spicio.domain.model;

/**
 * @author Long
 * @since 2016. 03. 03.
 */
public class Image {
    private String full;

    private String medium;

    private String thumb;

    public String getFull() {
        return full;
    }

    public void setFull(String full) {
        this.full = full;
    }

    public String getMedium() {
        return medium;
    }

    public void setMedium(String medium) {
        this.medium = medium;
    }

    public String getThumb() {
        return thumb;
    }

    public void setThumb(String thumb) {
        this.thumb = thumb;
    }
}
<file_sep>package com.tlongdev.spicio.presentation.ui.adapter;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.tlongdev.spicio.R;
import com.tlongdev.spicio.domain.model.Series;

import java.util.List;

import butterknife.Bind;
import butterknife.ButterKnife;

/**
 * Outer Layer, UI.
 *
 * @author Long
 * @since 2016. 02. 25.
 */
public class SearchSeriesAdapter extends RecyclerView.Adapter<SearchSeriesAdapter.ViewHolder> {

    private List<Series> mDataSet;

    private Context mContext;

    private OnItemSelectedListener listener;

    public SearchSeriesAdapter(Context context) {
        this.mContext = context;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_search_series, parent, false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        if (mDataSet != null) {
            final Series series = mDataSet.get(position);

            holder.text.setText(series.getTitle());

            Glide.with(mContext)
                    .load(series.getImages().getPoster().getFull())
                    .diskCacheStrategy(DiskCacheStrategy.ALL)
                    .error(R.drawable.ic_movie)
                    .into(holder.poster);

            holder.root.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (listener != null) {
                        listener.onItemSelected(series);
                    }
                }
            });
        }
    }

    @Override
    public int getItemCount() {
        return mDataSet == null ? 0 : mDataSet.size();
    }

    public void setDataSet(List<Series> dataSet) {
        this.mDataSet = dataSet;
        notifyDataSetChanged();
    }

    public void setListener(OnItemSelectedListener listener) {
        this.listener = listener;
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        @Bind(R.id.text) TextView text;
        @Bind(R.id.poster) ImageView poster;

        View root;

        public ViewHolder(View view) {
            super(view);
            root = view;
            ButterKnife.bind(this, view);
        }
    }

    public interface OnItemSelectedListener {
        void onItemSelected(Series series);
    }
}
<file_sep>package com.tlongdev.spicio.domain.interactor.impl;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.domain.interactor.AbstractInteractor;
import com.tlongdev.spicio.domain.interactor.TvdbSearchInteractor;
import com.tlongdev.spicio.domain.model.TvdbSeriesOld;
import com.tlongdev.spicio.domain.repository.TvdbRepository;
import com.tlongdev.spicio.util.Logger;

import java.util.List;

import javax.inject.Inject;

/**
 * Inner Layer, Interactor
 *
 * @author Long
 * @since 2016. 02. 28.
 */
public class TvdbSearchInteractorImpl extends AbstractInteractor implements TvdbSearchInteractor {

    private static final String LOG_TAG = TvdbSearchInteractorImpl.class.getSimpleName();

    @Inject TvdbRepository mRepository;
    @Inject Logger logger;

    private String mSearchQuery;
    private Callback mCallback;

    public TvdbSearchInteractorImpl(SpicioApplication app, String query, Callback callback) {
        super(app.getInteractorComponent());
        app.getInteractorComponent().inject(this);

        mSearchQuery = query;
        mCallback = callback;
    }

    @Override
    public void run() {
        logger.debug(LOG_TAG, "started");

        List<TvdbSeriesOld> searchResult = mRepository.searchSeries(mSearchQuery);

        if (searchResult == null) {
            postError();
        } else {
            postResult(searchResult);
        }

        logger.debug(LOG_TAG, "finished");
    }

    private void postError() {
        if (mCallback == null) {
            return;
        }

        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onTvdbSearchFailed();
            }
        });
    }

    private void postResult(final List<TvdbSeriesOld> searchResult) {
        if (mCallback == null) {
            return;
        }

        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onTvdbSearchFinish(searchResult);
            }
        });
    }
}
<file_sep>package com.tlongdev.spicio.presentation.presenter.activity;

import com.tlongdev.spicio.presentation.presenter.Presenter;
import com.tlongdev.spicio.presentation.ui.view.activity.MainView;

/**
 * Middle layer, Presenter.
 *
 * @author Long
 * @since 2016. 02. 23.
 */
public class MainPresenter implements Presenter<MainView> {

    private MainView mView;

    @Override
    public void attachView(MainView view) {
        this.mView = view;
    }

    @Override
    public void detachView() {
        mView = null;
    }
}
<file_sep>package com.tlongdev.spicio.network.model;

import com.google.gson.annotations.SerializedName;

/**
 * @author Long
 * @since 2016. 03. 01.
 */
public class TraktSearchResult {

    @SerializedName("type")
    private String type;

    @SerializedName("score")
    private double score;

    @SerializedName("show")
    private TraktSeries series;

    public String getType() {
        return type;
    }

    public double getScore() {
        return score;
    }

    public TraktSeries getSeries() {
        return series;
    }
}
<file_sep>package com.tlongdev.spicio;

import android.app.Application;

import com.facebook.FacebookSdk;
import com.tlongdev.spicio.component.ActivityComponent;
import com.tlongdev.spicio.component.DaggerActivityComponent;
import com.tlongdev.spicio.component.DaggerInteractorComponent;
import com.tlongdev.spicio.component.DaggerNetworkComponent;
import com.tlongdev.spicio.component.DaggerPresenterComponent;
import com.tlongdev.spicio.component.DaggerProfileManagerComponent;
import com.tlongdev.spicio.component.DaggerStorageComponent;
import com.tlongdev.spicio.component.InteractorComponent;
import com.tlongdev.spicio.component.NetworkComponent;
import com.tlongdev.spicio.component.PresenterComponent;
import com.tlongdev.spicio.component.ProfileManagerComponent;
import com.tlongdev.spicio.component.StorageComponent;
import com.tlongdev.spicio.module.AuthenticationModule;
import com.tlongdev.spicio.module.DaoModule;
import com.tlongdev.spicio.module.NetworkModule;
import com.tlongdev.spicio.module.NetworkRepositoryModule;
import com.tlongdev.spicio.module.SpicioAppModule;
import com.tlongdev.spicio.module.StorageModule;
import com.tlongdev.spicio.module.ThreadingModule;

import net.danlew.android.joda.JodaTimeAndroid;

/**
 * This is a subclass of {@link Application} used to provide shared objects for this app.
 *
 * @author Long
 * @since 2016. 02. 23.
 */
public class SpicioApplication extends Application {

    private InteractorComponent mInteractorComponent;

    private NetworkComponent mNetworkComponent;

    private StorageComponent mStorageComponent;

    private PresenterComponent mPresenterComponent;

    private ActivityComponent mActivityComponent;

    private ProfileManagerComponent mProfileManagerComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        JodaTimeAndroid.init(this);
        FacebookSdk.sdkInitialize(this);

        SpicioAppModule spicioAppModule = new SpicioAppModule(this);
        NetworkModule networkModule = new NetworkModule();
        StorageModule storageModule = new StorageModule();
        DaoModule daoModule = new DaoModule();
        NetworkRepositoryModule networkRepositoryModule = new NetworkRepositoryModule();
        ThreadingModule threadingModule = new ThreadingModule();
        AuthenticationModule authenticationModule = new AuthenticationModule();

        mNetworkComponent = DaggerNetworkComponent.builder()
                .spicioAppModule(spicioAppModule)
                .networkModule(networkModule)
                .build();

        mStorageComponent = DaggerStorageComponent.builder()
                .spicioAppModule(spicioAppModule)
                .storageModule(storageModule)
                .build();

        mInteractorComponent = DaggerInteractorComponent.builder()
                .spicioAppModule(spicioAppModule)
                .networkRepositoryModule(networkRepositoryModule)
                .daoModule(daoModule)
                .threadingModule(threadingModule)
                .build();

        mPresenterComponent = DaggerPresenterComponent.builder()
                .spicioAppModule(spicioAppModule)
                .authenticationModule(authenticationModule)
                .build();

        mActivityComponent = DaggerActivityComponent.builder()
                .spicioAppModule(spicioAppModule)
                .authenticationModule(authenticationModule)
                .build();

        mProfileManagerComponent = DaggerProfileManagerComponent.builder()
                .spicioAppModule(spicioAppModule)
                .build();
    }

    public NetworkComponent getNetworkComponent() {
        return mNetworkComponent;
    }

    public StorageComponent getStorageComponent() {
        return mStorageComponent;
    }

    public void setStorageComponent(StorageComponent storageComponent) {
        this.mStorageComponent = storageComponent;
    }

    public InteractorComponent getInteractorComponent() {
        return mInteractorComponent;
    }

    public PresenterComponent getPresenterComponent() {
        return mPresenterComponent;
    }

    public ActivityComponent getActivityComponent() {
        return mActivityComponent;
    }

    public ProfileManagerComponent getProfileManagerComponent() {
        return mProfileManagerComponent;
    }

    public void setProfileManagerComponent(ProfileManagerComponent profileManagerComponent) {
        mProfileManagerComponent = profileManagerComponent;
    }
}
<file_sep># spicio

| `master` | `development` |
| :---: | :---: |
| [![Build Status](https://travis-ci.org/Longi94/spicio.svg?branch=master)](https://travis-ci.org/Longi94/spicio) | [![Build Status](https://travis-ci.org/Longi94/spicio.svg?branch=development)](https://travis-ci.org/Longi94/spicio) | 
| [![codecov.io](https://codecov.io/github/Longi94/spicio/coverage.svg?branch=master)](https://codecov.io/github/Longi94/spicio?branch=master) | [![codecov.io](https://codecov.io/github/Longi94/spicio/coverage.svg?branch=development)](https://codecov.io/github/Longi94/spicio?branch=development) |
<file_sep>package com.tlongdev.spicio.storage;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.tlongdev.spicio.BuildConfig;
import com.tlongdev.spicio.storage.DatabaseContract.EpisodesEntry;
import com.tlongdev.spicio.storage.DatabaseContract.FeedEntry;
import com.tlongdev.spicio.storage.DatabaseContract.FriendsEntry;
import com.tlongdev.spicio.storage.DatabaseContract.SeriesEntry;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

import java.util.HashSet;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
 * @author Long
 * @since 2016. 02. 27.
 */
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class DatabaseTest {

    private Context mContext;

    @Before
    public void setUp() throws Exception {
        assertNotNull(RuntimeEnvironment.application);

        mContext = RuntimeEnvironment.application;
        mContext.deleteDatabase(DatabaseHelper.DATABASE_NAME);
    }

    @Test
    public void testCreateDatabase() {

        final HashSet<String> tableNameHashSet = new HashSet<String>();
        tableNameHashSet.add(SeriesEntry.TABLE_NAME);
        tableNameHashSet.add(EpisodesEntry.TABLE_NAME);
        tableNameHashSet.add(FeedEntry.TABLE_NAME);
        tableNameHashSet.add(FriendsEntry.TABLE_NAME);

        SQLiteDatabase db = new DatabaseHelper(mContext).getWritableDatabase();

        assertNotNull(db);
        assertTrue(db.isOpen());

        //See if we've created the tables we want
        Cursor tables = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null);

        assertTrue(tables.moveToFirst());

        // verify that the tables have been created
        do {
            tableNameHashSet.remove(tables.getString(0));
        } while (tables.moveToNext());

        tables.close();

        assertTrue("Error: Your database was created with missing tables", tableNameHashSet.isEmpty());

        Cursor columns = db.rawQuery("PRAGMA table_info(" + SeriesEntry.TABLE_NAME + ")",
                null);

        assertTrue("Error: This means that we were unable to query the database for table information.",
                columns.moveToFirst());

        //Check for table columns
        final HashSet<String> columnHashSet = new HashSet<String>();
        columnHashSet.add(SeriesEntry.COLUMN_TITLE);
        columnHashSet.add(SeriesEntry.COLUMN_YEAR);
        columnHashSet.add(SeriesEntry.COLUMN_TRAKT_ID);
        columnHashSet.add(SeriesEntry.COLUMN_TVDB_ID);
        columnHashSet.add(SeriesEntry.COLUMN_IMDB_ID);
        columnHashSet.add(SeriesEntry.COLUMN_TMDB_ID);
        columnHashSet.add(SeriesEntry.COLUMN_TV_RAGE_ID);
        columnHashSet.add(SeriesEntry.COLUMN_SLUG);
        columnHashSet.add(SeriesEntry.COLUMN_OVERVIEW);
        columnHashSet.add(SeriesEntry.COLUMN_FIRST_AIRED);
        columnHashSet.add(SeriesEntry.COLUMN_DAY_OF_AIR);
        columnHashSet.add(SeriesEntry.COLUMN_TIME_OF_AIR);
        columnHashSet.add(SeriesEntry.COLUMN_AIR_TIME_ZONE);
        columnHashSet.add(SeriesEntry.COLUMN_RUNTIME);
        columnHashSet.add(SeriesEntry.COLUMN_CONTENT_RATING);
        columnHashSet.add(SeriesEntry.COLUMN_NETWORK);
        columnHashSet.add(SeriesEntry.COLUMN_TRAILER);
        columnHashSet.add(SeriesEntry.COLUMN_STATUS);
        columnHashSet.add(SeriesEntry.COLUMN_TRAKT_RATING);
        columnHashSet.add(SeriesEntry.COLUMN_TRAKT_RATING_COUNT);
        columnHashSet.add(SeriesEntry.COLUMN_GENRES);
        columnHashSet.add(SeriesEntry.COLUMN_POSTER_FULL);
        columnHashSet.add(SeriesEntry.COLUMN_POSTER_THUMB);
        columnHashSet.add(SeriesEntry.COLUMN_THUMB);

        columnHashSet.add(SeriesEntry.COLUMN_TVDB_RATING);
        columnHashSet.add(SeriesEntry.COLUMN_TVDB_RATING_COUNT);

        int columnNameIndex = columns.getColumnIndex("name");

        do {
            String columnName = columns.getString(columnNameIndex);
            columnHashSet.remove(columnName);
        } while (columns.moveToNext());

        columns.close();

        assertTrue("Error: The database doesn't contain all of the required columns",
                columnHashSet.isEmpty());

        columns = db.rawQuery("PRAGMA table_info(" + EpisodesEntry.TABLE_NAME + ")",
                null);

        assertTrue("Error: This means that we were unable to query the database for table information.",
                columns.moveToFirst());

        //Check for table columns
        columnHashSet.clear();

        columnHashSet.add(EpisodesEntry.COLUMN_SERIES_ID);
        columnHashSet.add(EpisodesEntry.COLUMN_SEASON);
        columnHashSet.add(EpisodesEntry.COLUMN_EPISODE_NUMBER);
        columnHashSet.add(EpisodesEntry.COLUMN_TITLE);
        columnHashSet.add(EpisodesEntry.COLUMN_TRAKT_ID);
        columnHashSet.add(EpisodesEntry.COLUMN_TVDB_ID);
        columnHashSet.add(EpisodesEntry.COLUMN_IMDB_ID);
        columnHashSet.add(EpisodesEntry.COLUMN_TMDB_ID);
        columnHashSet.add(EpisodesEntry.COLUMN_TV_RAGE_ID);
        columnHashSet.add(EpisodesEntry.COLUMN_SLUG);
        columnHashSet.add(EpisodesEntry.COLUMN_ABSOLUTE_NUMBER);
        columnHashSet.add(EpisodesEntry.COLUMN_OVERVIEW);
        columnHashSet.add(EpisodesEntry.COLUMN_TRAKT_RATING);
        columnHashSet.add(EpisodesEntry.COLUMN_TRAKT_RATING_COUNT);
        columnHashSet.add(EpisodesEntry.COLUMN_FIRST_AIRED);
        columnHashSet.add(EpisodesEntry.COLUMN_SCREENSHOT_FULL);
        columnHashSet.add(EpisodesEntry.COLUMN_SCREENSHOT_THUMB);
        columnHashSet.add(EpisodesEntry.COLUMN_WATCHED);
        columnHashSet.add(EpisodesEntry.COLUMN_LIKED);

        columnHashSet.add(EpisodesEntry.COLUMN_TVDB_RATING);

        do {
            String columnName = columns.getString(columnNameIndex);
            columnHashSet.remove(columnName);
        } while (columns.moveToNext());

        columns.close();

        assertTrue("Error: The database doesn't contain all of the required columns",
                columnHashSet.isEmpty());

        columns = db.rawQuery("PRAGMA table_info(" + FeedEntry.TABLE_NAME + ")",
                null);

        assertTrue("Error: This means that we were unable to query the database for table information.",
                columns.moveToFirst());

        //Check for table columns
        columnHashSet.clear();
        columnHashSet.add(FeedEntry.COLUMN_CULPRIT_ID);
        columnHashSet.add(FeedEntry.COLUMN_EPISODE_ABSOLUTE_NUMBER);
        columnHashSet.add(FeedEntry.COLUMN_EPISODE_ID);
        columnHashSet.add(FeedEntry.COLUMN_EPISODE_NAME);
        columnHashSet.add(FeedEntry.COLUMN_EPISODE_NUMBER);
        columnHashSet.add(FeedEntry.COLUMN_ITEM_ID);
        columnHashSet.add(FeedEntry.COLUMN_SEASON_NUMBER);
        columnHashSet.add(FeedEntry.COLUMN_SERIES_ID);
        columnHashSet.add(FeedEntry.COLUMN_SERIES_NAME);
        columnHashSet.add(FeedEntry.COLUMN_TIMESTAMP);
        columnHashSet.add(FeedEntry.COLUMN_TYPE);
        columnHashSet.add(FeedEntry.COLUMN_VICTIM_ID);
        columnHashSet.add(FeedEntry.COLUMN_VICTIM_NAME);

        do {
            String columnName = columns.getString(columnNameIndex);
            columnHashSet.remove(columnName);
        } while (columns.moveToNext());

        columns.close();

        assertTrue("Error: The database doesn't contain all of the required columns",
                columnHashSet.isEmpty());

        columns = db.rawQuery("PRAGMA table_info(" + FriendsEntry.TABLE_NAME + ")",
                null);

        assertTrue("Error: This means that we were unable to query the database for table information.",
                columns.moveToFirst());

        //Check for table columns
        columnHashSet.clear();
        columnHashSet.add(FriendsEntry.COLUMN_AVATAR);
        columnHashSet.add(FriendsEntry.COLUMN_NAME);
        columnHashSet.add(FriendsEntry.COLUMN_USER_ID);

        do {
            String columnName = columns.getString(columnNameIndex);
            columnHashSet.remove(columnName);
        } while (columns.moveToNext());

        columns.close();

        assertTrue("Error: The database doesn't contain all of the required columns",
                columnHashSet.isEmpty());
    }
}
<file_sep>package com.tlongdev.spicio.presentation.ui.adapter;

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;

import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.presentation.ui.fragment.EpisodeFragment;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 11.
 */
public class EpisodePagerAdapter extends FragmentPagerAdapter {

    private List<Episode> mDataSet;

    public EpisodePagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        int episodeId = mDataSet.get(position).getTraktId();
        return EpisodeFragment.newInstance(episodeId);
    }

    @Override
    public int getCount() {
        return mDataSet == null ? 0 : mDataSet.size();
    }

    @Override
    public CharSequence getPageTitle(int position) {
        Episode episode = mDataSet.get(position);
        return "Episode " + episode.getNumber();
    }

    @Override
    public int getItemPosition(Object object) {
        // TODO: 2016. 03. 11. recreates fragments everytime notifyOnDataSetChanges is called
        // need a better mechanism to update fragments when he dataSet changes
        return POSITION_NONE;
    }

    public void setDataSet(List<Episode> dataSet) {
        mDataSet = dataSet;
    }
}
<file_sep>apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

def getKey(Properties properties, String name) {
    return properties.containsKey(name) ? properties.getProperty(name) : "DUMMY_KEY"
}

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.tlongdev.spicio"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        vectorDrawables.useSupportLibrary = true

        Properties keys = new Properties()
        if (project.rootProject.file('keys.properties').exists()) {
            keys.load(project.rootProject.file('keys.properties').newDataInputStream())
        }

        buildConfigField("String", "TRAKT_API_KEY", "\"${getKey(keys, "TRAKT_API_KEY")}\"")
        buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", "\"${getKey(keys, "GOOGLE_WEB_CLIENT_ID")}\"")

        resValue("string", "FACEBOOK_APP_ID", "${getKey(keys, "FACEBOOK_APP_ID")}")
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            testCoverageEnabled true
        }
    }
    lintOptions {
        disable 'InvalidPackage'
    }
    testOptions {
        unitTests.all {
            testLogging {
                events "passed", "skipped", "failed", "standardOut", "standardError"
            }
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:1.10.19'
    testCompile "org.robolectric:robolectric:3.1-SNAPSHOT"

    apt 'com.google.dagger:dagger-compiler:2.0.2'
    provided 'com.f2prateek.dart:dart-processor:2.0.0'
    provided 'org.glassfish:javax.annotation:10.0-b28'
    compile('com.squareup.retrofit2:converter-simplexml:2.0.0-beta4') {
        exclude group: 'xpp3', module: 'xpp3'
        exclude group: 'stax', module: 'stax-api'
        exclude group: 'stax', module: 'stax'
    }
    compile 'com.android.support:appcompat-v7:23.2.1'
    compile 'com.android.support:cardview-v7:23.2.1'
    compile 'com.android.support:design:23.2.1'
    compile 'com.android.support:support-v4:23.2.1'
    compile 'com.android.support:support-annotations:23.2.1'
    compile 'com.android.support:percent:23.2.1'
    compile 'com.f2prateek.dart:dart:2.0.0'
    compile 'com.facebook.android:facebook-android-sdk:4.10.0'
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.google.android.gms:play-services-auth:8.4.0'
    compile 'com.google.dagger:dagger:2.0.2'
    compile 'com.jakewharton:butterknife:7.0.1'
    compile 'com.squareup.okhttp3:okhttp:3.1.2'
    compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
    compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
    compile 'net.danlew:android.joda:2.9.2'
}

apply plugin: 'com.google.gms.google-services'<file_sep>package com.tlongdev.spicio.presentation.presenter.activity;

import android.util.Log;

import com.tlongdev.spicio.domain.interactor.SaveSeriesInteractor;
import com.tlongdev.spicio.domain.interactor.TraktFullSeriesInteractor;
import com.tlongdev.spicio.domain.interactor.TraktSeriesDetailsInteractor;
import com.tlongdev.spicio.domain.interactor.impl.SaveSeriesInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.TraktFullSeriesInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.TraktSeriesDetailsInteractorImpl;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.domain.model.Season;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.presentation.presenter.Presenter;
import com.tlongdev.spicio.presentation.ui.view.activity.SeriesSearchDetailsView;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 04.
 */
public class SeriesSearchDetailsPresenter implements Presenter<SeriesSearchDetailsView>,
        TraktSeriesDetailsInteractor.Callback, SaveSeriesInteractor.Callback,
        TraktFullSeriesInteractor.Callback {

    private static final String LOG_TAG = SeriesSearchDetailsPresenter.class.getSimpleName();

    private SeriesSearchDetailsView mView;

    @Override
    public void attachView(SeriesSearchDetailsView view) {
        mView = view;
    }

    @Override
    public void detachView() {
        mView = null;
    }

    public void loadDetails(int traktId) {
        TraktSeriesDetailsInteractor interactor = new TraktSeriesDetailsInteractorImpl(
                mView.getSpicioApplication(), traktId, this
        );

        interactor.execute();
    }

    @Override
    public void onTraktSeriesDetailsFinish(Series series) {
        if (mView != null) {
            mView.showDetails(series);
        }
    }

    @Override
    public void onTraktSeriesDetailsFail() {
        // TODO: 2016. 03. 11.  
    }

    @Override
    public void onTraktFullSeriesFinish(Series series, List<Season> seasons, List<Episode> episodes) {
        Log.d(LOG_TAG, "finished downloading all series data, inserting into db");

        SaveSeriesInteractor interactor = new SaveSeriesInteractorImpl(
                mView.getSpicioApplication(), series, seasons, episodes, this
        );
        interactor.execute();
    }

    @Override
    public void onTraktFullSeriesFail() {
        if (mView != null) {
            mView.reportError();
        }
    }

    public void saveSeries(Series series) {
        Log.d(LOG_TAG, "saveSeries() called");

        mView.showLoading();

        TraktFullSeriesInteractor interactor = new TraktFullSeriesInteractorImpl(
                mView.getSpicioApplication(), series.getTraktId(), this
        );
        interactor.execute();
    }

    @Override
    public void onSaveSeriesFinish() {
        if (mView != null) {
            mView.onSeriesSaved();
            mView.hideLoading();
        }
    }

    @Override
    public void onSaveSeriesFail() {
        // TODO: 2016. 03. 11.
    }
}
<file_sep>package com.tlongdev.spicio.presentation.ui.fragment;


import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.tlongdev.spicio.R;
import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.presentation.presenter.fragment.SeriesPresenter;
import com.tlongdev.spicio.presentation.ui.activity.SeriesActivity;
import com.tlongdev.spicio.presentation.ui.adapter.SeriesAdapter;
import com.tlongdev.spicio.presentation.ui.view.fragment.SeriesView;

import java.util.List;

import butterknife.Bind;
import butterknife.ButterKnife;

/**
 * A simple {@link Fragment} subclass.
 */
public class SeriesFragment extends Fragment implements SeriesView, SeriesAdapter.OnItemSelectedListener {

    @Bind(R.id.recycler_view) RecyclerView mRecyclerView;

    private SeriesPresenter presenter;

    private SeriesAdapter adapter;

    public SeriesFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        presenter = new SeriesPresenter();
        presenter.attachView(this);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View rootView = inflater.inflate(R.layout.fragment_series, container, false);
        ButterKnife.bind(this, rootView);

        //Set the toolbar to the main activity's action bar
        ((AppCompatActivity) getActivity()).setSupportActionBar((Toolbar) rootView.findViewById(R.id.toolbar));

        adapter = new SeriesAdapter(getActivity());
        adapter.setListener(this);

        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mRecyclerView.setAdapter(adapter);

        return rootView;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        presenter.loadSeries();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        presenter.detachView();
    }

    @Override
    public SpicioApplication getSpicioApplication() {
        return (SpicioApplication) getActivity().getApplication();
    }

    @Override
    public void showSeries(List<Series> series) {
        adapter.setDataSet(series);
        adapter.notifyDataSetChanged();
    }

    @Override
    public void onItemSelected(Series series) {
        Intent intent = new Intent(getActivity(), SeriesActivity.class);
        intent.putExtra(SeriesActivity.EXTRA_SERIES_ID, series.getTraktId());
        intent.putExtra(SeriesActivity.EXTRA_SERIES_TITLE, series.getTitle());
        startActivity(intent);
    }
}
<file_sep>package com.tlongdev.spicio.domain.model;

import org.joda.time.DateTime;
import org.joda.time.LocalTime;

/**
 * Inner Layer, Model.
 *
 * @author Long
 * @since 2016. 02. 28.
 */
public class TvdbSeriesOld {

    private int id;

    private String[] actors;

    private int AirsDayOfWeek;

    private LocalTime AirsTime;

    private String contentRating;

    private DateTime firstAired;

    private String[] genres;

    private String imdbId;

    private String netWork;

    private String overView;

    private double tvdbRating;

    private int tvdbRatingCount;

    private int runTime;

    private String name;

    private int status;

    private String bannerPath;

    private String posterPath;

    private String zapt2itId;

    private String[] aliases;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String[] getActors() {
        return actors;
    }

    public void setActors(String[] actors) {
        this.actors = actors;
    }

    @Day.Enum
    public int getAirsDayOfWeek() {
        return AirsDayOfWeek;
    }

    public void setAirsDayOfWeek(@Day.Enum int airsDayOfWeek) {
        AirsDayOfWeek = airsDayOfWeek;
    }

    public LocalTime getAirsTime() {
        return AirsTime;
    }

    public void setAirsTime(LocalTime airsTime) {
        AirsTime = airsTime;
    }

    public String getContentRating() {
        return contentRating;
    }

    public void setContentRating(String contentRating) {
        this.contentRating = contentRating;
    }

    public DateTime getFirstAired() {
        return firstAired;
    }

    public void setFirstAired(DateTime firstAired) {
        this.firstAired = firstAired;
    }

    public String[] getGenres() {
        return genres;
    }

    public void setGenres(String[] genres) {
        this.genres = genres;
    }

    public String getImdbId() {
        return imdbId;
    }

    public void setImdbId(String imdbId) {
        this.imdbId = imdbId;
    }

    public String getNetWork() {
        return netWork;
    }

    public void setNetWork(String netWork) {
        this.netWork = netWork;
    }

    public String getOverView() {
        return overView;
    }

    public void setOverView(String overView) {
        this.overView = overView;
    }

    public double getTvdbRating() {
        return tvdbRating;
    }

    public void setTvdbRating(double tvdbRating) {
        this.tvdbRating = tvdbRating;
    }

    public int getTvdbRatingCount() {
        return tvdbRatingCount;
    }

    public void setTvdbRatingCount(int tvdbRatingCount) {
        this.tvdbRatingCount = tvdbRatingCount;
    }

    public int getRunTime() {
        return runTime;
    }

    public void setRunTime(int runTime) {
        this.runTime = runTime;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Status.Enum
    public int getStatus() {
        return status;
    }

    public void setStatus(@Status.Enum int status) {
        this.status = status;
    }

    public String getBannerPath() {
        return bannerPath;
    }

    public void setBannerPath(String bannerPath) {
        this.bannerPath = bannerPath;
    }

    public String getPosterPath() {
        return posterPath;
    }

    public void setPosterPath(String posterPath) {
        this.posterPath = posterPath;
    }

    public String getZapt2itId() {
        return zapt2itId;
    }

    public void setZapt2itId(String zapt2itId) {
        this.zapt2itId = zapt2itId;
    }

    public String[] getAliases() {
        return aliases;
    }

    public void setAliases(String[] aliases) {
        this.aliases = aliases;
    }
}
<file_sep>package com.tlongdev.spicio.network.model;

import com.google.gson.annotations.SerializedName;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 01.
 */
public class TraktSeries {

    @SerializedName("title")
    private String title;

    @SerializedName("year")
    private int year;

    @SerializedName("ids")
    private TraktIds ids;

    @SerializedName("overview")
    private String overview;

    @SerializedName("first_aired")
    private String firstAired;

    @SerializedName("airs")
    private TraktAirTime airs;

    @SerializedName("runtime")
    private int runtime;

    @SerializedName("certification")
    private String certification;

    @SerializedName("network")
    private String network;

    @SerializedName("country")
    private String country;

    @SerializedName("trailer")
    private String trailer;

    @SerializedName("homepage")
    private String homepage;

    @SerializedName("status")
    private String status;

    @SerializedName("rating")
    private double rating;

    @SerializedName("votes")
    private int votes;

    @SerializedName("updated_at")
    private String updatedAt;

    @SerializedName("language")
    private String language;

    @SerializedName("available_translations")
    private List<String> availableTranslations = new ArrayList<String>();

    @SerializedName("genres")
    private List<String> genres = new ArrayList<String>();

    @SerializedName("aired_episodes")
    private int airedEpisodes;

    @SerializedName("images")
    private TraktImages images;

    public String getTitle() {
        return title;
    }

    public int getYear() {
        return year;
    }

    public TraktIds getIds() {
        return ids;
    }

    public String getOverview() {
        return overview;
    }

    public String getFirstAired() {
        return firstAired;
    }

    public TraktAirTime getAirs() {
        return airs;
    }

    public int getRuntime() {
        return runtime;
    }

    public String getCertification() {
        return certification;
    }

    public String getNetwork() {
        return network;
    }

    public String getCountry() {
        return country;
    }

    public String getTrailer() {
        return trailer;
    }

    public String getHomepage() {
        return homepage;
    }

    public String getStatus() {
        return status;
    }

    public double getRating() {
        return rating;
    }

    public int getVotes() {
        return votes;
    }

    public String getUpdatedAt() {
        return updatedAt;
    }

    public String getLanguage() {
        return language;
    }

    public List<String> getAvailableTranslations() {
        return availableTranslations;
    }

    public List<String> getGenres() {
        return genres;
    }

    public int getAiredEpisodes() {
        return airedEpisodes;
    }

    public TraktImages getImages() {
        return images;
    }
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.component.DaggerInteractorComponent;
import com.tlongdev.spicio.component.InteractorComponent;
import com.tlongdev.spicio.domain.interactor.impl.LoadSeriesDetailsInteractorImpl;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.module.FakeAppModule;
import com.tlongdev.spicio.module.FakeDaoModule;
import com.tlongdev.spicio.module.FakeThreadingModule;
import com.tlongdev.spicio.module.NetworkRepositoryModule;
import com.tlongdev.spicio.storage.dao.SeriesDao;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * @author Long
 * @since 2016. 03. 09.
 */
@RunWith(MockitoJUnitRunner.class)
public class LoadSeriesDetailsInteractorTest {

    @Mock
    private SeriesDao mSeriesDao;

    @Mock
    private LoadSeriesDetailsInteractor.Callback mMockedCallback;

    @Mock
    private SpicioApplication mApp;


    @Before
    public void setUp() {
        FakeDaoModule storageModule = new FakeDaoModule();
        storageModule.setSeriesDao(mSeriesDao);

        InteractorComponent component = DaggerInteractorComponent.builder()
                .spicioAppModule(new FakeAppModule(mApp))
                .daoModule(storageModule)
                .networkRepositoryModule(mock(NetworkRepositoryModule.class))
                .threadingModule(new FakeThreadingModule())
                .build();

        when(mApp.getInteractorComponent()).thenReturn(component);
    }

    @Test
    public void testSuccess() {

        Series series = new Series();
        when(mSeriesDao.getSeries(0)).thenReturn(series);

        LoadSeriesDetailsInteractorImpl interactor = new LoadSeriesDetailsInteractorImpl(
                mApp, 0, mMockedCallback
        );
        interactor.run();

        verify(mSeriesDao).getSeries(0);
        verifyNoMoreInteractions(mSeriesDao);
        verify(mMockedCallback).onLoadSeriesDetailsFinish(series);
    }

    @Test
    public void testFail() {
        when(mSeriesDao.getSeries(0)).thenReturn(null);

        LoadSeriesDetailsInteractorImpl interactor = new LoadSeriesDetailsInteractorImpl(
                mApp, 0, mMockedCallback
        );
        interactor.run();

        verify(mSeriesDao).getSeries(0);
        verifyNoMoreInteractions(mSeriesDao);
        verify(mMockedCallback).onLoadSeriesDetailsFail();
    }
}
<file_sep>package com.tlongdev.spicio.network;

import com.tlongdev.spicio.network.model.TvdbEpisodePayload;
import com.tlongdev.spicio.network.model.TvdbSeriesPayload;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;

/**
 * Outer layer, Network.
 * Retrofit interface for using the TVDb api.
 *
 * http://www.thetvdb.com/wiki/index.php/Programmers_API
 * @author Long
 * @since 2016. 02. 24.
 */
public interface TvdbInterface {

    String BASE_URL = "http://thetvdb.com/";

    /**
     * Searches for series using the api.
     * http://www.thetvdb.com/wiki/index.php?title=API:GetSeries
     *
     * @param seriesName the name to search for
     * @return the search result
     */
    @GET("api/GetSeries.php")
    Call<TvdbSeriesPayload> getSeries(@Query("seriesname") String seriesName);

    /**
     * Gets more information about a series.
     * http://www.thetvdb.com/wiki/index.php?title=API:Base_Series_Record
     *
     * @param apiKey   the TVDb API key
     * @param seriesId the ID of the series
     * @return more information about the series
     */
    @GET("api/{apiKey}/series/{id}")
    Call<TvdbSeriesPayload> getSeriesRecord(@Path("apiKey") String apiKey, @Path("id") int seriesId);

    /**
     * Gets information about an episode.
     * http://www.thetvdb.com/wiki/index.php?title=API:Base_Episode_Record
     *
     * @param apiKey   the TVDb API key
     * @param seriesId the ID of the series
     * @param season   the season nmber
     * @param episode  the episode number
     * @return the episode
     */
    @GET("api/{apiKey}/series/{id}/default/{season}/{episode}")
    Call<TvdbEpisodePayload> getEpisode(@Path("apiKey") String apiKey, @Path("id") int seriesId,
                                        @Path("season") int season, @Path("episode") int episode);

    /**
     * Gets information about an episode.
     * http://www.thetvdb.com/wiki/index.php?title=API:Base_Episode_Record
     *
     * @param apiKey          the TVDb API key
     * @param seriesId        the ID of the series
     * @param absoluteEpisode the absolute episode number
     * @return the episode
     */
    @GET("api/{apiKey}/series/{id}/absolute/{episode}")
    Call<TvdbEpisodePayload> getEpisode(@Path("apiKey") String apiKey, @Path("id") int seriesId,
                                        @Path("episode") int absoluteEpisode);
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.component.DaggerInteractorComponent;
import com.tlongdev.spicio.component.InteractorComponent;
import com.tlongdev.spicio.domain.interactor.impl.TraktSeriesDetailsInteractorImpl;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.domain.repository.TraktRepository;
import com.tlongdev.spicio.module.DaoModule;
import com.tlongdev.spicio.module.FakeAppModule;
import com.tlongdev.spicio.module.FakeNetworkRepositoryModule;
import com.tlongdev.spicio.module.FakeThreadingModule;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * @author Long
 * @since 2016. 03. 04.
 */
@RunWith(MockitoJUnitRunner.class)
public class TraktSeriesDetailsInteractorTest {

    @Mock
    private TraktRepository mRepository;

    @Mock
    private TraktSeriesDetailsInteractor.Callback mMockedCallback;

    @Mock
    private SpicioApplication mApp;

    @Before
    public void setUp() {
        FakeNetworkRepositoryModule networkRepositoryModule = new FakeNetworkRepositoryModule();
        networkRepositoryModule.setTraktRepository(mRepository);

        InteractorComponent component = DaggerInteractorComponent.builder()
                .spicioAppModule(new FakeAppModule(mApp))
                .networkRepositoryModule(networkRepositoryModule)
                .daoModule(mock(DaoModule.class))
                .threadingModule(new FakeThreadingModule())
                .build();

        when(mApp.getInteractorComponent()).thenReturn(component);
    }

    @Test
    public void testSearchResult() {

        Series series = mock(Series.class);
        when(mRepository.getSeriesDetails(0)).thenReturn(series);

        TraktSeriesDetailsInteractorImpl interactor = new TraktSeriesDetailsInteractorImpl(
                mApp, 0, mMockedCallback
        );
        interactor.run();
        verify(mRepository).getSeriesDetails(0);
        verifyNoMoreInteractions(mRepository);
        verify(mMockedCallback).onTraktSeriesDetailsFinish(series);
    }

    @Test
    public void testSearchFail() {

        when(mRepository.getSeriesDetails(0)).thenReturn(null);

        TraktSeriesDetailsInteractorImpl interactor = new TraktSeriesDetailsInteractorImpl(
                mApp, 0, mMockedCallback
        );
        interactor.run();

        verify(mRepository).getSeriesDetails(0);
        verifyNoMoreInteractions(mRepository);
        verify(mMockedCallback).onTraktSeriesDetailsFail();
    }
}
<file_sep>package com.tlongdev.spicio.util;

/**
 * @author Long
 * @since 2016. 03. 05.
 */
public class Utility {

    public static String join(String[] strings, String delimiter) {
        StringBuilder sb = new StringBuilder();
        String sep = "";
        for(String s: strings) {
            sb.append(sep).append(s);
            sep = delimiter;
        }
        return sb.toString();
    }
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.domain.model.Episode;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 10.
 */
public interface TraktSeasonEpisodesInteractor extends Interactor {
    interface Callback {
        void onTraktSeasonEpisodesFinish(List<Episode> episodes);

        void onTraktSeasonEpisodesFail();
    }
}<file_sep>package com.tlongdev.spicio.presentation.presenter.activity;

import android.util.Log;

import com.tlongdev.spicio.domain.interactor.SaveSeriesInteractor;
import com.tlongdev.spicio.domain.interactor.TraktFullSeriesInteractor;
import com.tlongdev.spicio.domain.interactor.impl.SaveSeriesInteractorImpl;
import com.tlongdev.spicio.domain.interactor.impl.TraktFullSeriesInteractorImpl;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.domain.model.Season;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.presentation.presenter.Presenter;
import com.tlongdev.spicio.presentation.ui.view.activity.SeriesView;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 12.
 */
public class SeriesPresenter implements Presenter<SeriesView>,TraktFullSeriesInteractor.Callback, SaveSeriesInteractor.Callback {

    private static final String LOG_TAG = SeriesPresenter.class.getSimpleName();

    private SeriesView mView;

    private int mSeriesId;

    public SeriesPresenter(int seriesId) {
        mSeriesId = seriesId;
    }

    @Override
    public void attachView(SeriesView view) {
        mView = view;
    }

    @Override
    public void detachView() {
        mView = null;
    }

    public void refreshSeries() {
        mView.showLoading();
        TraktFullSeriesInteractor interactor = new TraktFullSeriesInteractorImpl(
                mView.getSpicioApplication(), mSeriesId, this
        );
        interactor.execute();
    }

    @Override
    public void onTraktFullSeriesFinish(Series series, List<Season> seasons, List<Episode> episodes) {
        Log.d(LOG_TAG, "finished downloading all series data, inserting into db");

        SaveSeriesInteractor interactor = new SaveSeriesInteractorImpl(
                mView.getSpicioApplication(), series, seasons, episodes, this
        );
        interactor.execute();

    }

    @Override
    public void onTraktFullSeriesFail() {
        if (mView != null) {
            mView.hideLoading();
        }
    }

    @Override
    public void onSaveSeriesFinish() {
        if (mView != null) {
            mView.reloadData();
            mView.hideLoading();
        }
    }

    @Override
    public void onSaveSeriesFail() {
        if (mView != null) {
            mView.hideLoading();
        }
    }
}<file_sep>package com.tlongdev.spicio.domain.model;

import android.support.annotation.IntDef;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Inner Layer, Model.
 * Days of the week.
 *
 * @author Long
 * @since 2016. 02. 23.
 */
public abstract class Day {

    @IntDef({MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Enum {}

    public static final int MONDAY = 0;
    public static final int TUESDAY = 1;
    public static final int WEDNESDAY = 2;
    public static final int THURSDAY = 3;
    public static final int FRIDAY = 4;
    public static final int SATURDAY = 5;
    public static final int SUNDAY = 6;
}
<file_sep>package com.tlongdev.spicio.presentation.ui.view.activity;

import com.tlongdev.spicio.presentation.ui.view.BaseView;

/**
 * @author Long
 * @since 2016. 03. 13.
 */
public interface SettingsView extends BaseView {
    void startLoginActivity();
}<file_sep>package com.tlongdev.spicio.domain.interactor.impl;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.domain.interactor.AbstractInteractor;
import com.tlongdev.spicio.domain.interactor.SaveSeriesInteractor;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.domain.model.Season;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.storage.dao.EpisodeDao;
import com.tlongdev.spicio.storage.dao.SeriesDao;
import com.tlongdev.spicio.util.Logger;

import java.util.List;

import javax.inject.Inject;

/**
 * @author Long
 * @since 2016. 03. 05.
 */
public class SaveSeriesInteractorImpl extends AbstractInteractor implements SaveSeriesInteractor {

    private static final String LOG_TAG = SaveSeriesInteractorImpl.class.getSimpleName();

    @Inject SeriesDao mSeriesDao;
    @Inject EpisodeDao mEpisodeDao;
    @Inject Logger logger;

    private Series mSeries;
    private List<Season> mSeasons;
    private List<Episode> mEpisodes;
    private Callback mCallback;

    public SaveSeriesInteractorImpl(SpicioApplication app, Series series, List<Season> seasons,
                                    List<Episode> episodes, Callback callback) {
        super(app.getInteractorComponent());
        app.getInteractorComponent().inject(this);

        mSeries = series;
        mCallback = callback;
        mSeasons = seasons;
        mEpisodes = episodes;
    }

    @Override
    public void run() {
        logger.debug(LOG_TAG, "started");

        logger.debug(LOG_TAG, "inserting episodes into database");
        mEpisodeDao.insertAllEpisodes(mEpisodes);

        logger.debug(LOG_TAG, "inserting seasons into database");
        mEpisodeDao.insertAllSeasons(mSeasons);

        logger.debug(LOG_TAG, "inserting the series");
        mSeriesDao.insertSeries(mSeries);

        postFinish();

        logger.debug(LOG_TAG, "finished");
    }

    private void postError() {
        if (mCallback == null) {
            return;
        }

        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onSaveSeriesFail();
            }
        });
    }

    private void postFinish() {
        if (mCallback == null) {
            return;
        }

        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onSaveSeriesFinish();
            }
        });
    }
}
<file_sep>package com.tlongdev.spicio.component;

import com.tlongdev.spicio.module.SpicioAppModule;
import com.tlongdev.spicio.module.StorageModule;
import com.tlongdev.spicio.storage.dao.impl.EpisodeDaoImpl;
import com.tlongdev.spicio.storage.dao.impl.SeriesDaoImpl;

import javax.inject.Singleton;

import dagger.Component;

/**
 * @author Long
 * @since 2016. 03. 05.
 */
@Singleton
@Component(modules = {SpicioAppModule.class, StorageModule.class})
public interface StorageComponent {
    void inject(SeriesDaoImpl seriesDao);
    void inject(EpisodeDaoImpl episodeDao);
}
<file_sep>package com.tlongdev.spicio.component;

import com.tlongdev.spicio.module.SpicioAppModule;
import com.tlongdev.spicio.util.ProfileManager;

import javax.inject.Singleton;

import dagger.Component;

/**
 * @author Long
 * @since 2016. 03. 13.
 */
@Singleton
@Component(modules = SpicioAppModule.class)
public interface ProfileManagerComponent {
    void inject(ProfileManager profileManager);
}
<file_sep>package com.tlongdev.spicio.network;

import com.tlongdev.spicio.network.model.TraktEpisode;
import com.tlongdev.spicio.network.model.TraktSearchResult;
import com.tlongdev.spicio.network.model.TraktSeason;
import com.tlongdev.spicio.network.model.TraktSeries;

import java.util.List;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;

/**
 * Outer Layer, Network.
 *
 * @author Long
 * @since 2016. 03. 01.
 */
public interface TraktApiInterface {

    String BASE_URL = "https://api-v2launch.trakt.tv/";

    /**
     * Search trakt.tv database by text. Narrow down search result with the type paramater.
     *
     * @param query the query string to search for
     * @param type  can be movie, show, episode, person, list
     * @return the search result
     */
    @GET("search")
    Call<List<TraktSearchResult>> searchByText(@Query("query") String query, @Query("type") String type);

    /**
     * Search trakt.tv database by id. Narrow down search result with the type parameter.
     *
     * @param idType type of the ID, can be trakt-movie, trakt-show, trakt-episode, imdb, tmdb, tvdb, tvrage
     * @param id     the id of the item
     * @return the search result
     */
    @GET("search")
    Call<List<TraktSearchResult>> searchById(@Query("id_type") String idType, @Query("id") String id);

    /**
     * Get the details of a series.
     *
     * @param id the id of the series, can be trakt.tv ID, trakt.tv slug or IMDB ID
     * @return the details of a series
     */
    @GET("shows/{id}?extended=full")
    Call<TraktSeries> getSeriesDetails(@Path("id") String id);

    /**
     * Get the images for a series.
     *
     * @param id the id of the series, can be trakt.tv ID, trakt.tv slug or IMDB ID
     * @return the images for a series
     */
    @GET("shows/{id}?extended=images")
    Call<TraktSeries> getSeriesImages(@Path("id") String id);

    /**
     * Get the details of a season of a series.
     *
     * @param id the id of the series, can be trakt.tv ID, trakt.tv slug or IMDB ID
     * @return the details of a season
     */
    @GET("shows/{id}/seasons?extended=full")
    Call<List<TraktSeason>> getSeasonsDetails(@Path("id") String id);

    /**
     * Get the images for a season of a series.
     *
     * @param id the id of the series, can be trakt.tv ID, trakt.tv slug or IMDB ID
     * @return the images for a season
     */
    @GET("shows/{id}/seasons?extended=images")
    Call<List<TraktSeason>> getSeasonsImages(@Path("id") String id);

    @GET("shows/{id}/seasons?extended=episodes")
    Call<List<TraktSeason>> getSeasonsEpisodes(@Path("id") String id);

    /**
     * Get the episodes of a season of a series
     *
     * @param id     the id of the series, can be trakt.tv ID, trakt.tv slug or IMDB ID
     * @param season the season number (0 for special episodes)
     * @return the episodes of a season
     */
    @GET("shows/{id}/seasons/{season}?extended=full")
    Call<List<TraktEpisode>> getSeasonEpisodes(@Path("id") String id, @Path("season") int season);

    /**
     * Get the episodes of a season of a series
     *
     * @param id     the id of the series, can be trakt.tv ID, trakt.tv slug or IMDB ID
     * @param season the season number (0 for special episodes)
     * @return the episodes of a season
     */
    @GET("shows/{id}/seasons/{season}?extended=images")
    Call<List<TraktEpisode>> getSeasonEpisodesImages(@Path("id") String id, @Path("season") int season);

    /**
     * Get a single episode of a season of a series.
     *
     * @param id      the id of the series, can be trakt.tv ID, trakt.tv slug or IMDB ID
     * @param season  the season number
     * @param episode the episode number
     * @return the single episode
     */
    @GET("shows/{id}/seasons/{season}/episodes/{episode}?extended=full")
    Call<TraktEpisode> getSingleEpisodeDetails(@Path("id") String id, @Path("season") int season, @Path("episode") int episode);

    /**
     * Get a images for an episode of a season of a series.
     *
     * @param id      the id of the series, can be trakt.tv ID, trakt.tv slug or IMDB ID
     * @param season  the season number
     * @param episode the episode number
     * @return the images for the episode
     */
    @GET("shows/{id}/seasons/{season}/episodes/{episode}?extended=full")
    Call<TraktEpisode> getSingleEpisodeImages(@Path("id") String id, @Path("season") int season, @Path("episode") int episode);
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.component.DaggerInteractorComponent;
import com.tlongdev.spicio.component.InteractorComponent;
import com.tlongdev.spicio.domain.interactor.impl.TraktFullSeriesInteractorImpl;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.domain.model.Images;
import com.tlongdev.spicio.domain.model.Season;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.domain.repository.TraktRepository;
import com.tlongdev.spicio.module.DaoModule;
import com.tlongdev.spicio.module.FakeAppModule;
import com.tlongdev.spicio.module.FakeNetworkRepositoryModule;
import com.tlongdev.spicio.module.FakeThreadingModule;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.LinkedList;
import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * @author Long
 * @since 2016. 03. 08.
 */
@RunWith(MockitoJUnitRunner.class)
public class TraktFullSeriesInteractorTest {

    @Mock
    private TraktRepository mRepository;

    @Mock
    private TraktFullSeriesInteractor.Callback mMockedCallback;

    @Mock
    private SpicioApplication mApp;

    @Before
    public void setUp() {

        FakeNetworkRepositoryModule networkRepositoryModule = new FakeNetworkRepositoryModule();
        networkRepositoryModule.setTraktRepository(mRepository);

        InteractorComponent component = DaggerInteractorComponent.builder()
                .spicioAppModule(new FakeAppModule(mApp))
                .networkRepositoryModule(networkRepositoryModule)
                .daoModule(mock(DaoModule.class))
                .threadingModule(new FakeThreadingModule())
                .build();

        when(mApp.getInteractorComponent()).thenReturn(component);
    }

    @Test
    public void testSuccess() {

        Series series = mock(Series.class);
        List<Season> seasons = new LinkedList<>();
        seasons.add(mock(Season.class));
        when(seasons.get(0).getNumber()).thenReturn(0);
        List<Episode> episodes = new LinkedList<>();

        when(mRepository.getSeriesDetails(0)).thenReturn(series);
        when(mRepository.getImages(0)).thenReturn(new Images());
        when(mRepository.getSeasons(0)).thenReturn(seasons);
        when(mRepository.getEpisodeImages(0, 0)).thenReturn(episodes);
        when(mRepository.getSeasonEpisodes(0, 0)).thenReturn(episodes);

        TraktFullSeriesInteractorImpl interactor = new TraktFullSeriesInteractorImpl(
                mApp, 0, mMockedCallback
        );
        interactor.run();

        verify(mRepository).getSeriesDetails(0);
        verify(mRepository).getImages(0);
        verify(mRepository).getSeasons(0);
        verify(mRepository).getEpisodeImages(0, 0);
        verify(mRepository).getSeasonEpisodes(0, 0);
        verifyNoMoreInteractions(mRepository);
        verify(mMockedCallback).onTraktFullSeriesFinish(series, seasons, episodes);
    }

    @Test
    public void testFail() {
        when(mRepository.getSeriesDetails(0)).thenReturn(null);;

        TraktFullSeriesInteractorImpl interactor = new TraktFullSeriesInteractorImpl(
                mApp, 0, mMockedCallback
        );
        interactor.run();

        verify(mRepository).getSeriesDetails(0);
        verifyNoMoreInteractions(mRepository);
        verify(mMockedCallback).onTraktFullSeriesFail();
    }
}
<file_sep>package com.tlongdev.spicio.presentation.presenter.activity;

import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;

import com.facebook.login.LoginManager;
import com.google.android.gms.auth.api.Auth;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallbacks;
import com.google.android.gms.common.api.Status;
import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.domain.interactor.DeleteAllDataInteractor;
import com.tlongdev.spicio.domain.interactor.impl.DeleteAllDataInteractorImpl;
import com.tlongdev.spicio.presentation.presenter.Presenter;
import com.tlongdev.spicio.presentation.ui.view.activity.SettingsView;
import com.tlongdev.spicio.util.Logger;
import com.tlongdev.spicio.util.ProfileManager;

import javax.inject.Inject;

/**
 * @author Long
 * @since 2016. 03. 13.
 */
public class SettingsPresenter implements Presenter<SettingsView>,DeleteAllDataInteractor.Callback {

    private static final String LOG_TAG = SettingsPresenter.class.getSimpleName();

    @Inject ProfileManager mProfileManager;
    @Inject GoogleApiClient mGoogleApiClient;
    @Inject Logger mLogger;

    private SettingsView mView;
    private SpicioApplication mApplication;

    public SettingsPresenter(SpicioApplication application) {
        mApplication = application;
        application.getPresenterComponent().inject(this);
    }

    /**
     * A preference value change listener that updates the preference's summary
     * to reflect its new value.
     */
    private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
        @Override
        public boolean onPreferenceChange(Preference preference, Object value) {
            String stringValue = value.toString();

            if (preference instanceof ListPreference) {
                // For list preferences, look up the correct display value in
                // the preference's 'entries' list.
                ListPreference listPreference = (ListPreference) preference;
                int index = listPreference.findIndexOfValue(stringValue);

                // Set the summary to reflect the new value.
                preference.setSummary(
                        index >= 0
                                ? listPreference.getEntries()[index]
                                : null);

            } else {
                // For all other preferences, set the summary to the value's
                // simple string representation.
                preference.setSummary(stringValue);
            }
            return true;
        }
    };

    /**
     * Binds a preference's summary to its value. More specifically, when the
     * preference's value is changed, its summary (line of text below the
     * preference title) is updated to reflect the value. The summary is also
     * immediately updated upon calling this method. The exact display format is
     * dependent on the type of preference.
     *
     * @see #sBindPreferenceSummaryToValueListener
     */
    private static void bindPreferenceSummaryToValue(Preference preference) {
        // Set the listener to watch for value changes.
        preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);

        // Trigger the listener immediately with the preference's
        // current value.
        sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
                PreferenceManager
                        .getDefaultSharedPreferences(preference.getContext())
                        .getString(preference.getKey(), ""));
    }

    @Override
    public void attachView(SettingsView view) {
        mView = view;
    }

    @Override
    public void detachView() {
        mView = null;
    }

    public void logout() {
        mProfileManager.logout();

        LoginManager.getInstance().logOut();

        if (mGoogleApiClient.isConnected()) {
            Auth.GoogleSignInApi.signOut(mGoogleApiClient).setResultCallback(
                    new ResultCallbacks<Status>() {
                        @Override
                        public void onSuccess(@NonNull Status status) {
                            mLogger.debug(LOG_TAG, "onSuccess: ");
                        }

                        @Override
                        public void onFailure(@NonNull Status status) {
                            mLogger.debug(LOG_TAG, "onFailure: ");
                        }
                    }
            );
        }

        DeleteAllDataInteractor interactor = new DeleteAllDataInteractorImpl(
                mApplication, this
        );
        interactor.execute();
    }

    public void connectGoogleApiClient() {
        mGoogleApiClient.connect();
    }

    public void disconnectGoogleApiClient() {
        mGoogleApiClient.disconnect();
    }

    @Override
    public void onFinish() {
        if (mView != null) {
            mView.startLoginActivity();
        }
    }

    @Override
    public void onFail() {

    }
}<file_sep>package com.tlongdev.spicio.domain.interactor.impl;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.domain.interactor.AbstractInteractor;
import com.tlongdev.spicio.domain.interactor.TraktSearchInteractor;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.domain.repository.TraktRepository;
import com.tlongdev.spicio.util.Logger;

import java.util.List;

import javax.inject.Inject;

/**
 * @author Long
 * @since 2016. 03. 04.
 */
public class TraktSearchInteractorImpl extends AbstractInteractor implements TraktSearchInteractor {

    private static final String LOG_TAG = TraktSearchInteractorImpl.class.getSimpleName();

    @Inject TraktRepository mRepository;
    @Inject Logger logger;

    private String mSearchQuery;
    private Callback mCallback;

    public TraktSearchInteractorImpl(SpicioApplication app, String searchQuery, Callback callback) {
        super(app.getInteractorComponent());
        app.getInteractorComponent().inject(this);

        mSearchQuery = searchQuery;
        mCallback = callback;
    }

    @Override
    public void run() {
        logger.debug(LOG_TAG, "started");

        List<Series> searchResult = mRepository.searchSeries(mSearchQuery);

        if (searchResult == null) {
            postError();
        } else {
            postResult(searchResult);
        }

        logger.debug(LOG_TAG, "finished");
    }

    private void postError() {
        if (mCallback == null) {
            return;
        }

        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onTraktSearchFailed();
            }
        });
    }

    private void postResult(final List<Series> searchResult) {
        if (mCallback == null) {
            return;
        }

        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onTraktSearchFinish(searchResult);
            }
        });
    }
}
<file_sep>package com.tlongdev.spicio.network.converter;

import com.tlongdev.spicio.domain.model.Day;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.domain.model.Image;
import com.tlongdev.spicio.domain.model.Images;
import com.tlongdev.spicio.domain.model.Season;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.domain.model.Status;
import com.tlongdev.spicio.network.model.TraktEpisode;
import com.tlongdev.spicio.network.model.TraktImage;
import com.tlongdev.spicio.network.model.TraktImages;
import com.tlongdev.spicio.network.model.TraktSeason;
import com.tlongdev.spicio.network.model.TraktSeries;

import org.joda.time.DateTimeZone;
import org.joda.time.LocalTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

import java.util.List;
import java.util.Locale;

/**
 * @author Long
 * @since 2016. 03. 02.
 */
public class TraktModelConverter {

    public static Image convertToImage(TraktImage traktImage) {
        if (traktImage == null) {
            return null;
        }

        Image image = new Image();

        image.setFull(traktImage.getFull());
        image.setMedium(traktImage.getMedium());
        image.setThumb(traktImage.getThumb());

        return image;
    }

    public static Images convertToImages(TraktImages traktImages) {
        if (traktImages == null) {
            return null;
        }

        Images images = new Images();

        images.setBanner(convertToImage(traktImages.getBanner()));
        images.setClearArt(convertToImage(traktImages.getClearart()));
        images.setFanArt(convertToImage(traktImages.getFanart()));
        images.setLogo(convertToImage(traktImages.getLogo()));
        images.setPoster(convertToImage(traktImages.getPoster()));
        images.setScreenshot(convertToImage(traktImages.getScreenshot()));
        images.setThumb(convertToImage(traktImages.getThumb()));

        return images;
    }

    public static Series convertToSeries(TraktSeries traktSeries) {
        if (traktSeries == null) {
            return null;
        }

        Series series = new Series();

        series.setCertification(traktSeries.getCertification());
        List<String> genres = traktSeries.getGenres();
        series.setGenres(genres.toArray(new String[genres.size()]));
        series.setYear(traktSeries.getYear());
        series.setOverview(traktSeries.getOverview());
        series.setTitle(traktSeries.getTitle());
        series.setRuntime(traktSeries.getRuntime());
        series.setTrailer(traktSeries.getTrailer());
        series.setTraktRating(traktSeries.getRating());
        series.setTraktRatingCount(traktSeries.getVotes());
        series.setNetwork(traktSeries.getNetwork());

        series.setImages(convertToImages(traktSeries.getImages()));

        series.setImdbId(traktSeries.getIds().getImdb());
        series.setTmdbId(traktSeries.getIds().getTmdb() == null ? -1 : traktSeries.getIds().getTmdb());
        series.setTraktId(traktSeries.getIds().getTrakt() == null ? -1 : traktSeries.getIds().getTrakt());
        series.setTvdbId(traktSeries.getIds().getTvdb() == null ? -1 : traktSeries.getIds().getTvdb());
        series.setTvRageId(traktSeries.getIds().getTvrage() == null ? -1 : traktSeries.getIds().getTvrage());
        series.setSlugName(traktSeries.getIds().getSlug());

        switch (traktSeries.getStatus()) { // TODO: 2016. 03. 05. other status codes
            case "returning series":
                series.setStatus(Status.CONTINUING);
                break;
            default:
                series.setStatus(Status.NULL);
                break;
        }

        if (traktSeries.getAirs() != null) {
            if (traktSeries.getAirs().getDay() != null) {
                switch (traktSeries.getAirs().getDay()) {
                    case "Monday":
                        series.setDayOfAiring(Day.MONDAY);
                        break;
                    case "Tuesday":
                        series.setDayOfAiring(Day.TUESDAY);
                        break;
                    case "Wednesday":
                        series.setDayOfAiring(Day.WEDNESDAY);
                        break;
                    case "Thursday":
                        series.setDayOfAiring(Day.THURSDAY);
                        break;
                    case "Friday":
                        series.setDayOfAiring(Day.FRIDAY);
                        break;
                    case "Saturday":
                        series.setDayOfAiring(Day.SATURDAY);
                        break;
                    case "Sunday":
                        series.setDayOfAiring(Day.SUNDAY);
                        break;
                    default:
                        throw new IllegalStateException(traktSeries.getAirs().getDay());
                }
            }

            if (traktSeries.getAirs().getTimezone() != null) {
                series.setAirTimeZone(DateTimeZone.forID(traktSeries.getAirs().getTimezone()));
            }

            if (traktSeries.getAirs().getTime() != null) {
                DateTimeFormatter timeFormatter = DateTimeFormat.forPattern("H:mm").withLocale(Locale.US);
                series.setTimeOfAiring(LocalTime.parse(traktSeries.getAirs().getTime(), timeFormatter));
            }

            if (traktSeries.getFirstAired() != null) {
                DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTime(); //yyyyMMdd'T'HHmmss.SSSZ
                series.setFirstAired(dateFormatter.parseDateTime(traktSeries.getFirstAired()));
            }
        }

        return series;
    }

    public static Episode convertToEpisode(TraktEpisode traktEpisode) {
        Episode episode = new Episode();

        episode.setSeason(traktEpisode.getSeason());
        episode.setNumber(traktEpisode.getNumber());
        episode.setTitle(traktEpisode.getTitle());
        episode.setImages(convertToImages(traktEpisode.getImages()));
        episode.setAbsoluteNumber(traktEpisode.getNumberAbs() == null ? -1 : traktEpisode.getNumberAbs());
        episode.setOverview(traktEpisode.getOverview());
        episode.setTraktRating(traktEpisode.getRating());
        episode.setTraktRatingCount(traktEpisode.getVotes());

        episode.setImdbId(traktEpisode.getIds().getImdb());
        episode.setTmdbId(traktEpisode.getIds().getTmdb() == null ? -1 : traktEpisode.getIds().getTmdb());
        episode.setTraktId(traktEpisode.getIds().getTrakt() == null ? -1 : traktEpisode.getIds().getTrakt());
        episode.setTvdbId(traktEpisode.getIds().getTvdb() == null ? -1 : traktEpisode.getIds().getTvdb());
        episode.setTvRageId(traktEpisode.getIds().getTvrage() == null ? -1 : traktEpisode.getIds().getTvrage());
        episode.setSlugName(traktEpisode.getIds().getSlug());

        if (traktEpisode.getFirstAired() != null) {
            DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTime(); //yyyyMMdd'T'HHmmss.SSSZ
            episode.setFirstAired(dateFormatter.parseDateTime(traktEpisode.getFirstAired()));
        }

        return episode;
    }

    public static Season convertToSeason(int seriesId, TraktSeason traktSeason) {
        Season season = new Season();

        season.setSeriesId(seriesId);
        season.setNumber(traktSeason.getNumber());
        season.setImages(TraktModelConverter.convertToImages(traktSeason.getImages()));

        return season;
    }
}
<file_sep>apply plugin: 'jacoco'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.0.0-beta6'
        classpath 'com.google.gms:google-services:2.0.0-beta6'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

allprojects {
    repositories {
        jcenter()
        mavenCentral()
        maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

subprojects { prj ->
    apply plugin: 'jacoco'

    jacoco {
        toolVersion '0.7.1.201405082137'
    }

    task jacocoReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {
        group = 'Reporting'
        description = 'Generate Jacoco coverage reports after running tests.'

        reports {
            xml {
                enabled = true
                destination "${prj.buildDir}/reports/jacoco/jacoco.xml"
            }
            html {
                enabled = true
                destination "${prj.buildDir}/reports/jacoco"
            }
        }

        classDirectories = fileTree(
                dir: 'build/intermediates/classes/debug',
                excludes: [
                        '**/R*.class',
                        '**/BuildConfig*',
                        '**/*$$*'
                ]
        )

        sourceDirectories = files('src/main/java')
        executionData = files('build/jacoco/testDebugUnitTest.exec')

        doFirst {
            files('build/intermediates/classes/debug').getFiles().each { file ->
                if (file.name.contains('$$')) {
                    file.renameTo(file.path.replace('$$', '$'))
                }
            }
        }
    }
}

jacoco {
    toolVersion '0.7.1.201405082137'
}

task jacocoFullReport(type: JacocoReport, group: 'Coverage reports') {
    group = 'Reporting'
    description = 'Generates an aggregate report from all subprojects'

    //noinspection GrUnresolvedAccess
    dependsOn(subprojects.jacocoReport)

    additionalSourceDirs = project.files(subprojects.jacocoReport.sourceDirectories)
    sourceDirectories = project.files(subprojects.jacocoReport.sourceDirectories)
    classDirectories = project.files(subprojects.jacocoReport.classDirectories)
    executionData = project.files(subprojects.jacocoReport.executionData)

    reports {
        xml {
            enabled = true
            destination "${buildDir}/reports/jacoco/full/jacoco.xml"
        }
        html {
            enabled = true
            destination "${buildDir}/reports/jacoco/full"
        }
    }

    doFirst {
        //noinspection GroovyAssignabilityCheck
        executionData = files(executionData.findAll { it.exists() })
    }
}<file_sep>package com.tlongdev.spicio.network.model;

import com.google.gson.annotations.SerializedName;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 01.
 */
public class TraktSeason {

    @SerializedName("number")
    private int number;

    @SerializedName("ids")
    private TraktIds ids;

    @SerializedName("images")
    private TraktImages images;

    @SerializedName("rating")
    private double rating;

    @SerializedName("votes")
    private int votes;

    @SerializedName("episode_count")
    private int episodeCount;

    @SerializedName("aired_episodes")
    private int airedEpisodes;

    @SerializedName("overview")
    private String overview;

    @SerializedName("episodes")
    private List<TraktEpisode> episodes;

    public int getNumber() {
        return number;
    }

    public TraktIds getIds() {
        return ids;
    }

    public TraktImages getImages() {
        return images;
    }

    public double getRating() {
        return rating;
    }

    public int getVotes() {
        return votes;
    }

    public int getEpisodeCount() {
        return episodeCount;
    }

    public int getAiredEpisodes() {
        return airedEpisodes;
    }

    public String getOverview() {
        return overview;
    }

    public List<TraktEpisode> getEpisodes() {
        return episodes;
    }
}
<file_sep>package com.tlongdev.spicio.module;

import android.app.Application;

import com.tlongdev.spicio.storage.dao.EpisodeDao;
import com.tlongdev.spicio.storage.dao.SeriesDao;

/**
 * @author Long
 * @since 2016. 03. 07.
 */
public class FakeDaoModule extends DaoModule {

    private SeriesDao mSeriesDao;
    private EpisodeDao mEpisodeDao;

    @Override
    SeriesDao provideSeriesDao(Application application) {
        return mSeriesDao;
    }

    @Override
    EpisodeDao provideEpisodeDao(Application application) {
        return mEpisodeDao;
    }

    public void setSeriesDao(SeriesDao seriesDao) {
        this.mSeriesDao = seriesDao;
    }

    public void setEpisodeDao(EpisodeDao episodeDao) {
        this.mEpisodeDao = episodeDao;
    }
}
<file_sep>package com.tlongdev.spicio.module;

import android.app.Application;

import com.facebook.CallbackManager;
import com.google.android.gms.auth.api.Auth;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.common.api.GoogleApiClient;
import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.util.ProfileManager;

import javax.inject.Singleton;

import dagger.Module;
import dagger.Provides;

/**
 * @author Long
 * @since 2016. 03. 13.
 */
@Module
public class AuthenticationModule {

    @Provides
    @Singleton
    GoogleSignInOptions provideGoogleSignInOptions() {
        return new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestEmail()
                .build();
    }

    @Provides
    @Singleton
    GoogleApiClient provideGoogleApiClient(Application application, GoogleSignInOptions googleSignInOptions) {
        return new GoogleApiClient.Builder(application)
                .addApi(Auth.GOOGLE_SIGN_IN_API, googleSignInOptions)
                .build();
    }

    @Provides
    @Singleton
    CallbackManager provideCallbackManager() {
        return CallbackManager.Factory.create();
    }

    @Provides
    @Singleton
    ProfileManager provideProfileManager(Application application) {
        return new ProfileManager((SpicioApplication) application);
    }
}
<file_sep>package com.tlongdev.spicio.storage.dao;

import android.net.Uri;

import com.tlongdev.spicio.domain.model.Series;

import java.util.List;

/**
 * Middle Layer, Converter.
 *
 * @author Long
 * @since 2016. 02. 29.
 */
public interface SeriesDao {

    /**
     * Get a single series from the database.
     *
     * @param seriesId the id of the series
     * @return the series
     */
    Series getSeries(int seriesId);

    /**
     * Get all the series from the database.
     *
     * @return all the series
     */
    List<Series> getAllSeries();

    /**
     * Inserts a series into the database.
     *
     * @param series the series to delete
     */
    Uri insertSeries(Series series);

    /**
     * Deletes a series from the database
     *
     * @param seriesId the id of the series
     */
    void deleteSeries(int seriesId);

    /**
     * Deletes all the series records from the database.
     */
    void deleteAllData();
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.component.DaggerInteractorComponent;
import com.tlongdev.spicio.component.InteractorComponent;
import com.tlongdev.spicio.domain.interactor.impl.LoadEpisodeDetailsInteractorImpl;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.module.FakeAppModule;
import com.tlongdev.spicio.module.FakeDaoModule;
import com.tlongdev.spicio.module.FakeThreadingModule;
import com.tlongdev.spicio.module.NetworkRepositoryModule;
import com.tlongdev.spicio.storage.dao.EpisodeDao;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * @author Long
 * @since 2016. 03. 11.
 */
@RunWith(MockitoJUnitRunner.class)
public class LoadEpisodeDetailsInteractorTest {

    @Mock
    private EpisodeDao mEpisodeDao;

    @Mock
    private LoadEpisodeDetailsInteractor.Callback mMockedCallback;

    @Mock
    private SpicioApplication mApp;

    @Mock
    private Series mSeries;

    @Before
    public void setUp() {
        FakeDaoModule storageModule = new FakeDaoModule();
        storageModule.setEpisodeDao(mEpisodeDao);

        InteractorComponent component = DaggerInteractorComponent.builder()
                .spicioAppModule(new FakeAppModule(mApp))
                .daoModule(storageModule)
                .networkRepositoryModule(mock(NetworkRepositoryModule.class))
                .threadingModule(new FakeThreadingModule())
                .build();

        when(mApp.getInteractorComponent()).thenReturn(component);
    }

    @Test
    public void testSuccess() {

        Episode episode = mock(Episode.class);
        when(mEpisodeDao.getEpisode(0)).thenReturn(episode);

        LoadEpisodeDetailsInteractorImpl interactor = new LoadEpisodeDetailsInteractorImpl(
                mApp, 0, mMockedCallback
        );
        interactor.run();

        verify(mEpisodeDao).getEpisode(0);
        verifyNoMoreInteractions(mEpisodeDao);
        verify(mMockedCallback).onLoadEpisodeDetailsFinish(episode);
    }

    @Test
    public void testFail() {
        when(mEpisodeDao.getEpisode(0)).thenReturn(null);

        LoadEpisodeDetailsInteractorImpl interactor = new LoadEpisodeDetailsInteractorImpl(
                mApp, 0, mMockedCallback
        );
        interactor.run();

        verify(mEpisodeDao).getEpisode(0);
        verifyNoMoreInteractions(mEpisodeDao);
        verify(mMockedCallback).onLoadEpisodeDetailsFail();
    }
}
<file_sep>package com.tlongdev.spicio.presentation.ui.view.fragment;

import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.presentation.ui.view.BaseView;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 06.
 */
public interface SeriesView extends BaseView {
    void showSeries(List<Series> series);
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.domain.model.Series;

/**
 * @author Long
 * @since 2016. 03. 04.
 */
public interface TraktSeriesDetailsInteractor extends Interactor {
    interface Callback {
        void onTraktSeriesDetailsFinish(Series series);

        void onTraktSeriesDetailsFail(); // TODO: 2016. 02. 28. needs more descriptive error data
    }
}
<file_sep>package com.tlongdev.spicio.presentation.presenter.fragment;

import com.tlongdev.spicio.domain.interactor.LoadSeriesDetailsInteractor;
import com.tlongdev.spicio.domain.interactor.impl.LoadSeriesDetailsInteractorImpl;
import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.presentation.presenter.Presenter;
import com.tlongdev.spicio.presentation.ui.view.fragment.SeriesDetailsView;

/**
 * @author Long
 * @since 2016. 03. 09.
 */
public class SeriesDetailsPresenter implements Presenter<SeriesDetailsView>,LoadSeriesDetailsInteractor.Callback {

    private SeriesDetailsView mView;

    @Override
    public void attachView(SeriesDetailsView view) {
        mView = view;
    }

    @Override
    public void detachView() {
        mView = null;
    }

    public void loadSeasonDetails(int seriesId) {
        LoadSeriesDetailsInteractor interactor = new LoadSeriesDetailsInteractorImpl(
                mView.getSpicioApplication(), seriesId, this
        );
        interactor.execute();
    }

    @Override
    public void onLoadSeriesDetailsFinish(Series series) {
        if (mView != null) {
            mView.showDetails(series);
        }
    }

    @Override
    public void onLoadSeriesDetailsFail() {

    }
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

import com.tlongdev.spicio.domain.model.Series;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 07.
 */
public interface LoadAllSeriesInteractor extends Interactor {
    interface Callback {
        void onLoadAllSeriesFinish(List<Series> series);

        void onLoadAllSeriesFail();
    }
}<file_sep>package com.tlongdev.spicio.domain.interactor.impl;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.domain.interactor.AbstractInteractor;
import com.tlongdev.spicio.domain.interactor.TraktSeasonEpisodesInteractor;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.domain.repository.TraktRepository;
import com.tlongdev.spicio.util.Logger;

import java.util.List;

import javax.inject.Inject;

/**
 * @author Long
 * @since 2016. 03. 10.
 */
public class TraktSeasonEpisodesInteractorImpl extends AbstractInteractor implements TraktSeasonEpisodesInteractor {

    private static final String LOG_TAG = TraktSeasonEpisodesInteractorImpl.class.getSimpleName();

    @Inject TraktRepository mRepository;
    @Inject Logger logger;

    private int mSeriesId;
    private int mSeason;
    private Callback mCallback;

    public TraktSeasonEpisodesInteractorImpl(SpicioApplication app, int seriesId, int season,
                                             Callback callback) {
        super(app.getInteractorComponent());
        app.getInteractorComponent().inject(this);
        mSeriesId = seriesId;
        mSeason = season;
        mCallback = callback;
    }

    @Override
    public void run() {
        logger.debug(LOG_TAG, "started");

        logger.debug(LOG_TAG, "getting list of episodes with images");
        List<Episode> episodesImages = mRepository.getEpisodeImages(mSeriesId, mSeason);
        if (episodesImages == null) {
            logger.debug(LOG_TAG, "TraktRepository.getEpisodeImages returned null");
            postError();
            return;
        }

        logger.debug(LOG_TAG, "getting list of episodes with images");
        List<Episode> episodes = mRepository.getSeasonEpisodes(mSeriesId, mSeason);
        if (episodes == null) {
            logger.debug(LOG_TAG, "TraktRepository.getEpisodeImages returned null");
            postError();
            return;
        }

        for (int i = 0; i < episodes.size(); i++) {
            episodes.get(i).setImages(episodesImages.get(i).getImages());
        }

        postFinish(episodes);
        logger.debug(LOG_TAG, "ended");
    }

    private void postError() {
        if (mCallback == null) {
            return;
        }
        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onTraktSeasonEpisodesFail();
            }
        });
    }
    private void postFinish(final List<Episode> episodes) {
        if (mCallback == null) {
            return;
        }
        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onTraktSeasonEpisodesFinish(episodes);
            }
        });
    }
}<file_sep>package com.tlongdev.spicio.presentation.ui.view.fragment;

import com.tlongdev.spicio.domain.model.Series;
import com.tlongdev.spicio.presentation.ui.view.BaseView;

/**
 * @author Long
 * @since 2016. 03. 09.
 */
public interface SeriesDetailsView extends BaseView {
    void showDetails(Series series);
}
<file_sep>package com.tlongdev.spicio.domain.interactor;

/**
 * @author Long
 * @since 2016. 03. 05.
 */
public interface SaveSeriesInteractor extends Interactor {
    interface Callback {
        void onSaveSeriesFinish();

        void onSaveSeriesFail(); // TODO: 2016. 03. 07. better error handing
    }
}
<file_sep>package com.tlongdev.spicio.storage;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.annotation.NonNull;

import com.tlongdev.spicio.storage.DatabaseContract.EpisodesEntry;
import com.tlongdev.spicio.storage.DatabaseContract.FeedEntry;
import com.tlongdev.spicio.storage.DatabaseContract.FriendsEntry;
import com.tlongdev.spicio.storage.DatabaseContract.SeasonsEntry;
import com.tlongdev.spicio.storage.DatabaseContract.SeriesEntry;

/**
 * Outer Layer, Storage.
 *
 * @author Long
 * @since 2016. 02. 26.
 */
public class DatabaseProvider extends ContentProvider {

    public static final int SERIES = 100;
    public static final int EPISODES = 101;
    public static final int FEED = 102;
    public static final int FRIENDS = 103;
    public static final int SEASONS = 104;

    /**
     * The URI Matcher used by this content provider
     */
    private static final UriMatcher sUriMatcher = buildUriMatcher();

    /**
     * The database helper.
     */
    private DatabaseHelper mOpenHelper;

    /**
     * Builds an URI matcher to match the given URIs
     *
     * @return the URI matcher
     */
    private static UriMatcher buildUriMatcher() {
        // I know what you're thinking.  Why create a UriMatcher when you can use regular
        // expressions instead?  Because you're not crazy, that's why.

        // All paths added to the UriMatcher have a corresponding code to return when a match is
        // found.  The code passed into the constructor represents the code to return for the root
        // URI.  It's common to use NO_MATCH as the code for this case.
        final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
        final String authority = DatabaseContract.CONTENT_AUTHORITY;

        // For each type of URI you want to add, create a corresponding code.
        matcher.addURI(authority, DatabaseContract.PATH_SERIES, SERIES);
        matcher.addURI(authority, DatabaseContract.PATH_EPISODES, EPISODES);
        matcher.addURI(authority, DatabaseContract.PATH_FEED, FEED);
        matcher.addURI(authority, DatabaseContract.PATH_FRIENDS, FRIENDS);
        matcher.addURI(authority, DatabaseContract.PATH_SEASONS, SEASONS);

        return matcher;
    }

    public DatabaseProvider() {
    }

    @Override
    public boolean onCreate() {
        mOpenHelper = new DatabaseHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(@NonNull Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        Cursor retCursor;
        String tableName;
        switch (sUriMatcher.match(uri)) {
            case SERIES:
                tableName = SeriesEntry.TABLE_NAME;
                break;
            case EPISODES:
                tableName = EpisodesEntry.TABLE_NAME;
                break;
            case FEED:
                tableName = FeedEntry.TABLE_NAME;
                break;
            case FRIENDS:
                tableName = FriendsEntry.TABLE_NAME;
                break;
            case SEASONS:
                tableName = SeasonsEntry.TABLE_NAME;
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        retCursor = mOpenHelper.getReadableDatabase().query(
                tableName,
                projection,
                selection,
                selectionArgs,
                null,
                null,
                sortOrder
        );

        if (retCursor != null)
            //noinspection ConstantConditions
            retCursor.setNotificationUri(getContext().getContentResolver(), uri);
        return retCursor;
    }

    @Override
    public Uri insert(@NonNull Uri uri, ContentValues values) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        Uri returnUri;
        long _id;

        switch (sUriMatcher.match(uri)) {
            case SERIES:
                _id = db.insert(SeriesEntry.TABLE_NAME, null, values);
                returnUri = SeriesEntry.buildUri(_id);
                break;
            case EPISODES:
                _id = db.insert(EpisodesEntry.TABLE_NAME, null, values);
                returnUri = EpisodesEntry.buildUri(_id);
                break;
            case FEED:
                _id = db.insert(FeedEntry.TABLE_NAME, null, values);
                returnUri = FeedEntry.buildUri(_id);
                break;
            case FRIENDS:
                _id = db.insert(FriendsEntry.TABLE_NAME, null, values);
                returnUri = FriendsEntry.buildUri(_id);
                break;
            case SEASONS:
                _id = db.insert(SeasonsEntry.TABLE_NAME, null, values);
                returnUri = SeasonsEntry.buildUri(_id);
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        if (_id > 0)
            return returnUri;
        else
            throw new android.database.SQLException("Failed to insert row into " + uri);
    }

    @Override
    public int update(@NonNull Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int rowsUpdated;

        switch (sUriMatcher.match(uri)) {
            case SERIES:
                rowsUpdated = db.update(SeriesEntry.TABLE_NAME, values, selection, selectionArgs);
                break;
            case EPISODES:
                rowsUpdated = db.update(EpisodesEntry.TABLE_NAME, values, selection, selectionArgs);
                break;
            case FEED:
                rowsUpdated = db.update(FeedEntry.TABLE_NAME, values, selection, selectionArgs);
                break;
            case FRIENDS:
                rowsUpdated = db.update(FriendsEntry.TABLE_NAME, values, selection, selectionArgs);
                break;
            case SEASONS:
                rowsUpdated = db.update(SeasonsEntry.TABLE_NAME, values, selection, selectionArgs);
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        if (rowsUpdated != 0) {
            //noinspection ConstantConditions
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return rowsUpdated;
    }

    @Override
    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int rowsDeleted;

        switch (sUriMatcher.match(uri)) {
            case SERIES:
                rowsDeleted = db.delete(SeriesEntry.TABLE_NAME, selection, selectionArgs);
                break;
            case EPISODES:
                rowsDeleted = db.delete(EpisodesEntry.TABLE_NAME, selection, selectionArgs);
                break;
            case FEED:
                rowsDeleted = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);
                break;
            case FRIENDS:
                rowsDeleted = db.delete(FriendsEntry.TABLE_NAME, selection, selectionArgs);
                break;
            case SEASONS:
                rowsDeleted = db.delete(SeasonsEntry.TABLE_NAME, selection, selectionArgs);
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        // Because a null deletes all rows
        if (selection == null || rowsDeleted != 0) {
            //noinspection ConstantConditions
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return rowsDeleted;
    }

    @Override
    public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        String tableName;

        switch (sUriMatcher.match(uri)) {
            case SERIES:
                tableName = SeriesEntry.TABLE_NAME;
                break;
            case EPISODES:
                tableName = EpisodesEntry.TABLE_NAME;
                break;
            case FEED:
                tableName = FeedEntry.TABLE_NAME;
                break;
            case FRIENDS:
                tableName = FriendsEntry.TABLE_NAME;
                break;
            case SEASONS:
                tableName = SeasonsEntry.TABLE_NAME;
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        db.beginTransaction();
        int returnCount = 0;
        try {
            for (ContentValues value : values) {
                long _id = db.insert(tableName, null, value);
                if (_id != -1) {
                    returnCount++;
                }
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }

        //noinspection ConstantConditions
        getContext().getContentResolver().notifyChange(uri, null);

        return returnCount;
    }

    @Override
    public String getType(@NonNull Uri uri) {
        switch (sUriMatcher.match(uri)) {
            case SERIES:
                return "vnd.android.cursor.dir/" + DatabaseContract.CONTENT_AUTHORITY + "/" + DatabaseContract.PATH_SERIES;
            case EPISODES:
                return "vnd.android.cursor.dir/" + DatabaseContract.CONTENT_AUTHORITY + "/" + DatabaseContract.PATH_EPISODES;
            case FEED:
                return "vnd.android.cursor.dir/" + DatabaseContract.CONTENT_AUTHORITY + "/" + DatabaseContract.PATH_FEED;
            case FRIENDS:
                return "vnd.android.cursor.dir/" + DatabaseContract.CONTENT_AUTHORITY + "/" + DatabaseContract.PATH_FRIENDS;
            case SEASONS:
                return "vnd.android.cursor.dir/" + DatabaseContract.CONTENT_AUTHORITY + "/" + DatabaseContract.PATH_SEASONS;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }
    }

}
<file_sep>package com.tlongdev.spicio.presentation.ui.view;

import android.content.Context;

import com.tlongdev.spicio.SpicioApplication;

/**
 * Outer Layer, UI.
 * The base view interface of the MVP architecture.
 *
 * @author Long
 * @since 2016. 02. 23.
 */
public interface BaseView {
    Context getContext();
    SpicioApplication getSpicioApplication();
}
<file_sep>package com.tlongdev.spicio.network.model;

import com.google.gson.annotations.SerializedName;

import java.util.List;

/**
 * @author Long
 * @since 2016. 03. 01.
 */
public class TraktEpisode {

    @SerializedName("season")
    private int season;

    @SerializedName("number")
    private int number;

    @SerializedName("title")
    private String title;

    @SerializedName("ids")
    private TraktIds ids;

    @SerializedName("images")
    private TraktImages images;

    @SerializedName("number_abs")
    private Integer numberAbs;

    @SerializedName("overview")
    private String overview;

    @SerializedName("rating")
    private double rating;

    @SerializedName("votes")
    private int votes;

    @SerializedName("first_aired")
    private String firstAired;

    @SerializedName("updated_at")
    private String updatedAt;

    @SerializedName("available_translations")
    private List<String> availableTranslations;

    public int getSeason() {
        return season;
    }

    public int getNumber() {
        return number;
    }

    public String getTitle() {
        return title;
    }

    public TraktIds getIds() {
        return ids;
    }

    public TraktImages getImages() {
        return images;
    }

    public Integer getNumberAbs() {
        return numberAbs;
    }

    public String getOverview() {
        return overview;
    }

    public double getRating() {
        return rating;
    }

    public int getVotes() {
        return votes;
    }

    public String getFirstAired() {
        return firstAired;
    }

    public String getUpdatedAt() {
        return updatedAt;
    }

    public List<String> getAvailableTranslations() {
        return availableTranslations;
    }
}
<file_sep>package com.tlongdev.spicio.storage;

import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;

import com.google.gson.Gson;
import com.tlongdev.spicio.BuildConfig;
import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.component.DaggerStorageComponent;
import com.tlongdev.spicio.component.StorageComponent;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.domain.model.Season;
import com.tlongdev.spicio.domain.model.Watched;
import com.tlongdev.spicio.module.FakeAppModule;
import com.tlongdev.spicio.module.FakeStorageModule;
import com.tlongdev.spicio.network.converter.TraktModelConverter;
import com.tlongdev.spicio.network.model.TraktEpisode;
import com.tlongdev.spicio.storage.DatabaseContract.EpisodesEntry;
import com.tlongdev.spicio.storage.dao.EpisodeDao;
import com.tlongdev.spicio.storage.dao.impl.EpisodeDaoImpl;
import com.tlongdev.spicio.util.TestUtils;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowContentResolver;

import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
 * @author Long
 * @since 2016. 03. 07.
 */
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class EpisodeDaoTest {

    private ContentResolver mContentResolver;
    private ShadowContentResolver mShadowContentResolver;

    private EpisodeDao mEpisodeDao;

    private Episode mEpisode;
    private Episode mAnotherEpisode;

    @Before
    public void setUp() throws Exception {
        DatabaseProvider provider = new DatabaseProvider();
        mContentResolver = RuntimeEnvironment.application.getContentResolver();
        ShadowContentResolver.registerProvider(DatabaseContract.CONTENT_AUTHORITY, provider);
        provider.onCreate();
        mShadowContentResolver = Shadows.shadowOf(mContentResolver);

        deleteAllRecords(EpisodesEntry.CONTENT_URI);

        StorageComponent storageComponent = DaggerStorageComponent.builder()
                .spicioAppModule(new FakeAppModule(RuntimeEnvironment.application))
                .storageModule(new FakeStorageModule())
                .build();

        ((SpicioApplication) RuntimeEnvironment.application).setStorageComponent(storageComponent);

        mEpisodeDao = new EpisodeDaoImpl((SpicioApplication) RuntimeEnvironment.application);

        InputStream is = getClass().getClassLoader().getResourceAsStream("trakt_episode_detail_mock.json");
        String dummyResponse = TestUtils.convertStreamToString(is);
        Gson gson = new Gson();

        TraktEpisode traktEpisode = gson.fromJson(dummyResponse, TraktEpisode.class);
        mEpisode = TraktModelConverter.convertToEpisode(traktEpisode);


        is = getClass().getClassLoader().getResourceAsStream("trakt_episode_detail_mock2.json");
        dummyResponse = TestUtils.convertStreamToString(is);

        traktEpisode = gson.fromJson(dummyResponse, TraktEpisode.class);
        mAnotherEpisode = TraktModelConverter.convertToEpisode(traktEpisode);

        List<Episode> episodes = new LinkedList<>();

        episodes.add(mEpisode);
        episodes.add(mAnotherEpisode);

        int rowsInserted = mEpisodeDao.insertAllEpisodes(episodes);
        assertEquals(2, rowsInserted);

        Season season = new Season();
        season.setSeriesId(mEpisode.getSeriesId());
        season.setNumber(mEpisode.getSeason());

        List<Season> seasons = new LinkedList<>();
        seasons.add(season);

        rowsInserted = mEpisodeDao.insertAllSeasons(seasons);
        assertEquals(1, rowsInserted);
    }

    public void deleteAllRecords(Uri contentUri) {
        mContentResolver.delete(contentUri, null, null);

        Cursor cursor = mContentResolver.query(
                contentUri, null, null, null, null
        );

        assertNotNull("Error: Query returned null during delete", cursor);
        assertEquals("Error: Records not deleted from Repos table during delete", 0, cursor.getCount());

        cursor.close();
    }

    @Test
    public void testBasicQuery() {
        Episode episode = mEpisodeDao.getEpisode(mEpisode.getTraktId());
        assertNotNull(episode);

        assertEquals(mEpisode.getSeason(), episode.getSeason());
        assertEquals(mEpisode.getNumber(), episode.getNumber());
        assertEquals(mEpisode.getTitle(), episode.getTitle());
        assertEquals(mEpisode.getTraktId(), episode.getTraktId());
        assertEquals(mEpisode.getTvdbId(), episode.getTvdbId());
        assertEquals(mEpisode.getImdbId(), episode.getImdbId());
        assertEquals(mEpisode.getTmdbId(), episode.getTmdbId());
        assertEquals(mEpisode.getTvRageId(), episode.getTvRageId());
        assertEquals(mEpisode.getAbsoluteNumber(), episode.getAbsoluteNumber());
        assertEquals(mEpisode.getOverview(), episode.getOverview());
        assertEquals(mEpisode.getTraktRating(), episode.getTraktRating(), 0);
        assertEquals(mEpisode.getTraktRatingCount(), episode.getTraktRatingCount());
        assertEquals(mEpisode.getFirstAired().getMillis(), episode.getFirstAired().getMillis());
        assertEquals(Watched.NONE, episode.isWatched());
        assertFalse(episode.isLiked());
    }

    @Test
    public void testQueryAll() {
        List<Episode> episodes = mEpisodeDao.getAllEpisodes();
        assertNotNull(episodes);

        Episode episode = episodes.get(0);
        assertNotNull(episode);

        assertEquals(mEpisode.getSeason(), episode.getSeason());
        assertEquals(mEpisode.getNumber(), episode.getNumber());
        assertEquals(mEpisode.getTitle(), episode.getTitle());
        assertEquals(mEpisode.getTraktId(), episode.getTraktId());
        assertEquals(mEpisode.getTvdbId(), episode.getTvdbId());
        assertEquals(mEpisode.getImdbId(), episode.getImdbId());
        assertEquals(mEpisode.getTmdbId(), episode.getTmdbId());
        assertEquals(mEpisode.getTvRageId(), episode.getTvRageId());
        assertEquals(mEpisode.getAbsoluteNumber(), episode.getAbsoluteNumber());
        assertEquals(mEpisode.getOverview(), episode.getOverview());
        assertEquals(mEpisode.getTraktRating(), episode.getTraktRating(), 0);
        assertEquals(mEpisode.getTraktRatingCount(), episode.getTraktRatingCount());
        assertEquals(mEpisode.getFirstAired().getMillis(), episode.getFirstAired().getMillis());
        assertEquals(Watched.NONE, episode.isWatched());
        assertFalse(episode.isLiked());

        episode = episodes.get(1);
        assertNotNull(episode);

        assertEquals(mAnotherEpisode.getSeason(), episode.getSeason());
        assertEquals(mAnotherEpisode.getNumber(), episode.getNumber());
        assertEquals(mAnotherEpisode.getTitle(), episode.getTitle());
        assertEquals(mAnotherEpisode.getTraktId(), episode.getTraktId());
        assertEquals(mAnotherEpisode.getTvdbId(), episode.getTvdbId());
        assertEquals(mAnotherEpisode.getImdbId(), episode.getImdbId());
        assertEquals(mAnotherEpisode.getTmdbId(), episode.getTmdbId());
        assertEquals(mAnotherEpisode.getTvRageId(), episode.getTvRageId());
        assertEquals(mAnotherEpisode.getAbsoluteNumber(), episode.getAbsoluteNumber());
        assertEquals(mAnotherEpisode.getOverview(), episode.getOverview());
        assertEquals(mAnotherEpisode.getTraktRating(), episode.getTraktRating(), 0);
        assertEquals(mAnotherEpisode.getTraktRatingCount(), episode.getTraktRatingCount());
        assertEquals(mAnotherEpisode.getFirstAired().getMillis(), episode.getFirstAired().getMillis());
        assertEquals(Watched.NONE, episode.isWatched());
        assertFalse(episode.isLiked());

    }

    @Test
    public void testWatched() {
        int rowsUpdated = mEpisodeDao.setWatched(mEpisode.getTraktId(), Watched.WATCHED);
        assertEquals(rowsUpdated, 1);
        assertTrue(mEpisodeDao.isWatched(mEpisode.getTraktId()));

        Episode episode = mEpisodeDao.getEpisode(mEpisode.getTraktId());
        assertEquals(Watched.WATCHED, episode.isWatched());
    }

    @Test
    public void testLiked() {
        int rowsUpdated = mEpisodeDao.setLiked(mEpisode.getTraktId(), true);
        assertEquals(rowsUpdated, 1);

        Episode episode = mEpisodeDao.getEpisode(mEpisode.getTraktId());
        assertTrue(episode.isLiked());
    }

    @Test
    public void testSkipped() {
        int rowsUpdated =  mEpisodeDao.setWatched(mEpisode.getTraktId(), Watched.SKIPPED);
        assertEquals(rowsUpdated, 1);

        Episode episode = mEpisodeDao.getEpisode(mEpisode.getTraktId());
        assertEquals(Watched.SKIPPED, episode.isWatched());
    }

    @Test
    public void testGetSeasons() {
        List<Season> seasons = mEpisodeDao.getAllSeasons(mEpisode.getSeriesId());
        assertNotNull(seasons);
        assertEquals(1, seasons.size());

        Season season = seasons.get(0);
        assertNotNull(season);
        assertEquals(mEpisode.getSeriesId(), season.getSeriesId());
        assertEquals(mEpisode.getSeason(), season.getNumber());
        assertEquals(0, season.getWatchCount());
        assertEquals(0, season.getSkipCount());
    }

    @Test
    public void testGetSeasonsWithWatched() {

        testWatched();

        List<Season> seasons = mEpisodeDao.getAllSeasons(mEpisode.getSeriesId());
        assertNotNull(seasons);
        assertEquals(1, seasons.size());

        Season season = seasons.get(0);
        assertNotNull(season);
        assertEquals(mEpisode.getSeriesId(), season.getSeriesId());
        assertEquals(mEpisode.getSeason(), season.getNumber());
        assertEquals(1, season.getWatchCount());
        assertEquals(0, season.getSkipCount());
    }

    @Test
    public void testGetSeasonsWithSkipped() {

        testSkipped();

        List<Season> seasons = mEpisodeDao.getAllSeasons(mEpisode.getSeriesId());
        assertNotNull(seasons);
        assertEquals(1, seasons.size());

        Season season = seasons.get(0);
        assertNotNull(season);
        assertEquals(mEpisode.getSeriesId(), season.getSeriesId());
        assertEquals(mEpisode.getSeason(), season.getNumber());
        assertEquals(0, season.getWatchCount());
        assertEquals(1, season.getSkipCount());
    }

    @Test
    public void testRetainCheck() {
        testWatched();

        List<Episode> episodes = new LinkedList<>();

        episodes.add(mEpisode);
        episodes.add(mAnotherEpisode);

        int rowsInserted = mEpisodeDao.insertAllEpisodes(episodes);
        assertEquals(2, rowsInserted);

        Episode episode = mEpisodeDao.getEpisode(mEpisode.getTraktId());
        assertNotNull(episode);
        assertEquals(Watched.WATCHED, episode.isWatched());
    }
}
<file_sep>package com.tlongdev.spicio.domain.interactor.impl;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.domain.interactor.AbstractInteractor;
import com.tlongdev.spicio.domain.interactor.LoadEpisodeDetailsInteractor;
import com.tlongdev.spicio.domain.model.Episode;
import com.tlongdev.spicio.storage.dao.EpisodeDao;
import com.tlongdev.spicio.util.Logger;

import javax.inject.Inject;

/**
 * @author Long
 * @since 2016. 03. 11.
 */
public class LoadEpisodeDetailsInteractorImpl extends AbstractInteractor implements LoadEpisodeDetailsInteractor {

    private static final String LOG_TAG = LoadEpisodeDetailsInteractorImpl.class.getSimpleName();

    @Inject EpisodeDao mEpisodeDao;
    @Inject Logger logger;

    private int mEpisodeId;
    private Callback mCallback;

    public LoadEpisodeDetailsInteractorImpl(SpicioApplication application, int episodeId,
                                            Callback callback) {
        super(application.getInteractorComponent());
        application.getInteractorComponent().inject(this);

        mEpisodeId = episodeId;
        mCallback = callback;
    }

    @Override
    public void run() {
        logger.debug(LOG_TAG, "started");

        Episode episode = mEpisodeDao.getEpisode(mEpisodeId);
        if (episode == null) {
            logger.debug(LOG_TAG, "EpisodeDao.getEpisode returned null");
            postError();
            return;
        }

        postFinish(episode);
        logger.debug(LOG_TAG, "ended");
    }

    private void postError() {
        if (mCallback == null) {
            return;
        }
        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onLoadEpisodeDetailsFail();
            }
        });
    }

    private void postFinish(final Episode episode) {
        if (mCallback == null) {
            return;
        }
        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onLoadEpisodeDetailsFinish(episode);
            }
        });
    }
}<file_sep>package com.tlongdev.spicio.domain.interactor.impl;

import com.tlongdev.spicio.SpicioApplication;
import com.tlongdev.spicio.domain.interactor.AbstractInteractor;
import com.tlongdev.spicio.domain.interactor.LoadSeasonsInteractor;
import com.tlongdev.spicio.domain.model.Season;
import com.tlongdev.spicio.storage.dao.EpisodeDao;
import com.tlongdev.spicio.util.Logger;

import java.util.List;

import javax.inject.Inject;

/**
 * @author Long
 * @since 2016. 03. 09.
 */
public class LoadSeasonsInteractorImpl extends AbstractInteractor implements LoadSeasonsInteractor {

    private static final String LOG_TAG = LoadSeasonsInteractorImpl.class.getSimpleName();

    @Inject EpisodeDao mEpisodeDao;
    @Inject Logger logger;

    private int mSeriesId;
    private Callback mCallback;

    public LoadSeasonsInteractorImpl(SpicioApplication app, int seriesId, Callback callback) {
        super(app.getInteractorComponent());
        app.getInteractorComponent().inject(this);
        mSeriesId = seriesId;
        mCallback = callback;
    }

    @Override
    public void run() {
        logger.debug(LOG_TAG, "started");

        logger.debug(LOG_TAG, "querying seasons for " + mSeriesId);
        List<Season> seasons = mEpisodeDao.getAllSeasons(mSeriesId);

        if (seasons == null) {
            postError();
            return;
        } else {
            postFinish(seasons);
        }
        logger.debug(LOG_TAG, "ended");
    }

    private void postError() {
        if (mCallback == null) {
            return;
        }

        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onLoadSeasonsFail();
            }
        });
    }

    private void postFinish(final List<Season> seasons) {
        if (mCallback == null) {
            return;
        }

        mMainThread.post(new Runnable() {
            @Override
            public void run() {
                mCallback.onLoadSeasonsFinish(seasons);
            }
        });
    }
}
<file_sep>package com.tlongdev.spicio.domain.model;

import org.joda.time.DateTime;

/**
 * @author Long
 * @since 2016. 03. 03.
 */
public class Episode {

    private int season;

    private int number;

    private String title;

    private int traktId;

    private String slugName;

    private int tvdbId;

    private String imdbId;

    private int tmdbId;

    private int tvRageId;

    private Images images;

    private int absoluteNumber;

    private String overview;

    private double traktRating;

    private int traktRatingCount;

    private DateTime firstAired;

    private int watched;

    private boolean liked;

    public int seriesId;

    public int getSeason() {
        return season;
    }

    public void setSeason(int season) {
        this.season = season;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public String getTitle() {
        return title;
    }

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

    public int getTraktId() {
        return traktId;
    }

    public void setTraktId(int traktId) {
        this.traktId = traktId;
    }

    public String getSlugName() {
        return slugName;
    }

    public void setSlugName(String slugName) {
        this.slugName = slugName;
    }

    public int getTvdbId() {
        return tvdbId;
    }

    public void setTvdbId(int tvdbId) {
        this.tvdbId = tvdbId;
    }

    public String getImdbId() {
        return imdbId;
    }

    public void setImdbId(String imdbId) {
        this.imdbId = imdbId;
    }

    public int getTmdbId() {
        return tmdbId;
    }

    public void setTmdbId(int tmdbId) {
        this.tmdbId = tmdbId;
    }

    public int getTvRageId() {
        return tvRageId;
    }

    public void setTvRageId(int tvRageId) {
        this.tvRageId = tvRageId;
    }

    public Images getImages() {
        return images;
    }

    public void setImages(Images images) {
        this.images = images;
    }

    public int getAbsoluteNumber() {
        return absoluteNumber;
    }

    public void setAbsoluteNumber(int absoluteNumber) {
        this.absoluteNumber = absoluteNumber;
    }

    public String getOverview() {
        return overview;
    }

    public void setOverview(String overview) {
        this.overview = overview;
    }

    public double getTraktRating() {
        return traktRating;
    }

    public void setTraktRating(double traktRating) {
        this.traktRating = traktRating;
    }

    public int getTraktRatingCount() {
        return traktRatingCount;
    }

    public void setTraktRatingCount(int traktRatingCount) {
        this.traktRatingCount = traktRatingCount;
    }

    public DateTime getFirstAired() {
        return firstAired;
    }

    public void setFirstAired(DateTime firstAired) {
        this.firstAired = firstAired;
    }

    @Watched.Enum
    public int isWatched() {
        return watched;
    }

    public void setWatched(@Watched.Enum int watched) {
        this.watched = watched;
    }

    public boolean isLiked() {
        return liked;
    }

    public void setLiked(boolean liked) {
        this.liked = liked;
    }

    public int getSeriesId() {
        return seriesId;
    }

    public void setSeriesId(int seriesId) {
        this.seriesId = seriesId;
    }
}
