package com.kaltura.playersdk; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; import android.net.http.SslError; import android.os.Build; import android.os.Looper; import android.util.Base64; import android.view.MotionEvent; import android.view.View; import android.webkit.ConsoleMessage; import android.webkit.JavascriptInterface; import android.webkit.SslErrorHandler; import android.webkit.WebChromeClient; import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import com.kaltura.playersdk.helpers.CacheManager; import com.kaltura.playersdk.helpers.KStringUtilities; import com.kaltura.playersdk.types.KPError; import com.kaltura.playersdk.utils.LogUtils; import org.json.JSONObject; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Collections; import java.util.Map; import static com.kaltura.playersdk.utils.LogUtils.LOGD; import static com.kaltura.playersdk.utils.LogUtils.LOGE; import static com.kaltura.playersdk.utils.LogUtils.LOGW; /** * Created by nissopa on 6/7/15. */ public class KControlsView extends WebView implements View.OnTouchListener { private static String TAG = KControlsView.class.getSimpleName(); @Override public boolean onTouch(View v, MotionEvent event) { return false; } public interface KControlsViewClient { void handleHtml5LibCall(String functionName, int callbackId, String args); void openURL(String url); void handleKControlsError(KPError error); } public interface ControlsBarHeightFetcher { void fetchHeight(int height); } private KControlsViewClient controlsViewClient; private String entryId; private ControlsBarHeightFetcher fetcher; private CacheManager mCacheManager; private static String AddJSListener = "addJsListener"; private static String RemoveJSListener = "removeJsListener"; @SuppressLint("SetJavaScriptEnabled") public KControlsView(Context context) { super(context); getSettings().setJavaScriptEnabled(true); init(); } public void setCacheManager(CacheManager cacheManager) { mCacheManager = cacheManager; } private void init() { getSettings().setAllowFileAccessFromFileURLs(true); getSettings().setAllowUniversalAccessFromFileURLs(true); getSettings().setAllowFileAccess(true); getSettings().setDomStorageEnabled(true); getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); getSettings().setAppCacheEnabled(false); this.addJavascriptInterface(this, "android"); this.setWebViewClient(new CustomWebViewClient()); //this.setWebChromeClient(new WebChromeClient()); this.setWebChromeClient(new WebChromeClient() { @Override public boolean onConsoleMessage(ConsoleMessage cm) { String message = cm.message(); String sourceId = cm.sourceId(); int lineNumber = cm.lineNumber(); ConsoleMessage.MessageLevel messageLevel = cm.messageLevel(); if (LogUtils.isWebViewDebugModeOn()) { LOGD(TAG, "WebView console " + messageLevel.name() + ": " + message + " at " + sourceId + " : " + lineNumber); } // Special case: clear cache if this error is given. if (messageLevel == ConsoleMessage.MessageLevel.ERROR) { if (message.contains("Uncaught SyntaxError")) { // for cases like "Uncaught SyntaxError: Unexpected end of input" or "Uncaught SyntaxError: Invalid or unexpected token" LOGW(TAG, "Removing faulty cached resource: " + sourceId); mCacheManager.removeCachedResponse(Uri.parse(sourceId)); } } return true; } }); this.getSettings().setUserAgentString(this.getSettings().getUserAgentString() + " kalturaNativeCordovaPlayer"); this.setLayerType(View.LAYER_TYPE_SOFTWARE, null); this.setBackgroundColor(0); } public void setKControlsViewClient(KControlsViewClient client) { this.controlsViewClient = client; } public void setEntryId(String entryId) { if (!this.entryId.equals(entryId)) { this.entryId = entryId; String entry = "'{\"entryId\":\"" + entryId + "}'"; sendNotification("changeMedia", entry); } } public void fetchControlsBarHeight(ControlsBarHeightFetcher fetcher) { this.fetcher = fetcher; this.loadUrl("javascript:android.onData(NativeBridge.videoPlayer.getControlBarHeight())"); } public void addEventListener(String event) { this.loadUrl(KStringUtilities.addEventListener(event)); } public void removeEventListener(String event) { this.loadUrl(KStringUtilities.removeEventListener(event)); } public void evaluate(String expression, String evaluateID) { this.loadUrl(KStringUtilities.asyncEvaluate(expression, evaluateID)); } public void sendNotification(String notification, String params) { LOGD(TAG, "JavaSCRIPT " + KStringUtilities.sendNotification(notification, params)); this.loadUrl(KStringUtilities.sendNotification(notification, params)); } public void setKDPAttribute(String pluginName, String propertyName, String value) { this.loadUrl(KStringUtilities.setKDPAttribute(pluginName, propertyName, value)); } public void setStringKDPAttribute(String pluginName, String propertyName, String value) { this.loadUrl(KStringUtilities.setStringKDPAttribute(pluginName, propertyName, value)); } public void setKDPAttribute(String pluginName, String propertyName, JSONObject value) { this.loadUrl(KStringUtilities.setKDPAttribute(pluginName, propertyName, value)); } public void triggerEvent(final String event, final String value) { try { loadUrl(KStringUtilities.triggerEvent(event, value)); } catch(NullPointerException e) { //for old android there is bug in WebView internal that they through NPE LOGE(TAG, "WebView NullPointerException caught: " + e.getMessage()); } } public void triggerEventWithJSON(String event, String jsonString) { this.loadUrl(KStringUtilities.triggerEventWithJSON(event, jsonString)); } private WebResourceResponse getWhiteFaviconResponse() { // 16x16 white favicon byte[] data = Base64.decode("AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 0); return new WebResourceResponse("image/x-icon", null, new ByteArrayInputStream(data)); } private WebResourceResponse getResponse(Uri requestUrl, Map<String, String> headers, String method) { // Only handle http(s) if (!requestUrl.getScheme().startsWith("http")) { LOGD(TAG, "Will not handle " + requestUrl); return null; } WebResourceResponse response = null; if (mCacheManager != null) { try { LOGD(TAG, "getResponse: CacheManager - requestUrl=" + requestUrl); response = mCacheManager.getResponse(requestUrl, headers, method); } catch (IOException e) { if (requestUrl.getPath().endsWith("favicon.ico")) { response = getWhiteFaviconResponse(); } else { LOGE(TAG, "getResponse From CacheManager error:: " + e.getMessage(), e); } } } return response; } @JavascriptInterface public void onData(String value) { if (this.fetcher != null && value != null) { int height = Integer.parseInt(value) + 5; this.fetcher.fetchHeight(height); this.fetcher = null; } } @Override public void destroy() { LOGD(TAG, "destroy()"); super.destroy(); } private class CustomWebViewClient extends WebViewClient { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { LOGD(TAG, "onPageStarted:" + url); super.onPageStarted(view, url, favicon); } @Override public void onPageFinished(WebView view, String url) { LOGD(TAG, "onPageFinished:" + url); super.onPageFinished(view, url); } @SuppressWarnings("deprecation") // deprecated on lollipop, required on earlier versions. @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { LOGD(TAG, "shouldOverrideUrlLoading: " + url); if (url == null) { return false; } final KStringUtilities urlUtil = new KStringUtilities(url); if (urlUtil.isJSFrame()) { final String action = urlUtil.getAction(); Runnable runnable = new Runnable() { @Override public void run() { KControlsView.this.controlsViewClient.handleHtml5LibCall(action, 1, urlUtil.getArgsString()); } }; if (Looper.myLooper() == Looper.getMainLooper()) { runnable.run(); } else { post(runnable); } return true; } return false; } @TargetApi(Build.VERSION_CODES.M) @Override public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError webResourceError) { String errMsg = "WebViewError:"; if (webResourceError != null) { if (webResourceError.getErrorCode() == -2) { //view.loadData("<div></div>", "text/html", "UTF-8"); } errMsg += webResourceError.getErrorCode() + "-" ; errMsg += webResourceError.getDescription() + "-"; if (request != null && request.getUrl() != null) { errMsg += request.getUrl().toString(); } } if (errMsg.contains("favicon.ico")) { return; } controlsViewClient.handleKControlsError(new KPError(errMsg)); } @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { if (errorCode == -2) { //view.loadData("<div></div>", "text/html", "UTF-8"); } String errMsg = "WebViewError:"; errMsg += errorCode + "-" ; if (description != null) { errMsg += description + "-"; } if (failingUrl != null) { errMsg += failingUrl; } if (errMsg.contains("favicon.ico")) { return; } controlsViewClient.handleKControlsError(new KPError(errMsg)); } @TargetApi(Build.VERSION_CODES.M) @Override public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse webResourceResponse) { String errMsg = "WebViewError:"; errMsg += webResourceResponse.getStatusCode() + "-" ; if (request != null && request.getUrl() != null) { errMsg += request.getUrl().toString() + "-"; } if (webResourceResponse != null) { errMsg += webResourceResponse.getReasonPhrase(); } if (errMsg.contains("favicon.ico")) { return; } controlsViewClient.handleKControlsError(new KPError(errMsg)); } @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { controlsViewClient.handleKControlsError(new KPError(error.toString())); } private WebResourceResponse textResponse(String text) { return new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(text.getBytes())); } private WebResourceResponse handleWebRequest(WebView view, String url, Map<String, String> headers, String method) { // On some devices, shouldOverrideUrlLoading() misses the js-frame call. if (shouldOverrideUrlLoading(view, url)) { return textResponse("JS-FRAME"); } return getResponse(Uri.parse(url), headers, method); } @SuppressWarnings("deprecation") @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { return handleWebRequest(view, url, Collections.<String, String>emptyMap(), "GET"); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { Map<String, String> headers = request.getRequestHeaders(); String method = request.getMethod(); return handleWebRequest(view, request.getUrl().toString(), headers, method); } } }