/* * Copyright (c) 2015, Nils Braden * * This file is part of ttrss-reader-fork. This program is free software; you * can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; * either version 3 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. You should have received a * copy of the GNU General Public License along with this program; If * not, see http://www.gnu.org/licenses/. */ package org.ttrssreader.gui.fragments; import org.htmlcleaner.HtmlCleaner; import org.htmlcleaner.HtmlNode; import org.htmlcleaner.TagNode; import org.htmlcleaner.TagNodeVisitor; import org.jsoup.Jsoup; import org.jsoup.safety.Whitelist; import org.stringtemplate.v4.ST; import org.ttrssreader.R; import org.ttrssreader.controllers.Controller; import org.ttrssreader.controllers.DBHelper; import org.ttrssreader.controllers.ProgressBarManager; import org.ttrssreader.gui.ErrorActivity; import org.ttrssreader.gui.FeedHeadlineActivity; import org.ttrssreader.gui.MenuActivity; import org.ttrssreader.gui.TextInputAlert; import org.ttrssreader.gui.dialogs.ArticleLabelDialog; import org.ttrssreader.gui.dialogs.ImageCaptionDialog; import org.ttrssreader.gui.interfaces.TextInputAlertCallback; import org.ttrssreader.gui.view.ArticleWebViewClient; import org.ttrssreader.gui.view.MyGestureDetector; import org.ttrssreader.gui.view.MyWebView; import org.ttrssreader.imageCache.ImageCache; import org.ttrssreader.model.pojos.Article; import org.ttrssreader.model.pojos.Feed; import org.ttrssreader.model.pojos.Label; import org.ttrssreader.model.pojos.RemoteFile; import org.ttrssreader.model.updaters.ArticleReadStateUpdater; import org.ttrssreader.model.updaters.NoteUpdater; import org.ttrssreader.model.updaters.PublishedStateUpdater; import org.ttrssreader.model.updaters.StarredStateUpdater; import org.ttrssreader.model.updaters.Updater; import org.ttrssreader.preferences.Constants; import org.ttrssreader.utils.AsyncTask; import org.ttrssreader.utils.DateUtils; import org.ttrssreader.utils.FileUtils; import android.animation.Animator; import android.animation.AnimatorInflater; import android.annotation.SuppressLint; import android.app.Activity; import android.app.DialogFragment; import android.app.Fragment; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.text.Html; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; import android.webkit.JavascriptInterface; import android.webkit.WebSettings; import android.webkit.WebSettings.LayoutAlgorithm; import android.webkit.WebView; import android.webkit.WebView.HitTestResult; import android.widget.Button; import android.widget.FrameLayout; import java.lang.ref.WeakReference; import java.util.Collection; import java.util.Map; import java.util.Set; public class ArticleFragment extends Fragment implements TextInputAlertCallback { private static final String TAG = ArticleFragment.class.getSimpleName(); public static final String FRAGMENT = "ARTICLE_FRAGMENT"; private static final String ARTICLE_ID = "ARTICLE_ID"; private static final String ARTICLE_FEED_ID = "ARTICLE_FEED_ID"; private static final String ARTICLE_MOVE = "ARTICLE_MOVE"; private static final int CONTEXT_MENU_SHARE_URL = 1000; private static final int CONTEXT_MENU_SHARE_ARTICLE = 1001; private static final int CONTEXT_MENU_DISPLAY_CAPTION = 1002; private static final int CONTEXT_MENU_COPY_URL = 1003; private static final int CONTEXT_MENU_COPY_CONTENT = 1004; private static final String LABEL_COLOR_STRING = "<span style=\"color: %s; background-color: %s\">%s</span>"; // Extras private int articleId = -1; private int feedId = -1; private int categoryId = Integer.MIN_VALUE; private boolean selectArticlesForCategory = false; private int lastMove = 0; private Article article = null; private Feed feed = null; private String content; private boolean linkAutoOpened; private String cachedImages = ""; private FrameLayout webContainer = null; private MyWebView webView; private boolean webviewInitialized = false; private Button buttonNext; private Button buttonPrev; private String mSelectedExtra; private String mSelectedAltText; private ArticleJSInterface articleJSInterface; private GestureDetector gestureDetector = null; private View.OnTouchListener gestureListener = null; public static ArticleFragment newInstance(int id, int feedId, int categoryId, boolean selectArticles, int lastMove) { // Create a new fragment instance ArticleFragment detail = new ArticleFragment(); detail.articleId = id; detail.feedId = feedId; detail.categoryId = categoryId; detail.selectArticlesForCategory = selectArticles; detail.lastMove = lastMove; detail.setRetainInstance(true); return detail; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.articleitem, container, false); } @Override public void onCreate(Bundle instance) { if (instance != null) { articleId = instance.getInt(ARTICLE_ID); feedId = instance.getInt(ARTICLE_FEED_ID); categoryId = instance.getInt(FeedHeadlineListFragment.FEED_CAT_ID); selectArticlesForCategory = instance.getBoolean(FeedHeadlineListFragment.FEED_SELECT_ARTICLES); lastMove = instance.getInt(ARTICLE_MOVE); if (webView != null) webView.restoreState(instance); } super.onCreate(instance); } @Override public void onActivityCreated(Bundle instance) { super.onActivityCreated(instance); articleJSInterface = new ArticleJSInterface(getActivity()); initData(); initUI(); } @Override public void onConfigurationChanged(Configuration newConfig) { // Remove the WebView from the old placeholder if (webView != null) webContainer.removeView(webView); super.onConfigurationChanged(newConfig); initUI(); doRefresh(); } @Override public void onSaveInstanceState(Bundle instance) { instance.putInt(ARTICLE_ID, articleId); instance.putInt(ARTICLE_FEED_ID, feedId); instance.putInt(FeedHeadlineListFragment.FEED_CAT_ID, categoryId); instance.putBoolean(FeedHeadlineListFragment.FEED_SELECT_ARTICLES, selectArticlesForCategory); instance.putInt(ARTICLE_MOVE, lastMove); if (webView != null) webView.saveState(instance); super.onSaveInstanceState(instance); } @SuppressLint("ClickableViewAccessibility") private void initUI() { // Wrap webview inside another FrameLayout to avoid memory leaks as described here: // http://stackoverflow.com/questions/3130654/memory-leak-in-webview webContainer = (FrameLayout) getActivity().findViewById(R.id.article_webView_Container); buttonPrev = (Button) getActivity().findViewById(R.id.article_buttonPrev); buttonNext = (Button) getActivity().findViewById(R.id.article_buttonNext); buttonPrev.setOnClickListener(onButtonPressedListener); buttonNext.setOnClickListener(onButtonPressedListener); // Initialize the WebView if necessary if (webView == null) { webView = new MyWebView(getActivity()); webView.setWebViewClient(new ArticleWebViewClient()); webView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // Set theme background color. Should be possible via xml also. switch (Controller.getInstance().getTheme()) { case R.style.TTRSS_Light: webView.setBackgroundColor(getResources().getColor(R.color.themeLightBackground)); case R.style.TTRSS_White: webView.setBackgroundColor(getResources().getColor(R.color.themeWhiteBackground)); case R.style.TTRSS_Dark: webView.setBackgroundColor(getResources().getColor(R.color.themeDarkBackground)); case R.style.TTRSS_Black: webView.setBackgroundColor(getResources().getColor(R.color.themeBlackBackground)); } boolean supportZoom = Controller.getInstance().supportZoomControls(); webView.getSettings().setSupportZoom(supportZoom); webView.getSettings().setBuiltInZoomControls(supportZoom); webView.getSettings().setDisplayZoomControls(false); webView.getSettings().setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN); webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY); webView.setScrollbarFadingEnabled(true); webView.setOnKeyListener(keyListener); webView.getSettings().setTextZoom(Controller.getInstance().textZoom()); webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); if (gestureDetector == null || gestureListener == null) { ActionBar actionBar = ((MenuActivity) getActivity()).getSupportActionBar(); // Detect touch gestures like swipe and scroll down: gestureDetector = new GestureDetector(getActivity(), new ArticleGestureDetector(actionBar, Controller.getInstance().hideActionbar())); gestureListener = new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { gestureDetector.onTouchEvent(event); // Call webView.onTouchEvent(event) everytime, seems to fix issues with webview not beeing // refreshed after swiping: return webView.onTouchEvent(event) || v.performClick(); } }; } webView.setOnTouchListener(gestureListener); } registerForContextMenu(webView); getActivity().findViewById(R.id.article_button_view).setVisibility( Controller.getInstance().showButtonsMode() == Constants.SHOW_BUTTONS_MODE_ALLWAYS ? View.VISIBLE : View.GONE); // Attach the WebView to its placeholder if (webView.getParent() != null && webView.getParent() instanceof FrameLayout) ((FrameLayout) webView.getParent()).removeAllViews(); webContainer.addView(webView); setHasOptionsMenu(true); } private void initData() { if (feedId > 0) Controller.getInstance().lastOpenedFeeds.add(feedId); Controller.getInstance().lastOpenedArticles.add(articleId); /* Move database access to background: */ new AsyncTask<Void, Void, Void>() { protected Void doInBackground(Void... params) { // Get article from DB article = DBHelper.getInstance().getArticle(articleId); if (article == null) return null; feed = DBHelper.getInstance().getFeed(article.feedId); // Mark as read if necessary, do it here because in doRefresh() it will be done several times even if // you set it to "unread" in the meantime. if (article.isUnread) { article.isUnread = false; new Updater(null, new ArticleReadStateUpdater(article, 0)) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } cachedImages = getCachedImagesJS(article.id); // Reload content on next doRefresh() webviewInitialized = false; return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); // Has to be called from UI thread if (getActivity() != null) { doRefresh(); getActivity().invalidateOptionsMenu(); // Force redraw of menu items in actionbar } } }.execute(); } @Override public void onResume() { super.onResume(); if (getView() != null) getView().setVisibility(View.VISIBLE); } @Override public void onStop() { super.onStop(); if (getView() != null) getView().setVisibility(View.GONE); } @Override public void onDestroy() { super.onDestroy(); if (webContainer != null) webContainer.removeAllViews(); if (webView != null) webView.destroy(); } @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) private void doRefresh() { if (webView == null) return; try { ProgressBarManager.getInstance().addProgress((MenuActivity) getActivity()); if (Controller.getInstance().workOffline() || !Controller.getInstance().loadMedia()) { webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ONLY); } else { webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT); } if (!Controller.getInstance().loadMedia() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) webView.getSettings().setMediaPlaybackRequiresUserGesture(false); // No need to reload everything if (webviewInitialized) return; // Check for errors if (Controller.getInstance().getConnector().hasLastError()) { Intent i = new Intent(getActivity(), ErrorActivity.class); i.putExtra(ErrorActivity.ERROR_MESSAGE, Controller.getInstance().getConnector().pullLastError()); startActivityForResult(i, ErrorActivity.ACTIVITY_SHOW_ERROR); return; } StringBuilder labels = new StringBuilder(); for (Label label : article.labels) { if (label.checked) { if (labels.length() > 0) labels.append(", "); String labelString = label.caption; if (label.foregroundColor != null && label.backgroundColor != null) labelString = String .format(LABEL_COLOR_STRING, label.foregroundColor, label.backgroundColor, label.caption); labels.append(labelString); } } // Remove all html tags and content that doesn't meet this set of allowed stuff final String contentClean; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) contentClean = article.content; else contentClean = Jsoup.clean(article.content, Whitelist.relaxed()); // Load html from Controller and insert content// Article-Prefetch-Stuff from Raw-Ressources and System ST htmlTmpl = new ST(getString(R.string.HTML_TEMPLATE), '$', '$'); // Styles if (Controller.getInstance().allowHyphenation()) { ST javascriptST = new ST(getString(R.string.JAVASCRIPT_HYPHENATION_TEMPLATE), '$', '$'); javascriptST.add("LANGUAGE", Controller.getInstance().hyphenationLanguage()); htmlTmpl.add("HYPHENATION", javascriptST.render()); } // Replace alignment-marker: align:left or align:justify ST stylesST = new ST(getString(R.string.STYLE_TEMPLATE), '$', '$'); if (Controller.getInstance().alignFlushLeft()) { stylesST.add("TEXT_ALIGN", getString(R.string.ALIGN_LEFT)); } else { stylesST.add("TEXT_ALIGN", getString(R.string.ALIGN_JUSTIFY)); } htmlTmpl.add("STYLE", stylesST.render()); // General values htmlTmpl.add("THEME", getResources().getString(Controller.getInstance().getThemeHTML())); htmlTmpl.add("CACHE_DIR", Controller.getInstance().cacheFolder()); htmlTmpl.add("LANGUAGE", Controller.getInstance().hyphenationLanguage()); // Special values for this article htmlTmpl.add("article", article); htmlTmpl.add("feed", feed); htmlTmpl.add("CACHED_IMAGES", cachedImages); htmlTmpl.add("LABELS", labels.toString()); htmlTmpl.add("UPDATED", DateUtils.getDateTimeCustom(getActivity(), article.updated)); htmlTmpl.add("ATTACHMENTS", getAttachmentsMarkup(article.attachments)); htmlTmpl.add("CONTENT", contentClean); // Hyphenation Javascript if (Controller.getInstance().allowHyphenation()) { ST javascriptST = new ST(getString(R.string.JAVASCRIPT_HYPHENATION_TEMPLATE), '$', '$'); javascriptST.add("LANGUAGE", Controller.getInstance().hyphenationLanguage()); htmlTmpl.add("HYPHENATION", javascriptST.render()); } // Navigation buttons if (Controller.getInstance().showButtonsMode() == Constants.SHOW_BUTTONS_MODE_HTML) { htmlTmpl.add("NAVIGATION", getString(R.string.BOTTOM_NAVIGATION_TEMPLATE)); } // Note of the article if (article.note != null && article.note.length() > 0) { ST noteST = new ST(getResources().getString(R.string.NOTE_TEMPLATE), '$', '$'); noteST.add("NOTE", getResources().getString(R.string.Commons_HtmlPrefixNote) + " " + article.note); htmlTmpl.add("NOTE_TEMPLATE", noteST.render()); } content = htmlTmpl.render(); /* JavaScript should be safe since we use JSoup to remove all unwanted stuff from article.content */ webView.getSettings().setJavaScriptEnabled(true); webView.addJavascriptInterface(articleJSInterface, "articleController"); webView.loadDataWithBaseURL("file:///android_asset/", content, "text/html", "utf-8", null); if (!linkAutoOpened && article.content.length() < 3) { if (Controller.getInstance().openUrlEmptyArticle()) { Log.i(TAG, "Article-Content is empty, opening URL in browser"); linkAutoOpened = true; openLink(); } } // Everything did load, we dont have to do this again. webviewInitialized = true; } catch (Exception e) { Log.w(TAG, e.getClass().getSimpleName() + " in doRefresh(): " + e.getMessage() + " (" + e.getCause() + ")", e); } finally { ProgressBarManager.getInstance().removeProgress((MenuActivity) getActivity()); } } /** * Starts a new activity with the url of the current article. This should open a webbrowser in most cases. If the * url contains spaces or newline-characters it is first trim()'ed. */ private void openLink() { if (article.url == null || article.url.length() == 0) return; String url = article.url; if (article.url.contains(" ") || article.url.contains("\n")) url = url.trim(); try { Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); startActivity(i); } catch (ActivityNotFoundException e) { Log.e(TAG, "Couldn't find a suitable activity for the uri: " + url); } } /** * generate HTML code for attachments to be shown inside article * * @param attachments collection of attachment URLs */ private String getAttachmentsMarkup(Set<String> attachments) { StringBuilder content = new StringBuilder(); Map<String, Collection<String>> attachmentsByMimeType = FileUtils.groupFilesByMimeType(attachments); if (attachmentsByMimeType.isEmpty()) return ""; for (String mimeType : attachmentsByMimeType.keySet()) { Collection<String> mimeTypeUrls = attachmentsByMimeType.get(mimeType); if (mimeTypeUrls.isEmpty()) return ""; if (mimeType.equals(FileUtils.IMAGE_MIME)) { ST st = new ST(getResources().getString(R.string.ATTACHMENT_IMAGES_TEMPLATE)); st.add("items", mimeTypeUrls); content.append(st.render()); } else { ST st = new ST(getResources().getString(R.string.ATTACHMENT_MEDIA_TEMPLATE)); st.add("items", mimeTypeUrls); CharSequence linkText = mimeType.equals(FileUtils.AUDIO_MIME) || mimeType.equals(FileUtils.VIDEO_MIME) ? getText(R.string.ArticleActivity_MediaPlay) : getText(R.string.ArticleActivity_MediaDisplayLink); st.add("linkText", linkText); content.append(st.render()); } } return content.toString(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); HitTestResult result = ((WebView) v).getHitTestResult(); menu.setHeaderTitle(getResources().getString(R.string.ArticleActivity_ShareLink)); mSelectedExtra = null; mSelectedAltText = null; int type = result.getType(); boolean image = (type == HitTestResult.SRC_IMAGE_ANCHOR_TYPE || type == HitTestResult.IMAGE_TYPE); boolean anchor = (type == HitTestResult.SRC_IMAGE_ANCHOR_TYPE || type == HitTestResult.SRC_ANCHOR_TYPE); // Anchors get a context-menu with "Share URL" and "Copy URL" if (anchor) { mSelectedExtra = result.getExtra(); menu.add(ContextMenu.NONE, CONTEXT_MENU_SHARE_URL, 2, getResources().getString(R.string.ArticleActivity_ShareURL)); menu.add(ContextMenu.NONE, CONTEXT_MENU_COPY_URL, 3, getResources().getString(R.string.ArticleActivity_CopyURL)); } // Images get a context-menu with "Show caption" which displays the content of the title- or alt-attribute if (image) { mSelectedAltText = getAltTextForImageUrl(result.getExtra()); if (mSelectedAltText != null) menu.add(ContextMenu.NONE, CONTEXT_MENU_DISPLAY_CAPTION, 1, getResources().getString(R.string.ArticleActivity_ShowCaption)); } menu.add(ContextMenu.NONE, CONTEXT_MENU_SHARE_ARTICLE, 10, getResources().getString(R.string.ArticleActivity_ShareArticle)); menu.add(ContextMenu.NONE, CONTEXT_MENU_COPY_CONTENT, 4, getResources().getString(R.string.ArticleActivity_CopyContent)); } public void onPrepareOptionsMenu(Menu menu) { if (getActivity() == null || article == null) return; MenuItem read = menu.findItem(R.id.Article_Menu_MarkRead); if (read != null) { if (article.isUnread) { read.setTitle(getString(R.string.Commons_MarkRead)); read.setIcon(R.drawable.ic_menu_mark); } else { read.setTitle(getString(R.string.Commons_MarkUnread)); read.setIcon(R.drawable.ic_menu_clear_playlist); } } MenuItem publish = menu.findItem(R.id.Article_Menu_MarkPublish); if (publish != null) { if (article.isPublished) { publish.setTitle(getString(R.string.Commons_MarkUnpublish)); publish.setIcon(R.drawable.menu_published); } else { publish.setTitle(getString(R.string.Commons_MarkPublish)); publish.setIcon(R.drawable.menu_publish); } } MenuItem star = menu.findItem(R.id.Article_Menu_MarkStar); if (star != null) { if (article.isStarred) { star.setTitle(getString(R.string.Commons_MarkUnstar)); star.setIcon(R.drawable.menu_starred); } else { star.setTitle(getString(R.string.Commons_MarkStar)); star.setIcon(R.drawable.ic_menu_star); } } } public boolean onOptionsItemSelected(MenuItem item) { if (article == null) return false; // No article object -> no action! switch (item.getItemId()) { case R.id.Article_Menu_MarkRead: { new Updater(getActivity(), new ArticleReadStateUpdater(article, article.isUnread ? 0 : 1)) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); return true; } case R.id.Article_Menu_MarkStar: { new Updater(getActivity(), new StarredStateUpdater(article, article.isStarred ? 0 : 1)) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); return true; } case R.id.Article_Menu_MarkPublish: { new Updater(getActivity(), new PublishedStateUpdater(article, article.isPublished ? 0 : 1)) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); return true; } case R.id.Article_Menu_MarkNote: { new TextInputAlert(this, article).show(getActivity()); return true; } case R.id.Article_Menu_AddArticleLabel: { DialogFragment dialog = ArticleLabelDialog.newInstance(article.id); dialog.show(getFragmentManager(), "Edit Labels"); return true; } case R.id.Article_Menu_ShareLink: { Intent i = new Intent(Intent.ACTION_SEND); i.setType("text/plain"); i.putExtra(Intent.EXTRA_TEXT, article.url); i.putExtra(Intent.EXTRA_SUBJECT, article.title); startActivity(Intent.createChooser(i, getText(R.string.ArticleActivity_ShareTitle))); return true; } default: return false; } } /** * Using a small html parser with a visitor which goes through the html I extract the alt-attribute from the * content. If nothing is found it is left as null and the menu should'nt contain the item to display the caption. * * @param extra the * @return the alt-text or null if none was found. */ private String getAltTextForImageUrl(String extra) { if (content == null || !content.contains(extra)) return null; HtmlCleaner cleaner = new HtmlCleaner(); TagNode node = cleaner.clean(content); MyTagNodeVisitor tnv = new MyTagNodeVisitor(extra); node.traverse(tnv); if (tnv.alt == null) return null; return Html.fromHtml(tnv.alt).toString(); } /** * Create javascript associative array with article cached image url as key and image hash as value. Only * RemoteFiles which are "cached" are added to this array so if an image is not available locally it is left as it * is. * * @param articleId article ID * @return javascript associative array content as text */ private String getCachedImagesJS(int articleId) { StringBuilder hashes = new StringBuilder(""); Collection<RemoteFile> rfs = DBHelper.getInstance().getRemoteFiles(articleId); if (rfs != null && !rfs.isEmpty()) { for (RemoteFile rf : rfs) { if (rf.cached) { if (hashes.length() > 0) hashes.append(",\n"); hashes.append("'"); hashes.append(rf.url); hashes.append("': '"); hashes.append(ImageCache.getHashForKey(rf.url)); hashes.append("'"); } } } return hashes.toString(); } /** * This is necessary to iterate over all HTML-Nodes and scan for images with ALT-Attributes. */ private class MyTagNodeVisitor implements TagNodeVisitor { private String alt = null; private String extra; private MyTagNodeVisitor(String extra) { this.extra = extra; } public boolean visit(TagNode tagNode, HtmlNode htmlNode) { if (htmlNode instanceof TagNode) { TagNode tag = (TagNode) htmlNode; String tagName = tag.getName(); // Only if the image-url is the same as the url of the image the long-press was on: if ("img".equals(tagName) && extra.equals(tag.getAttributeByName("src"))) { // Prefer title-attribute over alt since this is the html default alt = tag.getAttributeByName("title"); if (alt != null) return false; alt = tag.getAttributeByName("alt"); if (alt != null) return false; } } return true; } } @Override public boolean onContextItemSelected(android.view.MenuItem item) { Intent shareIntent; switch (item.getItemId()) { case CONTEXT_MENU_SHARE_URL: if (mSelectedExtra != null) { shareIntent = getUrlShareIntent(mSelectedExtra); startActivity(Intent.createChooser(shareIntent, "Share URL")); } break; case CONTEXT_MENU_COPY_URL: if (mSelectedExtra != null) { ClipboardManager clipboard = (ClipboardManager) getActivity() .getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("URL from TTRSS", mSelectedExtra); clipboard.setPrimaryClip(clip); } break; case CONTEXT_MENU_DISPLAY_CAPTION: ImageCaptionDialog fragment = ImageCaptionDialog.getInstance(mSelectedAltText); fragment.show(getFragmentManager(), ImageCaptionDialog.DIALOG_CAPTION); return true; case CONTEXT_MENU_SHARE_ARTICLE: // default behavior is to share the article URL shareIntent = getUrlShareIntent(article.url); startActivity(Intent.createChooser(shareIntent, "Share URL")); break; case CONTEXT_MENU_COPY_CONTENT: articleJSInterface.javaCallCopyToClipoard(); } return super.onContextItemSelected(item); } private Intent getUrlShareIntent(String url) { Intent i = new Intent(Intent.ACTION_SEND); i.setType("text/plain"); i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL"); i.putExtra(Intent.EXTRA_TEXT, url); return i; } private OnClickListener onButtonPressedListener = new OnClickListener() { @Override public void onClick(View v) { if (v.equals(buttonNext)) { FeedHeadlineActivity activity = (FeedHeadlineActivity) getActivity(); activity.openNextArticle(-1); } else if (v.equals(buttonPrev)) { FeedHeadlineActivity activity = (FeedHeadlineActivity) getActivity(); activity.openNextArticle(1); } } }; private OnKeyListener keyListener = new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (Controller.getInstance().useVolumeKeys()) { if (keyCode == KeyEvent.KEYCODE_N) { FeedHeadlineActivity activity = (FeedHeadlineActivity) getActivity(); activity.openNextArticle(-1); return true; } else if (keyCode == KeyEvent.KEYCODE_B) { FeedHeadlineActivity activity = (FeedHeadlineActivity) getActivity(); activity.openNextArticle(1); return true; } } return false; } }; /** * this class represents an object, which methods can be called from article's {@code WebView} javascript to * manipulate the article activity */ private class ArticleJSInterface { /** * current article activity, all methods fail silently if this doesn't contain an activity anmore */ private WeakReference<Activity> activityRef; /** * public constructor, which saves calling activity as member variable * * @param aa current article activity */ private ArticleJSInterface(Activity aa) { activityRef = new WeakReference<>(aa); } @JavascriptInterface public void prev() { final Activity activity = activityRef.get(); if (activity == null) return; // Add this to avoid android.view.windowmanager$badtokenexception unable to add window if (activity.isFinishing()) return; // loadurl on UI main thread activity.runOnUiThread(new Runnable() { public void run() { FeedHeadlineActivity activity = (FeedHeadlineActivity) getActivity(); activity.openNextArticle(-1); } }); } @JavascriptInterface public void next() { final Activity activity = activityRef.get(); if (activity == null) return; // Add this to avoid android.view.windowmanager$badtokenexception unable to add window if (activity.isFinishing()) return; activity.runOnUiThread(new Runnable() { public void run() { FeedHeadlineActivity activity = (FeedHeadlineActivity) getActivity(); activity.openNextArticle(1); } }); } @JavascriptInterface public void copyContentToClipboard(String aContent) { final Activity activity = activityRef.get(); if (activity == null) return; final String contentPlain = aContent; // Add this to avoid android.view.windowmanager$badtokenexception unable to add window if (activity.isFinishing()) return; activity.runOnUiThread(new Runnable() { public void run() { ClipboardManager clipboard = (ClipboardManager) activity .getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData .newHtmlText("HTML Text", contentPlain, prepareHTMLContentForClipboard(content)); //ClipData clip = ClipData.newPlainText("HTML Text", contentPlain); clipboard.setPrimaryClip(clip); } }); } /** * This function handles call from Java to JavaScript */ public void javaCallCopyToClipoard() { final Activity activity = activityRef.get(); if (activity == null) return; final String webUrl = "javascript:window.articleController.copyContentToClipboard(document.getElementsByTagName" + "('body')[0].innerText);"; // Add this to avoid android.view.windowmanager$badtokenexception unable to add window if (activity.isFinishing()) return; // loadurl on UI main thread activity.runOnUiThread(new Runnable() { @Override public void run() { webView.loadUrl(webUrl); } }); } } /** * Cut out the head-Tag from the html-content. */ private String prepareHTMLContentForClipboard(String html) { int start = html.indexOf("<head"); int end = html.indexOf("</head>", start); if (0 < start && start < end && end < html.length()) return html.substring(0, start) + html.substring(end, html.length()); return html; } private class ArticleGestureDetector extends MyGestureDetector { private ArticleGestureDetector(ActionBar actionBar, boolean hideActionbar) { super(actionBar, hideActionbar); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // Refresh metrics-data in Controller Controller.refreshDisplayMetrics( ((WindowManager) getActivity().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay()); if (Math.abs(e1.getY() - e2.getY()) > Controller.relSwipeMaxOffPath) return false; float distX = Math.abs(e1.getX() - e2.getX()); float distY = Math.abs(e1.getY() - e2.getY()); // Only accept this movement as a swipe if the horizontal distance was greater then the vertical distance if (distX < 1.3 * distY) return false; if (e1.getX() - e2.getX() > Controller.relSwipeMinDistance && Math.abs(velocityX) > Controller.relSwipeThresholdVelocity) { // right to left swipe FeedHeadlineActivity activity = (FeedHeadlineActivity) getActivity(); activity.openNextArticle(1); return true; } else if (e2.getX() - e1.getX() > Controller.relSwipeMinDistance && Math.abs(velocityX) > Controller.relSwipeThresholdVelocity) { // left to right swipe FeedHeadlineActivity activity = (FeedHeadlineActivity) getActivity(); activity.openNextArticle(-1); return true; } return false; } } @Override public void onAddNoteResult(Article a, String note) { new Updater(getActivity(), new NoteUpdater(a, note)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @Override public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { if (Controller.sFragmentAnimationDirection != 0 && Controller.getInstance().animations()) { Animator a; if (Controller.sFragmentAnimationDirection > 0) a = AnimatorInflater.loadAnimator(getActivity(), R.animator.slide_out_left); else a = AnimatorInflater.loadAnimator(getActivity(), R.animator.slide_out_right); // Reset: Controller.sFragmentAnimationDirection = 0; return a; } return super.onCreateAnimator(transit, enter, nextAnim); } }