package net.ggelardi.flucso; import java.util.Timer; import java.util.TimerTask; import net.ggelardi.flucso.data.FeedAdapter; import net.ggelardi.flucso.data.SubscrListAdapter; import net.ggelardi.flucso.serv.Commons; import net.ggelardi.flucso.serv.Commons.PK; import net.ggelardi.flucso.serv.FFAPI; import net.ggelardi.flucso.serv.FFAPI.Entry; import net.ggelardi.flucso.serv.FFAPI.Feed; import net.ggelardi.flucso.serv.FFAPI.Like; import net.ggelardi.flucso.serv.FFAPI.SimpleResponse; import retrofit.Callback; import retrofit.RetrofitError; import retrofit.client.Response; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.DataSetObserver; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.widget.SwipeRefreshLayout; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.squareup.picasso.Picasso.LoadedFrom; import com.squareup.picasso.Target; public class FeedFragment extends BaseFragment implements SwipeRefreshLayout.OnRefreshListener, OnClickListener { public static final String FRAGMENT_TAG = "net.ggelardi.flucso.FeedFragment"; private static final int AMOUNT_BASE = 30; private static final int AMOUNT_INCR = 20; private FeedAdapter adapter; private String cursor; private String eid; private int amount; private int lastext = -1; private boolean paused = false; private Timer timer; private LoaderTask loader; private UpdaterTask updater; private ExtenderTask extender; private DialogInterface.OnDismissListener onDismissDialog; private SwipeRefreshLayout srl; private ListView lvFeed; private LinearLayout llFooter; private ImageView imgGoUp; private TextView txtFooter; private MenuItem miWrite; private MenuItem miAutoU; private MenuItem miPause; private MenuItem miSubsc; public String fid; public String fname; public String fquery; public static FeedFragment newInstance(String name, String feed_id, String query) { FeedFragment fragment = new FeedFragment(); Bundle args = new Bundle(); args.putString("id", feed_id); args.putString("name", name); args.putString("query", query); fragment.setArguments(args); return fragment; } public FeedFragment() { // Required empty public constructor } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); adapter = new FeedAdapter(getActivity(), this); adapter.registerDataSetObserver(new DataSetObserver() { @Override public void onChanged() { if (fquery == "") session.cachedFeed = adapter.feed; } }); Bundle args = getArguments(); fid = args.getString("id"); fname = args.getString("name"); fquery = args.getString("query"); cursor = args.getString("cursor", null); amount = args.getInt("amount", AMOUNT_BASE); paused = args.getBoolean("paused"); onDismissDialog = new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { resumeUpdates(true, false); } }; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_feed, container, false); getActivity().setTitle(fname); srl = (SwipeRefreshLayout) view.findViewById(R.id.swipe_feed); lvFeed = (ListView) view.findViewById(R.id.lv_feed); imgGoUp = (ImageView) view.findViewById(R.id.img_feed_first); llFooter = (LinearLayout) inflater.inflate(R.layout.footer_feed, null); txtFooter = (TextView) llFooter.findViewById(R.id.txt_fetch_info); srl.setOnRefreshListener(this); srl.setColorSchemeResources(android.R.color.holo_blue_bright, android.R.color.holo_green_light, android.R.color.holo_orange_light, android.R.color.holo_red_light); lvFeed.addHeaderView(inflater.inflate(R.layout.header_feed, null)); lvFeed.addFooterView(llFooter); lvFeed.setAdapter(adapter); lvFeed.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (view == llFooter) return; Entry entry = adapter.getItem(position - 1); // because of the header. if (entry.hidden) { doHideUnhide(entry, true); } else { session.cachedEntry = entry; mContainer.openEntry(entry.id); } } }); lvFeed.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { switch (scrollState) { case OnScrollListener.SCROLL_STATE_IDLE: imgGoUp.setAlpha((float) 1.0); break; case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL: imgGoUp.setAlpha((float) 0.5); break; case OnScrollListener.SCROLL_STATE_FLING: imgGoUp.setAlpha((float) 0.2); break; } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (adapter.feed == null) return; imgGoUp.setVisibility(firstVisibleItem > 0 ? View.VISIBLE : View.GONE); if (firstVisibleItem + visibleItemCount >= totalItemCount && lastext != 0) { if (extender == null) { txtFooter.setText(R.string.fetching_wait); extender = (ExtenderTask) new ExtenderTask().execute(); } } else txtFooter.setText(R.string.fetching_none); } }); imgGoUp.setVisibility(View.GONE); imgGoUp.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { lvFeed.smoothScrollToPosition(0); } }); return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (savedInstanceState == null) return; eid = savedInstanceState.getString("eid", null); cursor = savedInstanceState.getString("cursor", null); amount = savedInstanceState.getInt("amount", AMOUNT_BASE); paused = savedInstanceState.getBoolean("paused"); } @Override public void onResume() { super.onResume(); } @Override public void onPause() { super.onPause(); pauseUpdates(false); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("eid", lvFeed != null && adapter.feed != null && adapter.feed.entries.size() > 0 ? adapter.getItem(lvFeed.getFirstVisiblePosition()).id : ""); outState.putString("name", fname); outState.putString("cursor", cursor); outState.putInt("amount", amount); outState.putBoolean("paused", paused); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.feed, menu); miWrite = menu.findItem(R.id.action_feed_post); miAutoU = menu.findItem(R.id.action_feed_auto); miPause = menu.findItem(R.id.action_feed_pause); miSubsc = menu.findItem(R.id.action_feed_subscr); } @Override public void onPrepareOptionsMenu(Menu menu) { checkMenu(); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item == miWrite) { mContainer.openPostNew(new String[] { fid }, null, null, null); return true; } if (item == miAutoU) { resumeUpdates(true, false); return true; } if (item == miPause) { pauseUpdates(false); return true; } if (item == miSubsc) { pauseUpdates(false); final LayoutInflater inflater = getActivity().getLayoutInflater(); Commons.picasso(getActivity()).load(adapter.feed.getAvatarUrl()).placeholder(R.drawable.nomugshot).into( new Target() { @Override public void onPrepareLoad(Drawable arg0) { } @Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom arg1) { View view = inflater.inflate(R.layout.dialog_subscr, null); ListView lvSubsc = (ListView) view.findViewById(R.id.lv_subs_lists); final AlertDialog dlg = new AlertDialog.Builder(getActivity()).setTitle( adapter.feed.getName()).setView(view).setOnDismissListener(onDismissDialog).setCancelable( true).setIcon(new BitmapDrawable(getActivity().getResources(), bitmap)).create(); lvSubsc.setAdapter(new SubscrListAdapter(getActivity(), adapter.feed)); dlg.show(); } @Override public void onBitmapFailed(Drawable arg0) { } }); return true; } Toast.makeText(getActivity(), item.getTitle(), Toast.LENGTH_SHORT).show(); return false; } @Override public void onRefresh() { checkMenu(); if ((updater != null && updater.scheduledExecutionTime() >= System.currentTimeMillis()) || (loader != null && loader.scheduledExecutionTime() >= System.currentTimeMillis())) return; amount = AMOUNT_BASE; if (timer != null) timer.cancel(); timer = new Timer(true); loader = new LoaderTask(); timer.schedule(loader, 500); if (isAutoUpdGoing()) { updater = new UpdaterTask(); timer.schedule(updater, 30000, 30000); } } @Override public void onClick(View v) { int pos; try { pos = (Integer) v.getTag(); } catch (Exception err) { return; // wtf? } final Entry entry = adapter.getItem(pos); switch (v.getId()) { case R.id.img_entry_from: mContainer.openFeed(entry.from.name, entry.from.id, null); break; case R.id.img_feed_thumb: if (entry.thumbnails.length <= 0) startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(entry.getFirstUrl()))); else { session.cachedEntry = entry; mContainer.openGallery(entry.id, entry.files.length + entry.thumbpos); } break; case R.id.txt_feed_files: session.cachedEntry = entry; mContainer.openGallery(entry.id, entry.files.length + entry.thumbpos); break; case R.id.img_feed_tprev: entry.thumbPrior(); adapter.notifyDataSetChanged(); break; case R.id.img_feed_tnext: entry.thumbNext(); adapter.notifyDataSetChanged(); break; case R.id.txt_feed_likes: if (entry.canLike()) doLike(entry); else if (entry.canUnlike()) doUnlike(entry); break; case R.id.txt_feed_fwd: final String[] dsts = new String[] { session.getUsername() }; final String body = "FWD: " + entry.rawBody; final String link = entry.url; final String[] imgs = entry.getMediaUrls(false); mContainer.openPostNew(dsts, body, link, imgs); break; case R.id.txt_feed_hide: if (entry.canHide()) doHideUnhide(entry, false); break; } } @Override protected void initFragment() { if (adapter != null && session.cachedFeed != null && session.cachedFeed.isIt(fid)) { adapter.feed = session.cachedFeed; adapter.notifyDataSetChanged(); cursor = adapter.feed.realtime.cursor; } if (adapter != null && adapter.feed != null) adapter.feed.checkLocalHide(); resumeUpdates(false, false); } private void checkMenu() { if (miWrite == null || miAutoU == null || miPause == null || miSubsc == null) return; boolean fl = adapter != null && adapter.feed != null; miWrite.setVisible(fl); miWrite.setIcon(fl && adapter.feed.canDM() ? R.drawable.menu_dm : R.drawable.menu_post); miAutoU.setVisible(fl && !isAutoUpdGoing()); miPause.setVisible(fl && !miAutoU.isVisible()); miSubsc.setVisible(fl && adapter.feed.canSetSubscriptions()); } private boolean isAutoUpdSet() { return session.getPrefs().getBoolean(PK.FEED_UPD, true); } private boolean isAutoUpdGoing() { return isAutoUpdSet() && !paused; } private void pauseUpdates(boolean showHourglass) { Log.v(logTag(), "pauseUpdates"); paused = true; if (timer != null) timer.cancel(); checkMenu(); if (showHourglass) getActivity().setProgressBarIndeterminateVisibility(true); } private void resumeUpdates(boolean removeHourglass, boolean reload) { Log.v(logTag(), "resumeUpdates"); paused = false; reload = reload || adapter.feed == null || TextUtils.isEmpty(cursor); if (timer != null) timer.cancel(); timer = new Timer(true); loader = new LoaderTask(); updater = new UpdaterTask(); if (reload) timer.schedule(loader, 500); if (isAutoUpdGoing()) timer.schedule(updater, reload ? 30000 : 500, 30000); checkMenu(); if (removeHourglass) getActivity().setProgressBarIndeterminateVisibility(false); } private void doLike(final Entry entry) { pauseUpdates(true); Callback<Like> callback = new Callback<Like>() { @Override public void success(Like result, Response response) { Toast.makeText(getActivity(), getString(R.string.res_inslike_ok), Toast.LENGTH_SHORT).show(); resumeUpdates(true, false); } @Override public void failure(RetrofitError error) { getActivity().setProgressBarIndeterminateVisibility(false); new AlertDialog.Builder(getActivity()).setTitle(R.string.res_rfcall_failed).setMessage( Commons.retrofitErrorText(error)).setOnDismissListener(onDismissDialog).setPositiveButton( R.string.dlg_btn_retry, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { doLike(entry); } }).setIcon(android.R.drawable.ic_dialog_alert).setCancelable(true).create().show(); } }; FFAPI.client_write(session).ins_like(entry.id, callback); } private void doUnlike(final Entry entry) { pauseUpdates(true); Callback<SimpleResponse> callback = new Callback<SimpleResponse>() { @Override public void success(SimpleResponse result, Response response) { Toast.makeText(getActivity(), getString(R.string.res_dellike_ok), Toast.LENGTH_SHORT).show(); int i = entry.indexOfLike(session.getUsername()); if (i >= 0) { entry.likes.remove(i); entry.commands.set(entry.commands.indexOf("unlike"), "like"); // sadly, we need to do this. } resumeUpdates(true, false); } @Override public void failure(RetrofitError error) { getActivity().setProgressBarIndeterminateVisibility(false); new AlertDialog.Builder(getActivity()).setTitle(R.string.res_rfcall_failed).setMessage( Commons.retrofitErrorText(error)).setOnDismissListener(onDismissDialog).setPositiveButton( R.string.dlg_btn_retry, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { doUnlike(entry); } }).setIcon(android.R.drawable.ic_dialog_alert).setCancelable(true).create().show(); } }; FFAPI.client_write(session).del_like(entry.id, callback); } private void doHideUnhide(final Entry entry, final boolean unhide) { pauseUpdates(true); Callback<Entry> callback = new Callback<Entry>() { @Override public void success(Entry result, Response response) { Toast.makeText(getActivity(), getString(unhide ? R.string.res_unhide_ok : R.string.res_hidden_ok), Toast.LENGTH_SHORT).show(); entry.hidden = !unhide; resumeUpdates(true, false); } @Override public void failure(RetrofitError error) { getActivity().setProgressBarIndeterminateVisibility(false); new AlertDialog.Builder(getActivity()).setTitle(R.string.res_rfcall_failed).setMessage( Commons.retrofitErrorText(error)).setOnDismissListener(onDismissDialog).setPositiveButton( R.string.dlg_btn_retry, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { doHideUnhide(entry, unhide); } }).setIcon(android.R.drawable.ic_dialog_alert).setCancelable(true).create().show(); } }; if (unhide) FFAPI.client_write(session).set_unhide(entry.id, 1, callback); else FFAPI.client_write(session).set_hidden(entry.id, callback); } public void subscribe() { Callback<SimpleResponse> callback = new Callback<SimpleResponse>() { @Override public void success(SimpleResponse result, Response response) { if (result.status.equals("subscribed")) { Toast.makeText(getActivity(), result.status, Toast.LENGTH_LONG).show(); resumeUpdates(true, true); } else if (result.status.equals("requested")) { Toast.makeText(getActivity(), result.status, Toast.LENGTH_LONG).show(); getActivity().setProgressBarIndeterminateVisibility(false); getActivity().getFragmentManager().popBackStack(); } } @Override public void failure(RetrofitError error) { getActivity().setProgressBarIndeterminateVisibility(false); new AlertDialog.Builder(getActivity()).setTitle(R.string.res_rfcall_failed).setMessage( Commons.retrofitErrorText(error)).setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { getActivity().getFragmentManager().popBackStack(); } }).setPositiveButton(R.string.dlg_btn_retry, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { subscribe(); } }).setIcon(android.R.drawable.ic_dialog_alert).setCancelable(true).create().show(); } }; FFAPI.client_write(session).subscribe(fid, "home", callback); } private class LoaderTask extends TimerTask { @Override public void run() { final Activity context = getActivity(); try { srl.setRefreshing(true); Log.v(logTag(), "(loader) fetching " + Integer.toString(amount) + " items..."); // According to API documentation, when cursor="" we should get all the entries, but we don't. FF // returns an empty feed (with the cursor we'll use for the next call), so we have to make a call // for a complete feed anyway. final Feed updates; if (TextUtils.isEmpty(fquery)) { adapter.feed = FFAPI.client_feed(session).get_feed_normal(fid, 0, amount); updates = FFAPI.client_feed(session).get_feed_updates(fid, amount, "", 0, 1); } else { adapter.feed = FFAPI.client_feed(session).get_search_normal(fquery, 0, amount); updates = FFAPI.client_feed(session).get_search_updates(fquery, amount, "", 0, 1); } context.runOnUiThread(new Runnable() { @Override public void run() { if (!FeedFragment.this.isVisible() || FeedFragment.this.isRemoving()) return; srl.setRefreshing(false); adapter.feed.checkLocalHide(); adapter.feed.update(updates); fname = adapter.feed.name; cursor = updates.realtime.cursor; amount = adapter.feed.entries.size(); adapter.notifyDataSetChanged(); int i = TextUtils.isEmpty(eid) ? 0 : adapter.feed.indexOf(eid); lvFeed.smoothScrollToPosition(i > 0 ? i : 0); context.setTitle(fname); checkMenu(); lastext = -1; // reset the extender task. eid = null; // reset the saved pos. } }); } catch (final Exception error) { context.runOnUiThread(new Runnable() { @Override public void run() { srl.setRefreshing(false); if (error instanceof RetrofitError && Commons.retrofitErrorCode((RetrofitError) error) == 403) { new AlertDialog.Builder(context).setTitle(fname).setMessage(R.string.ask_subscr_private ).setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { context.getFragmentManager().popBackStack(); } }).setPositiveButton(R.string.dlg_btn_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { subscribe(); } }).setIcon(R.drawable.ic_action_add_group).setCancelable(true).create().show(); return; } if (!FeedFragment.this.isVisible() || FeedFragment.this.isRemoving()) return; if (adapter == null || adapter.feed == null) context.getFragmentManager().popBackStack(); new AlertDialog.Builder(context).setTitle("LoaderTask").setMessage( error instanceof RetrofitError ? Commons.retrofitErrorText((RetrofitError) error) : error.getMessage()).setIcon(android.R.drawable.ic_dialog_alert).show(); } }); } } } private class UpdaterTask extends TimerTask { @Override public void run() { final Activity context = getActivity(); if (TextUtils.isEmpty(cursor)) { Log.v(logTag(), "invalid cursor, rescheduling..."); context.runOnUiThread(new Runnable() { @Override public void run() { if (loader == null || loader.scheduledExecutionTime() <= 0) timer.schedule(loader, 2000); } }); return; } try { Log.v(logTag(), "(updater) fetching " + Integer.toString(amount) + " items..."); final Feed updates; if (TextUtils.isEmpty(fquery)) updates = FFAPI.client_feed(session).get_feed_updates(fid, amount, cursor, 0, 1); else updates = FFAPI.client_feed(session).get_search_updates(fquery, amount, cursor, 0, 1); context.runOnUiThread(new Runnable() { @Override public void run() { if (!FeedFragment.this.isVisible() || FeedFragment.this.isRemoving()) return; srl.setRefreshing(false); int added = adapter.feed.update(updates); fname = adapter.feed.name; cursor = updates.realtime.cursor; amount = adapter.feed.entries.size(); int offset = lvFeed.getFirstVisiblePosition(); adapter.notifyDataSetChanged(); if (added > 0) { lvFeed.smoothScrollToPosition(added + offset); if (imgGoUp.getVisibility() == View.VISIBLE) imgGoUp.startAnimation(blink); lastext = -1; // reset the extender task. } context.setTitle(fname); checkMenu(); } }); } catch (Exception error) { final String text = error instanceof RetrofitError ? Commons.retrofitErrorText((RetrofitError) error) : error.getMessage(); context.runOnUiThread(new Runnable() { @Override public void run() { if (!FeedFragment.this.isVisible() || FeedFragment.this.isRemoving()) return; srl.setRefreshing(false); new AlertDialog.Builder(context).setTitle("UpdaterTask").setMessage(text).setIcon( android.R.drawable.ic_dialog_alert).show(); } }); } } } private class ExtenderTask extends AsyncTask<Void, Void, Feed> { private String error = null; @Override protected void onPreExecute() { srl.setRefreshing(true); Log.v(logTag(), "(extender) fetching more items starting from " + Integer.toString(amount + 1) + "..."); } @Override protected Feed doInBackground(Void... params) { try { return TextUtils.isEmpty(fquery) ? FFAPI.client_feed(session).get_feed_normal(fid, amount + 1, AMOUNT_INCR) : FFAPI.client_feed(session).get_search_normal(fquery, amount + 1, AMOUNT_INCR); } catch (Exception e) { error = e instanceof RetrofitError ? Commons.retrofitErrorText((RetrofitError) e) : e.getMessage(); return null; } } @Override protected void onPostExecute(Feed result) { Context context = getActivity(); if (context == null || !isAdded() || isDetached() || !isVisible() || isRemoving()) return; try { srl.setRefreshing(false); if (!TextUtils.isEmpty(error)) new AlertDialog.Builder(context).setTitle("ExtenderTask").setMessage(error).setIcon( android.R.drawable.ic_dialog_alert).show(); else { Log.v(logTag(), "got " + Integer.toString(result.entries.size())); lastext = adapter.feed.append(result); amount = adapter.feed.entries.size(); Log.v(logTag(), "appended " + Integer.toString(lastext)); int y = lvFeed.getScrollY(); adapter.notifyDataSetChanged(); lvFeed.scrollTo(0, y); checkMenu(); } } finally { extender = null; } } } }