public final class GifActionCreator extends BaseActionCreator {
    public static final String GIF_PLAY = "GIF_PLAY";
    public static final String GIF_PAUSE = "GIF_PAUSE";
    public static final String GIF_DOWNLOAD_SUCCESS = "GIF_DOWNLOAD_SUCCESS";
    public static final String GIF_DOWNLOAD_FAILURE = "GIF_DOWNLOAD_FAILURE";
    public static final String GIF_DOWNLOAD_START = "GIF_DOWNLOAD_START";

    private static GifActionCreator sInstance;

    private GifActionCreator() {
        MyApp.getFluxxan().inject(this);
    }

    public static GifActionCreator getInstance() {
        if (sInstance == null) {
            sInstance = new GifActionCreator();
        }
        return sInstance;
    }

    public void gifDownloadStart(Gif gif) {
        dispatch(new Action(GIF_DOWNLOAD_START, gif.getUuid()));
    }

    public void gifClick(Gif gif) {
        if (gif.getStatus() == Gif.Status.DOWNLOADED || gif.getStatus() == Gif
                .Status.PAUSED) {
            gifPlay(gif.getUuid());
        } else if (gif.getStatus() == Gif.Status.LOOPING) {
            gifPause(gif.getUuid());
        }
    }

    private void gifPlay(String uuid) {
        dispatch(new Action(GIF_PLAY, uuid));
    }

