package com.vaguehope.onosendai.ui; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.Fragment; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener; import android.view.LayoutInflater; 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.AdapterView.OnItemClickListener; import android.widget.Button; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; import com.vaguehope.onosendai.C; import com.vaguehope.onosendai.R; import com.vaguehope.onosendai.config.Account; import com.vaguehope.onosendai.config.Column; import com.vaguehope.onosendai.config.Config; import com.vaguehope.onosendai.config.InlineMediaStyle; import com.vaguehope.onosendai.config.InternalColumnType; import com.vaguehope.onosendai.images.CachedImageFileProvider; import com.vaguehope.onosendai.images.ImageLoader; import com.vaguehope.onosendai.images.ImageLoaderUtils; import com.vaguehope.onosendai.model.LinkedTweetLoader; import com.vaguehope.onosendai.model.Meta; import com.vaguehope.onosendai.model.MetaType; import com.vaguehope.onosendai.model.MetaUtils; import com.vaguehope.onosendai.model.ScrollState; import com.vaguehope.onosendai.model.ScrollState.ScrollDirection; import com.vaguehope.onosendai.model.Tweet; import com.vaguehope.onosendai.model.TweetListCursorAdapter; import com.vaguehope.onosendai.payload.InReplyToLoaderTask; import com.vaguehope.onosendai.payload.InReplyToPayload; import com.vaguehope.onosendai.payload.MentionPayload; import com.vaguehope.onosendai.payload.Payload; import com.vaguehope.onosendai.payload.PayloadClickListener; import com.vaguehope.onosendai.payload.PayloadListAdapter; import com.vaguehope.onosendai.payload.PayloadListClickListener; import com.vaguehope.onosendai.payload.PayloadType; import com.vaguehope.onosendai.payload.ReplyLoaderTask; import com.vaguehope.onosendai.payload.SharePayload; import com.vaguehope.onosendai.provider.NetworkType; import com.vaguehope.onosendai.provider.OutboxActionFactory; import com.vaguehope.onosendai.provider.ProviderMgr; import com.vaguehope.onosendai.provider.SendOutboxService; import com.vaguehope.onosendai.provider.ServiceRef; import com.vaguehope.onosendai.provider.twitter.TwitterUrls; import com.vaguehope.onosendai.storage.DbClient; import com.vaguehope.onosendai.storage.DbInterface; import com.vaguehope.onosendai.storage.DbInterface.ColumnState; import com.vaguehope.onosendai.storage.DbInterface.ScrollChangeType; import com.vaguehope.onosendai.storage.DbInterface.Selection; import com.vaguehope.onosendai.storage.DbInterface.TwUpdateListener; import com.vaguehope.onosendai.storage.DbProvider; import com.vaguehope.onosendai.update.FetchColumn; import com.vaguehope.onosendai.update.KvKeys; import com.vaguehope.onosendai.util.DateHelper.FriendlyDateTimeFormat; import com.vaguehope.onosendai.util.DialogHelper; import com.vaguehope.onosendai.util.DialogHelper.Listener; import com.vaguehope.onosendai.util.FileHelper; import com.vaguehope.onosendai.util.LogWrapper; import com.vaguehope.onosendai.util.Result; import com.vaguehope.onosendai.util.StringHelper; import com.vaguehope.onosendai.util.Titleable; import com.vaguehope.onosendai.util.exec.ExecutorEventListener; import com.vaguehope.onosendai.util.exec.TrackingAsyncTask; import com.vaguehope.onosendai.widget.ScrollIndicator; import com.vaguehope.onosendai.widget.SidebarLayout; /** * https://developer.android.com/intl/fr/guide/components/fragments.html# * Creating */ public class TweetListFragment extends Fragment implements DbProvider { static final String ARG_COLUMN_ID = "column_id"; static final String ARG_COLUMN_POSITION = "column_pos"; static final String ARG_COLUMN_TITLE = "column_title"; static final String ARG_COLUMN_IS_LATER = "column_is_later"; static final String ARG_COLUMN_SHOW_INLINEMEDIA = "column_show_inlinemedia"; static final String ARG_SHOW_FILTERED = "show_filtered"; private final LogWrapper log = new LogWrapper(); private int columnId = -1; private int columnPosition = -1; private boolean isLaterColumn; private InlineMediaStyle inlineMediaStyle; private boolean showFiltered; private Config conf; private FriendlyDateTimeFormat friendlyDateTimeFormat; private ImageLoader imageLoader; private LinkedTweetLoader tweetLoader; private RefreshUiHandler refreshUiHandler; private MainActivity mainActivity; private SidebarLayout sidebar; private SwipeRefreshLayout tweetListSwiper; private ListView tweetList; private TextView tweetListStatus; private Button tweetListEmptyRefresh; private PayloadListAdapter lstTweetPayloadAdaptor; private Button btnDetailsLater; private ScrollState scrollState; private Tweet scrollToTweet; private ScrollIndicator scrollIndicator; private TweetListCursorAdapter adapter; private DbClient bndDb; @Override public void onCreate (final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } @Override public View onCreateView (final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { this.columnId = getArguments().getInt(ARG_COLUMN_ID); this.columnPosition = getArguments().getInt(ARG_COLUMN_POSITION); this.isLaterColumn = getArguments().getBoolean(ARG_COLUMN_IS_LATER, false); this.inlineMediaStyle = InlineMediaStyle.parseJson(getArguments().getString(ARG_COLUMN_SHOW_INLINEMEDIA)); this.showFiltered = getArguments().getBoolean(ARG_SHOW_FILTERED, false); this.log.setPrefix("C" + this.columnId); this.log.d("onCreateView()"); this.mainActivity = (MainActivity) getActivity(); this.conf = this.mainActivity.getConf(); this.friendlyDateTimeFormat = new FriendlyDateTimeFormat(this.mainActivity); this.imageLoader = ImageLoaderUtils.fromActivity(getActivity()); this.tweetLoader = new LinkedTweetLoader(this.mainActivity, getLocalEs(), getNetEs(), getExecutorEventListener(), this.conf, this.mainActivity, getColumn().isHdMedia()); /* * Fragment life cycles are strange. onCreateView() is called multiple * times before onSaveInstanceState() is called. Do not overwrite * perfectly good stated stored in member var. */ if (this.scrollState == null) { this.scrollState = ScrollState.fromBundle(savedInstanceState); } final View rootView = inflater.inflate(R.layout.tweetlist, container, false); this.sidebar = (SidebarLayout) rootView.findViewById(R.id.tweetListLayout); rootView.setFocusableInTouchMode(true); rootView.requestFocus(); rootView.setOnKeyListener(new SidebarLayout.BackButtonListener(this.sidebar)); updateScrollLabelToIdle(); this.tweetListEmptyRefresh = (Button) rootView.findViewById(R.id.tweetListEmptyRefresh); this.tweetListEmptyRefresh.setOnClickListener(this.refreshClickListener); this.tweetListSwiper = (SwipeRefreshLayout) rootView.findViewById(R.id.tweetListListSwiper); this.tweetList = (ListView) rootView.findViewById(R.id.tweetListList); this.adapter = new TweetListCursorAdapter(container.getContext(), this.inlineMediaStyle, this.imageLoader, this.tweetLoader, this.tweetList); this.tweetList.setAdapter(this.adapter); this.tweetList.setOnItemClickListener(this.tweetItemClickedListener); this.tweetList.setEmptyView(this.tweetListEmptyRefresh); if (this.inlineMediaStyle == InlineMediaStyle.SEAMLESS) this.tweetList.setDrawSelectorOnTop(true); this.refreshUiHandler = new RefreshUiHandler(this); final ListView lstTweetPayload = (ListView) rootView.findViewById(R.id.tweetDetailPayloadList); this.lstTweetPayloadAdaptor = new PayloadListAdapter(container.getContext(), this.imageLoader, lstTweetPayload, this.payloadClickListener); lstTweetPayload.setAdapter(this.lstTweetPayloadAdaptor); final PayloadListClickListener payloadListClickListener = new PayloadListClickListener(this.payloadClickListener); lstTweetPayload.setOnItemClickListener(payloadListClickListener); lstTweetPayload.setOnItemLongClickListener(payloadListClickListener); ((Button) rootView.findViewById(R.id.tweetDetailClose)).setOnClickListener(new SidebarLayout.ToggleSidebarListener(this.sidebar)); this.btnDetailsLater = (Button) rootView.findViewById(R.id.tweetDetailLater); this.scrollIndicator = ScrollIndicator.attach(getActivity(), (ViewGroup) rootView.findViewById(R.id.tweetListView), this.tweetList, this.tweetListScrollListener); this.tweetListSwiper.setOnRefreshListener(new OnRefreshListener() { @Override public void onRefresh () { if (!getMainActivity().scheduleRefreshInteractive(getColumnId())) { TweetListFragment.this.tweetListSwiper.setRefreshing(false); } } }); this.tweetListStatus = (TextView) rootView.findViewById(R.id.tweetListStatus); this.tweetListStatus.setOnClickListener(this.tweetListStatusClickListener); return rootView; } @Override public void onDestroy () { if (this.adapter != null) this.adapter.dispose(); if (this.bndDb != null) this.bndDb.dispose(); super.onDestroy(); } @Override public void onResume () { super.onResume(); resumeDb(); this.mainActivity.onFragmentResumed(getColumnId(), this); } @Override public void onPause () { saveScroll(); saveSavedScrollToDb(); suspendDb(); this.mainActivity.onFragmentPaused(getColumnId()); super.onPause(); } @Override public void onSaveInstanceState (final Bundle outState) { super.onSaveInstanceState(outState); if (this.scrollState != null) this.scrollState.addToBundle(outState); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - protected LogWrapper getLog () { return this.log; } protected MainActivity getMainActivity () { return this.mainActivity; } public SidebarLayout getSidebar () { return this.sidebar; } public int getColumnId () { return this.columnId; } public int getColumnPosition () { return this.columnPosition; } protected InlineMediaStyle getInlineMediaStyle () { return this.inlineMediaStyle; } protected Config getConf () { return this.conf; } protected Column getColumn () { return this.conf.getColumnById(this.columnId); } protected TweetListCursorAdapter getAdapter () { return this.adapter; } private ProviderMgr getProviderMgr () { return getMainActivity().getProviderMgr(); } private ExecutorEventListener getExecutorEventListener () { return getMainActivity().getExecutorEventListener(); } private Executor getLocalEs () { return getMainActivity().getLocalEs(); } private Executor getNetEs () { return getMainActivity().getNetEs(); } private ServiceRef findFullService (final Account account, final ServiceRef svc) { final List<ServiceRef> accounts = getProviderMgr().getSuccessWhaleProvider().getPostToAccountsCached(account); if (accounts == null) return null; for (final ServiceRef a : accounts) { if (svc.equals(a)) return a; } return null; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - protected ScrollState getCurrentScroll () { return ScrollState.from(this.tweetList, this.scrollIndicator, this.lastScrollDirection); } private void saveScroll () { final ScrollState newState = getCurrentScroll(); if (newState != null) { this.scrollState = newState; this.log.d("Saved scroll: %s", this.scrollState); } } private void saveScrollIfNotSaved () { if (this.scrollState != null) return; saveScroll(); } private void restoreScroll () { if (this.scrollState == null) return; this.scrollState.applyTo(this.tweetList, this.scrollIndicator); this.log.d("Restored scroll: %s", this.scrollState); this.scrollState = null; } private void saveSavedScrollToDb () { final DbInterface db = getDb(); if (db != null) db.storeScroll(this.columnId, this.scrollState); } protected void saveCurrentScrollToDb () { final ScrollState newState = getCurrentScroll(); final DbInterface db = getDb(); if (db != null) db.storeScroll(this.columnId, newState); } protected void restoreSavedScrollFromDb () { final ScrollState fromDb = getDb().getScroll(this.columnId); if (fromDb != null) this.scrollState = fromDb; if (this.scrollState != null && this.scrollState.getScrollDirection() != ScrollDirection.UNKNOWN) { this.lastScrollDirection = this.scrollState.getScrollDirection(); } } protected void restoreScrollFromDbIfNewer (final ScrollChangeType type) { final DbInterface db = getDb(); if (db == null) return; final ScrollState ss = db.getScroll(this.columnId); if (ss != null) { ss.applyToIfNewer(this.scrollIndicator); if (type == ScrollChangeType.UNREAD_AND_SCROLL) ss.applyToIfNewer(this.tweetList, this.lastScrollDirection); } } protected void scrollJumpUp () { final int unreadPosition = this.scrollIndicator.getUnreadPosition(); this.tweetList.setSelectionFromTop(unreadPosition, 0); this.lastScrollDirection = ScrollDirection.UP; } public void scrollToTweet (final Tweet tweet) { if (tweet == null) return; if (this.adapter.getCount() > 0) { for (int i = 0; i < this.adapter.getCount(); i++) { if (this.adapter.getItemId(i) == tweet.getUid()) { this.tweetList.setSelectionFromTop(i, 0); break; } } } else { this.scrollToTweet = tweet; } } private void scrollToStashedTweet () { if (this.scrollToTweet == null) return; final Tweet tweet = this.scrollToTweet; this.scrollToTweet = null; scrollToTweet(tweet); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - private void resumeDb () { if (this.bndDb == null) { this.log.d("Binding DB service..."); this.bndDb = new DbClient(getActivity(), this.log.getPrefix(), new Runnable() { @Override public void run () { /* * this convoluted method is because the service connection * won't finish until this thread processes messages again * (i.e., after it exits this thread). if we try to talk to * the DB service before then, it will NPE. */ getDb().addTwUpdateListener(getGuiUpdateListener()); restoreSavedScrollFromDb(); refreshUi(); getLog().d("DB service bound."); } }); } else if (getDb() != null) { // because we stop listening in onPause(), we must resume if the user comes back. getDb().addTwUpdateListener(getGuiUpdateListener()); restoreSavedScrollFromDb(); refreshUi(); this.log.d("DB service rebound."); } else { this.log.w("resumeDb() called while DB service is half bound. I do not know what to do."); } } private void suspendDb () { if (getDb() != null) { // We might be pausing before the callback has come. getDb().removeTwUpdateListener(getGuiUpdateListener()); } else { // If we have not even had the callback yet, cancel it. this.bndDb.clearReadyListener(); } this.log.d("DB service released."); } @Override public DbInterface getDb () { final DbClient d = this.bndDb; if (d == null) return null; return d.getDb(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - private final OnItemClickListener tweetItemClickedListener = new OnItemClickListener() { @Override public void onItemClick (final AdapterView<?> parent, final View view, final int position, final long id) { showTweetDetails(getAdapter().getItemSid(position)); } }; private final OnScrollListener tweetListScrollListener = new OnScrollListener() { private boolean scrolling = false; private int lastFirstVisibleItem = -1; @Override public void onScrollStateChanged (final AbsListView view, final int newScrollState) { this.scrolling = (newScrollState != OnScrollListener.SCROLL_STATE_IDLE); } @Override public void onScroll (final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { if (this.scrolling && firstVisibleItem != this.lastFirstVisibleItem) { ScrollDirection direction = ScrollDirection.UNKNOWN; if (this.lastFirstVisibleItem >= 0) { if (firstVisibleItem < this.lastFirstVisibleItem) { direction = ScrollDirection.UP; } else if (firstVisibleItem > this.lastFirstVisibleItem) { direction = ScrollDirection.DOWN; } } onTweetListScroll(direction); this.lastFirstVisibleItem = firstVisibleItem; } } }; private final PayloadClickListener payloadClickListener = new PayloadClickListener() { @Override public boolean payloadClicked (final Payload payload) { return payloadClick(payload); } @Override public boolean payloadLongClicked (final Payload payload) { return payloadLongClick(payload); } @Override public void subviewClicked (final View view, final Payload payload, final int index) { payloadSubviewClick(view, payload, index); } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - protected boolean payloadClick (final Payload payload) { if (payload.getType() == PayloadType.INREPLYTO) { showTweetDetails(((InReplyToPayload) payload).getInReplyToTweet().getSid()); return true; } return false; } protected boolean payloadLongClick (final Payload payload) { if (payload.getType() == PayloadType.MENTION) { final Account account = MetaUtils.accountFromMeta(payload.getOwnerTweet(), this.conf); if (account == null) { DialogHelper.alert(getActivity(), getMainActivity().getString(R.string.tweetlist_can_not_find_this_tweet_s_account_metadata)); } else { ProfileDialog.show(getActivity(), getNetEs(), this.imageLoader, account, ((MentionPayload) payload).getScreenName()); } return true; } return false; } protected void payloadSubviewClick (final View btn, final Payload payload, final int index) { if (payload.getType() == PayloadType.SHARE) { switch (index) { case SharePayload.BTN_SHARE_RT: askRt(payload.getOwnerTweet()); break; case SharePayload.BTN_SHARE_QUOTE: askQuote(payload.getOwnerTweet()); break; case SharePayload.BTN_SHARE_FAV: askFav(payload.getOwnerTweet()); break; case SharePayload.BTN_SHARE_INTENT: shareIntentBtnClicked(btn, payload.getOwnerTweet()); break; default: } } else if (payload.getType() == PayloadType.EDIT) { switch (index) { case 0: askDeleteTweet(payload.getOwnerTweet()); break; default: } } } protected void showTweetDetails (final String tweetSid) { Tweet tweet = getDb().getTweetDetails(this.columnId, tweetSid); if (tweet == null) tweet = getDb().getTweetDetails(tweetSid); // TODO better way of showing this error. if (tweet == null) tweet = new Tweet(tweetSid, "", "", null, null, "Error: tweet with SID=" + tweetSid + " not found.", TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()), null, null, null, Collections.<Meta> emptyList()); this.lstTweetPayloadAdaptor.setInput(getConf(), tweet); new ReplyLoaderTask(getExecutorEventListener(), getActivity(), getDb(), tweet, this.lstTweetPayloadAdaptor).executeOnExecutor(getLocalEs()); new InReplyToLoaderTask(getExecutorEventListener(), getActivity().getApplicationContext(), getConf(), getProviderMgr(), tweet, getColumn().isHdMedia(), this.lstTweetPayloadAdaptor, getNetEs()).executeOnExecutor(getNetEs()); setReadLaterButton(tweet, this.isLaterColumn); this.sidebar.openSidebar(); } protected void setReadLaterButton (final Tweet tweet, final boolean laterColumn) { this.btnDetailsLater.setText(laterColumn ? R.string.tweetlist_details_read : R.string.tweetlist_details_later); this.btnDetailsLater.setOnClickListener(new DetailsLaterClickListener(this, tweet, laterColumn)); } private ServiceRef tweetAndAccoutToServiceRef (final Account account, final Tweet tweet) { final Meta svcMeta = tweet.getFirstMetaOfType(MetaType.SERVICE); if (svcMeta != null) { final ServiceRef svc = ServiceRef.parseServiceMeta(svcMeta); if (svc != null) { final ServiceRef fullSvc = findFullService(account, svc); if (fullSvc != null) return fullSvc; return svc; } } return null; } private String summariseAccountAndSubAccount (final Account account, final Tweet tweet) { final ServiceRef svcRef = tweetAndAccoutToServiceRef(account, tweet); return summariseAccountAndSubAccount(account, svcRef); } private static String summariseAccountAndSubAccount (final Account account, final ServiceRef svcRef) { if (svcRef != null) return String.format("%s\n%s", svcRef.getUiTitle(), account.getUiTitle()); return account.getUiTitle(); } // TODO Dedup between next few methods. private void askRt (final Tweet tweet) { final Account account = MetaUtils.accountFromMeta(tweet, this.conf); if (account == null) { DialogHelper.alert(getActivity(), getMainActivity().getString(R.string.tweetlist_can_not_find_this_tweet_s_account_metadata)); return; } final ServiceRef svcRef = tweetAndAccoutToServiceRef(account, tweet); final boolean isLike = svcRef != null && svcRef.getType() == NetworkType.FACEBOOK; final String question; final String button; if (isLike) { question = getMainActivity().getString(R.string.tweetlist_confirm_like_question); button = getMainActivity().getString(R.string.tweetlist_confirm_like_btn); } else { question = getMainActivity().getString(R.string.tweetlist_confirm_rt_question); button = getMainActivity().getString(R.string.tweetlist_confirm_rt_button); } final String as = summariseAccountAndSubAccount(account, svcRef); String msg = String.format("%s\n%s", question, as); final AlertDialog.Builder dlgBld = new AlertDialog.Builder(getActivity()); dlgBld.setMessage(msg); dlgBld.setPositiveButton(button, new DialogInterface.OnClickListener() { @Override public void onClick (final DialogInterface dialog, final int which) { doRt(account, tweet); } }); dlgBld.setNegativeButton(android.R.string.cancel, DialogHelper.DLG_CANCEL_CLICK_LISTENER); dlgBld.show(); } protected void doRt (final Account account, final Tweet tweet) { getDb().addPostToOutput(OutboxActionFactory.newRt(account, tweet)); getActivity().startService(new Intent(getActivity(), SendOutboxService.class)); Toast.makeText(getActivity(), R.string.tweetlist_requested_via_outbox, Toast.LENGTH_SHORT).show(); } private enum QuoteType implements Titleable { INLINE("Inline"), //ES LINK("Link"); //ES private final String title; private QuoteType (final String title) { this.title = title; } @Override public String getUiTitle () { return this.title; } } private void askQuote (final Tweet tweet) { DialogHelper.askItem(getActivity(), getString(R.string.tweetlist_ask_quote_type), QuoteType.values(), new Listener<QuoteType>() { @Override public void onAnswer (final QuoteType answer) { doQuote(tweet, answer); } }); } protected void doQuote (final Tweet tweet, final QuoteType type) { final String body; final int cursorPosition; switch (type) { case INLINE: body = String.format("RT @%s %s", StringHelper.firstLine(tweet.getUsername()), tweet.getBody()); cursorPosition = body.length(); break; case LINK: body = String.format(" %s", TwitterUrls.tweet(tweet)); cursorPosition = 0; break; default: return; } getMainActivity().showPost(Collections.singleton(getColumn()), body, cursorPosition); } private void askFav (final Tweet tweet) { final Account account = MetaUtils.accountFromMeta(tweet, this.conf); if (account == null) { DialogHelper.alert(getActivity(), getMainActivity().getString(R.string.tweetlist_can_not_find_this_tweet_s_account_metadata)); return; } final String question = getMainActivity().getString(R.string.tweetlist_confirm_favourite_question); final String button = getMainActivity().getString(R.string.tweetlist_confirm_favourite_button); final ServiceRef svcRef = tweetAndAccoutToServiceRef(account, tweet); final String as = summariseAccountAndSubAccount(account, svcRef); String msg = String.format("%s\n%s", question, as); final AlertDialog.Builder dlgBld = new AlertDialog.Builder(getActivity()); dlgBld.setMessage(msg); dlgBld.setPositiveButton(button, new DialogInterface.OnClickListener() { @Override public void onClick (final DialogInterface dialog, final int which) { doFav(account, tweet); } }); dlgBld.setNegativeButton(android.R.string.cancel, DialogHelper.DLG_CANCEL_CLICK_LISTENER); dlgBld.show(); } protected void doFav (final Account account, final Tweet tweet) { getDb().addPostToOutput(OutboxActionFactory.newFav(account, tweet)); getActivity().startService(new Intent(getActivity(), SendOutboxService.class)); Toast.makeText(getActivity(), R.string.tweetlist_requested_via_outbox, Toast.LENGTH_SHORT).show(); } private List<File> cachedPictureFilesFor (final Tweet tweet) { final List<File> ret = new ArrayList<File>(); for (final Meta m : tweet.getMetas()) { if (m.getType() == MetaType.MEDIA) { final File file = getMainActivity().getImageCache().getCachedFile(m.getData()); if (file != null) ret.add(file); } } return ret; } private void shareIntentBtnClicked (final View btn, final Tweet tweet) { final PopupMenu menu = new PopupMenu(getActivity(), btn); menu.getMenuInflater().inflate(R.menu.sharetweetmenu, menu.getMenu()); menu.setOnMenuItemClickListener(new ShareMenuClickListener(this, tweet)); if (cachedPictureFilesFor(tweet).size() < 1) { menu.getMenu().findItem(R.id.mnuPicture).setVisible(false); } menu.show(); } private static class ShareMenuClickListener implements PopupMenu.OnMenuItemClickListener { private final TweetListFragment host; private final Tweet tweet; public ShareMenuClickListener (final TweetListFragment host, final Tweet tweet) { this.host = host; this.tweet = tweet; } @Override public boolean onMenuItemClick (final MenuItem item) { return this.host.shareMenuItemClick(item, this.tweet); } } protected boolean shareMenuItemClick (final MenuItem item, final Tweet tweet) { switch (item.getItemId()) { case R.id.mnuLink: doShareIntentLink(tweet); return true; case R.id.mnuText: doShareIntentText(tweet); return true; case R.id.mnuPicture: doShareIntentPicture(tweet); return true; default: return false; } } private void doShareIntentLink (final Tweet tweet) { startActivity(new Intent(Intent.ACTION_VIEW) .setData(Uri.parse(TwitterUrls.tweet(tweet)))); } private void doShareIntentText (final Tweet tweet) { final Intent i = new Intent(); i.setAction(Intent.ACTION_SEND); i.putExtra(Intent.EXTRA_SUBJECT, tweet.toHumanLine()); i.putExtra(Intent.EXTRA_TEXT, tweet.toHumanParagraph()); i.setType("text/plain"); startActivity(Intent.createChooser(i, getMainActivity().getString(R.string.tweetlist_send_text_to))); } private void doShareIntentPicture (final Tweet tweet) { final ArrayList<Uri> uris = FileHelper.filesToProvidedUris(getActivity(), CachedImageFileProvider.addFileExtensions( cachedPictureFilesFor(tweet))); this.log.i("Sharing URIs: %s", uris); // https://developer.android.com/intl/en/training/sharing/send.html // https://developer.android.com/intl/en/reference/android/content/Intent.html final Intent i = new Intent(); i.setAction(Intent.ACTION_SEND_MULTIPLE); i.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); i.putExtra(Intent.EXTRA_SUBJECT, tweet.toHumanLine()); i.putExtra(Intent.EXTRA_TEXT, tweet.toHumanParagraph()); i.setType("image/*"); i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(i, getMainActivity().getString(R.string.tweetlist_send_picutre_to))); } private void askDeleteTweet (final Tweet tweet) { final Account account = MetaUtils.accountFromMeta(tweet, this.conf); if (account == null) { DialogHelper.alert(getActivity(), getMainActivity().getString(R.string.tweetlist_can_not_find_this_tweet_s_account_metadata)); return; } final AlertDialog.Builder dlgBld = new AlertDialog.Builder(getActivity()); dlgBld.setMessage(String.format("%s\n%s", getMainActivity().getString(R.string.tweetlist_permanently_delete_update), summariseAccountAndSubAccount(account, tweet))); dlgBld.setPositiveButton(R.string.tweetlist_btn_delete, new DialogInterface.OnClickListener() { @Override public void onClick (final DialogInterface dialog, final int which) { doDelete(account, tweet); } }); dlgBld.setNegativeButton(android.R.string.cancel, DialogHelper.DLG_CANCEL_CLICK_LISTENER); dlgBld.show(); } protected void doDelete (final Account account, final Tweet tweet) { getDb().addPostToOutput(OutboxActionFactory.newDelete(account, tweet)); getActivity().startService(new Intent(getActivity(), SendOutboxService.class)); Toast.makeText(getActivity(), R.string.tweetlist_deleted_via_outbox, Toast.LENGTH_SHORT).show(); } private static class DetailsLaterClickListener implements OnClickListener { private final TweetListFragment tweetListFragment; private final Tweet tweet; private final boolean isLaterColumn; public DetailsLaterClickListener (final TweetListFragment tweetListFragment, final Tweet tweet, final boolean isLaterColumn) { this.tweetListFragment = tweetListFragment; this.tweet = tweet; this.isLaterColumn = isLaterColumn; } @Override public void onClick (final View v) { if (this.isLaterColumn) { new SetLaterTask(this.tweetListFragment, this.tweet, LaterState.READ).executeOnExecutor(this.tweetListFragment.getLocalEs()); } else { new SetLaterTask(this.tweetListFragment, this.tweet, LaterState.UNREAD).executeOnExecutor(this.tweetListFragment.getLocalEs()); } } } public enum LaterState { UNREAD, READ; } private static class SetLaterTask extends AsyncTask<Void, Void, Exception> { private final TweetListFragment parent; private final Tweet tweet; private final LaterState laterState; public SetLaterTask (final TweetListFragment parent, final Tweet tweet, final LaterState laterState) { this.parent = parent; this.tweet = tweet; this.laterState = laterState; } @Override protected void onPreExecute () { this.parent.btnDetailsLater.setEnabled(false); } @Override protected Exception doInBackground (final Void... params) { try { final Column col = this.parent.getConf().findInternalColumn(InternalColumnType.LATER); if (col == null) return new IllegalStateException("Read later column not configured."); switch (this.laterState) { case UNREAD: this.parent.getDb().storeTweets(col, Collections.singletonList(this.tweet.cloneWithCurrentTimestamp())); return null; case READ: this.parent.getDb().deleteTweet(col, this.tweet); return null; default: return new IllegalStateException("Unknown read later state: " + this.laterState); } } catch (final Exception e) { // NOSONAR To report status. return e; } } @Override protected void onPostExecute (final Exception result) { if (result == null) { if (this.parent.lstTweetPayloadAdaptor.isForTweet(this.tweet)) this.parent.setReadLaterButton(this.tweet, this.laterState != LaterState.READ); switch (this.laterState) { case UNREAD: Toast.makeText(this.parent.getMainActivity(), R.string.tweetlist_saved_for_later, Toast.LENGTH_SHORT).show(); break; case READ: Toast.makeText(this.parent.getMainActivity(), R.string.tweetlist_removed_from_later, Toast.LENGTH_SHORT).show(); break; default: DialogHelper.alert(this.parent.getMainActivity(), "Unknown read later state: " + this.laterState); } } else { DialogHelper.alert(this.parent.getMainActivity(), result); } this.parent.btnDetailsLater.setEnabled(true); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - protected TwUpdateListener getGuiUpdateListener () { return this.guiUpdateListener; } private final TwUpdateListener guiUpdateListener = new TwUpdateListener() { @Override public void columnChanged (final int changeColumnId) { if (changeColumnId == getColumnId()) { refreshUi(); } else { final Set<Integer> ex = getColumn().getExcludeColumnIds(); if (ex != null && ex.contains(Integer.valueOf(changeColumnId))) refreshUi(); } } @Override public void columnStatus (final int eventColumnId, final ColumnState eventType) { if (eventColumnId != getColumnId()) return; statusChanged(eventType); } @Override public void unreadOrScrollChanged (final int eventColumnId, final ScrollChangeType type) { if (eventColumnId != getColumnId()) return; refreshUnread(type); } @Override public Integer requestStoreScrollStateNow () { requestSaveCurrentScrollToDb(); return getColumnId(); } @Override public void scrollStored (final int eventColumnId) {/* unused */} }; private enum Msgs { REFRESH, UPDATE_NOT_STARTED, UPDATE_RUNNING, UPDATE_OVER, STILL_SCROLLING_CHECK, UNREAD_CHANGED, UNREAD_AND_SCROLL_CHANGED, SAVE_SCROLL; public static final Msgs values[] = values(); // Optimisation to avoid new array every time. } protected void refreshUi () { this.refreshUiHandler.sendEmptyMessage(Msgs.REFRESH.ordinal()); } protected void statusChanged (final ColumnState state) { switch (state) { case NOT_STARTED: this.refreshUiHandler.sendEmptyMessage(Msgs.UPDATE_NOT_STARTED.ordinal()); break; case UPDATE_RUNNING: this.refreshUiHandler.sendEmptyMessage(Msgs.UPDATE_RUNNING.ordinal()); break; case UPDATE_OVER: this.refreshUiHandler.sendEmptyMessage(Msgs.UPDATE_OVER.ordinal()); break; default: } } protected void refreshUnread (final ScrollChangeType type) { switch (type) { case UNREAD: this.refreshUiHandler.sendEmptyMessage(Msgs.UNREAD_CHANGED.ordinal()); break; case UNREAD_AND_SCROLL: this.refreshUiHandler.sendEmptyMessage(Msgs.UNREAD_AND_SCROLL_CHANGED.ordinal()); break; default: } } protected void requestSaveCurrentScrollToDb () { this.refreshUiHandler.sendEmptyMessage(Msgs.SAVE_SCROLL.ordinal()); } private static class RefreshUiHandler extends Handler { private final WeakReference<TweetListFragment> parentRef; public RefreshUiHandler (final TweetListFragment parent) { this.parentRef = new WeakReference<TweetListFragment>(parent); } @Override public void handleMessage (final Message msg) { final TweetListFragment parent = this.parentRef.get(); if (parent != null) parent.msgOnUiThread(msg); } } protected void msgOnUiThread (final Message msg) { switch (Msgs.values[msg.what]) { case REFRESH: refreshUiOnUiThread(); break; case UPDATE_NOT_STARTED: this.tweetListSwiper.setRefreshing(false); break; case UPDATE_RUNNING: getMainActivity().progressIndicator(true); this.tweetListEmptyRefresh.setEnabled(false); break; case UPDATE_OVER: getMainActivity().progressIndicator(false); this.tweetListEmptyRefresh.setEnabled(true); this.tweetListSwiper.setRefreshing(false); redrawLastUpdateError(); break; case STILL_SCROLLING_CHECK: checkIfTweetListStillScrolling(); break; case UNREAD_CHANGED: restoreScrollFromDbIfNewer(ScrollChangeType.UNREAD); break; case UNREAD_AND_SCROLL_CHANGED: restoreScrollFromDbIfNewer(ScrollChangeType.UNREAD_AND_SCROLL); break; case SAVE_SCROLL: saveCurrentScrollToDb(); break; default: } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - private void refreshUiOnUiThread () { new LoadTweets(getExecutorEventListener(), this).executeOnExecutor(getLocalEs()); } private static class LoadTweets extends TrackingAsyncTask<Void, Void, Result<Cursor>> { private final TweetListFragment host; public LoadTweets (final ExecutorEventListener eventListener, final TweetListFragment host) { super(eventListener); this.host = host; } @Override public String toString () { return "loadTweets:" + this.host.getColumnPosition() + ":" + this.host.getColumn().getTitle(); } @Override protected void onPreExecute () { this.host.getMainActivity().progressIndicator(true); this.host.tweetListEmptyRefresh.setEnabled(false); } @Override protected Result<Cursor> doInBackgroundWithTracking (final Void... params) { try { final DbInterface db = this.host.getDb(); if (db != null) { final Cursor cursor = db.getTweetsCursor( this.host.getColumnId(), this.host.showFiltered && this.host.getInlineMediaStyle() != InlineMediaStyle.SEAMLESS ? Selection.ALL : Selection.FILTERED, this.host.getColumn().getExcludeColumnIds(), this.host.getInlineMediaStyle() == InlineMediaStyle.SEAMLESS); return new Result<Cursor>(cursor); } return new Result<Cursor>(new IllegalStateException("Failed to refresh column as DB was not bound.")); } catch (final Exception e) { // NOSONAR needed to report errors. return new Result<Cursor>(e); } } @Override protected void onPostExecute (final Result<Cursor> result) { if (result.isSuccess()) { this.host.saveScrollIfNotSaved(); this.host.getAdapter().changeCursor(result.getData()); this.host.getLog().d("Refreshed tweets cursor."); this.host.restoreScroll(); this.host.scrollToStashedTweet(); this.host.redrawLastUpdateError(); } else { this.host.getLog().w("Failed to refresh column.", result.getE()); } this.host.getMainActivity().progressIndicator(false); this.host.tweetListEmptyRefresh.setEnabled(true); } } private final OnClickListener refreshClickListener = new OnClickListener() { @Override public void onClick (final View v) { getMainActivity().scheduleRefreshInteractive(getColumnId()); } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - private long lastScrollFirstVisiblePosition = -1; private long lastScrollTime = 0L; private ScrollDirection lastScrollDirection = ScrollDirection.UNKNOWN; protected void onTweetListScroll (final ScrollDirection direction) { if (direction != ScrollDirection.UNKNOWN && direction != this.lastScrollDirection) this.lastScrollDirection = direction; final int position = this.tweetList.getFirstVisiblePosition(); if (position < 0 || position == this.lastScrollFirstVisiblePosition) return; this.lastScrollFirstVisiblePosition = position; final long now = System.currentTimeMillis(); this.lastScrollTime = now; this.refreshUiHandler.sendEmptyMessageDelayed(Msgs.STILL_SCROLLING_CHECK.ordinal(), C.SCROLL_TIME_LABEL_TIMEOUT_MILLIS); final long time = this.adapter.getItemTime(position); if (time > 0L) { getMainActivity().setTempColumnTitle(this.columnPosition, this.friendlyDateTimeFormat.format(now, TimeUnit.SECONDS.toMillis(time))); } } private void checkIfTweetListStillScrolling () { if (this.lastScrollTime < 1L || System.currentTimeMillis() - this.lastScrollTime >= C.SCROLL_TIME_LABEL_TIMEOUT_MILLIS) { updateScrollLabelToIdle(); } } private void updateScrollLabelToIdle () { getMainActivity().setTempColumnTitle(this.columnPosition, null); this.lastScrollFirstVisiblePosition = -1; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - private void redrawLastUpdateError () { final String msg; final DbInterface db = getDb(); if (db != null) { msg = db.getValue(KvKeys.colLastRefreshError(this.columnId)); } else { msg = "Can not get last update status; DB not connected."; this.log.w(msg); } if (msg != null) { this.tweetListStatus.setText(msg); this.tweetListStatus.setVisibility(View.VISIBLE); } else { this.tweetListStatus.setVisibility(View.GONE); this.tweetListStatus.setText(""); } } protected void popupLastUpdateError () { DialogHelper.alert(getActivity(), this.tweetListStatus.getText().toString()); } protected void dismissLastUpdateError () { FetchColumn.storeDismiss(getDb(), getColumn()); redrawLastUpdateError(); } protected void copyLastUpdateError () { final ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); clipboard.setPrimaryClip(ClipData.newPlainText("Error Message", this.tweetListStatus.getText().toString())); //ES } private enum ErrorMessageAction implements Titleable { VIEW("View") { //ES @Override public void onClick (final TweetListFragment tlf) { tlf.popupLastUpdateError(); } }, COPY("Copy") { //ES @Override public void onClick (final TweetListFragment tlf) { tlf.copyLastUpdateError(); } }, DISMISS("Dismiss") { //ES @Override public void onClick (final TweetListFragment tlf) { tlf.dismissLastUpdateError(); } }; private final String title; private ErrorMessageAction (final String title) { this.title = title; } @Override public String getUiTitle () { return this.title; } public abstract void onClick (TweetListFragment tlf); } private final OnClickListener tweetListStatusClickListener = new OnClickListener() { @Override public void onClick (final View v) { DialogHelper.askItem(getActivity(), "Message", ErrorMessageAction.values(), new Listener<ErrorMessageAction>() { //ES @Override public void onAnswer (final ErrorMessageAction answer) { answer.onClick(TweetListFragment.this); } }); } }; }