package info.guardianproject.securereaderinterface.views; import info.guardianproject.securereader.SocialReader; import info.guardianproject.securereaderinterface.App; import info.guardianproject.securereaderinterface.MainActivity; import info.guardianproject.securereaderinterface.adapters.StoryListAdapter; import info.guardianproject.securereaderinterface.adapters.StoryListAdapter.OnHeaderCreatedListener; import info.guardianproject.securereaderinterface.adapters.StoryListAdapter.OnTagClickedListener; import info.guardianproject.securereaderinterface.models.FeedFilterType; import info.guardianproject.securereaderinterface.ui.UICallbackListener; import info.guardianproject.securereaderinterface.ui.UICallbacks; import info.guardianproject.securereaderinterface.ui.UICallbacks.OnCallbackListener; import info.guardianproject.securereaderinterface.uiutil.UIHelpers; import info.guardianproject.securereaderinterface.widgets.AppearingFrameLayout; import info.guardianproject.securereaderinterface.widgets.AppearingRelativeLayout; import info.guardianproject.securereaderinterface.widgets.HeightLimitedLinearLayout; import info.guardianproject.securereaderinterface.widgets.SyncableListView; import info.guardianproject.securereaderinterface.widgets.SyncableListView.OnPullDownListener; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import android.content.Context; import android.content.res.Configuration; import android.database.DataSetObserver; import android.graphics.Color; import android.text.style.ForegroundColorSpan; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.animation.AnimationUtils; import android.widget.AbsListView.OnScrollListener; import android.widget.AbsListView; import android.widget.FrameLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import info.guardianproject.securereaderinterface.R; import com.tinymission.rss.Feed; import com.tinymission.rss.Item; public class StoryListView extends FrameLayout implements OnTagClickedListener, OnPullDownListener, OnHeaderCreatedListener { public interface StoryListListener { /** * Called when list has been pulled down to resync. */ void onResync(); void onStoryClicked(ArrayList<Item> items, int indexOfStory, View storyView); void onHeaderCreated(View headerView, int resIdHeader); void onListViewUpdated(ListView newList); } private TextView mTvTagResults; private View mBtnCloseTagSearch; private SyncableListView mListStories; private HeightLimitedLinearLayout mListHeader; private OnCallbackListener mCallbackListener; private StoryListAdapter mAdapter; private String mCurrentSearchTag; private StoryListListener mListener; private boolean mIsLoading; private AppearingFrameLayout mFrameLoading; private AppearingRelativeLayout mFrameError; private View mIvLoading; private long mOldItemId = Integer.MIN_VALUE; private int mOldY; public StoryListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } public StoryListView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public StoryListView(Context context) { super(context); init(); } private void init() { LayoutInflater inflater = LayoutInflater.from(getContext()); View rootView = inflater.inflate(R.layout.story_list, this, true); mTvTagResults = (TextView) rootView.findViewById(R.id.tvTagResults); mBtnCloseTagSearch = rootView.findViewById(R.id.btnCloseTagSearch); mTvTagResults.setVisibility(View.GONE); mFrameLoading = (AppearingFrameLayout) rootView.findViewById(R.id.frameLoading); mFrameError = (AppearingRelativeLayout) rootView.findViewById(R.id.frameError); mFrameError.findViewById(R.id.ivErrorClose).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { hideError(); } }); mIvLoading = rootView.findViewById(R.id.ivLoading); mBtnCloseTagSearch.setVisibility(View.GONE); mBtnCloseTagSearch.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Clear tag filter UICallbacks.setTagFilter(null, null); } }); mListStories = (SyncableListView) rootView.findViewById(R.id.lvStories); if (mAdapter == null) createOrUpdateAdapter(getContext(), null, 0); mListStories.setAdapter(mAdapter); mListHeader = (HeightLimitedLinearLayout) rootView.findViewById(R.id.storyListHeader); mListHeader.setVisibility(View.INVISIBLE); mFrameLoading.setVisibility(View.GONE); mFrameError.setVisibility(View.GONE); hideHeaderControls(true); setIsLoading(mIsLoading); mListStories.setPullDownListener(this); mListStories.setOnScrollListener(new AbsListView.OnScrollListener() { private boolean isFlinging = false; @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { isFlinging = true; mAdapter.setDeferMediaLoading(true); } else { mAdapter.setDeferMediaLoading(false); if (isFlinging) { isFlinging = false; mAdapter.notifyDataSetChanged(); } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); searchByTag(null); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); addCallbackListener(); } @Override protected void onDetachedFromWindow() { removeCallbackListener(); super.onDetachedFromWindow(); } public void setListener(StoryListListener listener) { mListener = listener; if (mAdapter != null) mAdapter.setListener(mListener); } public ListView getListView() { return mListStories; } private void addCallbackListener() { mCallbackListener = new UICallbackListener() { @Override public void onFeedSelect(FeedFilterType type, long feedId, Object source) { if (type != FeedFilterType.SINGLE_FEED) { mAdapter.setShowTags(false); mListStories.invalidateViews(); } else { if (App.UI_ENABLE_TAGS) mAdapter.setShowTags(true); mListStories.invalidateViews(); } } @Override public void onTagSelect(String tag) { searchByTag(tag); } }; UICallbacks.getInstance().addListener(mCallbackListener); } private void removeCallbackListener() { if (mCallbackListener != null) UICallbacks.getInstance().removeListener(mCallbackListener); mCallbackListener = null; } private void searchByTag(String tag) { mCurrentSearchTag = tag; if (mCurrentSearchTag == null) { mTvTagResults.setVisibility(View.GONE); mBtnCloseTagSearch.setVisibility(View.GONE); } else { mTvTagResults.setText(UIHelpers.setSpanBetweenTokens(getContext().getString(R.string.story_item_short_tag_results, tag), "##", new ForegroundColorSpan(getContext().getResources().getColor(R.color.accent)))); mTvTagResults.setVisibility(View.VISIBLE); mBtnCloseTagSearch.setVisibility(View.VISIBLE); } mAdapter.setTagFilter(tag); } @Override public void onTagClicked(String tag) { UICallbacks.setTagFilter(tag, null); } private int mHeaderState; // 0 = hidden, 1 = shown, 2 = fully shown, 3 = No net private Feed mFeed; @Override public void onListPulledDown(int heightVisible) { int newState = 1; if (heightVisible == 0) newState = 0; else if (heightVisible == mListHeader.getHeight()) newState = 2; // Offline mode? if (mHeaderState == 0 && newState != 0) { int onlineMode = App.getInstance().socialReader.isOnline(); if (onlineMode == SocialReader.NOT_ONLINE_NO_WIFI || onlineMode == SocialReader.NOT_ONLINE_NO_WIFI_OR_NETWORK) newState = 3; } else if (mHeaderState == 3 && newState != 0) { newState = 3; } View arrow = mListHeader.findViewById(R.id.arrow); View sadface = mListHeader.findViewById(R.id.sadface); if (mHeaderState != newState) { hideHeaderControls(newState == 0); switch (newState) { case 0: break; case 1: { TextView tv = (TextView) mListHeader.findViewById(R.id.tvHeader); if (mFeed != null && mFeed.getNetworkPullDate() != null) { Date synced = new Date(); synced = mFeed.getNetworkPullDate(); String lastSyncedAt = UIHelpers.dateDiffDisplayString(synced, getContext(), R.string.last_synced_never, R.string.last_synced_recently, R.string.last_synced_minutes, R.string.last_synced_minute, R.string.last_synced_hours, R.string.last_synced_hour, R.string.last_synced_days, R.string.last_synced_day); tv.setText(lastSyncedAt); } else { tv.setText(R.string.pulldown_to_sync); } break; } case 2: { TextView tv = (TextView) mListHeader.findViewById(R.id.tvHeader); tv.setText(R.string.release_to_sync); break; } case 3: { arrow.setVisibility(View.GONE); sadface.setVisibility(View.VISIBLE); TextView tv = (TextView) mListHeader.findViewById(R.id.tvHeader); tv.setText(R.string.pulldown_to_sync_no_net); break; } } } mHeaderState = newState; if (newState != 3) { arrow.setVisibility(View.VISIBLE); sadface.setVisibility(View.GONE); } //float degrees = 180.0f * (heightVisible / (float) mListHeader.getHeight()); mListHeader.setDrawHeightLimit(heightVisible); // AnimationHelpers.rotate(arrow, degrees, degrees, 0); } private void hideHeaderControls(boolean hide) { if (hide) { mListHeader.findViewById(R.id.arrow).setVisibility(View.INVISIBLE); mListHeader.findViewById(R.id.sadface).setVisibility(View.INVISIBLE); mListHeader.findViewById(R.id.tvHeader).setVisibility(View.INVISIBLE); mListHeader.setBackgroundColor(Color.TRANSPARENT); } else { mListHeader.findViewById(R.id.arrow).setVisibility(View.VISIBLE); mListHeader.findViewById(R.id.sadface).setVisibility(View.VISIBLE); mListHeader.findViewById(R.id.tvHeader).setVisibility(View.VISIBLE); mListHeader.setBackgroundColor(0xffffffff); } } @Override public void onListDroppedWhilePulledDown() { if (mListener != null) mListener.onResync(); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); post(new Runnable() { @Override public void run() { // Remember old scroll position, so we can restore that after // orientation change! // saveListPosition(false); removeAllViews(); init(); if (mAdapter != null) mAdapter.notifyDataSetChanged(); // Tell our listener that we recreated the list view! if (mListener != null) mListener.onListViewUpdated(mListStories); } }); } private void createOrUpdateAdapter(Context context, ArrayList<Item> items, int headerView) { if (mAdapter == null) { mAdapter = new StoryListAdapter(context, null); mAdapter.setListener(mListener); mAdapter.setOnTagClickedListener(this); mAdapter.registerDataSetObserver(new DataSetObserver() { @Override public void onChanged() { super.onChanged(); scrollToSavedPosition(); } }); } mAdapter.setOnHeaderCreatedListener(this); mAdapter.setHeaderView(headerView, false); mAdapter.updateItems(sortItemsOnPublicationTime(items)); } public void updateItems(Context context, ArrayList<Item> items, int headerView, final boolean rememberPosition) { // Remember old position so we can restore it after update if we want. saveListPosition(!rememberPosition); createOrUpdateAdapter(context, items, headerView); } private void saveListPosition(boolean reset) { long oldItemId = -1; int oldY = 0; if (mListStories != null && !reset && mListStories.getCount() > 0) { int oldIndex = mListStories.getFirstVisiblePosition(); if (oldIndex != -1) oldItemId = mListStories.getAdapter().getItemId(oldIndex); View child = mListStories.getChildAt(0); if (child != null) oldY = child.getTop(); Log.v(MainActivity.LOGTAG, "Remember list position " + oldItemId + "," + oldY); } mOldItemId = oldItemId; mOldY = oldY; } private void scrollToSavedPosition() { if (mOldItemId != Integer.MIN_VALUE) { int index = 0; int offset = 0; Log.v(MainActivity.LOGTAG, "Scrolling list back to item " + mOldItemId + "," + mOldY); if (mOldItemId != -1) { ListAdapter adapter = mListStories.getAdapter(); for (int iItem = 0; iItem < adapter.getCount(); iItem++) { if (adapter.getItemId(iItem) == mOldItemId) { index = iItem; offset = mOldY; break; } } } mListStories.setSelectionFromTop(index, offset); mOldItemId = Integer.MIN_VALUE; } } public void setIsLoading(boolean loading) { mIsLoading = loading; if (loading) { if (mListStories != null) { mListStories.setHeaderEnabled(false); } if (mListHeader != null) { mListHeader.setVisibility(View.INVISIBLE); } if (mFrameLoading != null) mFrameLoading.expand(); if (mIvLoading != null) mIvLoading.startAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.rotate)); } else { if (mFrameLoading != null) mFrameLoading.collapse(); if (mIvLoading != null) mIvLoading.clearAnimation(); if (mListStories != null && mListHeader != null) { mListHeader.setVisibility(View.VISIBLE); mListStories.setHeaderEnabled(true); } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); setIsLoading(mIsLoading); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) setIsLoading(mIsLoading); if (mListStories != null) mListStories.setHeaderHeight(mListHeader.getHeight()); } @Override public void onHeaderCreated(View headerView, int resIdHeader) { if (mListener != null) mListener.onHeaderCreated(headerView, resIdHeader); } private ArrayList<Item> sortItemsOnPublicationTime(ArrayList<Item> unsortedItems) { if (unsortedItems == null) return null; ArrayList<Item> items = new ArrayList<Item>(unsortedItems); Collections.sort(items, new Comparator<Item>() { @Override public int compare(Item i1, Item i2) { if (i1.equals(i2)) return 0; else if (i1.getPublicationTime() == null && i2.getPublicationTime() == null) return 0; else if (i1.getPublicationTime() == null) return 1; else if (i2.getPublicationTime() == null) return -1; return i2.getPublicationTime().compareTo(i1.getPublicationTime()); } }); return items; } public void showError(String error) { if (mFrameError != null) { TextView tv = (TextView) mFrameError.findViewById(R.id.tvError); if (tv != null) tv.setText(error); mFrameError.expand(); } } public void hideError() { if (mFrameError != null) mFrameError.collapse(); } public void setCurrentFeed(Feed feed) { mFeed = feed; } }