    private void gifPause(String uuid) {
        dispatch(new Action(GIF_PAUSE, uuid));
    }
} public class DataManager {
    private static Float sRiffsyNext = null;
    private final RiffsyRoutes mRiffsyRoutes;

    private Database mDatabase;
    private boolean mIsSavingAppState = false;
    private SaveAppStateRunnable mNextSaveAppStateRunnable;

    public DataManager(Context context) {
        mDatabase = new Database(context);
        mRiffsyRoutes = RetrofitBuilder.getInstance()
                .createApiEndpoint(RiffsyRoutes.class, RiffsyRoutes.BASE_URL);
    }

    public boolean isSavingAppState() {
        return mIsSavingAppState;
    }

    public void saveAppState(AppState appState) {
        mNextSaveAppStateRunnable = new DataManager.SaveAppStateRunnable(appState);
        executeSaveAppStateTask();
    }

    public void loadAppState(final LoadAppStateListener listener) {
        new Thread(() -> {
            try {
                listener.onLoaded(mDatabase.getAppState());
            } catch (SnappydbException | IOException e) {
                listener.onLoaded(getAppStateDefault());
                Log.e(getClass().getSimpleName(), "appstate not loaded", e);
            }
        }).start();
    }

    public void fetchGifs(final GifListLoadListener listener) {
        Call call = mRiffsyRoutes.getTrendingResults(RiffsyRoutes.DEFAULT_LIMIT_COUNT, sRiffsyNext);
        call.enqueue(new Callback<RiffsyResponse>() {
            @Override
            public void onResponse(Call<RiffsyResponse> call, Response<RiffsyResponse> response) {
                Map<String, Gif> gifs = extractGifsFromRiffsyResponse(response);

                sRiffsyNext = response.body().next();
                boolean hasMore = sRiffsyNext != null;
                listener.onLoaded(gifs, hasMore);
            }

            @Override
            public void onFailure(Call<RiffsyResponse> call, Throwable t) {
                Log.e(getClass().getSimpleName(), "onFailure", t);
            }
        });
    }

    private void executeSaveAppStateTask() {
        if (mNextSaveAppStateRunnable != null && !mIsSavingAppState) {
            mIsSavingAppState = true;
            ThreadUtils.runInBackground(mNextSaveAppStateRunnable);
            mNextSaveAppStateRunnable = null;
        }
    }

    private void onFinishedSaveAppStateTask() {
        ThreadUtils.runOnMain(() -> {
            mIsSavingAppState = false;
            executeSaveAppStateTask();
        });
    }

    @NonNull
    private Map<String, Gif> extractGifsFromRiffsyResponse(Response<RiffsyResponse> response) {
        List<RiffsyResult> results = response.body().results();
        Map<String, Gif> gifs = new LinkedHashMap<>();
        for (RiffsyResult result : results) {
            List<RiffsyMedia> riffsyMedias = result.media();
            if (riffsyMedias.size() > 0) {
                Gif gif = ImmutableGif.builder()
                        .url(riffsyMedias.get(0).gif().url())
                        .title(result.title())
                        .uuid(UUID.randomUUID().toString()).build();
                gifs.put(gif.getUuid(), gif);
            }
        }
        return gifs;
    }

    public static AppState getAppStateDefault() {
        String[] uuids = {"1", "2", "3", "4", "5" };
        String[] titles = {"Gif 1", "Gif 2", "Gif 3", "Gif 4", "Gif 5" };
        String[] urls = {
                "https://media.giphy.com/media/l0HlE56oAxpngfnWM/giphy.gif",
                "http://inspirandoideias.com.br/blog/wp-content/uploads/2015/03/"
                        + "b3368a682fc5ff891e41baad2731f4b6.gif",
                "https://media.giphy.com/media/9fbYYzdf6BbQA/giphy.gif",
                "https://media.giphy.com/media/l2YWl1oQlNvthGWrK/giphy.gif",
                "https://media.giphy.com/media/3oriNQHSU0bVcFW5sA/giphy.gif"
        };

        Map<String, Gif> gifs = new LinkedHashMap<>();
        for (int i = 0; i < titles.length; i++) {
            Gif gif = buildGif(uuids[i], titles[i], urls[i]);
            gifs.put(gif.getUuid(), gif);
        }

        return ImmutableAppState.builder().putAllGifs(gifs).build();
    }

    private static Gif buildGif(String uuid, String title, String url) {
        return ImmutableGif.builder().uuid(uuid).title(title).url(url).build();
    }

    public interface LoadAppStateListener {
        void onLoaded(AppState appState);
    }

    public interface GifListLoadListener {
        void onLoaded(Map<String, Gif> gifs, boolean hasMore);
    }

    private class SaveAppStateRunnable implements Runnable {

        private AppState mAppState;

        public SaveAppStateRunnable(AppState appState) {
            mAppState = appState;
        }

        @Override
        public void run() {
            try {
                mDatabase.saveAppState(mAppState);
            } catch (SnappydbException | JsonProcessingException e) {
                Log.e(getClass().getSimpleName(), "unsaved appstate", e);
            }
            onFinishedSaveAppStateTask();
        }

    }

} public class AnvilTestLocker extends BaseTestLocker implements AnvilRenderListener {

    private AnvilRenderable mAnvilRenderable;
    private int mRenderTimes = 0;
    private int mLastRenderTimes = 0;

    public AnvilTestLocker(AnvilRenderable anvilRenderable) {
        mAnvilRenderable = anvilRenderable;
        mAnvilRenderable.setAnvilRenderListener(this);
    }

    @Override
    protected boolean isIdle() {
        boolean isIdle = mRenderTimes != 0 && mRenderTimes == mLastRenderTimes;
        mLastRenderTimes = mRenderTimes;

        return isIdle;
    }

    @Override
    public void unregisterIdlingResource() {
        mAnvilRenderable.setAnvilRenderListener(null);
        super.unregisterIdlingResource();
    }

    @Override
    public void onAnvilRendered() {
        mRenderTimes++;
    }
} public class RecyclerTestLocker extends BaseTestLocker {

    private RecyclerView mRecycler;
    private int mExpectedItemsCount;

    public RecyclerTestLocker(RecyclerView recycler, int expectedItemsCount) {
        mRecycler = recycler;
        mExpectedItemsCount = expectedItemsCount;
    }

    @Override
    protected boolean isIdle() {
        return mRecycler.getAdapter().getItemCount() == mExpectedItemsCount;
    }
} public class RecyclerViewMatcher {
    private final int mRecyclerViewId;

    public RecyclerViewMatcher(int recyclerViewId) {
        this.mRecyclerViewId = recyclerViewId;
    }

    public Matcher<View> atPosition(final int position) {
        return atPositionOnView(position, -1);
    }

    public Matcher<View> atPositionOnView(final int position, final int targetViewId) {

        return new TypeSafeMatcher<View>() {
            private Resources mResources = null;
            private View mChildView;

            public void describeTo(Description description) {
                String idDescription = Integer.toString(mRecyclerViewId);
                if (this.mResources != null) {
                    try {
                        idDescription = this.mResources.getResourceName(mRecyclerViewId);
                    } catch (Resources.NotFoundException var4) {
                        idDescription = String.format("%s (resource name not found)",
                                mRecyclerViewId);
                    }
                }

                description.appendText("with id: " + idDescription);
            }

            public boolean matchesSafely(View view) {

                this.mResources = view.getResources();

                if (mChildView == null) {
                    RecyclerView recyclerView
                            = (RecyclerView) view.getRootView().findViewById(mRecyclerViewId);
                    if (recyclerView != null && recyclerView.getId() == mRecyclerViewId) {
                        mChildView = recyclerView.getChildAt(position);
                    } else {
                        return false;
                    }
                }

                if (targetViewId == -1) {
                    return view == mChildView;
                } else {
                    View targetView = mChildView.findViewById(targetViewId);
                    return view == targetView;
                }

            }
        };
    }
} @SuppressWarnings("PMD.BooleanGetMethodName")
@Value.Immutable
@JsonSerialize(as = ImmutableAppState.class)
@JsonDeserialize(as = ImmutableAppState.class)
public abstract class AppState {

