package org.edx.mobile.view.custom;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.google.inject.Inject;
import org.edx.mobile.logger.Logger;
import org.edx.mobile.util.BrowserUtil;
import org.edx.mobile.util.Config;
import org.edx.mobile.util.ConfigUtil;
import org.edx.mobile.util.NetworkUtil;
import org.edx.mobile.util.StandardCharsets;
import org.edx.mobile.util.links.EdxCourseInfoLink;
import org.edx.mobile.util.links.EdxEnrollLink;
import roboguice.RoboGuice;
/**
* Created by rohan on 2/2/15.
* <p/>
* This class represents a custom {@link android.webkit.WebViewClient}.
* This class is responsible for setting up a given {@link android.webkit.WebView}, assign itself
* as a {@link android.webkit.WebViewClient} delegate and to intercept URLs being loaded.
* Depending on the form of URL, this client may forward URL back to the app.
* <p/>
* This implementation detects host of the first URL being loaded. Further, if any URL intercepted has a different host
* than the current one, then treats it as an external link and may open in external browser.
*/
public class URLInterceptorWebViewClient extends WebViewClient {
private final Logger logger = new Logger(URLInterceptorWebViewClient.class);
private final FragmentActivity activity;
private IActionListener actionListener;
private IPageStatusListener pageStatusListener;
private String hostForThisPage = null;
@Inject
Config config;
/*
To help a few views (like Announcements) to treat every link as external link and open outside the view.
*/
private boolean isAllLinksExternal = false;
public URLInterceptorWebViewClient(FragmentActivity activity, WebView webView) {
this.activity = activity;
RoboGuice.injectMembers(activity, this);
setupWebView(webView);
}
/**
* Sets action listener for this client. Use this method to get callbacks
* of actions as declared in {@link org.edx.mobile.view.custom.URLInterceptorWebViewClient.IActionListener}.
*
* @param actionListener
*/
public void setActionListener(IActionListener actionListener) {
this.actionListener = actionListener;
}
/**
* Gives page status callbacks like page loading started, finished or error.
*
* @param pageStatusListener
*/
public void setPageStatusListener(IPageStatusListener pageStatusListener) {
this.pageStatusListener = pageStatusListener;
}
/**
* Sets up the WeView, applies minimal required settings and
* sets this class itself as WebViewClient.
*
* @param webView
*/
private void setupWebView(WebView webView) {
webView.setWebViewClient(this);
//We need to hide the loading progress if the Page starts rendering.
webView.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
if (progress > 50) {
if (pageStatusListener != null) {
pageStatusListener.onPagePartiallyLoaded();
}
}
}
});
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
// hold on the host of this page, just once
if (this.hostForThisPage == null && url != null) {
this.hostForThisPage = Uri.parse(url).getHost();
}
if (pageStatusListener != null) {
pageStatusListener.onPageStarted();
}
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (pageStatusListener != null) {
pageStatusListener.onPageFinished();
}
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
if (pageStatusListener != null) {
pageStatusListener.onPageLoadError();
}
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (actionListener == null) {
logger.warn("you have not set IActionLister to this WebViewClient, " +
"you might miss some event");
}
logger.debug("loading: " + url);
if (parseCourseInfoLinkAndCallActionListener(url)) {
// we handled this URL
return true;
} else if (parseEnrollLinkAndCallActionListener(url)) {
// we handled this URL
return true;
} else if (isAllLinksExternal || isExternalLink(url)) {
// open URL in external web browser
// return true means the host application handles the url
// this should open the URL in the browser with user's confirmation
BrowserUtil.open(activity, url);
return true;
} else {
// return false means the current WebView handles the url.
return false;
}
}
public void setAllLinksAsExternal(boolean isAllLinksExternal) {
this.isAllLinksExternal = isAllLinksExternal;
}
@Override
@SuppressWarnings("deprecation")
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
Context context = view.getContext().getApplicationContext();
// suppress external links on ZeroRated network
if (isExternalLink(url)
&& !ConfigUtil.isWhiteListedURL(url, config)
&& NetworkUtil.isOnZeroRatedNetwork(context, config)
&& NetworkUtil.isConnectedMobile(context)) {
return new WebResourceResponse("text/html", StandardCharsets.UTF_8.name(), null);
}
return super.shouldInterceptRequest(view, url);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return shouldInterceptRequest(view, request.getUrl().toString());
}
/**
* Checks if the URL pattern matches with that of COURSE_INFO URL.
* Extracts path_id from the URL and gives a callback to the registered
* action listener with path_id parameter.
* Returns true if pattern matches with COURSE_INFO URL pattern and callback succeeds with
* extracted parameter, false otherwise.
*
* @param strUrl
* @return
*/
/**
* Checks if {@param strUrl} is valid course info link and, if so,
* calls {@link org.edx.mobile.view.custom.URLInterceptorWebViewClient.IActionListener#onClickCourseInfo(String)}
*
* @return true if an action listener is set and URL was a valid course info link, false otherwise
*/
private boolean parseCourseInfoLinkAndCallActionListener(String strUrl) {
if (null == actionListener) {
return false;
}
final EdxCourseInfoLink link = EdxCourseInfoLink.parse(strUrl);
if (null == link) {
return false;
}
actionListener.onClickCourseInfo(link.pathId);
logger.debug("found course-info URL: " + strUrl);
return true;
}
/**
* Returns true if the pattern of the url matches with that of EXTERNAL URL pattern,
* false otherwise.
*
* @param strUrl
* @return
*/
private boolean isExternalLink(String strUrl) {
return hostForThisPage != null && strUrl != null &&
!hostForThisPage.equals(Uri.parse(strUrl).getHost());
}
/**
* Checks if {@param strUrl} is valid enroll link and, if so,
* calls {@link org.edx.mobile.view.custom.URLInterceptorWebViewClient.IActionListener#onClickEnroll(String, boolean)}
*
* @return true if an action listener is set and URL was a valid enroll link, false otherwise
*/
private boolean parseEnrollLinkAndCallActionListener(@Nullable String strUrl) {
if (null == actionListener) {
return false;
}
final EdxEnrollLink link = EdxEnrollLink.parse(strUrl);
if (null == link) {
return false;
}
actionListener.onClickEnroll(link.courseId, link.emailOptIn);
logger.debug("found enroll URL: " + strUrl);
return true;
}
/**
* Action listener interface for handling enroll link click action
* and course-info link click action.
* We may need to add more actions to this interface in future.
*/
public static interface IActionListener {
/**
* Callback that gets called when this client has intercepted Course Info URL.
* Sub-classes or any implementation of this class should override this method to handle
* tap of course info URL.
*
* @param pathId
*/
void onClickCourseInfo(String pathId);
/**
* Callback that gets called when this client has intercepted Enroll action.
* Sub-classes or any implementation of this class should override this method to handle
* enroll action further.
*
* @param courseId
* @param emailOptIn
*/
void onClickEnroll(String courseId, boolean emailOptIn);
}
/**
* Page state callbacks.
*/
public static interface IPageStatusListener {
/**
* Callback that indicates page loading has started.
*/
void onPageStarted();
/**
* Callback that indicates page loading has finished.
*/
void onPageFinished();
/**
* Callback that indicates error during page load.
*/
void onPageLoadError();
/**
* Callback that indicates that the page is 50 percent loaded.
*/
void onPagePartiallyLoaded();
}
}