package com.airlocksoftware.hackernews.fragment;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import com.airlocksoftware.hackernews.R;
import com.airlocksoftware.hackernews.activity.ReplyActivity;
import com.airlocksoftware.hackernews.activity.UserActivity;
import com.airlocksoftware.hackernews.adapter.CommentsAdapter;
import com.airlocksoftware.hackernews.data.UserPrefs;
import com.airlocksoftware.hackernews.interfaces.SharePopupInterface;
import com.airlocksoftware.hackernews.interfaces.TabletLayout;
import com.airlocksoftware.hackernews.loader.CommentsLoader;
import com.airlocksoftware.hackernews.model.Comment;
import com.airlocksoftware.hackernews.model.Request;
import com.airlocksoftware.hackernews.model.Result;
import com.airlocksoftware.hackernews.model.Story;
import com.airlocksoftware.hackernews.parser.CommentsParser.CommentsResponse;
import com.airlocksoftware.hackernews.view.SharePopup;
import com.airlocksoftware.holo.actionbar.ActionBarButton;
import com.airlocksoftware.holo.actionbar.ActionBarButton.Priority;
import com.airlocksoftware.holo.actionbar.interfaces.ActionBarClient;
import com.airlocksoftware.holo.actionbar.interfaces.ActionBarController;
import com.airlocksoftware.holo.image.IconView;
import com.airlocksoftware.holo.utils.Utils;
import com.airlocksoftware.holo.utils.ViewUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.util.List;
/**
* Displays a page of comments with the parent story as a header. Uses CommentsLoader to get data from the cache or the
* web.
**/
public class CommentsFragment extends Fragment implements ActionBarClient, LoaderCallbacks<CommentsResponse> {
/**
* The story whose comments we are loading & displaying. Initialized to a new, empty Story to avoid null check on the
* object, but it's fields should always be null checked.
**/
private Story mStory = new Story();
{
mStory.storyId = NO_STORY_ID;
}
private Request mRequest = Request.NEW;
private Result mLastResult = Result.EMPTY;
private boolean mIsLoading = false;
private boolean mOpenInBrowser = false;
private boolean mIsPaused = false;
// Interfaces to the Activity
private TabletLayout mTabletLayout;
private CommentsFragment.Callbacks mCallbacks;
// Views & Adapters
private ListView mList;
private CommentsAdapter mAdapter;
/** The backing array of comments stored in onSaveInstanceState and restored in onActivityCreated **/
private List<Comment> mTempComments;
private View mHeaderView, mError, mEmpty, mLoading;
private TextView mHeaderTitle, mHeaderUsername, mHeaderPoints, mHeaderSelfText;
private IconView mUserIcon, mShareIcon, mUpvoteIcon;
// mReplyIcon; disabled because the way replies work has changed
private View mUpvoteButton, mSelfTextContainer;
private ActionBarButton mBrowserButton, mRefreshButton;
private SharePopup mShare;
// Listeners
private View.OnClickListener mVoteListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mStory.upvote(getActivity())) bindStoryHeader();
}
};
private View.OnClickListener mRefreshListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
mRequest = Request.REFRESH;
getLoaderManager().restartLoader(0, null, CommentsFragment.this);
}
};
private View.OnClickListener mBrowserListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW).setData(Uri.parse(mStory.url));
getActivity().startActivity(intent);
}
};
private View.OnClickListener mReplyListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
ReplyActivity.startStoryReplyActivity(getActivity(), mStory);
}
};
private View.OnClickListener mUserListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
UserActivity.startUserActivity(getActivity(), mStory.username);
}
};
private View.OnClickListener mShareListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
mShare.shareStory(mStory);
}
};
// Constants
public static final String STORY = CommentsFragment.class.getSimpleName() + ".story";
public static final String LIST_STATE = CommentsFragment.class.getSimpleName() + ".listState";
public static final String COMMENTS = CommentsFragment.class.getSimpleName() + ".comments";
@SuppressWarnings("unused")
private static final String TAG = CommentsFragment.class.getSimpleName();
/** Time (in milliseconds) before a set of comments is considered expired and should be reloaded. **/
private static final long CACHE_EXPIRATION = 1000 * 60 * 20; // 20 minutes
public static final int NO_STORY_ID = -1;
private int TEXT_COLOR_PRIMARY;
private int HIGHLIGHT_COLOR;
public CommentsFragment() {
// default empty constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (container == null) return null;
View view = inflater.inflate(R.layout.frg_comments, container, false);
return view;
}
@SuppressWarnings("unchecked")
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Activity activity = getActivity();
// cache colors
Resources res = activity.getResources();
HIGHLIGHT_COLOR = res.getColor(R.color.bright_accent);
TEXT_COLOR_PRIMARY = res.getColor(Utils.getThemedResourceId(activity, R.attr.textColorPrimary));
// get the Callbacks from parent Activity
if (mCallbacks == null || mTabletLayout == null) retrieveCallbacks(activity);
// return early if fragment isn't in layout
if (!mCallbacks.commentsFragmentIsInLayout()) {
return;
}
mOpenInBrowser = new UserPrefs(activity).getOpenInBrowser();
if (savedInstanceState != null) {
setStory((Story) savedInstanceState.getSerializable(STORY));
// restore the List<Comment> that backs the ListAdapter (can't use the automatic one from the
// loader.onLoadFinished because it breaks comment folding)
mTempComments = (List<Comment>) savedInstanceState.getSerializable(COMMENTS);
}
// start loading
getLoaderManager().initLoader(0, null, this);
// find all the views
findViews(savedInstanceState);
// get share interface
mShare = ((SharePopupInterface) activity).getSharePopup();
// setup adapter
if (mAdapter == null) {
mAdapter = new CommentsAdapter(activity, mList, mShare);
mList.setAdapter(mAdapter);
if (mTempComments != null) mAdapter.addAll(mTempComments);
}
ensureActionBarButtonsCreated(activity);
refreshContentVisibility();
refreshActionBarButtonVisibility();
}
private void findViews(Bundle savedInstanceState) {
// make sure to findViewById from container view
View container = getActivity().findViewById(R.id.cnt_comments);
findLoadingAndErrorViews(container);
setupHeader();
findHeaderViews();
// get list
mList = (ListView) container.findViewById(android.R.id.list);
mList.addHeaderView(mHeaderView, null, false);
}
private void findHeaderViews() {
mHeaderTitle = (TextView) mHeaderView.findViewById(R.id.txt_title);
mHeaderUsername = (TextView) mHeaderView.findViewById(R.id.txt_username);
mHeaderSelfText = (TextView) mHeaderView.findViewById(R.id.txt_self);
mHeaderPoints = (TextView) mHeaderView.findViewById(R.id.txt_points);
mUserIcon = (IconView) mHeaderView.findViewById(R.id.icv_user);
mShareIcon = (IconView) mHeaderView.findViewById(R.id.icv_share);
mUpvoteIcon = (IconView) mHeaderView.findViewById(R.id.icv_upvote);
// mReplyIcon = (IconView) mHeaderView.findViewById(R.id.icv_reply);
mUserIcon.setOnClickListener(mUserListener);
mShareIcon.setOnClickListener(mShareListener);
// mReplyIcon.setOnClickListener(mReplyListener);
mUpvoteButton = mHeaderView.findViewById(R.id.btn_upvote);
mSelfTextContainer = mHeaderView.findViewById(R.id.cnt_txt_self);
}
private void setupHeader() {
mHeaderView = LayoutInflater.from(getActivity())
.inflate(R.layout.vw_comment_header, null);
mHeaderView.setVisibility(View.GONE);
}
private void findLoadingAndErrorViews(View container) {
mLoading = container.findViewById(R.id.cnt_loading);
mEmpty = container.findViewById(R.id.cnt_emtpy);
mError = container.findViewById(R.id.cnt_error);
mError.findViewById(R.id.btn_error)
.setOnClickListener(mRefreshListener);
ViewUtils.fixBackgroundRepeat(mError);
ViewUtils.fixBackgroundRepeat(mEmpty);
ViewUtils.fixBackgroundRepeat(mLoading);
}
@Override
public void onResume() {
super.onResume();
// set mTempComments to null here because on orientation change onLoadFinished is called (twice) from
// Fragment.performStart(). onResume is called after that so we clear mTempComments.
mTempComments = null;
}
@Override
public void onPause() {
super.onPause();
mIsPaused = true;
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putSerializable(STORY, mStory);
if (mAdapter != null) outState.putSerializable(COMMENTS, (Serializable) mAdapter.getArray());
super.onSaveInstanceState(outState);
}
// Loader Callbacks
@Override
public Loader<CommentsResponse> onCreateLoader(int id, Bundle args) {
mIsLoading = true;
refreshContentVisibility();
return new CommentsLoader(getActivity(), mRequest, mStory.storyId);
}
@Override
public void onLoadFinished(Loader<CommentsResponse> loader, CommentsResponse response) {
if (mIsPaused) {
// this loading came after onPause & onResume were called, but all data is already set so we ignore it
mIsPaused = false;
return;
}
// CommentsFragment.onLoadFinished exits early if this fragment isn't part of the layout anymore.
if (!mCallbacks.commentsFragmentIsInLayout()) {
return;
}
Story story = response.story;
mIsLoading = false;
mLastResult = response.result;
if (response.result == Result.SUCCESS) {
if (mTempComments == null) {
// setup adapter & list
mAdapter.clear();
mAdapter.addAll(response.comments);
// if it was a new request, we should scroll to the top of the page
if (mRequest == Request.NEW) {
mList.setSelection(0);
}
} else {
// Comments have already been restored from List<Comment> in onActivityCreated() and onCreateView()
// this fixes the bug where Comments that have been folded would get duplicated
// onLoadFinished gets called twice on orientation change (seems like a bug in Fragment.performStart())
}
// check for cache expiration
if (System.currentTimeMillis() - response.timestamp.time > CACHE_EXPIRATION) {
// still show stuff, but restart loading
mRequest = Request.REFRESH;
getLoaderManager().restartLoader(0, null, this);
}
// if we're coming from ThreadsFragment or SearchActivity, we don't know the URL until now. Notify Callbacks,
if (StringUtils.isNotBlank(story.url) && (mStory.url == null || !mStory.url.equals(story.url))) {
mCallbacks.receivedStory(mStory);
}
// setup story header
setStory(response.story);
bindStoryHeader();
}
refreshContentVisibility();
}
@Override
public void onLoaderReset(Loader<CommentsResponse> loader) {
// no implementation necessary
}
@Override
public void setupActionBar(Context context, ActionBarController controller) {
ensureActionBarButtonsCreated(context);
controller.addButton(mBrowserButton);
controller.addButton(mRefreshButton);
}
@Override
public void cleanupActionBar(Context context, ActionBarController controller) {
controller.removeButton(mRefreshButton);
controller.removeButton(mBrowserButton);
}
/**
* Called to set the inital story. Either this method or setStoryId should be called immediately after creating the
* fragment.
**/
public void setStory(Story story) {
mStory = story;
refreshActionBarButtonVisibility();
if (mAdapter != null) mAdapter.setParentStory(mStory);
}
/** Used by MainActivity to set the active story on StoryFragment. **/
public Story getStory() {
return mStory;
}
/**
* Called to set the inital story. Either this method or setStoryId should be called immediately after creating the
* fragment.
**/
public void setStoryId(long storyId) {
mStory.storyId = storyId;
}
/** Called when this Fragment should display a new Story in a Tablet layout. **/
public void loadNewStory(Story story) {
setStory(story);
mRequest = Request.NEW;
mAdapter.clear();
getLoaderManager().restartLoader(0, null, CommentsFragment.this);
}
private void ensureActionBarButtonsCreated(Context context) {
// create ActionBarButtons
if (mBrowserButton == null) {
mBrowserButton = new ActionBarButton(context);
mBrowserButton.text(context.getString(R.string.open_in_browser))
.icon(R.drawable.ic_action_web)
.priority(Priority.HIGH)
.onClick(mBrowserListener);
}
// setup refresh button
if (mRefreshButton == null) {
mRefreshButton = new ActionBarButton(context);
mRefreshButton.text(context.getString(R.string.refresh_comments))
.icon(R.drawable.ic_action_refresh)
.priority(Priority.HIGH)
.onClick(mRefreshListener);
}
}
/** Bind data from mStory to views in mHeader **/
private void bindStoryHeader() {
mHeaderTitle.setText(mStory.title);
mHeaderUsername.setText(mStory.username + " \u2022 " + mStory.ago);
mHeaderPoints.setText(Integer.toString(mStory.numPoints));
// setup upvote button
if (mStory.isUpvoted) {
// mUpvoteButton.setClickable(false);
mUpvoteButton.setOnClickListener(null);
mUpvoteIcon.iconColor(HIGHLIGHT_COLOR);
} else {
// mUpvoteButton.setClickable(true);
mUpvoteButton.setOnClickListener(mVoteListener);
mUpvoteIcon.iconColor(TEXT_COLOR_PRIMARY);
}
// setup self text
boolean hasSelfText = StringUtils.isNotBlank(mStory.selfText);
if (hasSelfText) mHeaderSelfText.setText(Html.fromHtml(mStory.selfText));
mSelfTextContainer.setVisibility(ViewUtils.boolToVis(hasSelfText));
// setup reply button
// mReplyIcon.setVisibility(ViewUtils.boolToVis(!mStory.isArchived)); // disabled because reply is broken
// mReplyIcon.setVisibility(ViewUtils.boolToVis(!mStory.isArchived));
mHeaderView.findViewById(R.id.icv_reply).setVisibility(View.GONE);
mHeaderView.setVisibility(View.VISIBLE);
}
/**
* Ensure that the Activity this fragment is attached to implements the CommentsFragment.Callbacks & TabletLayout
* interface,
* and store it in mCallbacks.
**/
private void retrieveCallbacks(Activity activity) {
if (activity instanceof CommentsFragment.Callbacks) {
mCallbacks = (CommentsFragment.Callbacks) activity;
} else {
throw new RuntimeException("The parent activity of a CommentFragment must implement CommentsFragment.Callbacks");
}
if (activity instanceof TabletLayout) {
mTabletLayout = (TabletLayout) activity;
} else {
throw new RuntimeException("The parent activity of a CommentFragment must implement TabletLayout");
}
}
private void refreshContentVisibility() {
// can't refresh visibility until / unless all views are created
if (mAdapter == null || mList == null || mError == null || mLoading == null || mEmpty == null) return;
boolean isJobPost = Story.isYCombinatorJobPost(mStory);
boolean errorVis = mLastResult == Result.FAILURE && mAdapter.getCount() < 1 && !mIsLoading;
boolean emptyVis = (mLastResult == Result.EMPTY && mStory.storyId == NO_STORY_ID && !mIsLoading) || isJobPost;
boolean loadingVis = mIsLoading && mAdapter.getCount() < 1 && !errorVis && !emptyVis;
boolean listVis = !(emptyVis || errorVis || loadingVis || loadingVis);
mRefreshButton.icon(errorVis ? R.drawable.ic_action_refresh_error : R.drawable.ic_action_refresh);
mRefreshButton.showProgress(mIsLoading);
mRefreshButton.setVisibility(ViewUtils.boolToVis(!emptyVis));
mError.setVisibility(ViewUtils.boolToVis(errorVis));
mEmpty.setVisibility(ViewUtils.boolToVis(emptyVis));
mLoading.setVisibility(ViewUtils.boolToVis(loadingVis));
mList.setVisibility(ViewUtils.boolToVis(listVis));
}
private void refreshActionBarButtonVisibility() {
boolean showBrowser = mOpenInBrowser && StringUtils.isNotBlank(mStory.url);
if (mBrowserButton != null) mBrowserButton.setVisibility(ViewUtils.boolToVis(showBrowser));
}
public interface Callbacks {
/**
* Implemented by the parent Activity of this fragment. Used to notify the Activity if/when this fragment has
* received a new Story from CommentsLoader.
**/
public void receivedStory(Story story);
/**
* Allows the CommentsFragment to check if it is part of the layout in MainActivity after being recreated on
* orientation change.
* If it is not in layout, it's method's should return early.
**/
public boolean commentsFragmentIsInLayout();
}
}