org.lol.reddit.fragments.PostListingFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.lol.reddit.fragments.PostListingFragment.java

Source

/*******************************************************************************
 * This file is part of RedReader.
 *
 * RedReader is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * RedReader is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with RedReader.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package org.lol.reddit.fragments;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.Toast;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.http.StatusLine;
import org.holoeverywhere.LayoutInflater;
import org.holoeverywhere.app.Fragment;
import org.holoeverywhere.preference.PreferenceManager;
import org.holoeverywhere.preference.SharedPreferences;
import org.holoeverywhere.widget.LinearLayout;
import org.holoeverywhere.widget.ListView;
import org.holoeverywhere.widget.TextView;
import org.lol.reddit.R;
import org.lol.reddit.account.RedditAccount;
import org.lol.reddit.account.RedditAccountManager;
import org.lol.reddit.activities.BugReportActivity;
import org.lol.reddit.activities.SessionChangeListener;
import org.lol.reddit.adapters.PostListingAdapter;
import org.lol.reddit.cache.CacheManager;
import org.lol.reddit.cache.CacheRequest;
import org.lol.reddit.cache.RequestFailureType;
import org.lol.reddit.common.*;
import org.lol.reddit.io.RequestResponseHandler;
import org.lol.reddit.jsonwrap.JsonBufferedArray;
import org.lol.reddit.jsonwrap.JsonBufferedObject;
import org.lol.reddit.jsonwrap.JsonValue;
import org.lol.reddit.listingcontrollers.PostListingController;
import org.lol.reddit.reddit.RedditSubredditManager;
import org.lol.reddit.reddit.api.RedditSubredditSubscriptionManager;
import org.lol.reddit.reddit.api.SubredditRequestFailure;
import org.lol.reddit.reddit.prepared.RedditChangeDataManager;
import org.lol.reddit.reddit.prepared.RedditPreparedPost;
import org.lol.reddit.reddit.things.RedditPost;
import org.lol.reddit.reddit.things.RedditSubreddit;
import org.lol.reddit.reddit.things.RedditThing;
import org.lol.reddit.reddit.url.PostListingURL;
import org.lol.reddit.reddit.url.RedditURLParser;
import org.lol.reddit.reddit.url.SubredditPostListURL;
import org.lol.reddit.views.CachedHeaderView;
import org.lol.reddit.views.PostListingHeader;
import org.lol.reddit.views.RedditPostView;
import org.lol.reddit.views.list.ListOverlayView;
import org.lol.reddit.views.liststatus.ErrorView;
import org.lol.reddit.views.liststatus.LoadingView;

import java.text.NumberFormat;
import java.util.HashSet;
import java.util.Locale;
import java.util.UUID;

public class PostListingFragment extends Fragment
        implements RedditPostView.PostSelectionListener, AbsListView.OnScrollListener {

    private PostListingURL postListingURL;

    private RedditSubreddit subreddit;

    private UUID session = null;
    private CacheRequest.DownloadType downloadType;
    private PrefsUtility.PostCount downloadPostCount;
    private PostListingAdapter adapter;
    private ListView lv;
    private TextView loadMoreView;

    private SharedPreferences sharedPrefs;

    private LinearLayout fragmentHeader, listHeader, listHeaderNotifications, listFooterNotifications;

    private String after = null, lastAfter = null;
    private CacheRequest request;
    private boolean readyToDownloadMore = false;
    private long timestamp;

    private LoadingView loadingView;

    private int postCount = 0;
    private int postTotalCount = 0;
    private int postRefreshCount = 0;

    private static final int NOTIF_DOWNLOAD_NECESSARY = 1, NOTIF_DOWNLOAD_START = 2, NOTIF_STARTING = 3,
            NOTIF_AGE = 4, NOTIF_ERROR = 5, NOTIF_PROGRESS = 6, NOTIF_DOWNLOAD_DONE = 7, NOTIF_ERROR_FOOTER = 8;

    private final Handler notificationHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(final Message msg) {

            final Context context = getSupportActivity();

            // TODO check if attached? if not, queue, and send on "resume"
            switch (msg.what) {
            case NOTIF_DOWNLOAD_NECESSARY:
                loadingView = new LoadingView(context, R.string.download_waiting, true, true);
                listFooterNotifications.addView(loadingView);
                adapter.notifyDataSetChanged();
                break;

            case NOTIF_DOWNLOAD_START:
                loadingView.setIndeterminate(R.string.download_connecting);
                break;

            case NOTIF_STARTING:
                if (loadingView != null)
                    loadingView.setIndeterminate(R.string.download_downloadstarting);
                break;

            case NOTIF_AGE: {
                final CachedHeaderView cacheNotif = new CachedHeaderView(context,
                        context.getString(R.string.listing_cached) + " "
                                + RRTime.formatDateTime((Long) msg.obj, context),
                        null);

                listHeaderNotifications.addView(cacheNotif);
                listHeaderNotifications.requestLayout();
                adapter.notifyDataSetChanged();
                break;
            }

            case NOTIF_ERROR: {
                if (loadingView != null)
                    loadingView.setDone(R.string.download_failed);
                final RRError error = (RRError) msg.obj;
                fragmentHeader.addView(new ErrorView(getSupportActivity(), error));
                break;
            }

            case NOTIF_PROGRESS:
                if (loadingView != null)
                    loadingView.setProgress(R.string.download_loading, (Float) msg.obj);
                break;

            case NOTIF_DOWNLOAD_DONE:
                if (loadingView != null)
                    loadingView.setDone(R.string.download_done);
                break;

            case NOTIF_ERROR_FOOTER: {
                if (loadingView != null)
                    loadingView.setDone(R.string.download_failed);
                final RRError error = (RRError) msg.obj;
                listFooterNotifications.addView(new ErrorView(getSupportActivity(), error));
                adapter.notifyDataSetChanged();
                break;
            }
            }
        }
    };

    public static PostListingFragment newInstance(final Uri url, final UUID session,
            final CacheRequest.DownloadType downloadType) {

        final PostListingFragment f = new PostListingFragment();

        final Bundle bundle = new Bundle(4);

        bundle.putString("url", url.toString());
        if (session != null)
            bundle.putString("session", session.toString());
        bundle.putString("downloadType", downloadType.name());

        f.setArguments(bundle);

        return f;
    }

    @Override
    public void onCreate(final Bundle savedInstanceState) {
        // TODO load position/etc?
        super.onCreate(savedInstanceState);

        final Bundle arguments = getArguments();

        final Uri url = Uri.parse(arguments.getString("url"));

        try {
            postListingURL = (PostListingURL) RedditURLParser.parseProbablePostListing(url);
        } catch (ClassCastException e) {
            Toast.makeText(getSupportActivity(), "Invalid post listing URL.", Toast.LENGTH_LONG);
            return;
        }

        if (arguments.containsKey("session")) {
            session = UUID.fromString(arguments.getString("session"));
        }

        downloadType = CacheRequest.DownloadType.valueOf(arguments.getString("downloadType"));
    }

    private LinearLayout createVerticalLinearLayout(Context context) {
        final LinearLayout result = new LinearLayout(context);
        result.setOrientation(LinearLayout.VERTICAL);
        return result;
    }

    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
            final Bundle savedInstanceState) {

        super.onCreateView(inflater, container, savedInstanceState);
        final Context context = container.getContext();
        sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);

        final LinearLayout outer = new LinearLayout(context) {
            @Override
            protected void onAttachedToWindow() {
                super.onAttachedToWindow();
                getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
            }
        };

        outer.setOrientation(android.widget.LinearLayout.VERTICAL);

        fragmentHeader = createVerticalLinearLayout(context);

        // TODO output failed URL
        if (postListingURL == null) {
            fragmentHeader.addView(new ErrorView(getSupportActivity(),
                    new RRError("Invalid post listing URL", "Could not navigate to that URL.")));
            return outer;
        }

        listHeader = createVerticalLinearLayout(context);
        listHeaderNotifications = createVerticalLinearLayout(context);
        listFooterNotifications = createVerticalLinearLayout(context);

        downloadPostCount = PrefsUtility.pref_behaviour_post_count(context, sharedPrefs);
        restackRefreshCount();
        loadMoreView = (TextView) LayoutInflater.from(context).inflate(R.layout.load_more_posts, null);
        loadMoreView.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                listFooterNotifications.removeView(loadMoreView);
                restackRefreshCount();
                onLoadMoreItemsCheck();
            }
        });

        listHeader.addView(listHeaderNotifications);

        lv = (ListView) inflater.inflate(R.layout.reddit_post_list);
        lv.setOnScrollListener(this);
        lv.addHeaderView(listHeader);
        lv.addFooterView(listFooterNotifications, null, true);

        lv.setPersistentDrawingCache(ViewGroup.PERSISTENT_ALL_CACHES);
        lv.setAlwaysDrawnWithCacheEnabled(true);

        adapter = new PostListingAdapter(lv, this);
        lv.setAdapter(adapter);

        final ListOverlayView lov = new ListOverlayView(context, lv);

        outer.addView(fragmentHeader);
        outer.addView(lov);

        lv.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;

        request = new PostListingRequest(postListingURL.generateJsonUri(),
                RedditAccountManager.getInstance(context).getDefaultAccount(), session, downloadType, true);

        CacheManager.getInstance(context).makeRequest(request);

        switch (postListingURL.pathType()) {

        case UserPostListingURL:
        case SearchPostListingURL:
            setHeader(postListingURL.humanReadableName(getSupportActivity(), true),
                    postListingURL.humanReadableUrl());
            break;

        case SubredditPostListingURL:

            SubredditPostListURL subredditPostListURL = (SubredditPostListURL) postListingURL;

            switch (subredditPostListURL.type) {

            case FRONTPAGE:
            case ALL:
            case SUBREDDIT_COMBINATION:
            case ALL_SUBTRACTION:
                setHeader(postListingURL.humanReadableName(getSupportActivity(), true),
                        postListingURL.humanReadableUrl());
                break;

            case SUBREDDIT: {

                // Request the subreddit data

                final RequestResponseHandler<RedditSubreddit, SubredditRequestFailure> subredditHandler = new RequestResponseHandler<RedditSubreddit, SubredditRequestFailure>() {
                    @Override
                    public void onRequestFailed(SubredditRequestFailure failureReason) {
                        // Ignore
                    }

                    @Override
                    public void onRequestSuccess(RedditSubreddit result, long timeCached) {
                        subreddit = result;
                        onSubredditReceived();
                    }
                };

                try {
                    RedditSubredditManager
                            .getInstance(getSupportActivity(),
                                    RedditAccountManager.getInstance(getSupportActivity()).getDefaultAccount())
                            .getSubreddit(RedditSubreddit.getCanonicalName(subredditPostListURL.subreddit),
                                    TimestampBound.NONE, subredditHandler, null);
                } catch (RedditSubreddit.InvalidSubredditNameException e) {
                    throw new RuntimeException(e);
                }
                break;
            }
            }

            break;
        }

        return outer;
    }

    @Override
    public void onSaveInstanceState(final Bundle outState) {
        // TODO save menu position
    }

    public void cancel() {
        if (request != null)
            request.cancel();
    }

    public void restackRefreshCount() {
        if (postRefreshCount == 0) {
            switch (downloadPostCount) {
            case R25:
                postRefreshCount = 25;
                break;
            case R50:
                postRefreshCount = 50;
                break;
            case R100:
                postRefreshCount = 100;
                break;
            }
        }
    }

    private void onSubredditReceived() {

        final String subtitle;

        if (postListingURL.getOrder() == null || postListingURL.getOrder() == PostListingController.Sort.HOT) {
            if (subreddit.subscribers == null) {
                subtitle = getString(R.string.header_subscriber_count_unknown);
            } else {
                subtitle = NumberFormat.getNumberInstance(Locale.getDefault()).format(subreddit.subscribers) + " "
                        + getString(R.string.header_subscriber_count);
            }

        } else {
            subtitle = postListingURL.humanReadableUrl();
        }

        getSupportActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                setHeader(StringEscapeUtils.unescapeHtml4(subreddit.title), subtitle);
                getSupportActivity().invalidateOptionsMenu();
            }
        });

    }

    private void setHeader(final String title, final String subtitle) {
        getSupportActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                final PostListingHeader postListingHeader = new PostListingHeader(getSupportActivity(), title,
                        subtitle);
                listHeader.addView(postListingHeader, 0);
                adapter.notifyDataSetChanged();
            }
        });
    }

    public void onPostSelected(final RedditPreparedPost post) {
        ((RedditPostView.PostSelectionListener) getSupportActivity()).onPostSelected(post);

        new Thread() {
            public void run() {
                post.markAsRead(getSupportActivity());
            }
        }.start();
    }

    public void onPostCommentsSelected(final RedditPreparedPost post) {

        ((RedditPostView.PostSelectionListener) getSupportActivity()).onPostCommentsSelected(post);

        new Thread() {
            public void run() {
                post.markAsRead(getSupportActivity());
            }
        }.start();
    }

    public void onScrollStateChanged(AbsListView view, int scrollState) {
    }

    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        onLoadMoreItemsCheck();
    }

    private synchronized void onLoadMoreItemsCheck() {

        if (readyToDownloadMore && after != null && !after.equals(lastAfter) && adapter.getDownloadedCount() > 0
                && adapter.getDownloadedCount() - lv.getLastVisiblePosition() < 20
                && (downloadPostCount == PrefsUtility.PostCount.ALL || postRefreshCount > 0)) {

            lastAfter = after;
            readyToDownloadMore = false;

            final Uri newUri = postListingURL.after(after).generateJsonUri();

            // TODO customise (currently 3 hrs)
            CacheRequest.DownloadType type = (RRTime.since(timestamp) < 3 * 60 * 60 * 1000)
                    ? CacheRequest.DownloadType.IF_NECESSARY
                    : CacheRequest.DownloadType.NEVER;

            request = new PostListingRequest(newUri,
                    RedditAccountManager.getInstance(getSupportActivity()).getDefaultAccount(), session, type,
                    false);
            CacheManager.getInstance(getSupportActivity()).makeRequest(request);
        } else if ((!(downloadPostCount == PrefsUtility.PostCount.ALL) && postRefreshCount == 0)
                && loadMoreView.getParent() == null) {
            listFooterNotifications.addView(loadMoreView);
        }
    }

    public void onSubscribe() {

        if (postListingURL.pathType() != RedditURLParser.PathType.SubredditPostListingURL)
            return;

        try {
            RedditSubredditSubscriptionManager
                    .getSingleton(getSupportActivity(),
                            RedditAccountManager.getInstance(getSupportActivity()).getDefaultAccount())
                    .subscribe(RedditSubreddit.getCanonicalName(postListingURL.asSubredditPostListURL().subreddit),
                            getSupportActivity());
        } catch (RedditSubreddit.InvalidSubredditNameException e) {
            throw new RuntimeException(e);
        }
    }

    public void onUnsubscribe() {

        if (subreddit == null)
            return;

        try {
            RedditSubredditSubscriptionManager
                    .getSingleton(getSupportActivity(),
                            RedditAccountManager.getInstance(getSupportActivity()).getDefaultAccount())
                    .unsubscribe(subreddit.getCanonicalName(), getSupportActivity());
        } catch (RedditSubreddit.InvalidSubredditNameException e) {
            throw new RuntimeException(e);
        }
    }

    public RedditSubreddit getSubreddit() {
        return subreddit;
    }

    private class PostListingRequest extends CacheRequest {

        private final boolean firstDownload;

        protected PostListingRequest(Uri url, RedditAccount user, UUID requestSession, DownloadType downloadType,
                boolean firstDownload) {
            super(General.uriFromString(url.toString()), user, requestSession, Constants.Priority.API_POST_LIST, 0,
                    downloadType, Constants.FileType.POST_LIST, true, true, false, getSupportActivity());
            this.firstDownload = firstDownload;
        }

        @Override
        protected void onDownloadNecessary() {
            notificationHandler.sendMessage(General.handlerMessage(NOTIF_DOWNLOAD_NECESSARY, null));
        }

        @Override
        protected void onDownloadStarted() {
            notificationHandler.sendMessage(General.handlerMessage(NOTIF_DOWNLOAD_START, null));
        }

        @Override
        protected void onCallbackException(final Throwable t) {
            BugReportActivity.handleGlobalError(context, t);
        }

        @Override
        protected void onFailure(final RequestFailureType type, final Throwable t, final StatusLine status,
                final String readableMessage) {
            final RRError error = General.getGeneralErrorForFailure(context, type, t, status, url.toString());
            notificationHandler.sendMessage(General.handlerMessage(NOTIF_ERROR, error));
        }

        @Override
        protected void onProgress(final long bytesRead, final long totalBytes) {
        }

        @Override
        protected void onSuccess(final CacheManager.ReadableCacheFile cacheFile, final long timestamp,
                final UUID session, final boolean fromCache, final String mimetype) {
        }

        @Override
        public void onJsonParseStarted(final JsonValue value, final long timestamp, final UUID session,
                final boolean fromCache) {

            notificationHandler.sendMessage(General.handlerMessage(NOTIF_STARTING, null));

            postTotalCount += 25; // TODO this can vary with the user's reddit settings

            // TODO pref (currently 10 mins)
            if (firstDownload && fromCache && RRTime.since(timestamp) > 10 * 60 * 1000) {
                notificationHandler.sendMessage(General.handlerMessage(NOTIF_AGE, timestamp));
            } // TODO resuming a copy

            if (firstDownload) {
                ((SessionChangeListener) getSupportActivity()).onSessionChanged(session,
                        SessionChangeListener.SessionChangeType.POSTS, timestamp);
                PostListingFragment.this.session = session;
                PostListingFragment.this.timestamp = timestamp;
            }

            // TODO {"error": 403} is received for unauthorized subreddits

            try {

                final Context context = getSupportActivity();
                final JsonBufferedObject thing = value.asObject();
                final JsonBufferedObject listing = thing.getObject("data");
                final JsonBufferedArray posts = listing.getArray("children");

                final boolean isNsfwAllowed = PrefsUtility.pref_behaviour_nsfw(context, sharedPrefs);
                final boolean isConnectionWifi = General.isConnectionWifi(context);

                final PrefsUtility.AppearanceThumbnailsShow thumbnailsPref = PrefsUtility
                        .appearance_thumbnails_show(context, sharedPrefs);
                final boolean downloadThumbnails = thumbnailsPref == PrefsUtility.AppearanceThumbnailsShow.ALWAYS
                        || (thumbnailsPref == PrefsUtility.AppearanceThumbnailsShow.WIFIONLY && isConnectionWifi);

                final boolean showNsfwThumbnails = PrefsUtility.appearance_thumbnails_nsfw_show(context,
                        sharedPrefs);

                final PrefsUtility.CachePrecacheImages imagePrecachePref = PrefsUtility
                        .cache_precache_images(context, sharedPrefs);
                final boolean precacheImages = (imagePrecachePref == PrefsUtility.CachePrecacheImages.ALWAYS
                        || (imagePrecachePref == PrefsUtility.CachePrecacheImages.WIFIONLY && isConnectionWifi))
                        && !General.isCacheDiskFull(context);

                final CacheManager cm = CacheManager.getInstance(context);

                // TODO rewrite change data manager
                final HashSet<String> needsChanging = RedditChangeDataManager.getInstance(context)
                        .getChangedForParent("posts", user);

                final boolean showSubredditName = !(postListingURL != null
                        && postListingURL.pathType() == RedditURLParser.PathType.SubredditPostListingURL
                        && postListingURL.asSubredditPostListURL().type == SubredditPostListURL.Type.SUBREDDIT);

                for (final JsonValue postThingValue : posts) {

                    final RedditThing postThing = postThingValue.asObject(RedditThing.class);

                    if (!postThing.getKind().equals(RedditThing.Kind.POST))
                        continue;

                    final RedditPost post = postThing.asPost();

                    after = post.name;

                    if (!post.over_18 || isNsfwAllowed) {

                        final boolean downloadThisThumbnail = downloadThumbnails
                                && (!post.over_18 || showNsfwThumbnails);

                        final RedditPreparedPost preparedPost = new RedditPreparedPost(context, cm, postCount, post,
                                timestamp, showSubredditName, needsChanging.contains(post.name),
                                downloadThisThumbnail, precacheImages, user, false);
                        adapter.onPostDownloaded(preparedPost);
                    }

                    postCount++;
                    postRefreshCount--;
                    // TODO make specific to this download? don't keep global post count
                    notificationHandler.sendMessage(
                            General.handlerMessage(NOTIF_PROGRESS, (float) postCount / (float) postTotalCount));
                }

                notificationHandler.sendMessage(General.handlerMessage(NOTIF_DOWNLOAD_DONE, null));

                request = null;
                readyToDownloadMore = true;
                onLoadMoreItemsCheck();

            } catch (Throwable t) {
                notifyFailure(RequestFailureType.PARSE, t, null, "Parse failure");
            }
        }
    }
}