/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.plaidapp.data;
import android.content.Context;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.plaidapp.data.api.designernews.model.Story;
import io.plaidapp.data.api.dribbble.DribbbleSearchService;
import io.plaidapp.data.api.dribbble.DribbbleService;
import io.plaidapp.data.api.dribbble.model.Like;
import io.plaidapp.data.api.dribbble.model.Shot;
import io.plaidapp.data.api.dribbble.model.User;
import io.plaidapp.data.api.producthunt.model.Post;
import io.plaidapp.data.prefs.SourceManager;
import io.plaidapp.ui.FilterAdapter;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Responsible for loading data from the various sources. Instantiating classes are responsible for
* providing the {code onDataLoaded} method to do something with the data.
*/
public abstract class DataManager extends BaseDataManager<List<? extends PlaidItem>> {
private final FilterAdapter filterAdapter;
private Map<String, Integer> pageIndexes;
private Map<String, Call> inflight;
public DataManager(Context context,
FilterAdapter filterAdapter) {
super(context);
this.filterAdapter = filterAdapter;
filterAdapter.registerFilterChangedCallback(filterListener);
setupPageIndexes();
inflight = new HashMap<>();
}
public void loadAllDataSources() {
for (Source filter : filterAdapter.getFilters()) {
loadSource(filter);
}
}
@Override
public void cancelLoading() {
if (inflight.size() > 0) {
for (Call call : inflight.values()) {
call.cancel();
}
inflight.clear();
}
}
private final FilterAdapter.FiltersChangedCallbacks filterListener =
new FilterAdapter.FiltersChangedCallbacks() {
@Override
public void onFiltersChanged(Source changedFilter) {
if (changedFilter.active) {
loadSource(changedFilter);
} else { // filter deactivated
final String key = changedFilter.key;
if (inflight.containsKey(key)) {
final Call call = inflight.get(key);
if (call != null) call.cancel();
inflight.remove(key);
}
// clear the page index for the source
pageIndexes.put(key, 0);
}
}
};
private void loadSource(Source source) {
if (source.active) {
loadStarted();
final int page = getNextPageIndex(source.key);
switch (source.key) {
case SourceManager.SOURCE_DESIGNER_NEWS_POPULAR:
loadDesignerNewsTopStories(page);
break;
case SourceManager.SOURCE_DESIGNER_NEWS_RECENT:
loadDesignerNewsRecent(page);
break;
case SourceManager.SOURCE_DRIBBBLE_POPULAR:
loadDribbblePopular(page);
break;
case SourceManager.SOURCE_DRIBBBLE_FOLLOWING:
loadDribbbleFollowing(page);
break;
case SourceManager.SOURCE_DRIBBBLE_USER_LIKES:
loadDribbbleUserLikes(page);
break;
case SourceManager.SOURCE_DRIBBBLE_USER_SHOTS:
loadDribbbleUserShots(page);
break;
case SourceManager.SOURCE_DRIBBBLE_RECENT:
loadDribbbleRecent(page);
break;
case SourceManager.SOURCE_DRIBBBLE_DEBUTS:
loadDribbbleDebuts(page);
break;
case SourceManager.SOURCE_DRIBBBLE_ANIMATED:
loadDribbbleAnimated(page);
break;
case SourceManager.SOURCE_PRODUCT_HUNT:
loadProductHunt(page);
break;
default:
if (source instanceof Source.DribbbleSearchSource) {
loadDribbbleSearch((Source.DribbbleSearchSource) source, page);
} else if (source instanceof Source.DesignerNewsSearchSource) {
loadDesignerNewsSearch((Source.DesignerNewsSearchSource) source, page);
}
break;
}
}
}
private void setupPageIndexes() {
final List<Source> dateSources = filterAdapter.getFilters();
pageIndexes = new HashMap<>(dateSources.size());
for (Source source : dateSources) {
pageIndexes.put(source.key, 0);
}
}
private int getNextPageIndex(String dataSource) {
int nextPage = 1; // default to one – i.e. for newly added sources
if (pageIndexes.containsKey(dataSource)) {
nextPage = pageIndexes.get(dataSource) + 1;
}
pageIndexes.put(dataSource, nextPage);
return nextPage;
}
private boolean sourceIsEnabled(String key) {
return pageIndexes.get(key) != 0;
}
private void sourceLoaded(List<? extends PlaidItem> data, int page, String key) {
loadFinished();
if (data != null && !data.isEmpty() && sourceIsEnabled(key)) {
setPage(data, page);
setDataSource(data, key);
onDataLoaded(data);
}
inflight.remove(key);
}
private void loadFailed(String key) {
loadFinished();
inflight.remove(key);
}
private void loadDesignerNewsTopStories(final int page) {
final Call<List<Story>> topStories = getDesignerNewsApi().getTopStories(page);
topStories.enqueue(new Callback<List<Story>>() {
@Override
public void onResponse(Call<List<Story>> call, Response<List<Story>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, SourceManager.SOURCE_DESIGNER_NEWS_POPULAR);
} else {
loadFailed(SourceManager.SOURCE_DESIGNER_NEWS_POPULAR);
}
}
@Override
public void onFailure(Call<List<Story>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_DESIGNER_NEWS_POPULAR);
}
});
inflight.put(SourceManager.SOURCE_DESIGNER_NEWS_POPULAR, topStories);
}
private void loadDesignerNewsRecent(final int page) {
final Call<List<Story>> recentStoriesCall = getDesignerNewsApi().getRecentStories(page);
recentStoriesCall.enqueue(new Callback<List<Story>>() {
@Override
public void onResponse(Call<List<Story>> call, Response<List<Story>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, SourceManager.SOURCE_DESIGNER_NEWS_RECENT);
} else {
loadFailed(SourceManager.SOURCE_DESIGNER_NEWS_RECENT);
}
}
@Override
public void onFailure(Call<List<Story>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_DESIGNER_NEWS_RECENT);
}
});
inflight.put(SourceManager.SOURCE_DESIGNER_NEWS_RECENT, recentStoriesCall);
}
private void loadDesignerNewsSearch(final Source.DesignerNewsSearchSource source,
final int page) {
final Call<List<Story>> searchCall = getDesignerNewsApi().search(source.query, page);
searchCall.enqueue(new Callback<List<Story>>() {
@Override
public void onResponse(Call<List<Story>> call, Response<List<Story>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, source.key);
} else {
loadFailed(source.key);
}
}
@Override
public void onFailure(Call<List<Story>> call, Throwable t) {
loadFailed(source.key);
}
});
inflight.put(source.key, searchCall);
}
private void loadDribbblePopular(final int page) {
final Call<List<Shot>> popularCall = getDribbbleApi()
.getPopular(page, DribbbleService.PER_PAGE_DEFAULT);
popularCall.enqueue(new Callback<List<Shot>>() {
@Override
public void onResponse(Call<List<Shot>> call, Response<List<Shot>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, SourceManager.SOURCE_DRIBBBLE_POPULAR);
} else {
loadFailed(SourceManager.SOURCE_DRIBBBLE_POPULAR);
}
}
@Override
public void onFailure(Call<List<Shot>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_DRIBBBLE_POPULAR);
}
});
inflight.put(SourceManager.SOURCE_DRIBBBLE_POPULAR, popularCall);
}
private void loadDribbbleDebuts(final int page) {
final Call<List<Shot>> debutsCall = getDribbbleApi()
.getDebuts(page, DribbbleService.PER_PAGE_DEFAULT);
debutsCall.enqueue(new Callback<List<Shot>>() {
@Override
public void onResponse(Call<List<Shot>> call, Response<List<Shot>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, SourceManager.SOURCE_DRIBBBLE_DEBUTS);
} else {
loadFailed(SourceManager.SOURCE_DRIBBBLE_DEBUTS);
}
}
@Override
public void onFailure(Call<List<Shot>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_DRIBBBLE_DEBUTS);
}
});
inflight.put(SourceManager.SOURCE_DRIBBBLE_DEBUTS, debutsCall);
}
private void loadDribbbleAnimated(final int page) {
final Call<List<Shot>> animatedCall = getDribbbleApi()
.getAnimated(page, DribbbleService.PER_PAGE_DEFAULT);
animatedCall.enqueue(new Callback<List<Shot>>() {
@Override
public void onResponse(Call<List<Shot>> call, Response<List<Shot>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, SourceManager.SOURCE_DRIBBBLE_ANIMATED);
} else {
loadFailed(SourceManager.SOURCE_DRIBBBLE_ANIMATED);
}
}
@Override
public void onFailure(Call<List<Shot>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_DRIBBBLE_ANIMATED);
}
});
inflight.put(SourceManager.SOURCE_DRIBBBLE_ANIMATED, animatedCall);
}
private void loadDribbbleRecent(final int page) {
final Call<List<Shot>> recentCall = getDribbbleApi()
.getRecent(page, DribbbleService.PER_PAGE_DEFAULT);
recentCall.enqueue(new Callback<List<Shot>>() {
@Override
public void onResponse(Call<List<Shot>> call, Response<List<Shot>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, SourceManager.SOURCE_DRIBBBLE_RECENT);
} else {
loadFailed(SourceManager.SOURCE_DRIBBBLE_RECENT);
}
}
@Override
public void onFailure(Call<List<Shot>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_DRIBBBLE_RECENT);
}
});
inflight.put(SourceManager.SOURCE_DRIBBBLE_RECENT, recentCall);
}
private void loadDribbbleFollowing(final int page) {
final Call<List<Shot>> followingCall = getDribbbleApi()
.getFollowing(page, DribbbleService.PER_PAGE_DEFAULT);
followingCall.enqueue(new Callback<List<Shot>>() {
@Override
public void onResponse(Call<List<Shot>> call, Response<List<Shot>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, SourceManager.SOURCE_DRIBBBLE_FOLLOWING);
} else {
loadFailed(SourceManager.SOURCE_DRIBBBLE_FOLLOWING);
}
}
@Override
public void onFailure(Call<List<Shot>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_DRIBBBLE_FOLLOWING);
}
});
inflight.put(SourceManager.SOURCE_DRIBBBLE_FOLLOWING, followingCall);
}
private void loadDribbbleUserLikes(final int page) {
if (getDribbblePrefs().isLoggedIn()) {
final Call<List<Like>> userLikesCall = getDribbbleApi()
.getUserLikes(page, DribbbleService.PER_PAGE_DEFAULT);
userLikesCall.enqueue(new Callback<List<Like>>() {
@Override
public void onResponse(Call<List<Like>> call, Response<List<Like>> response) {
if (response.isSuccessful()) {
// API returns Likes but we just want the Shots
final List<Like> likes = response.body();
List<Shot> likedShots = null;
if (likes != null && !likes.isEmpty()) {
likedShots = new ArrayList<>(likes.size());
for (Like like : likes) {
likedShots.add(like.shot);
}
}
sourceLoaded(likedShots, page, SourceManager.SOURCE_DRIBBBLE_USER_LIKES);
} else {
loadFailed(SourceManager.SOURCE_DRIBBBLE_USER_LIKES);
}
}
@Override
public void onFailure(Call<List<Like>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_DRIBBBLE_USER_LIKES);
}
});
inflight.put(SourceManager.SOURCE_DRIBBBLE_USER_LIKES, userLikesCall);
} else {
loadFinished();
}
}
private void loadDribbbleUserShots(final int page) {
if (getDribbblePrefs().isLoggedIn()) {
final Call<List<Shot>> userShotsCall = getDribbbleApi()
.getUserShots(page, DribbbleService.PER_PAGE_DEFAULT);
userShotsCall.enqueue(new Callback<List<Shot>>() {
@Override
public void onResponse(Call<List<Shot>> call, Response<List<Shot>> response) {
if (response.isSuccessful()) {
loadFinished();
final List<Shot> shots = response.body();
if (shots != null && !shots.isEmpty()) {
// this api call doesn't populate the shot user field but we need it
final User user = getDribbblePrefs().getUser();
for (Shot shot : shots) {
shot.user = user;
}
}
sourceLoaded(shots, page, SourceManager.SOURCE_DRIBBBLE_USER_SHOTS);
} else {
loadFailed(SourceManager.SOURCE_DRIBBBLE_USER_SHOTS);
}
}
@Override
public void onFailure(Call<List<Shot>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_DRIBBBLE_USER_SHOTS);
}
});
inflight.put(SourceManager.SOURCE_DRIBBBLE_USER_SHOTS, userShotsCall);
} else {
loadFinished();
}
}
private void loadDribbbleSearch(final Source.DribbbleSearchSource source, final int page) {
final Call<List<Shot>> searchCall = getDribbbleSearchApi().search(source.query, page,
DribbbleSearchService.PER_PAGE_DEFAULT, DribbbleSearchService.SORT_RECENT);
searchCall.enqueue(new Callback<List<Shot>>() {
@Override
public void onResponse(Call<List<Shot>> call, Response<List<Shot>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, source.key);
} else {
loadFailed(source.key);
}
}
@Override
public void onFailure(Call<List<Shot>> call, Throwable t) {
loadFailed(source.key);
}
});
inflight.put(source.key, searchCall);
}
private void loadProductHunt(final int page) {
// this API's paging is 0 based but this class (& sorting) is 1 based so adjust locally
final Call<List<Post>> postsCall = getProductHuntApi().getPosts(page - 1);
postsCall.enqueue(new Callback<List<Post>>() {
@Override
public void onResponse(Call<List<Post>> call, Response<List<Post>> response) {
if (response.isSuccessful()) {
sourceLoaded(response.body(), page, SourceManager.SOURCE_PRODUCT_HUNT);
} else {
loadFailed(SourceManager.SOURCE_PRODUCT_HUNT);
}
}
@Override
public void onFailure(Call<List<Post>> call, Throwable t) {
loadFailed(SourceManager.SOURCE_PRODUCT_HUNT);
}
});
inflight.put(SourceManager.SOURCE_PRODUCT_HUNT, postsCall);
}
}