package ml.docilealligator.infinityforreddit.post; import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.paging.ListenableFuturePagingSource; import androidx.paging.PagingState; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.concurrent.Executor; import ml.docilealligator.infinityforreddit.readpost.ReadPostsListInterface; import ml.docilealligator.infinityforreddit.thing.SortType; import ml.docilealligator.infinityforreddit.account.Account; import ml.docilealligator.infinityforreddit.apis.RedditAPI; import ml.docilealligator.infinityforreddit.postfilter.PostFilter; import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import retrofit2.HttpException; import retrofit2.Response; import retrofit2.Retrofit; public class PostPagingSource extends ListenableFuturePagingSource { public static final int TYPE_FRONT_PAGE = 0; public static final int TYPE_SUBREDDIT = 1; public static final int TYPE_USER = 2; public static final int TYPE_SEARCH = 3; public static final int TYPE_MULTI_REDDIT = 4; public static final int TYPE_ANONYMOUS_FRONT_PAGE = 5; public static final int TYPE_ANONYMOUS_MULTIREDDIT = 6; public static final String USER_WHERE_SUBMITTED = "submitted"; public static final String USER_WHERE_UPVOTED = "upvoted"; public static final String USER_WHERE_DOWNVOTED = "downvoted"; public static final String USER_WHERE_HIDDEN = "hidden"; public static final String USER_WHERE_SAVED = "saved"; private final Executor executor; private final Retrofit retrofit; private final String accessToken; private final String accountName; private final SharedPreferences sharedPreferences; private final SharedPreferences postFeedScrolledPositionSharedPreferences; private String subredditOrUserName; private String query; private String trendingSource; private final int postType; private final SortType sortType; private final PostFilter postFilter; private final ReadPostsListInterface readPostsList; private String userWhere; private String multiRedditPath; private final LinkedHashSet postLinkedHashSet; private String previousLastItem; PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.postType = postType; this.sortType = sortType == null ? new SortType(SortType.Type.BEST) : sortType; this.postFilter = postFilter; this.readPostsList = readPostsList; postLinkedHashSet = new LinkedHashSet<>(); } // PostPagingSource.TYPE_SUBREDDIT || PostPagingSource.TYPE_ANONYMOUS_FRONT_PAGE || PostPagingSource.TYPE_ANONYMOUS_MULTIREDDIT: PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, String name, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.subredditOrUserName = name; if (subredditOrUserName == null) { subredditOrUserName = "popular"; } this.postType = postType; if (sortType == null) { if ("popular".equals(name) || "all".equals(name)) { this.sortType = new SortType(SortType.Type.HOT); } else { this.sortType = new SortType(SortType.Type.BEST); } } else { this.sortType = sortType; } this.postFilter = postFilter; this.readPostsList = readPostsList; postLinkedHashSet = new LinkedHashSet<>(); } // PostPagingSource.TYPE_MULTI_REDDIT PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, String path, String query, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; if (path.endsWith("/")) { multiRedditPath = path.substring(0, path.length() - 1); } else { multiRedditPath = path; } this.query = query; this.postType = postType; if (sortType == null) { this.sortType = new SortType(SortType.Type.HOT); } else { this.sortType = sortType; } this.postFilter = postFilter; this.readPostsList = readPostsList; postLinkedHashSet = new LinkedHashSet<>(); } PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, String subredditOrUserName, int postType, SortType sortType, PostFilter postFilter, String where, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.subredditOrUserName = subredditOrUserName; this.postType = postType; this.sortType = sortType == null ? new SortType(SortType.Type.NEW) : sortType; this.postFilter = postFilter; userWhere = where; this.readPostsList = readPostsList; postLinkedHashSet = new LinkedHashSet<>(); } PostPagingSource(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName, SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences, String subredditOrUserName, String query, String trendingSource, int postType, SortType sortType, PostFilter postFilter, ReadPostsListInterface readPostsList) { this.executor = executor; this.retrofit = retrofit; this.accessToken = accessToken; this.accountName = accountName; this.sharedPreferences = sharedPreferences; this.postFeedScrolledPositionSharedPreferences = postFeedScrolledPositionSharedPreferences; this.subredditOrUserName = subredditOrUserName; this.query = query; this.trendingSource = trendingSource; this.postType = postType; this.sortType = sortType == null ? new SortType(SortType.Type.RELEVANCE) : sortType; this.postFilter = postFilter; postLinkedHashSet = new LinkedHashSet<>(); this.readPostsList = readPostsList; } @Nullable @Override public String getRefreshKey(@NonNull PagingState pagingState) { return null; } @NonNull @Override public ListenableFuture> loadFuture(@NonNull LoadParams loadParams) { RedditAPI api = retrofit.create(RedditAPI.class); switch (postType) { case TYPE_FRONT_PAGE: return loadHomePosts(loadParams, api); case TYPE_SUBREDDIT: return loadSubredditPosts(loadParams, api); case TYPE_USER: return loadUserPosts(loadParams, api); case TYPE_SEARCH: return loadSearchPosts(loadParams, api); case TYPE_MULTI_REDDIT: return loadMultiRedditPosts(loadParams, api); default: return loadAnonymousHomePosts(loadParams, api); } } public LoadResult transformData(Response response) { if (response.isSuccessful()) { String responseString = response.body(); LinkedHashSet newPosts = ParsePost.parsePostsSync(responseString, -1, postFilter, readPostsList); String lastItem = ParsePost.getLastItem(responseString); if (newPosts == null) { return new LoadResult.Error<>(new Exception("Error parsing posts")); } else { int currentPostsSize = postLinkedHashSet.size(); if (lastItem != null && lastItem.equals(previousLastItem)) { lastItem = null; } previousLastItem = lastItem; postLinkedHashSet.addAll(newPosts); if (currentPostsSize == postLinkedHashSet.size()) { return new LoadResult.Page<>(new ArrayList<>(), null, lastItem); } else { return new LoadResult.Page<>(new ArrayList<>(postLinkedHashSet).subList(currentPostsSize, postLinkedHashSet.size()), null, lastItem); } } } else { return new LoadResult.Error<>(new Exception("Response failed")); } } private ListenableFuture> loadHomePosts(@NonNull LoadParams loadParams, RedditAPI api) { ListenableFuture> bestPost; String afterKey; if (loadParams.getKey() == null) { boolean savePostFeedScrolledPosition = sortType != null && sortType.getType() == SortType.Type.BEST && sharedPreferences.getBoolean(SharedPreferencesUtils.SAVE_FRONT_PAGE_SCROLLED_POSITION, false); if (savePostFeedScrolledPosition) { String accountNameForCache = accountName.equals(Account.ANONYMOUS_ACCOUNT) ? SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_ANONYMOUS : accountName; afterKey = postFeedScrolledPositionSharedPreferences.getString(accountNameForCache + SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_FRONT_PAGE_BASE, null); } else { afterKey = null; } } else { afterKey = loadParams.getKey(); } bestPost = api.getBestPostsListenableFuture(sortType.getType(), sortType.getTime(), afterKey, APIUtils.getOAuthHeader(accessToken)); ListenableFuture> pageFuture = Futures.transform(bestPost, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> loadSubredditPosts(@NonNull LoadParams loadParams, RedditAPI api) { ListenableFuture> subredditPost; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { subredditPost = api.getSubredditBestPostsListenableFuture(subredditOrUserName, sortType.getType(), sortType.getTime(), loadParams.getKey()); } else { subredditPost = api.getSubredditBestPostsOauthListenableFuture(subredditOrUserName, sortType.getType(), sortType.getTime(), loadParams.getKey(), APIUtils.getOAuthHeader(accessToken)); } ListenableFuture> pageFuture = Futures.transform(subredditPost, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> loadUserPosts(@NonNull LoadParams loadParams, RedditAPI api) { ListenableFuture> userPosts; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { userPosts = api.getUserPostsListenableFuture(subredditOrUserName, loadParams.getKey(), sortType.getType(), sortType.getTime()); } else { userPosts = api.getUserPostsOauthListenableFuture(APIUtils.AUTHORIZATION_BASE + accessToken, subredditOrUserName, userWhere, loadParams.getKey(), sortType.getType(), sortType.getTime()); } ListenableFuture> pageFuture = Futures.transform(userPosts, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> loadSearchPosts(@NonNull LoadParams loadParams, RedditAPI api) { ListenableFuture> searchPosts; if (subredditOrUserName == null) { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { searchPosts = api.searchPostsListenableFuture(query, loadParams.getKey(), sortType.getType(), sortType.getTime(), trendingSource); } else { searchPosts = api.searchPostsOauthListenableFuture(query, loadParams.getKey(), sortType.getType(), sortType.getTime(), trendingSource, APIUtils.getOAuthHeader(accessToken)); } } else { if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { searchPosts = api.searchPostsInSpecificSubredditListenableFuture(subredditOrUserName, query, sortType.getType(), sortType.getTime(), loadParams.getKey()); } else { searchPosts = api.searchPostsInSpecificSubredditOauthListenableFuture(subredditOrUserName, query, sortType.getType(), sortType.getTime(), loadParams.getKey(), APIUtils.getOAuthHeader(accessToken)); } } ListenableFuture> pageFuture = Futures.transform(searchPosts, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> loadMultiRedditPosts(@NonNull LoadParams loadParams, RedditAPI api) { ListenableFuture> multiRedditPosts; if (accountName.equals(Account.ANONYMOUS_ACCOUNT)) { if (query != null && !query.isEmpty()) { multiRedditPosts = api.searchMultiRedditPostsListenableFuture(multiRedditPath, query, loadParams.getKey(), sortType.getType(), sortType.getTime()); } else { multiRedditPosts = api.getMultiRedditPostsListenableFuture(multiRedditPath, sortType.getType(), loadParams.getKey(), sortType.getTime()); } } else { if (query != null && !query.isEmpty()) { multiRedditPosts = api.searchMultiRedditPostsOauthListenableFuture(multiRedditPath, query, loadParams.getKey(), sortType.getType(), sortType.getTime(), APIUtils.getOAuthHeader(accessToken)); } else { multiRedditPosts = api.getMultiRedditPostsOauthListenableFuture(multiRedditPath, sortType.getType(), loadParams.getKey(), sortType.getTime(), APIUtils.getOAuthHeader(accessToken)); } } ListenableFuture> pageFuture = Futures.transform(multiRedditPosts, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } private ListenableFuture> loadAnonymousHomePosts(@NonNull LoadParams loadParams, RedditAPI api) { ListenableFuture> anonymousHomePosts = api.getSubredditBestPostsListenableFuture(subredditOrUserName, sortType.getType(), sortType.getTime(), loadParams.getKey()); ListenableFuture> pageFuture = Futures.transform(anonymousHomePosts, this::transformData, executor); ListenableFuture> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, executor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, executor); } }