package es.usc.citius.servando.calendula.activities;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.net.http.SslError;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import android.util.Log;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import com.mikepenz.community_material_typeface_library.CommunityMaterial;
import com.mikepenz.iconics.IconicsDrawable;
import org.joda.time.Duration;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import es.usc.citius.servando.calendula.CalendulaActivity;
import es.usc.citius.servando.calendula.R;
import es.usc.citius.servando.calendula.database.DB;
import es.usc.citius.servando.calendula.util.HtmlCacheManager;
public class WebViewActivity extends CalendulaActivity {
/**
* Request bean for WebViewActivity. Must be provided and must contain at least a URL.
*/
public static final String PARAM_WEBVIEW_REQUEST = "webview_param_request";
/**
* Max cache size for AppCache
*/
private static final Integer CACHE_MAX_SIZE = 8388608; //8mb
private static final String TAG = "WebViewActivity";
private static final String HTTP_ERROR_REGEXP = "^.*?(404|403|[nN]ot [fF]ound).*$";
private WebView webView;
private String url;
// switch to disable JavaScript in API<17
private boolean isJavaScriptInsecure = false;
// switch to disable caching
private boolean errorDisableCache = false;
// reference to the request params
WebViewRequest request;
// handler to access activity methods from javascript interface
Handler handler;
ProgressDialog progressDialog;
View toolbarSahdow;
int color;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
handler = new Handler();
//check api version to see if we can use JavaScript
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
isJavaScriptInsecure = true;
}
//check for request and URL and finish if not present
request = getIntent().getParcelableExtra(PARAM_WEBVIEW_REQUEST);
if (request == null || (url = request.getUrl()) == null) {
Log.e(TAG, "onCreate: No WebViewRequest provided in intent!");
showErrorToast(null);
finish();
} else {
webView = (WebView) findViewById(R.id.webView1);
toolbarSahdow = findViewById(R.id.tabs_shadow);
//setup toolbar and statusbar
color = DB.patients().getActive(this).color();
String title = request.getTitle();
setupToolbar(title, color);
setupStatusBar(color);
//setup the webView
setupWebView(request);
}
}
@Override
public void onActionModeStarted(ActionMode mode) {
super.onActionModeStarted(mode);
Log.d(TAG, "onActionModeStarted");
if (toolbar != null) {
toolbarSahdow.setVisibility(View.GONE);
toolbar.setVisibility(View.GONE);
setupStatusBar(getResources().getColor(R.color.dark_grey_home));
}
}
@Override
public void onActionModeFinished(ActionMode mode) {
super.onActionModeFinished(mode);
Log.d(TAG, "onActionModeFinished");
if (toolbar != null) {
setupStatusBar(color);
handler.postDelayed(new Runnable() {
@Override
public void run() {
toolbar.setAlpha(0);
toolbarSahdow.setAlpha(0);
toolbar.setVisibility(View.VISIBLE);
toolbarSahdow.setVisibility(View.VISIBLE);
toolbar.animate().alpha(1).start();
toolbarSahdow.animate().alpha(1).start();
}
}, 300);
}
}
private void setupWebView(final WebViewRequest request) {
//enable JavaScript if it is explicitly enabled or custom css sheet must be injected
if (request.isJavaScriptEnabled() || request.getCustomCss() != null) {
if (!isJavaScriptInsecure) {
Log.d(TAG, "Enabling JavaScript!");
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setJavaScriptEnabled(true);
} else {
Log.w(TAG, "Javascript cannot be enabled with API version < 17 due to security reasons, disabling.");
}
}
final String originalUrl = url;
Log.d(TAG, "Opening URL: " + originalUrl);
//setup progressDialog
String loadingMessage = request.getLoadingMessage();
if (loadingMessage == null) loadingMessage = getString(R.string.message_generic_pleasewait);
webView.setVisibility(View.INVISIBLE);
progressDialog = ProgressDialog.show(this, getString(R.string.title_generic_loading), loadingMessage);
progressDialog.setCancelable(true);
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
progressDialog.dismiss();
finish();
}
});
//misc webView settings
//set single column layout
webView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
//enable pinch to zoom
webView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
webView.getSettings().setBuiltInZoomControls(true);
webView.getSettings().setDisplayZoomControls(false);
webView.getSettings().setEnableSmoothTransition(true);
//enable AppCache if requested
if (request.getCacheType().equals(WebViewRequest.CacheType.APP_CACHE))
enableAppCache();
//enable download cache if requested
String cachedData = null;
if (!isJavaScriptInsecure && request.getCacheType().equals(WebViewRequest.CacheType.DOWNLOAD_CACHE)) {
if (!isCached()) {
webView.addJavascriptInterface(new SimpleJSCacheInterface(this), "HtmlCache");
} else {
cachedData = HtmlCacheManager.getInstance().get(originalUrl);
}
}
final String customCssSheet = isJavaScriptInsecure ? null : request.getCustomCss();
webView.setWebViewClient(
new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//use webview only for the requested URL or suburls, unless external links are enabled
if (url.contains(originalUrl) || request.isExternalLinksEnabled()) {
return super.shouldOverrideUrlLoading(view, url);
} else {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
}
@Override
public void onPageFinished(WebView view, String url) {
if (view.getTitle().matches(HTTP_ERROR_REGEXP)) {
Log.e(TAG, "Received HTTP error, page title is: " + view.getTitle());
showErrorToast(request.getNotFoundErrorMessage());
finish();
}
if (customCssSheet != null) {
injectCSS(customCssSheet, request.getCustomCssOverrides());
//if JavaScript is not enabled explicitly, turn it off after CSS injection
webView.getSettings().setJavaScriptEnabled(request.isJavaScriptEnabled());
}
// setup javascript interface if the request needs access to html
if (needsHtmlAccess(request)) {
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl("javascript:window.HtmlCache.writeToCache" +
"('<html>'+document.getElementsByTagName('html')[0].innerHTML+'</html>');");
webView.getSettings().setJavaScriptEnabled(request.isJavaScriptEnabled());
} else {
webView.setVisibility(View.VISIBLE);
progressDialog.dismiss();
}
Log.d(TAG, "Finished loading URL: " + url);
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
Log.e(TAG, "Received error when trying to load page");
showErrorToast(request.getConnectionErrorMessage());
finish();
}
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest r, WebResourceResponse errorResponse) {
Log.e(TAG, "Received HTTP Error when trying to load page");
showErrorToast(request.getNotFoundErrorMessage());
finish();
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
Log.e(TAG, "Received SSL Error when trying to load page");
showErrorToast(request.getConnectionErrorMessage());
finish();
}
});
if (cachedData != null) {
Log.d(TAG, "setupWebView: Loading page from cache");
webView.loadData(cachedData, "text/html; charset=UTF-8", null);
} else {
Log.d(TAG, "setupWebView: Loading page from URL");
webView.loadUrl(originalUrl);
}
}
private boolean isCached() {
return HtmlCacheManager.getInstance().isCached(url);
}
private void injectCSS(final String file, Map<String, String> overrides) {
try {
// read CSS from file
InputStream inputStream = getAssets().open(file);
byte[] buffer = new byte[inputStream.available()];
inputStream.read(buffer);
inputStream.close();
// perform css replacements if any
if (overrides != null && overrides.size() > 0) {
String css = new String(buffer);
for (Map.Entry<String, String> entry : overrides.entrySet()) {
css = css.replaceAll(entry.getKey(), entry.getValue());
}
buffer = css.getBytes();
}
//encode CSS string in base64
String encoded = Base64.encodeToString(buffer, Base64.NO_WRAP);
//inject CSS into the webpage <head> element
webView.loadUrl("javascript:(function() {" +
"var parent = document.getElementsByTagName('head').item(0);" +
"var style = document.createElement('style');" +
"style.type = 'text/css';" +
"style.innerHTML = window.atob('" + encoded + "');" +
"parent.appendChild(style)" +
"})()");
} catch (Exception e) {
Log.w(TAG, "injectCSS:" + file);
}
}
private void enableAppCache() {
Log.d(TAG, "Enabling cache with max size " + CACHE_MAX_SIZE + " bytes");
webView.getSettings().setDomStorageEnabled(true);
webView.getSettings().setAppCacheMaxSize(CACHE_MAX_SIZE);
webView.getSettings().setAppCachePath("/data/data/" + getPackageName() + "/cache");
webView.getSettings().setAllowFileAccess(true);
webView.getSettings().setAppCacheEnabled(true);
webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
}
private void showErrorToast(String error) {
errorDisableCache = false;
if (error == null) error = getString(R.string.message_generic_pageloaderror);
Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
}
@Override
public void onBackPressed() {
finish();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_webview, menu);
IconicsDrawable icon = new IconicsDrawable(this, CommunityMaterial.Icon.cmd_share_variant)
.sizeDp(48)
.paddingDp(6)
.color(Color.WHITE);
menu.getItem(0).setIcon(icon);
IconicsDrawable icon2 = new IconicsDrawable(this, CommunityMaterial.Icon.cmd_web)
.sizeDp(48)
.paddingDp(6)
.color(Color.WHITE);
menu.getItem(1).setIcon(icon2);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_share_link:
Intent i = new Intent(Intent.ACTION_SEND);
i.putExtra(Intent.EXTRA_TEXT,
url);
i.putExtra(Intent.EXTRA_SUBJECT, webView.getTitle());
i.setType("text/plain");
startActivity(Intent.createChooser(i, getString(R.string.title_share_link)));
return true;
case R.id.action_open_with_browser:
Intent i1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(i1);
return true;
default:
onBackPressed();
return true;
}
}
/**
* Encapsulates a request for a {@link WebViewActivity}.
* <p>
*/
public static class WebViewRequest implements Parcelable {
private final String url;
private String title = null;
private String loadingMessage = null;
private String connectionErrorMessage = null;
private String notFoundErrorMessage = null;
private String postProcessorClassname = null;
private boolean javaScriptEnabled = false;
private boolean externalLinksEnabled = false;
private CacheType cacheType = CacheType.NO_CACHE;
private String customCss = null;
private Map<String, String> customCssOverrides = null;
private Duration cacheTTL;
public enum CacheType {
NO_CACHE,
APP_CACHE,
DOWNLOAD_CACHE
}
public WebViewRequest(String url) {
this.url = url;
}
public WebViewRequest(String url, String title, String loadingMessage,
String connectionErrorMessage, String notFoundErrorMessage,
String postProcessorClassname, boolean javaScriptEnabled,
boolean externalLinksEnabled, CacheType cacheType, String customCss,
Map<String, String> customCssOverrides, Duration cacheTTL) {
this.url = url;
this.title = title;
this.loadingMessage = loadingMessage;
this.connectionErrorMessage = connectionErrorMessage;
this.notFoundErrorMessage = notFoundErrorMessage;
this.postProcessorClassname = postProcessorClassname;
this.javaScriptEnabled = javaScriptEnabled;
this.externalLinksEnabled = externalLinksEnabled;
this.cacheType = cacheType;
this.customCss = customCss;
this.customCssOverrides = customCssOverrides;
this.cacheTTL = cacheTTL;
}
protected WebViewRequest(Parcel in) {
url = in.readString();
title = in.readString();
loadingMessage = in.readString();
connectionErrorMessage = in.readString();
notFoundErrorMessage = in.readString();
postProcessorClassname = in.readString();
javaScriptEnabled = in.readByte() != 0;
externalLinksEnabled = in.readByte() != 0;
cacheType = CacheType.valueOf(in.readString());
customCss = in.readString();
cacheTTL = Duration.parse(in.readString());
// read overrides map
customCssOverrides = new HashMap<>();
int size = in.readInt();
for (int i = 0; i < size; i++) {
String key = in.readString();
String value = in.readString();
customCssOverrides.put(key, value);
}
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(url);
dest.writeString(title);
dest.writeString(loadingMessage);
dest.writeString(connectionErrorMessage);
dest.writeString(notFoundErrorMessage);
dest.writeString(postProcessorClassname);
dest.writeByte((byte) (javaScriptEnabled ? 1 : 0));
dest.writeByte((byte) (externalLinksEnabled ? 1 : 0));
dest.writeString(cacheType.toString());
dest.writeString(customCss);
dest.writeString(cacheTTL.toString());
// write overrides map
dest.writeInt(customCssOverrides.size());
for (Map.Entry<String, String> entry : customCssOverrides.entrySet()) {
dest.writeString(entry.getKey());
dest.writeString(entry.getValue());
}
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<WebViewRequest> CREATOR = new Creator<WebViewRequest>() {
@Override
public WebViewRequest createFromParcel(Parcel in) {
return new WebViewRequest(in);
}
@Override
public WebViewRequest[] newArray(int size) {
return new WebViewRequest[size];
}
};
public String getUrl() {
return url;
}
public String getTitle() {
return title;
}
/**
* Set the title for the webview title bar. Can be <code>null</code>, no title will be displayed if so.
*
* @param title the title
*/
public void setTitle(String title) {
this.title = title;
}
public String getLoadingMessage() {
return loadingMessage;
}
/**
* Set a custom loading message. A default message will be used if <code>null</code>.
*
* @param loadingMessage the message
*/
public void setLoadingMessage(String loadingMessage) {
this.loadingMessage = loadingMessage;
}
public String getConnectionErrorMessage() {
return connectionErrorMessage;
}
public String getNotFoundErrorMessage() {
return notFoundErrorMessage;
}
/**
* Set a custom error message in case the page can't be loaded for connection reasons. A default message will be used if <code>null</code>.
*
* @param errorMessage the message
*/
public void setConnectionErrorMessage(String errorMessage) {
this.connectionErrorMessage = errorMessage;
}
/**
* Set a custom error message in case the page does not exists. A default message will be used if <code>null</code>.
*
* @param errorMessage the message
*/
public void setNotFoundErrorMessage(String errorMessage) {
this.notFoundErrorMessage = errorMessage;
}
public boolean isJavaScriptEnabled() {
return javaScriptEnabled;
}
/**
* Enable/disable JavaScript for the webpage. Default is <code>false</code>.
*
* @param javaScriptEnabled
*/
public void setJavaScriptEnabled(boolean javaScriptEnabled) {
this.javaScriptEnabled = javaScriptEnabled;
}
public boolean needsPostprocessing() {
return this.postProcessorClassname != null;
}
/**
* Name of a class that implements the HtmlPostprocessor interface, for accessing and modifying the
* html before it is displayed. Default is <code>false</code>.
*
* @param processor
*/
public void setPostProcessorClassname(String processor) {
this.postProcessorClassname = processor;
}
public String getCustomCss() {
return customCss;
}
public Map<String, String> getCustomCssOverrides() {
return customCssOverrides;
}
/**
* Set a custom CSS sheet to be injected into the page. If <code>null</code>, no CSS will be loaded.
* Injecting the CSS <b>requires JavaScript</b> and will override setJavaScriptEnabled during the injection.
*
* @param filename
*/
public void setCustomCss(String filename, Map<String, String> overrides) {
this.customCss = filename;
this.customCssOverrides = overrides;
}
public boolean isExternalLinksEnabled() {
return externalLinksEnabled;
}
/**
* Set if external links should be opened in the webview (<code>true</code>) or should launch an action intent (<code>false</code>).
* Default value is <code>false</code>.
*
* @param externalLinksEnabled
*/
public void setExternalLinksEnabled(boolean externalLinksEnabled) {
this.externalLinksEnabled = externalLinksEnabled;
}
public CacheType getCacheType() {
return cacheType;
}
/**
* Set cache type for the webview:
* - <code>NO_CACHE</code>: deactivate caching. Default value.
* - <code>APP_CACHE</code>: HTML5 App Cache
* - <code>DOWNLOAD_CACHE</code>: Cache full HTML document in database. Warning! Locally linked resources will be lost.
*
* @param cacheType
*/
public void setCacheType(CacheType cacheType) {
this.cacheType = cacheType;
}
public Duration getCacheTTL() {
return cacheTTL;
}
/**
* Set TTL for download cache. Only works if CacheType.DOWNLOAD_CACHE is set.
* If <code>null</code>, a default TTL will be used.
*
* @param cacheTTL
*/
public void setCacheTTL(Duration cacheTTL) {
this.cacheTTL = cacheTTL;
}
}
/**
* Whether a request needs html access after loading, that is, whether is must be
* cached or processed and it has not been cached yet
*
* @param request
*/
public boolean needsHtmlAccess(WebViewRequest request) {
if (isCached()) {
return false;
} else if (request.needsPostprocessing()) {
return true;
}
return !isJavaScriptInsecure &&
!errorDisableCache &&
request.cacheType.equals(WebViewRequest.CacheType.DOWNLOAD_CACHE);
}
private class SimpleJSCacheInterface {
private Context ctx;
SimpleJSCacheInterface(Context ctx) {
this.ctx = ctx;
}
@JavascriptInterface
public void writeToCache(String html) {
final Duration ttl = request.getCacheTTL(); //can be null
// if there is a postprocessor enabled
if (request.needsPostprocessing()) {
// instantiate postprocessor
try {
HtmlPostprocessor processor = (HtmlPostprocessor) Class.forName(request.postProcessorClassname).newInstance();
// get processed html
final String processed = processor.process(html);
// save it to cache if needed
if (request.cacheType.equals(WebViewRequest.CacheType.DOWNLOAD_CACHE)) {
HtmlCacheManager.getInstance().put(url, processed, ttl);
}
// ... and finally load new content
handler.post(new Runnable() {
@Override
public void run() {
webView.loadData(processed, "text/html; charset=UTF-8", null);
webView.clearHistory();
}
});
} catch (Exception e) {
Log.e(TAG, "Error trying to post process content", e);
}
}
// in other case, simply write html content to cache
else if (request.cacheType.equals(WebViewRequest.CacheType.DOWNLOAD_CACHE)) {
HtmlCacheManager.getInstance().put(url, html, ttl);
}
// dismiss the loading dialog
handler.post(new Runnable() {
@Override
public void run() {
if (progressDialog.isShowing()) {
webView.setVisibility(View.VISIBLE);
progressDialog.dismiss();
}
}
});
}
}
/**
* Interface that must be implemented in order to access the page html
* and make changes before it is displayed
*/
public interface HtmlPostprocessor {
String process(String html);
}
}