/**
* Android ownCloud News
*
* @author David Luhmer
* @copyright 2013 David Luhmer david-dev@live.de
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
package de.luhmer.owncloudnewsreader;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.ConsoleMessage;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import com.nostra13.universalimageloader.cache.disc.DiskCache;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.apache.commons.lang3.StringEscapeUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import butterknife.Bind;
import butterknife.ButterKnife;
import de.luhmer.owncloudnewsreader.database.model.Feed;
import de.luhmer.owncloudnewsreader.database.model.RssItem;
import de.luhmer.owncloudnewsreader.helper.AsyncTaskHelper;
import de.luhmer.owncloudnewsreader.helper.ColorHelper;
import de.luhmer.owncloudnewsreader.helper.ImageHandler;
import de.luhmer.owncloudnewsreader.helper.ThemeChooser;
public class NewsDetailFragment extends Fragment {
public static final String ARG_SECTION_NUMBER = "ARG_SECTION_NUMBER";
public final String TAG = getClass().getCanonicalName();
public static int background_color = Integer.MIN_VALUE;
@Bind(R.id.webview) WebView mWebView;
@Bind(R.id.progressBarLoading) ProgressBar mProgressBarLoading;
@Bind(R.id.progressbar_webview) ProgressBar mProgressbarWebView;
private int section_number;
public List<String> urls = new ArrayList<>();
protected String html;
public NewsDetailFragment() {
//setRetainInstance(true);
}
public int getSectionNumber() {
return section_number;
}
@Override
public void onResume() {
super.onResume();
ResumeCurrentPage();
}
@Override
public void onPause() {
super.onPause();
PauseCurrentPage();
}
@Override
public void onDestroy() {
super.onDestroy();
if(mWebView != null) {
mWebView.destroy();
}
}
public void PauseCurrentPage()
{
if(mWebView != null) {
mWebView.onPause();
mWebView.pauseTimers();
}
}
public void ResumeCurrentPage()
{
if(mWebView != null) {
mWebView.onResume();
mWebView.resumeTimers();
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_news_detail, container, false);
section_number = (Integer) getArguments().get(ARG_SECTION_NUMBER);
ButterKnife.bind(this, rootView);
startLoadRssItemToWebViewTask();
return rootView;
}
public void startLoadRssItemToWebViewTask() {
AsyncTaskHelper.StartAsyncTask(new LoadRssItemToWebViewAsyncTask());
}
private class LoadRssItemToWebViewAsyncTask extends AsyncTask<Void, Void, String> {
@Override
protected void onPreExecute() {
NewsDetailActivity ndActivity = ((NewsDetailActivity)getActivity());
if(background_color != Integer.MIN_VALUE && ThemeChooser.isDarkTheme(ndActivity))
{
mWebView.setBackgroundColor(background_color);
ndActivity.mViewPager.setBackgroundColor(background_color);
}
init_webView();
mWebView.setVisibility(View.GONE);
mProgressBarLoading.setVisibility(View.VISIBLE);
super.onPreExecute();
}
@Override
protected String doInBackground(Void... voids) {
NewsDetailActivity ndActivity = ((NewsDetailActivity)getActivity());
RssItem rssItem = ndActivity.rssItems.get(section_number);
return getHtmlPage(ndActivity, rssItem, true);
}
@Override
protected void onPostExecute(String htmlPage) {
mWebView.setVisibility(View.VISIBLE);
mProgressBarLoading.setVisibility(View.GONE);
SetSoftwareRenderModeForWebView(htmlPage, mWebView);
html = htmlPage;
mWebView.loadDataWithBaseURL("file:///android_asset/", htmlPage, "text/html", "UTF-8", "");
super.onPostExecute(htmlPage);
}
}
/**
* This function has no effect on devices with api level < HONEYCOMB
* @param htmlPage
* @param webView
*/
public static void SetSoftwareRenderModeForWebView(String htmlPage, WebView webView) {
if (htmlPage.contains(".gif")) {
webView.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null);
Log.v("NewsDetailFragment", "Using LAYER_TYPE_SOFTWARE");
} else {
//webView.setLayerType(WebView.LAYER_TYPE_HARDWARE, null);
//Log.v("NewsDetailFragment", "Using LAYER_TYPE_HARDWARE");
if(webView.getLayerType() == WebView.LAYER_TYPE_HARDWARE) {
Log.v("NewsDetailFragment", "Using LAYER_TYPE_HARDWARE");
} else if (webView.getLayerType() == WebView.LAYER_TYPE_SOFTWARE){
Log.v("NewsDetailFragment", "Using LAYER_TYPE_SOFTWARE");
} else {
Log.v("NewsDetailFragment", "Using LAYER_TYPE_DEFAULT");
}
}
}
boolean changedUrl = false;
@SuppressLint("SetJavaScriptEnabled")
private void init_webView() {
int backgroundColor = ColorHelper.getColorFromAttribute(getContext(),
R.attr.news_detail_background_color);
mWebView.setBackgroundColor(backgroundColor);
WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setAllowFileAccess(true);
webSettings.setDomStorageEnabled(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(false);
webSettings.setSupportMultipleWindows(false);
webSettings.setSupportZoom(false);
webSettings.setAppCacheEnabled(true);
registerForContextMenu(mWebView);
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onConsoleMessage(ConsoleMessage cm) {
Log.v(TAG, cm.message() + " at " + cm.sourceId() + ":" + cm.lineNumber());
return true;
}
@Override
public void onProgressChanged(WebView view, int progress) {
if (progress < 100 && mProgressbarWebView.getVisibility() == ProgressBar.GONE) {
mProgressbarWebView.setVisibility(ProgressBar.VISIBLE);
}
mProgressbarWebView.setProgress(progress);
if (progress == 100) {
mProgressbarWebView.setVisibility(ProgressBar.GONE);
//The following three lines are a workaround for websites which don't use a background color
int bgColor = ContextCompat.getColor(getContext(), R.color.slider_listview_text_color_dark_theme);
NewsDetailActivity ndActivity = ((NewsDetailActivity) getActivity());
mWebView.setBackgroundColor(bgColor);
ndActivity.mViewPager.setBackgroundColor(bgColor);
if (ThemeChooser.isDarkTheme(getActivity())) {
mWebView.setBackgroundColor(ContextCompat.getColor(getContext(), android.R.color.transparent));
}
}
}
});
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
if (changedUrl) {
changedUrl = false;
if (!url.equals("file:///android_asset/") && (urls.isEmpty() || !urls.get(0).equals(url))) {
urls.add(0, url);
Log.v(TAG, "Page finished (added): " + url);
}
}
super.onPageStarted(view, url, favicon);
}
});
mWebView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v.getId() == R.id.webview && event.getAction() == MotionEvent.ACTION_DOWN) {
changedUrl = true;
}
return false;
}
});
}
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
if (v instanceof WebView) {
WebView.HitTestResult result = ((WebView) v).getHitTestResult();
if (result != null) {
int type = result.getType();
Document htmldoc = Jsoup.parse(html);
FragmentTransaction ft = getFragmentManager().beginTransaction();
if (type == WebView.HitTestResult.IMAGE_TYPE || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
String imageUrl = result.getExtra();
if (imageUrl.startsWith("http") || imageUrl.startsWith("file")) {
URL mImageUrl;
String imgtitle;
String imgaltval;
String imgsrcval;
imgsrcval = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.length());
Elements imgtag = htmldoc.getElementsByAttributeValueContaining("src", imageUrl);
try {
imgtitle = imgtag.first().attr("title");
} catch (NullPointerException e) {
imgtitle = "";
}
try {
imgaltval = imgtag.first().attr("alt");
} catch (NullPointerException e) {
imgaltval = "";
}
try {
mImageUrl = new URL(imageUrl);
} catch (MalformedURLException e) {
return;
}
String title = imgsrcval;
int titleIcon = android.R.drawable.ic_menu_gallery;
String text = (imgtitle.isEmpty()) ? imgaltval : imgtitle;
// Create and show the dialog.
DialogFragment newFragment =
NewsDetailImageDialogFragment.newInstanceImage(title, titleIcon, text, mImageUrl);
newFragment.show(ft, "menu_fragment_dialog");
}
}
else if (type == WebView.HitTestResult.SRC_ANCHOR_TYPE) {
String url = result.getExtra();
URL mUrl;
String text;
try {
Elements urltag = htmldoc.getElementsByAttributeValueContaining("href", url);
text = urltag.text();
mUrl = new URL(url);
} catch (MalformedURLException e) {
return;
}
// Create and show the dialog.
DialogFragment newFragment =
NewsDetailImageDialogFragment.newInstanceUrl(text, mUrl.toString());
newFragment.show(ft, "menu_fragment_dialog");
}
//else if (type == WebView.HitTestResult.EMAIL_TYPE) { }
//else if (type == WebView.HitTestResult.GEO_TYPE) { }
//else if (type == WebView.HitTestResult.PHONE_TYPE) { }
//else if (type == WebView.HitTestResult.EDIT_TEXT_TYPE) { }
}
}
}
@SuppressLint("SimpleDateFormat")
public static String getHtmlPage(Context context, RssItem rssItem, boolean showHeader)
{
String feedTitle = "Undefined";
String favIconUrl = null;
Feed feed = rssItem.getFeed();
int[] colors = ColorHelper.getColorsFromAttributes(context,
R.attr.dividerLineColor,
R.attr.rssItemListBackground);
int feedColor = colors[0];
if(feed != null) {
feedTitle = StringEscapeUtils.escapeHtml4(feed.getFeedTitle());
favIconUrl = feed.getFaviconUrl();
if(feed.getAvgColour() != null)
feedColor = Integer.parseInt(feed.getAvgColour());
}
if(favIconUrl != null)
{
DiskCache diskCache = ImageLoader.getInstance().getDiskCache();
File file = diskCache.get(favIconUrl);
if(file != null)
favIconUrl = "file://" + file.getAbsolutePath();
} else {
favIconUrl = "file:///android_res/drawable/default_feed_icon_light.png";
}
String body_id;
if(ThemeChooser.isDarkTheme(context)) {
body_id = "darkTheme";
} else {
body_id = "lightTheme";
}
boolean isRightToLeft = context.getResources().getBoolean(R.bool.is_right_to_left);
String rtlClass = isRightToLeft ? "rtl" : "";
String borderSide = isRightToLeft ? "right" : "left";
StringBuilder builder = new StringBuilder();
builder.append("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0\" />");
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"web.css\" />");
builder.append("<style type=\"text/css\">");
builder.append(String.format(
"#top_section { border-%s: 4px solid %s; border-bottom: 1px solid %s; background: %s }",
borderSide,
ColorHelper.getCssColor(feedColor),
ColorHelper.getCssColor(colors[0]),
ColorHelper.getCssColor(colors[1]))
);
builder.append("</style>");
builder.append(String.format("</head><body id=\"%s\" class=\"%s\">", body_id, rtlClass));
if(showHeader) {
builder.append("<div id=\"top_section\">");
builder.append("<div id=\"header\">");
String title = StringEscapeUtils.escapeHtml4(rssItem.getTitle());
String linkToFeed = StringEscapeUtils.escapeHtml4(rssItem.getLink());
builder.append(String.format("<a href=\"%s\">%s</a>", linkToFeed, title));
builder.append("</div>");
String authorOfArticle = StringEscapeUtils.escapeHtml4(rssItem.getAuthor());
if (authorOfArticle != null)
if (!authorOfArticle.trim().equals(""))
feedTitle += " - " + authorOfArticle.trim();
builder.append("<div id=\"header_small_text\">");
builder.append("<div id=\"subscription\">");
builder.append(String.format("<img id=\"imgFavicon\" src=\"%s\" />", favIconUrl));
builder.append(feedTitle.trim());
builder.append("</div>");
Date date = rssItem.getPubDate();
if (date != null) {
String dateString = (String) DateUtils.getRelativeTimeSpanString(date.getTime());
builder.append("<div id=\"datetime\">");
builder.append(dateString);
builder.append("</div>");
}
builder.append("</div>");
builder.append("</div>");
}
String description = rssItem.getBody();
description = getDescriptionWithCachedImages(description).trim();
//StopWatch stopWatch = new StopWatch();
// stopWatch.start();
description = removePreloadAttributeFromVideos(description);
//stopWatch.stop();
//Log.d("NewsDetailFragment", "Time needed for removing preload attribute: " + stopWatch.toString() + " - " + feedTitle);
builder.append("<div id=\"content\">");
builder.append(description);
builder.append("</div>");
builder.append("</body></html>");
String htmlData = builder.toString().replaceAll("\"//", "\"https://");
return htmlData;
}
private static Pattern PATTERN_PRELOAD_VIDEOS = Pattern.compile("(<video[^>]*)(preload=\".*?\")");
private static String removePreloadAttributeFromVideos(String text) {
Matcher m = PATTERN_PRELOAD_VIDEOS.matcher(text);
if(m.find()) {
StringBuffer sb = new StringBuffer();
do {
//$1 represents the 1st group
m.appendReplacement(sb, "$1" + "preload=\"none\"");
} while (m.find());
m.appendTail(sb);
text = sb.toString();
}
return text;
}
private static String getDescriptionWithCachedImages(String text)
{
List<String> links = ImageHandler.getImageLinksFromText(text);
DiskCache diskCache = ImageLoader.getInstance().getDiskCache();
for(String link : links)
{
link = link.trim();
try
{
File file = diskCache.get(link);
if(file != null)
text = text.replace(link, "file://" + file.getAbsolutePath());
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
return text;
}
static String getTextFromAssets(String fileName, Context context) {
InputStream input;
try {
input = context.getAssets().open(fileName);
int size = input.available();
byte[] buffer = new byte[size];
input.read(buffer);
input.close();
// byte buffer into a string
return new String(buffer);
} catch(Exception ex) {
ex.printStackTrace();
}
return "";
}
private static String SearchString(String data, String startString, String endString)
{
int start = data.indexOf(startString) + startString.length();
int end = data.indexOf(endString, start);
if(start != (-1 + startString.length()) && end != -1)
data = data.substring(start, end).trim();
return data;
}
private static String convertHexColorFrom3To6Characters(String color)
{
for(int i = 1; i < 6; i += 2)
color = color.substring(0, i) + color.charAt(i) + color.substring(i);
return color;
}
}