package com.airlocksoftware.hackernews.fragment; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.airlocksoftware.hackernews.R; import com.airlocksoftware.hackernews.activity.MainActivity; import com.airlocksoftware.hackernews.adapter.StoryAdapter; import com.airlocksoftware.hackernews.data.AppData; import com.airlocksoftware.hackernews.interfaces.SharePopupInterface; import com.airlocksoftware.hackernews.interfaces.TabletLayout; import com.airlocksoftware.hackernews.loader.StoryLoader; import com.airlocksoftware.hackernews.model.*; import com.airlocksoftware.hackernews.parser.StoryParser.StoryResponse; 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 com.crashlytics.android.Crashlytics; import com.airlocksoftware.hackernews.R; public class StoryFragment extends Fragment implements ActionBarClient, LoaderManager.LoaderCallbacks<StoryResponse> { // State private StoryAdapter mAdapter; private Page mPage = Page.FRONT; private Request mRequest = Request.NEW; private Result mLastResult = Result.EMPTY; private boolean mIsLoading = false; private boolean mShouldRestoreListState; // Activity interfaces TabletLayout mTabletLayout = null; StoryFragment.Callbacks mCallbacks = null; // Views View mError, mLoading; private ListView mList; private ActionBarButton mRefreshButton; private View mMoreButton; private TextView mMoreButtonText; private IconView mMoreIcon; // Listeners private OnClickListener mMoreListener = new OnClickListener() { @Override public void onClick(View v) { mRequest = Request.MORE; // change more link text & icon mMoreButtonText.setText(getActivity().getString(R.string.loading_)); mMoreIcon.setVisibility(View.GONE); getLoaderManager().restartLoader(0, null, StoryFragment.this); } }; private OnClickListener mRefreshListener = new OnClickListener() { @Override public void onClick(View v) { mRequest = Request.REFRESH; getLoaderManager().restartLoader(0, null, StoryFragment.this); } }; // Constants public static final String PAGE = StoryFragment.class.getSimpleName() + ".page"; @SuppressWarnings("unused") private static final String TAG = StoryFragment.class.getSimpleName(); // Constructors public StoryFragment() { // default empty constructor } // Fragment Lifecycle @Override public void onAttach(Activity activity) { super.onAttach(activity); // get StoryFragment.Callbacks if (activity instanceof StoryFragment.Callbacks) { mCallbacks = (Callbacks) activity; } else { throw new RuntimeException( "The activity StoryFragment attached to is required to implement the StoryFragment.Callbacks interface."); } // get TabletLayoutInterface if (activity instanceof TabletLayout) { mTabletLayout = (TabletLayout) activity; } else { throw new RuntimeException( "The activity StoryFragment attached to is required to implement the TabletLayoutInterface interface."); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // start loading if this Fragment is part of the layout if (mCallbacks.storyFragmentIsInLayout()) { getLoaderManager().initLoader(0, null, this); } // Default Crashlytics custom keys Crashlytics.setString("StoryFragment :: mPage", mPage.toString()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.frg_stories, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (!mCallbacks.storyFragmentIsInLayout()) { // if this Fragment is no longer part of the layout, return early return; } findViews(); setupMoreButton(); setupAdapter(); // if in tablet layout, change background & cache color hint of the list to a different color if (mTabletLayout.isTabletLayout()) { setupTabletBackgroundColors(); } // restoring saved state if (savedInstanceState != null) { mPage = ((Page) savedInstanceState.getSerializable(PAGE)); mAdapter.onRestoreInstanceState(savedInstanceState); } Crashlytics.setString("StoryFragment :: mPage", mPage == null ? "" : mPage.toString()); mShouldRestoreListState = true; refreshContentVisibility(); } @Override public void onPause() { /* Save list state to shared prefs (since we kill the activity when we switch to the comments list) */ FragmentActivity fragActivity = getActivity(); // Make sure we're not going to nullPointerException if (fragActivity != null) { AppData actData = new AppData(fragActivity); if (mList != null) { actData.saveStoryListPosition(mList.getFirstVisiblePosition()); } else { Log.d(TAG, "mList is null! Story position NOT saved!"); } } super.onPause(); } @SuppressWarnings("deprecation") private void setupTabletBackgroundColors() { Resources res = getActivity().getResources(); // bg int tabletBgResId = Utils.getThemedResourceId(getActivity(), R.attr.bgStorylistTablet); Drawable tabletBg = res.getDrawable(tabletBgResId); ViewUtils.fixDrawableRepeat(tabletBg); mList.setBackgroundDrawable(tabletBg); mError.setBackgroundDrawable(tabletBg); mLoading.setBackgroundDrawable(tabletBg); // cache color hint int cacheColorHintResId = Utils.getThemedResourceId(getActivity(), R.attr.cacheColorHintStorylistTablet); mList.setCacheColorHint(res.getColor(cacheColorHintResId)); } private void setupAdapter() { SharePopup popup = ((SharePopupInterface) getActivity()).getSharePopup(); mAdapter = new StoryAdapter(getActivity(), mList, popup, mCallbacks, mTabletLayout); mList.setAdapter(mAdapter); } /** inflate & add the more button **/ private void setupMoreButton() { LayoutInflater inflater = getActivity().getLayoutInflater(); mMoreButton = inflater.inflate(R.layout.vw_more_link, mList, false); mMoreButton.setOnClickListener(mMoreListener); mMoreButton.setVisibility(View.GONE); mMoreButtonText = (TextView) mMoreButton.findViewById(R.id.txt_more); mMoreIcon = (IconView) mMoreButton.findViewById(R.id.icv_more); mList.addFooterView(mMoreButton, null, false); } private void findViews() { // have to make sure we're finding views inside of stories container (fixes bug where StoryFragment & // CommentsFragment would confused each other's views when they're part of the same activity) View container = getActivity().findViewById(R.id.cnt_stories); mLoading = container.findViewById(R.id.cnt_loading); mError = container.findViewById(R.id.cnt_error); mError.findViewById(R.id.btn_error) .setOnClickListener(mRefreshListener); ViewUtils.fixBackgroundRepeat(mError); ViewUtils.fixBackgroundRepeat(mLoading); mList = (ListView) container.findViewById(android.R.id.list); mList.setItemsCanFocus(true); } @Override public void onSaveInstanceState(Bundle outState) { /* Save to bundle */ if (!mCallbacks.storyFragmentIsInLayout()) return; outState.putSerializable(PAGE, mPage); outState = mAdapter.onSaveInstanceState(outState); super.onSaveInstanceState(outState); } // Loader interface @Override public Loader<StoryResponse> onCreateLoader(int id, Bundle args) { mIsLoading = true; refreshContentVisibility(); return new StoryLoader(getActivity(), mPage, mRequest); } @Override public void onLoadFinished(Loader<StoryResponse> loader, StoryResponse response) { // if this Fragment is no longer part of the layout, return early if (!mCallbacks.storyFragmentIsInLayout()) { return; } // Variable for showing Toasts Toast msg; // if NULL_RESPONSE, make a toast! Cheers! Drink responsibly; return early. if (response == null || response.isNull()) { msg = Toast.makeText(getActivity(), getActivity().getString(R.string.error_no_page), Toast.LENGTH_SHORT); msg.show(); return; } mIsLoading = false; mLastResult = response.result; switch (mLastResult) { case SUCCESS: // first page mAdapter.clear(); mAdapter.addAll(response.stories); break; case MORE: // new data from web mAdapter.addAll(response.stories); break; case FNID_EXPIRED: // the link was expired - refresh the page msg = Toast.makeText(getActivity(), getActivity().getString(R.string.link_expired), Toast.LENGTH_SHORT); msg.show(); // start loader with refresh request mRequest = Request.REFRESH; getLoaderManager().restartLoader(0, null, this); break; case FAILURE: // Show error message msg = Toast.makeText(getActivity(), getActivity().getString(R.string.problem_downloading_content), Toast.LENGTH_SHORT); msg.show(); break; default: break; } mAdapter.setActiveStory(mCallbacks.getActiveStory()); showMoreLink(); checkForExpiredCache(response); refreshContentVisibility(); if(mShouldRestoreListState) { mShouldRestoreListState = false; int listPosition = new AppData(getActivity()).getStoryListPosition(); mList.setSelectionFromTop(listPosition, 0); } } private void checkForExpiredCache(StoryResponse response) { if (response.timestamp != null && response.timestamp.time > Comment.JAN_1_2012 && System.currentTimeMillis() - response.timestamp.time > Comment.CACHE_EXPIRATION) { // still display cached values, but need to start refresh mRequest = Request.REFRESH; getLoaderManager().restartLoader(0, null, this); } } private void showMoreLink() { mMoreButtonText.setText(getActivity().getString(R.string.load_more)); mMoreIcon.setVisibility(View.VISIBLE); mMoreButton.setVisibility(View.VISIBLE); } @Override public void onLoaderReset(Loader<StoryResponse> loader) { // no implementation necessary } // ActionBar interface @Override public void setupActionBar(Context context, ActionBarController controller) { if (mRefreshButton == null) { mRefreshButton = new ActionBarButton(context); mRefreshButton.text(context.getString(R.string.refresh_stories)) .icon(R.drawable.ic_action_refresh) .priority(Priority.HIGH) .onClick(mRefreshListener); } controller.addButton(mRefreshButton); } @Override public void cleanupActionBar(Context context, ActionBarController controller) { controller.removeButton(mRefreshButton); } public void setPage(Page page) { mPage = page; Crashlytics.setString("StoryFragment :: mPage", mPage.toString()); } public StoryAdapter getStoryAdapter() { return mAdapter; } /** Determines which view (List, Error, or Loading) show be visible. **/ private void refreshContentVisibility() { // can't refresh visibility until / unless all views are created if (mAdapter == null || mList == null || mError == null || mLoading == null) return; boolean errorVis = mLastResult == Result.FAILURE && mAdapter.getCount() < 1 && !mIsLoading; boolean loadingVis = mIsLoading && mAdapter.getCount() < 1 && !errorVis; boolean listVis = !(errorVis || loadingVis); mRefreshButton.icon(errorVis ? R.drawable.ic_action_refresh_error : R.drawable.ic_action_refresh); mRefreshButton.showProgress(mIsLoading); mList.setVisibility(ViewUtils.boolToVis(listVis)); mError.setVisibility(ViewUtils.boolToVis(errorVis)); mLoading.setVisibility(ViewUtils.boolToVis(loadingVis)); } // Static creator method public static StoryFragment newInstance(Page page) { StoryFragment fragment = new StoryFragment(); fragment.setPage(page); return fragment; } // Callbacks /** To be implemented by any Activity containing this Fragment. Notifies when this fragment receives new data. **/ public interface Callbacks { /** * Called when the User has clicked on a Story in the ListView (called by StoryAdapter). **/ public void showCommentsForStory(Story story, MainActivity.CommentsTab initalTab); /** Called by StoryFragment.onLoadFinished to make sure this Fragment's StoryAdapter has the active story **/ public Story getActiveStory(); /** * Allows the StoryFragment 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 storyFragmentIsInLayout(); } }