    @Value.Default
    public Map<String, Gif> getGifs() {
        return new LinkedHashMap<>();
    }

    @Value.Default
    public boolean getHasMoreGifs() {
        return true;
    }

    public String toJson() throws JsonProcessingException {
        return new ObjectMapper().writeValueAsString(this);
    }

    public static AppState fromJson(String json) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new GuavaModule());
        return mapper.readValue(json, AppState.class);
    }

} @SuppressWarnings("PMD.BooleanGetMethodName")
@Value.Immutable
@JsonSerialize(as = ImmutableGif.class)
@JsonDeserialize(as = ImmutableGif.class)
public abstract class Gif {

    public enum Status {
        PAUSED, LOOPING, DOWNLOADING, DOWNLOADED, NOT_DOWNLOADED, DOWNLOAD_FAILED
    }

    //abstract method are immutable by default
    public abstract String getUuid();

    @Value.Default
    public String getPath() {
        return "";
    }

    public abstract String getUrl();
    
    public abstract String getTitle();

    @Value.Default
    public boolean getWatched() {
        return false;
    }

    @Value.Default
    public Status getStatus() {
        return Status.NOT_DOWNLOADED;
    }

    public String toJson() throws JsonProcessingException {
        return new ObjectMapper().writeValueAsString(this);
    }

    public static Gif fromJson(String json) throws IOException {
        return new ObjectMapper().readValue(json, Gif.class);
    }

} public class GifListActivity extends StateListenerActivity<AppState>
        implements AnvilRenderable {

    //Binding Data
    private boolean mGifProgressVisibility;
    private AnvilRenderListener mAnvilRenderListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_gif_list);
        initialState();
        bindingViews();
    }

    private void initialState() {
        mGifProgressVisibility = MyApp.getFluxxan().getState().getGifs().isEmpty();
    }

    @Override
    protected Fluxxan<AppState> getFlux() {
        return MyApp.getFluxxan();
    }

    @Override
    protected void onStart() {
        super.onStart();
        AppStateActionCreator.getInstance().loadAppState();
    }

    @Override
    public boolean hasStateChanged(AppState newState, AppState oldState) {
        return !newState.equals(oldState);
    }

    @Override
    public void onStateChanged(AppState appState) {
        if (!appState.getGifs().isEmpty()) {
            mGifProgressVisibility = false;
        }
    }

    @Override
    public void setAnvilRenderListener(AnvilRenderListener listener) {
        mAnvilRenderListener = listener;
    }

    private void bindingViews() {
        //Bindings Defaults
        Anvil.mount(findViewById(R.id.activity_gif_list), () -> {
            withId(R.id.loading, () -> visibility(mGifProgressVisibility));

            if (mAnvilRenderListener != null) mAnvilRenderListener.onAnvilRendered();
        });
    }
} public class FeedView extends ReactiveView {

    private Map<String, Gif> mGifs;
    private DrawableCache mDrawables = new DrawableCache();
    private LinearLayoutManager mLayoutManager;
    private GifsAdapter mGifsAdapter;
    private boolean mHasMoreGifs;

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

    public FeedView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FeedView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public void view() {
        Recycler.view(() -> {
            if (mLayoutManager == null) {
                mLayoutManager = new LinearLayoutManager(getContext(),
                        LinearLayoutManager.VERTICAL, false);
            }
            if (mGifsAdapter == null) {
                mGifsAdapter = gifsAdapter(mGifs, this::renderGifView);
            }

            Recycler.layoutManager(mLayoutManager);
            Recycler.adapter(mGifsAdapter);
            Recycler.hasFixedSize(true);
            setupEndlessScrolling();

            if (!mGifs.equals(mGifsAdapter.getGifs())) {
                mGifsAdapter.setGifs(mGifs);
                mGifsAdapter.notifyDataSetChanged();
            }
        });

        onAnvilRendered();
    }

    private void setupEndlessScrolling() {
        //TODO: PR on Anvil Recycler to exposes addOnScrollListener
        ((RecyclerView) Anvil.currentView())
                .addOnScrollListener(new EndlessRecyclerScrollListener(mLayoutManager) {
                    @Override
                    public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
                        if (mHasMoreGifs) {
                            GifListActionCreator.getInstance().fetchGifs();
                        }
                    }
                });
    }

    @Override
    public boolean hasStateChanged(AppState newState, AppState oldState) {
        return !newState.getGifs().equals(mGifs)
                || newState.getHasMoreGifs() != oldState.getHasMoreGifs();
    }

    @Override
    public void onStateChanged(AppState appState) {
        mGifs = appState.getGifs();
        mHasMoreGifs = appState.getHasMoreGifs();
    }

    @Override
    protected void initialState() {
        super.initialState();
        mGifs = MyApp.getFluxxan().getState().getGifs();
        mHasMoreGifs = MyApp.getFluxxan().getState().getHasMoreGifs();
    }

    private void renderGifView(Gif gif) {
        v(GifView.class, () -> ((GifView) Anvil.currentView())
                .withGifDrawable(createDrawable(gif))
                .withGif(gif));
        onAnvilRendered();
    }

    private GifDrawable createDrawable(Gif gif) {
        GifDrawable drawable = null;
        try {
            if (mDrawables.containsKey(gif.getUuid())) {
                drawable = mDrawables.get(gif.getUuid());
            } else if (gif.getStatus() != NOT_DOWNLOADED
                    && gif.getStatus() != DOWNLOADING
                    && gif.getStatus() != DOWNLOAD_FAILED) {
                drawable = new GifDrawable(gif.getPath());
                mDrawables.put(gif.getUuid(), drawable);
            }
        } catch (IOException e) {
            Log.e("Feed", gif.toString(), e);
        }

        return drawable;
    }

    private class DrawableCache extends LinkedHashMap<String, GifDrawable> {
        private static final int CACHE_CAPACITY = 10;
        /* The load factor indicates the size where the linked hash will trigger to
         * increase his size. */
        private static final float LOAD_FACTOR = 0.7f;
        /* The initial capacity should be a little bit greater than cache capacity to keep
         * a threshold that ensures that removeEldest will be triggered before the list be full.
         */
        private static final int INITIAL_CAPACITY = (int) (CACHE_CAPACITY * LOAD_FACTOR);
        /* The accessOrder ensures that eldestEntry will be the most oldest entry accessed.
         * This fits well in the feed, since the eldest gif accessed usually will be
         * the one that the user have viewed moments ago and isn't currently displayed
         * in the screen.
         **/
        private static final boolean ACCESS_ORDER = true;

        public DrawableCache() {
            super(INITIAL_CAPACITY, LOAD_FACTOR, ACCESS_ORDER);
        }

        @Override
        protected boolean removeEldestEntry(Entry<String, GifDrawable> eldest) {
            return size() > CACHE_CAPACITY;
        }
    }
} public class GifView extends ReactiveView {

    private Gif mGif;
    private GifDrawable mGifDrawable;
    private boolean mHasRequestedDownload = false;

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

    public GifView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public GifView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public GifView withGif(Gif gif) {
        if (gif != null) {
            if (!gif.getUuid().equals(mGif.getUuid())) mHasRequestedDownload = false;
            mGif = gif;
        }
        return this;
    }

    public GifView withGifDrawable(GifDrawable gifDrawable) {
        mGifDrawable = gifDrawable;
        return this;
    }

    public Gif getGif() {
        return mGif;
    }

    @Override
    public void view() {
        xml(R.layout.gif_item, () -> {
            setBackground();
            withId(R.id.gif_image, () -> {
                imageDrawable(mGifDrawable);
                requestContent();
                defineBehavior();
            });
            withId(R.id.gif_loading, () -> visibility(mGif.getStatus() == DOWNLOADING));
        });
        onAnvilRendered();
    }

    @Override
    public boolean hasStateChanged(AppState newState, AppState oldState) {
        Gif newGif = newState.getGifs().get(mGif.getUuid());
        return newGif != null && !newGif.equals(mGif);
    }

    @Override
    public void onStateChanged(AppState appState) {
        withGif(appState.getGifs().get(mGif.getUuid()));

        if (mGif.getStatus() == DOWNLOAD_FAILED) {
            mHasRequestedDownload = false;
        }
    }

    @Override
    protected void initialState() {
        super.initialState();
        mGif = ImmutableGif.builder().url("").path("").title("").uuid("")
                .build();
    }

    private void setBackground() {
        if (mGif.getStatus() == DOWNLOAD_FAILED) {
            backgroundColor(ContextCompat.getColor(getContext(), R.color.error));
            return;
        }

        if (mGif.getWatched()) {
            backgroundColor(ContextCompat.getColor(getContext(), R.color.watched));
        } else {
            backgroundColor(ContextCompat.getColor(getContext(), R.color.notWatched));
        }
    }

    private void requestContent() {
        if (mGif.getStatus() == NOT_DOWNLOADED && !mHasRequestedDownload) {
            mHasRequestedDownload = true;
            GifActionCreator.getInstance().gifDownloadStart(mGif);
        }
    }

    private void defineBehavior() {
        if (mGifDrawable != null) {
            if (mGif.getStatus() == LOOPING) {
                mGifDrawable.start();
            } else {

                mGifDrawable.stop();
            }
            onClick(v -> GifActionCreator.getInstance().gifClick(mGif));
        }
    }
} public final class GifsAdapter extends RenderableRecyclerViewAdapter {
    private GifsAdapterRenderable mRenderable;
    private Map<String, Gif> mGifs;
    private List mUuids;

    private GifsAdapter(Map<String, Gif> gifs, GifsAdapterRenderable renderable) {
        setGifs(gifs);
        mRenderable = renderable;
    }

    public static GifsAdapter gifsAdapter(Map<String, Gif> gifs, GifsAdapterRenderable renderable) {
        GifsAdapter adapter = new GifsAdapter(gifs, renderable);
        adapter.setHasStableIds(false);
        return adapter;
    }

    public Map getGifs() {
        return mGifs;
    }

    public void setGifs(Map<String, Gif> gifs) {
        mGifs = gifs;
        mUuids = Arrays.asList(mGifs.keySet().toArray());
    }

    public int getItemCount() {
        return mGifs.size();
    }

    public long getItemId(int pos) {
        return pos;
    }

    public int getItemViewType(int pos) {
        Object item = mGifs.get(mUuids.get(pos));
        return item == null ? 0 : item.getClass().hashCode();
    }

    public void view(RecyclerView.ViewHolder holder) {
        mRenderable.view(mGifs.get(mUuids.get(holder.getAdapterPosition())));
    }

    public interface GifsAdapterRenderable {
        void view(Gif gif);
    }
} public class ReduxBaseTest {
    @Rule
    public Timeout mGlobalTimeout = new Timeout(1, TimeUnit.MINUTES);

    protected TestHelper mHelper = null;

    @Before
    public void setup() {
        if (mHelper != null) {
            mHelper.getFluxxan().registerReducer(new FakeReducer());
            mHelper.activateStateListener();
        } else {
            informTestHelperNotInitialized();
        }
    }

    @After
    public void cleanup() {
        if (mHelper != null) {
            mHelper.deactivateStateListener();
            mHelper.getFluxxan().unregisterReducer(FakeReducer.class);
        } else {
            informTestHelperNotInitialized();
        }
    }

    private void informTestHelperNotInitialized() {
        Log.e(getClass().getSimpleName(),
                "TestHelper not initialized: set super.mHelper with properly Fluxxan instance");
    }
} public class TestHelper {
    public static final String STRING_UNIQUE = UUID.randomUUID().toString();
    public static final String STRING_UNIQUE_2 = UUID.randomUUID().toString();
    public static final String STRING_UNIQUE_3 = UUID.randomUUID().toString();
    public static final String DEFAULT_UUID = "default-uuid";
    public static final Float FLOAT_RANDOM = new Random().nextFloat();
    public static final float DELTA = 0.00001f;

    private final Object mLock = new Object();

    private Fluxxan mFluxxan;
    private boolean mStateChanged = false;

    private StateListener mStateListener = new StateListener() {
        @Override
        public boolean hasStateChanged(Object newState, Object oldState) {
            setStateChanged(true);
            return newState != oldState;
        }

        @Override
        public void onStateChanged(Object o) {
        }
    };

    public TestHelper(Fluxxan fluxxan) {
        mFluxxan = fluxxan;
    }

    public void activateStateListener() {
        getFluxxan().getDispatcher().addListener(mStateListener);
    }

    public void deactivateStateListener() {
        getFluxxan().getDispatcher().removeListener(mStateListener);
    }

    private boolean isStateChanged() {
        synchronized (mLock) {
            return mStateChanged;
        }
    }

    private void setStateChanged(boolean value) {
        synchronized (mLock) {
            mStateChanged = value;
        }
    }

    public Fluxxan<AppState> getFluxxan() {
        return mFluxxan;
    }

    public void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Log.e(getClass().getSimpleName(), "", e);
        }
    }

    // Helpers methods to dry up unit tests
    public void dispatchAction(Action action) {
        getFluxxan().getDispatcher().dispatch(action);
        // Since the dispatcher send actions in background we need wait the response arrives
        while (!isStateChanged() || getFluxxan().getDispatcher().isDispatching()) {
            sleep(5);
        }
        setStateChanged(false);
    }

    public void dispatchFakeAppState(AppState state) {
        dispatchAction(new Action(FakeReducer.FAKE_REDUCE_ACTION, state));
    }

    public Gif firstAppStateGif() {
        Map<String, Gif> gifs = getFluxxan().getState().getGifs();
        if (gifs.size() <= 0) return null;
        return gifs.values().iterator().next();
    }

    public static AppState buildAppState(Gif gif) {
        return ImmutableAppState.builder().putGifs(gif.getUuid(), gif).build();
    }

    public static AppState buildAppState(Map<String, Gif> gifs) {
        return ImmutableAppState.builder().gifs(gifs).build();
    }

    public static Gif buildGif() {
        return gifBuilderWithDefault().build();
    }

    public static Gif buildGif(Gif.Status status) {
        return buildGif(status, DEFAULT_UUID);
    }

    public static Gif buildGif(Gif.Status status, String uuid) {
        return gifBuilderWithDefault()
                .uuid(uuid)
                .status(status)
                .build();
    }

    public static ImmutableGif.Builder gifBuilderWithEmpty() {
        return ImmutableGif.builder()
                .uuid(UUID.randomUUID().toString())
                .title("")
                .url("");
    }

    public static ImmutableGif.Builder gifBuilderWithDefault() {
        return ImmutableGif.builder()
                .uuid(DEFAULT_UUID)
                .title("Gif")
                .url("https://media.giphy.com/media/l0HlE56oAxpngfnWM/giphy.gif")
                .status(Gif.Status.NOT_DOWNLOADED);

    }

    public static Map<String, Gif> buildFiveGifs() {
        String[] titles = {"Gif 1", "Gif 2", "Gif 3", "Gif 4", "Gif 5" };
        String[] urls = {
                "https://media.giphy.com/media/l0HlE56oAxpngfnWM/giphy.gif",
                "http://inspirandoideias.com.br/blog/wp-content/uploads/2015/03/"
                        + "b3368a682fc5ff891e41baad2731f4b6.gif",
                "https://media.giphy.com/media/9fbYYzdf6BbQA/giphy.gif",
                "https://media.giphy.com/media/l2YWl1oQlNvthGWrK/giphy.gif",
                "https://media.giphy.com/media/3oriNQHSU0bVcFW5sA/giphy.gif"
        };

        Map<String, Gif> gifs = new LinkedHashMap<>();
        for (int i = 0; i < titles.length; i++) {
            Gif gif = ImmutableGif.builder().uuid(UUID.randomUUID().toString())
                    .title(titles[i])
                    .url(urls[i])
                    .build();
            gifs.put(gif.getUuid(), gif);
        }

        return gifs;
    }

    public DataManager mockDataManagerFetch(Map<String, Gif> expectedGifs,
                                            boolean expectedHasMore) {
        DataManager dataManager = mock(DataManager.class);
        ArgumentCaptor<DataManager.GifListLoadListener> listenerCaptor
                = ArgumentCaptor.forClass(DataManager.GifListLoadListener.class);
        doAnswer((invocationOnMock) -> {
            new Thread(() -> {
                // The fluxxan don't let we dispatch when it's dispatching
                while (mFluxxan.getDispatcher().isDispatching()) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        Log.e("Test", e.getMessage(), e);
                    }
                }
                listenerCaptor.getValue().onLoaded(expectedGifs, expectedHasMore);
            }).start();
            return null;
        }).when(dataManager).fetchGifs(listenerCaptor.capture());
        return dataManager;
    }

    private void clearRestMiddleware() {
        getFluxxan().getDispatcher().unregisterMiddleware(RestMiddleware.class);
    }

    private void clearPersistenceMiddleware() {
        PersistenceMiddleware persistenceMiddleware = (PersistenceMiddleware) getFluxxan()
                .getDispatcher().unregisterMiddleware(PersistenceMiddleware.class);
        getFluxxan().getDispatcher().removeListener(persistenceMiddleware);
    }

    public void clearMiddlewares() {
        clearRestMiddleware();
        clearPersistenceMiddleware();
    }

    public void replaceRestMiddleware(DataManager dm, Context context) {
        clearRestMiddleware();

        RestMiddleware restMiddleware = new RestMiddleware(context, dm, new FileDownloader());
        getFluxxan().getDispatcher().registerMiddleware(restMiddleware);
    }

    public void replacePersistenceMiddleware(DataManager dm) {
        clearPersistenceMiddleware();

        PersistenceMiddleware persistenceMiddleware = new PersistenceMiddleware(dm);
        getFluxxan().getDispatcher().registerMiddleware(persistenceMiddleware);
        getFluxxan().addListener(persistenceMiddleware);
    }

    public void replaceMiddlewares(DataManager dm, Context context) {
        replaceRestMiddleware(dm, context);
        replacePersistenceMiddleware(dm);
    }
} public static Fluxxan<AppState> getFluxxan() {
    return sFluxxan;
} public static AppState getAppStateDefault() {
    String[] uuids = {"1", "2", "3", "4", "5" };
    String[] titles = {"Gif 1", "Gif 2", "Gif 3", "Gif 4", "Gif 5" };
    String[] urls = {
            "https://media.giphy.com/media/l0HlE56oAxpngfnWM/giphy.gif",
            "http://inspirandoideias.com.br/blog/wp-content/uploads/2015/03/"
                    + "b3368a682fc5ff891e41baad2731f4b6.gif",
            "https://media.giphy.com/media/9fbYYzdf6BbQA/giphy.gif",
            "https://media.giphy.com/media/l2YWl1oQlNvthGWrK/giphy.gif",
            "https://media.giphy.com/media/3oriNQHSU0bVcFW5sA/giphy.gif"
    };

    Map<String, Gif> gifs = new LinkedHashMap<>();
    for (int i = 0; i < titles.length; i++) {
        Gif gif = buildGif(uuids[i], titles[i], urls[i]);
        gifs.put(gif.getUuid(), gif);
    }

    return ImmutableAppState.builder().putAllGifs(gifs).build();
} public static Matcher<View> withBGColor(final int color) {
    Checks.checkNotNull(color);
    return new BoundedMatcher<View, View>(View.class) {
        @Override
        public boolean matchesSafely(View view) {
            int currentColor = ((ColorDrawable) view.getBackground()).getColor();
            return color == currentColor;
        }
        @Override
        public void describeTo(Description description) {
            description.appendText("with background color: " + color);
        }
    };
} public static Matcher<View> withEqualsGifUuid(final String uuid) {
    return new BoundedMatcher<View, View>(View.class) {
        @Override
        public boolean matchesSafely(View view) {
            GifView gifView = (GifView) ((FrameLayout) view).getChildAt(0);
            return gifView.getGif().getUuid().equals(uuid);
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("with equals Gif UUID");
        }
    };
} public static Matcher<View> withGifDrawable() {
    return new BoundedMatcher<View, View>(View.class) {
        @Override
        public boolean matchesSafely(View view) {
            GifImageView gifImageView = (GifImageView) view.findViewById(R.id.gif_image);
            return gifImageView.getDrawable() != null;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("with a not null drawable");
        }
    };
} public static Matcher<View> withPlayingGifDrawable() {
    return new BoundedMatcher<View, View>(View.class) {
        @Override
        public boolean matchesSafely(View view) {
            GifImageView gifImageView = (GifImageView) view.findViewById(R.id.gif_image);
            return ((GifDrawable) gifImageView.getDrawable()).isPlaying();
        }
        @Override
        public void describeTo(Description description) {
            description.appendText("with gif image view playing");
        }
    };
} public static File createFakeGifFile() {
    File dst = new File(getInstrumentation().getTargetContext()
            .getExternalCacheDir() + "/test.gif");
    try {
        InputStream iStream = getInstrumentation().getContext()
                .getResources().getAssets().open("test.gif");
        OutputStream out = new FileOutputStream(dst);
        // Transfer bytes from in to out
        byte[] buf = new byte[1024];
        int len;
        while ((len = iStream.read(buf)) > 0) {
            out.write(buf, 0, len);
        }
        iStream.close();
        out.close();
    } catch (IOException e) {
        Log.e(FileUtils.class.getSimpleName(), "", e);
    }
    return dst;
} public static AppState buildAppState(Gif gif) {
    return ImmutableAppState.builder().putGifs(gif.getUuid(), gif).build();
} public static ImmutableGif.Builder gifBuilderWithEmpty() {
    return ImmutableGif.builder()
            .uuid(UUID.randomUUID().toString())
            .title("")
            .url("");
}
import android.content.Intent;
import android.support.test.espresso.core.deps.guava.base.Strings;
import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import com.snappydb.DB;
import com.snappydb.DBFactory;
import com.umaplay.fluxxan.impl.DispatcherImpl;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import br.com.catbag.gifreduxsample.R;
import br.com.catbag.gifreduxsample.actions.GifActionCreator;
import br.com.catbag.gifreduxsample.asyncs.data.DataManager;
import br.com.catbag.gifreduxsample.lockers.AnvilTestLocker;
import br.com.catbag.gifreduxsample.lockers.RecyclerTestLocker;
import br.com.catbag.gifreduxsample.matchers.RecyclerViewMatcher;
import br.com.catbag.gifreduxsample.models.AppState;
import br.com.catbag.gifreduxsample.models.Gif;
import br.com.catbag.gifreduxsample.models.ImmutableAppState;
import br.com.catbag.gifreduxsample.ui.GifListActivity;
import br.com.catbag.gifreduxsample.ui.views.FeedView;
import br.com.catbag.gifreduxsample.ui.views.GifView;
import br.com.catbag.gifreduxsample.ui.views.GifsAdapter;
import shared.ReduxBaseTest;
import shared.TestHelper;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static android.support.test.espresso.contrib.RecyclerViewActions.scrollToPosition;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.view.View.generateViewId;
import static br.com.catbag.gifreduxsample.MyApp.getFluxxan;
import static br.com.catbag.gifreduxsample.asyncs.data.DataManager.getAppStateDefault;
import static br.com.catbag.gifreduxsample.matchers.Matchers.withBGColor;
import static br.com.catbag.gifreduxsample.matchers.Matchers.withEqualsGifUuid;
import static br.com.catbag.gifreduxsample.matchers.Matchers.withGifDrawable;
import static br.com.catbag.gifreduxsample.matchers.Matchers.withPlayingGifDrawable;
import static br.com.catbag.gifreduxsample.utils.FileUtils.createFakeGifFile;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotSame;
import static org.hamcrest.Matchers.not;
import static org.mockito.Mockito.mock;
import static shared.TestHelper.buildAppState;
import static shared.TestHelper.gifBuilderWithEmpty;
package br.com.catbag.gifreduxsample.ui.giflist;








/**
 Copyright 26/10/2016
 Felipe Piñeiro (fpbitencourt@gmail.com),
 Nilton Vasques (nilton.vasques@gmail.com) and
 Raul Abreu (raulccabreu@gmail.com)

 Be free to ask for help, email us!

 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 in compliance with the License. You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software distributed under the License
 is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 implied. See the License for the specific language governing permissions and limitations under
 the License.
 **/

@LargeTest
@RunWith(AndroidJUnit4.class)
public class GifListActivityTest extends ReduxBaseTest {

    @Rule
    public ActivityTestRule<GifListActivity> mActivityTestRule
            = new ActivityTestRule<>(GifListActivity.class, false, false);

    public GifListActivityTest() {
        mHelper = new TestHelper(getFluxxan());
    }

    @Override
    public void setup() {
        super.setup();
        mHelper.replaceMiddlewares(mock(DataManager.class), getTargetContext());
    }

    @Override
    public void cleanup() {
        mHelper.replaceMiddlewares(new DataManager(getTargetContext()), getTargetContext());
        super.cleanup();
    }

    @Test
    public void whenGifIsDownloadingTest() {
        Gif gif = gifBuilderWithEmpty()
                .status(Gif.Status.DOWNLOADING)
                .build();

        mHelper.dispatchFakeAppState(buildAppState(gif));

        //Espresso waits for activity completely launch
        mActivityTestRule.launchActivity(new Intent());

        onView(withId(R.id.gif_loading)).check(matches(isDisplayed()));
    }

    @Test
    public void whenGifIsDownloadedTest() {
        File fakeGifFile = createFakeGifFile();
        Gif gif = gifBuilderWithEmpty()
                .path(fakeGifFile.getAbsolutePath())
                .status(Gif.Status.DOWNLOADED)
                .build();

        mHelper.dispatchFakeAppState(buildAppState(gif));

        mActivityTestRule.launchActivity(new Intent());

        onView(withId(R.id.gif_loading)).check(matches(not(isDisplayed())));

        onView(withId(R.id.gif_image)).check(matches(isDisplayed()));

        onView(withId(R.id.gif_image)).check(matches(withGifDrawable()));